Skip to content

Commit

Permalink
Merge pull request #48 from CodeForBuffalo/async-emails
Browse files Browse the repository at this point in the history
Add automatic asynchronous emails with Celery and RabbitMQ
  • Loading branch information
mpbrown authored Aug 6, 2020
2 parents b9dafd0 + 08b9ce3 commit 553b382
Show file tree
Hide file tree
Showing 31 changed files with 4,230 additions and 101 deletions.
22 changes: 15 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ node_js:

addons:
chrome: stable
apt:
packages:
- rabbitmq-server

services:
- rabbitmq

before_install:
- pip install -U pip
Expand All @@ -18,12 +24,16 @@ install:
- pip install pipenv
- pipenv sync --dev
- npm install
- npm install -g gulp-cli
- wget https://chromedriver.storage.googleapis.com/78.0.3904.70/chromedriver_linux64.zip -P ~/
- unzip ~/chromedriver_linux64.zip -d ~/
- rm ~/chromedriver_linux64.zip
- sudo mv -f ~/chromedriver /usr/local/share/
- sudo chmod +x /usr/local/share/chromedriver
- export CHROME_BIN=chromium-browser
- gulp
- python manage.py collectstatic --noinput
- python manage.py compress

cache:
directories:
Expand All @@ -32,18 +42,16 @@ cache:
- node_modules
- $HOME/.npm


before_script:
- npm install -g gulp-cli
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build

script:
- gulp
- python manage.py collectstatic --noinput
- python manage.py compress
- celery multi start worker1 -A affordable_water --pidfile="$HOME/run/celery/%n.pid" --logfile="$HOME/log/celery/%n%I.log"
- coverage run manage.py test -v 2

after_script:
- coverage xml
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT


1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ python-magic = "*"
django-simple-history = "*"
django-two-factor-auth = "*"
phonenumbers = "*"
celery = "*"

[requires]
python_version = "3.7"
226 changes: 149 additions & 77 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
web: gunicorn affordable_water.wsgi
web: gunicorn affordable_water.wsgi
worker: celery worker --app=affordable_water -l info
7 changes: 7 additions & 0 deletions affordable_water/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from __future__ import absolute_import, unicode_literals

# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app

__all__ = ('celery_app',)
25 changes: 25 additions & 0 deletions affordable_water/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import absolute_import, unicode_literals

import os

from celery import Celery
from django.conf import settings

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'affordable_water.settings')

app = Celery('affordable_water', backend='amqp://')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django app configs.
app.autodiscover_tasks()


@app.task(bind=True)
def debug_task(self):
print('Request: {0!r}'.format(self.request))
8 changes: 8 additions & 0 deletions affordable_water/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@

DEFAULT_FROM_EMAIL = 'Get Water Wise Buffalo <[email protected]>'

# Celery Config
CELERY_BROKER_URL = os.getenv('CLOUDAMQP_URL', 'amqp://guest:guest@localhost:5672//')
BROKER_POOL_LIMIT = 1
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_RESULT_BACKEND = 'rpc'

# AWS
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"migrate": "pipenv run python manage.py migrate",
"start": "pipenv run python manage.py runserver",
"heroku-prebuild": "npm install",
"heroku-postbuild": "npm run gulp && npm run heroku-collectstatic && npm run heroku-compress"
"heroku-postbuild": "npm run gulp && npm run heroku-collectstatic && npm run heroku-compress",
"celery": "celery worker -A affordable_water -l info --pool=solo"
},
"author": "",
"license": "MIT",
Expand Down
90 changes: 85 additions & 5 deletions pathways/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import Application, Document, ForgivenessApplication
from .models import Application, Document, ForgivenessApplication, EmailCommunication
from . import tasks

# Register your models here.

Expand All @@ -12,14 +13,93 @@ class DocumentInline(admin.TabularInline):
model = Document
extra = 0

def make_enrolled_discount(modeladmin, request, queryset):
for app in queryset:
app.status = 'enrolled'
app.save()

make_enrolled_discount.short_description = "Mark selected Discount Applications as enrolled"
make_enrolled_discount.allowed_permissions = ('change',)

def make_enrolled_amnesty(modeladmin, request, queryset):
for app in queryset:
app.status = 'enrolled'
app.save()

make_enrolled_amnesty.short_description = "Mark selected Amnesty Applications as enrolled"
make_enrolled_amnesty.allowed_permissions = ('change',)

@admin.register(Application)
class ApplicationAdmin(SimpleHistoryAdmin):
inlines = [
DocumentInline,
]
inlines = [DocumentInline,]
actions = [make_enrolled_discount]
list_display = ['__str__', 'date_created', 'full_name', 'account_name',
'rent_or_own', 'street_address', 'apartment_unit',
'zip_code', 'household_size', 'has_household_benefits',
'has_residence_docs', 'has_eligible_docs', 'status']
list_editable = ['status']
list_filter = ['status']

def full_name(self, obj):
if obj.middle_initial == '':
return obj.first_name + ' ' + obj.last_name
else:
return obj.first_name + ' ' + obj.middle_initial + ' ' + obj.last_name

def account_name(self, obj):
return obj.account_first + ' ' + obj.account_middle + ' ' + obj.account_last

def date_created(self, obj):
return obj.history.all()[0].history_date

def has_residence_docs(self, obj):
residence_count = Document.objects.filter(application=obj, doc_type='residence').count()
return residence_count > 0
has_residence_docs.boolean = True
has_residence_docs.description = 'Residence documents'

def has_eligible_docs(self, obj):
income_count = Document.objects.filter(application=obj, doc_type='income').count()
benefits_count = Document.objects.filter(application=obj, doc_type='benefits').count()
return income_count + benefits_count > 0
has_eligible_docs.boolean = True
has_eligible_docs.description = 'Income or benefits documents'

@admin.register(ForgivenessApplication)
class ForgivenessApplicationAdmin(SimpleHistoryAdmin):
pass
actions = [make_enrolled_amnesty]
list_display = ['__str__', 'date_created', 'full_name', 'street_address',
'apartment_unit', 'zip_code', 'phone_number',
'email_address', 'status']
list_editable = ['status']
list_filter = ['status']
list_per_page = 12

def date_created(self, obj):
return obj.history.all()[0].history_date

def full_name(self, obj):
if obj.middle_initial == '':
return obj.first_name + ' ' + obj.last_name
else:
return obj.first_name + ' ' + obj.middle_initial + ' ' + obj.last_name

@admin.register(EmailCommunication)
class EmailCommunicationAdmin(SimpleHistoryAdmin):
readonly_fields = ['email_address',
'discount_application_received',
'amnesty_application_received',
'enrolled_in_amnesty_program',
'enrolled_in_discount_program']
list_display = ['email_address',
'discount_application_received',
'amnesty_application_received',
'enrolled_in_amnesty_program',
'enrolled_in_discount_program']
list_filter = ['email_address',
'discount_application_received',
'amnesty_application_received',
'enrolled_in_amnesty_program',
'enrolled_in_discount_program']

admin.site.site_header = "GetWaterWiseBuffalo Admin"
3 changes: 3 additions & 0 deletions pathways/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

class PathwaysConfig(AppConfig):
name = 'pathways'

def ready(self):
import pathways.signals
13 changes: 13 additions & 0 deletions pathways/management/commands/send_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand, CommandError
from pathways.tasks import send_email

class Command(BaseCommand):
help = 'Sends email'

def handle(self, *args, **kwargs):
if(all(x in kwargs for x in ['subject', 'recipient_list', 'email_template'])):
subject = kwargs['subject']
recipient_list = kwargs['recipient_list']
email_template = kwargs['email_template']
send_email(subject=subject, recipient_list=recipient_list, email_template=email_template)

49 changes: 49 additions & 0 deletions pathways/migrations/0011_auto_20200730_1505.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 2.2.14 on 2020-07-30 19:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pathways', '0010_forgivenessapplication_historicalforgivenessapplication'),
]

operations = [
migrations.AlterModelOptions(
name='application',
options={'verbose_name': 'Discount Application', 'verbose_name_plural': 'Discount Applications'},
),
migrations.AlterModelOptions(
name='forgivenessapplication',
options={'verbose_name': 'Amnesty Application', 'verbose_name_plural': 'Amnesty Applications'},
),
migrations.AlterModelOptions(
name='historicalapplication',
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Discount Application'},
),
migrations.AlterModelOptions(
name='historicalforgivenessapplication',
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Amnesty Application'},
),
migrations.AddField(
model_name='forgivenessapplication',
name='notes',
field=models.TextField(blank=True, help_text='Enter any notes for this case'),
),
migrations.AddField(
model_name='forgivenessapplication',
name='status',
field=models.CharField(choices=[('new', 'New'), ('in_progress', 'In Progress'), ('enrolled', 'Enrolled'), ('denied', 'Denied')], default='new', max_length=12),
),
migrations.AddField(
model_name='historicalforgivenessapplication',
name='notes',
field=models.TextField(blank=True, help_text='Enter any notes for this case'),
),
migrations.AddField(
model_name='historicalforgivenessapplication',
name='status',
field=models.CharField(choices=[('new', 'New'), ('in_progress', 'In Progress'), ('enrolled', 'Enrolled'), ('denied', 'Denied')], default='new', max_length=12),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 2.2.14 on 2020-07-14 19:36

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('pathways', '0010_forgivenessapplication_historicalforgivenessapplication'),
]

operations = [
migrations.CreateModel(
name='EmailCommunication',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email_address', models.EmailField(max_length=254)),
('discount_application_received', models.BooleanField(default=False)),
('amnesty_application_received', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='HistoricalEmailCommunication',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('email_address', models.EmailField(max_length=254)),
('discount_application_received', models.BooleanField(default=False)),
('amnesty_application_received', models.BooleanField(default=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField()),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical email communication',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]
31 changes: 31 additions & 0 deletions pathways/migrations/0012_auto_20200716_1519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 2.2.14 on 2020-07-16 19:19

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pathways', '0011_emailcommunication_historicalemailcommunication'),
]

operations = [
migrations.RemoveField(
model_name='emailcommunication',
name='id',
),
migrations.RemoveField(
model_name='historicalemailcommunication',
name='id',
),
migrations.AlterField(
model_name='emailcommunication',
name='email_address',
field=models.EmailField(editable=False, max_length=254, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='historicalemailcommunication',
name='email_address',
field=models.EmailField(db_index=True, editable=False, max_length=254),
),
]
Loading

0 comments on commit 553b382

Please sign in to comment.