Merge pull request #124 from bornhack/emailworker

add emailworker for async emails
This commit is contained in:
Stephan Telling 2017-05-21 22:37:56 +02:00 committed by GitHub
commit 469b708782
14 changed files with 306 additions and 80 deletions

View file

@ -1,9 +1,9 @@
from utils.email import _send_email from utils.email import add_outgoing_email
import logging import logging
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
def send_creditnote_email(creditnote): def add_creditnote_email(creditnote):
# put formatdict together # put formatdict together
formatdict = { formatdict = {
'creditnote': creditnote, 'creditnote': creditnote,
@ -11,11 +11,11 @@ def send_creditnote_email(creditnote):
subject = 'BornHack creditnote %s' % creditnote.pk subject = 'BornHack creditnote %s' % creditnote.pk
# send mail # add email to outgoing email queue
return _send_email( return add_outgoing_email(
text_template='emails/creditnote_email.txt', text_template='emails/creditnote_email.txt',
html_template='emails/creditnote_email.html', html_template='emails/creditnote_email.html',
recipient=creditnote.user.email, to_recipients=creditnote.user.email,
formatdict=formatdict, formatdict=formatdict,
subject=subject, subject=subject,
attachment=creditnote.pdf.read(), attachment=creditnote.pdf.read(),
@ -23,7 +23,7 @@ def send_creditnote_email(creditnote):
) )
def send_invoice_email(invoice): def add_invoice_email(invoice):
# put formatdict together # put formatdict together
formatdict = { formatdict = {
'ordernumber': invoice.order.pk, 'ordernumber': invoice.order.pk,
@ -33,11 +33,11 @@ def send_invoice_email(invoice):
subject = 'BornHack invoice %s' % invoice.pk subject = 'BornHack invoice %s' % invoice.pk
# send mail # add email to outgoing email queue
return _send_email( return add_outgoing_email(
text_template='emails/invoice_email.txt', text_template='emails/invoice_email.txt',
html_template='emails/invoice_email.html', html_template='emails/invoice_email.html',
recipient=invoice.order.user.email, to_recipients=invoice.order.user.email,
formatdict=formatdict, formatdict=formatdict,
subject=subject, subject=subject,
attachment=invoice.pdf.read(), attachment=invoice.pdf.read(),
@ -45,9 +45,9 @@ def send_invoice_email(invoice):
) )
def send_test_email(recipient): def add_test_email(recipient):
return _send_email( return add_outgoing_email(
text_template='emails/testmail.txt', text_template='emails/testmail.txt',
recipient=recipient, to_recipients=recipient,
subject='testmail from bornhack website' subject='testmail from bornhack website'
) )

View file

@ -1,10 +1,8 @@
from django.core.files import File from django.core.files import File
from django.utils import timezone
from shop.pdf import generate_pdf_letter from shop.pdf import generate_pdf_letter
from shop.email import send_invoice_email, send_creditnote_email from shop.email import add_invoice_email, add_creditnote_email
from shop.models import Order, CustomOrder, Invoice, CreditNote from shop.models import Order, CustomOrder, Invoice, CreditNote
from decimal import Decimal import logging
import logging, importlib
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('bornhack.%s' % __name__) logger = logging.getLogger('bornhack.%s' % __name__)
@ -16,31 +14,26 @@ def do_work():
that have no PDF. It also emails invoices for shop orders. that have no PDF. It also emails invoices for shop orders.
""" """
###############################################################
# check if we need to generate any invoices for shop orders # check if we need to generate any invoices for shop orders
for order in Order.objects.filter(paid=True, invoice__isnull=True): for order in Order.objects.filter(paid=True, invoice__isnull=True):
# generate invoice for this Order # generate invoice for this Order
Invoice.objects.create(order=order) Invoice.objects.create(order=order)
logger.info('Generated Invoice object for %s' % order) logger.info('Generated Invoice object for %s' % order)
###############################################################
# check if we need to generate any invoices for custom orders # check if we need to generate any invoices for custom orders
for customorder in CustomOrder.objects.filter(invoice__isnull=True): for customorder in CustomOrder.objects.filter(invoice__isnull=True):
# generate invoice for this CustomOrder # generate invoice for this CustomOrder
Invoice.objects.create(customorder=customorder) Invoice.objects.create(customorder=customorder)
logger.info('Generated Invoice object for %s' % customorder) logger.info('Generated Invoice object for %s' % customorder)
###############################################################
# check if we need to generate any pdf invoices # check if we need to generate any pdf invoices
for invoice in Invoice.objects.filter(pdf=''): for invoice in Invoice.objects.filter(pdf=''):
# generate the pdf # generate the pdf
try: try:
if invoice.customorder: if invoice.customorder:
template='pdf/custominvoice.html' template = 'pdf/custominvoice.html'
else: else:
template='pdf/invoice.html' template = 'pdf/invoice.html'
pdffile = generate_pdf_letter( pdffile = generate_pdf_letter(
filename=invoice.filename, filename=invoice.filename,
template=template, template=template,
@ -57,21 +50,23 @@ def do_work():
invoice.pdf.save(invoice.filename, File(pdffile)) invoice.pdf.save(invoice.filename, File(pdffile))
invoice.save() invoice.save()
###############################################################
# check if we need to send out any invoices (only for shop orders, and only where pdf has been generated) # check if we need to send out any invoices (only for shop orders, and only where pdf has been generated)
for invoice in Invoice.objects.filter(order__isnull=False, sent_to_customer=False).exclude(pdf=''): for invoice in Invoice.objects.filter(order__isnull=False, sent_to_customer=False).exclude(pdf=''):
logger.info("found unmailed Invoice object: %s" % invoice) logger.info("found unmailed Invoice object: %s" % invoice)
# send the email # add email to the outgoing email queue
if send_invoice_email(invoice=invoice): if add_invoice_email(invoice=invoice):
invoice.sent_to_customer=True invoice.sent_to_customer = True
invoice.save() invoice.save()
logger.info('OK: Invoice email sent to %s' % invoice.order.user.email) logger.info('OK: Invoice email to {} added to queue.'.format(
invoice.order.user.email)
)
else: else:
logger.error('Unable to send invoice email for order %s to %s' % (invoice.order.pk, invoice.order.user.email)) logger.error('Unable to add email for invoice {} to {}'.format(
invoice.pk,
invoice.order.user.email
)
)
###############################################################
# check if we need to generate any pdf creditnotes? # check if we need to generate any pdf creditnotes?
for creditnote in CreditNote.objects.filter(pdf=''): for creditnote in CreditNote.objects.filter(pdf=''):
# generate the pdf # generate the pdf
@ -92,15 +87,13 @@ def do_work():
creditnote.pdf.save(creditnote.filename, File(pdffile)) creditnote.pdf.save(creditnote.filename, File(pdffile))
creditnote.save() creditnote.save()
###############################################################
# check if we need to send out any creditnotes (only where pdf has been generated) # check if we need to send out any creditnotes (only where pdf has been generated)
for creditnote in CreditNote.objects.filter(sent_to_customer=False).exclude(pdf=''): for creditnote in CreditNote.objects.filter(sent_to_customer=False).exclude(pdf=''):
# send the email # send the email
if send_creditnote_email(creditnote=creditnote): if add_creditnote_email(creditnote=creditnote):
logger.info('OK: Creditnote email sent to %s' % creditnote.user.email) logger.info('OK: Creditnote email to %s added' % creditnote.user.email)
creditnote.sent_to_customer=True creditnote.sent_to_customer = True
creditnote.save() creditnote.save()
else: else:
logger.error('Unable to send creditnote email for creditnote %s to %s' % (creditnote.pk, creditnote.user.email)) logger.error('Unable to add creditnote email for creditnote %s to %s' % (creditnote.pk, creditnote.user.email))

View file

@ -3,7 +3,9 @@ from wkhtmltopdf.views import PDFTemplateResponse
from PyPDF2 import PdfFileWriter, PdfFileReader from PyPDF2 import PdfFileWriter, PdfFileReader
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.conf import settings from django.conf import settings
import io, logging import logging
import io
import os
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
@ -13,10 +15,10 @@ def generate_pdf_letter(filename, template, formatdict):
request.user = AnonymousUser() request.user = AnonymousUser()
request.session = {} request.session = {}
### produce text-only PDF from template # produce text-only PDF from template
pdfgenerator = PDFTemplateResponse( pdfgenerator = PDFTemplateResponse(
request=request, request=request,
template=template, template=template,
context=formatdict, context=formatdict,
cmd_options={ cmd_options={
'margin-top': 50, 'margin-top': 50,
@ -26,33 +28,35 @@ def generate_pdf_letter(filename, template, formatdict):
textonlypdf = io.BytesIO() textonlypdf = io.BytesIO()
textonlypdf.write(pdfgenerator.rendered_content) textonlypdf.write(pdfgenerator.rendered_content)
### create a blank pdf to work with # create a blank pdf to work with
finalpdf = PdfFileWriter() finalpdf = PdfFileWriter()
### open the text-only pdf # open the text-only pdf
pdfreader = PdfFileReader(textonlypdf) pdfreader = PdfFileReader(textonlypdf)
### get watermark from watermark file # get watermark from watermark file
watermark = PdfFileReader(open("%s/pdf/%s" % (settings.STATICFILES_DIRS[0], settings.PDF_LETTERHEAD_FILENAME), 'rb')) watermark = PdfFileReader(
open(os.path.join(settings.STATICFILES_DIRS[0], 'pdf', settings.PDF_LETTERHEAD_FILENAME), 'rb')
)
### add the watermark to all pages # add the watermark to all pages
for pagenum in range(pdfreader.getNumPages()): for pagenum in range(pdfreader.getNumPages()):
page = watermark.getPage(0) page = watermark.getPage(0)
try: try:
page.mergePage(pdfreader.getPage(pagenum)) page.mergePage(pdfreader.getPage(pagenum))
except ValueError: except ValueError:
### watermark pdf might be broken? # watermark pdf might be broken?
return False return False
### add page to output # add page to output
finalpdf.addPage(page) finalpdf.addPage(page)
### save the generated pdf to the archive # save the generated pdf to the archive
fullpath = settings.PDF_ARCHIVE_PATH+filename fullpath = os.path.join(settings.PDF_ARCHIVE_PATH, filename)
with open(fullpath, 'wb') as fh: with open(fullpath, 'wb') as fh:
finalpdf.write(fh) finalpdf.write(fh)
logger.info('Saved pdf to archive: %s' % fullpath) logger.info('Saved pdf to archive: %s' % fullpath)
### return a file object with the data # return a file object with the data
returnfile = io.BytesIO() returnfile = io.BytesIO()
finalpdf.write(returnfile) finalpdf.write(returnfile)
return returnfile return returnfile

View file

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Team, TeamArea, TeamMember from .models import Team, TeamArea, TeamMember
from .email import send_add_membership_email, send_remove_membership_email from .email import add_added_membership_email, add_removed_membership_email
admin.site.register(TeamArea) admin.site.register(TeamArea)
@ -41,7 +41,7 @@ class TeamMemberAdmin(admin.ModelAdmin):
membership.approved = True membership.approved = True
membership.save() membership.save()
updated += 1 updated += 1
send_add_membership_email(membership) add_added_membership_email(membership)
self.message_user( self.message_user(
request, request,
@ -57,7 +57,7 @@ class TeamMemberAdmin(admin.ModelAdmin):
updated = 0 updated = 0
for membership in queryset: for membership in queryset:
send_remove_membership_email(membership) add_removed_membership_email(membership)
membership.delete() membership.delete()
updated += 1 updated += 1
@ -69,4 +69,3 @@ class TeamMemberAdmin(admin.ModelAdmin):
) )
) )
remove_member.description = 'Remove a user from the team.' remove_member.description = 'Remove a user from the team.'

View file

@ -1,24 +1,24 @@
from utils.email import _send_email from utils.email import add_outgoing_email
import logging import logging
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
def send_add_membership_email(membership): def add_added_membership_email(membership):
formatdict = { formatdict = {
'team': membership.team.name, 'team': membership.team.name,
'camp': membership.team.camp.title 'camp': membership.team.camp.title
} }
return _send_email( return add_outgoing_email(
text_template='emails/add_membership_email.txt', text_template='emails/add_membership_email.txt',
html_template='emails/add_membership_email.html', html_template='emails/add_membership_email.html',
recipient=membership.user.email, to_recipients=membership.user.email,
formatdict=formatdict, formatdict=formatdict,
subject='Team update from {}'.format(membership.team.camp.title) subject='Team update from {}'.format(membership.team.camp.title)
) )
def send_remove_membership_email(membership): def add_removed_membership_email(membership):
formatdict = { formatdict = {
'team': membership.team.name, 'team': membership.team.name,
'camp': membership.team.camp.title 'camp': membership.team.camp.title
@ -31,25 +31,25 @@ def send_remove_membership_email(membership):
text_template = 'emails/unapproved_membership_email.txt', text_template = 'emails/unapproved_membership_email.txt',
html_template = 'emails/unapproved_membership_email.html' html_template = 'emails/unapproved_membership_email.html'
return _send_email( return add_outgoing_email(
text_template=text_template, text_template=text_template,
html_template=html_template, html_template=html_template,
recipient=membership.user.email, to_recipients=membership.user.email,
formatdict=formatdict, formatdict=formatdict,
subject='Team update from {}'.format(membership.team.camp.title) subject='Team update from {}'.format(membership.team.camp.title)
) )
def send_new_membership_email(membership): def add_new_membership_email(membership):
formatdict = { formatdict = {
'team': membership.team.name, 'team': membership.team.name,
'camp': membership.team.camp.title 'camp': membership.team.camp.title
} }
return _send_email( return add_outgoing_email(
text_template='emails/new_membership_email.txt', text_template='emails/new_membership_email.txt',
html_template='emails/new_membership_email.html', html_template='emails/new_membership_email.html',
recipient=[resp.email for resp in membership.team.responsible], to_recipients=[resp.email for resp in membership.team.responsible],
formatdict=formatdict, formatdict=formatdict,
subject='New membership request for {} at {}'.format( subject='New membership request for {} at {}'.format(
membership.team.name, membership.team.name,

View file

@ -3,9 +3,11 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.text import slugify from django.utils.text import slugify
from utils.models import CampRelatedModel from utils.models import CampRelatedModel
from .email import send_new_membership_email from .email import add_new_membership_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
import logging
logger = logging.getLogger("bornhack.%s" % __name__)
class TeamArea(CampRelatedModel): class TeamArea(CampRelatedModel):
@ -96,6 +98,7 @@ class TeamMember(models.Model):
@receiver(post_save, sender=TeamMember) @receiver(post_save, sender=TeamMember)
def send_responsible_email(sender, instance, created, **kwargs): def add_responsible_email(sender, instance, created, **kwargs):
if created: if created:
send_new_membership_email(instance) if not add_new_membership_email(instance):
logger.error('Error adding email to outgoing queue')

8
src/utils/admin.py Normal file
View file

@ -0,0 +1,8 @@
from django.contrib import admin
from .models import OutgoingEmail
@admin.register(OutgoingEmail)
class OutgoingEmailAdmin(admin.ModelAdmin):
pass

View file

@ -1,49 +1,105 @@
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.conf import settings from django.conf import settings
from django.template.loader import render_to_string from django.template.loader import render_to_string
from .models import OutgoingEmail
import logging import logging
logger = logging.getLogger("bornhack.%s" % __name__) logger = logging.getLogger("bornhack.%s" % __name__)
def _send_email( def _send_email(
text_template, text_template,
recipient,
formatdict,
subject, subject,
html_template=None, to_recipients=[],
cc_recipients=[],
bcc_recipients=[],
html_template='',
sender='BornHack <info@bornhack.dk>', sender='BornHack <info@bornhack.dk>',
attachment=None, attachment=None,
attachment_filename=None attachment_filename=''
): ):
if not isinstance(recipient, list): if not isinstance(to_recipients, list):
recipient = [recipient] to_recipients = [to_recipients]
try: try:
# put the basic email together # put the basic email together
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
subject, subject,
render_to_string(text_template, formatdict), text_template,
sender, sender,
recipient, to_recipients,
[settings.ARCHIVE_EMAIL] bcc_recipients + [settings.ARCHIVE_EMAIL] if bcc_recipients else [settings.ARCHIVE_EMAIL],
cc_recipients
) )
# is there a html version of this email? # is there a html version of this email?
if html_template: if html_template:
msg.attach_alternative( msg.attach_alternative(
render_to_string(html_template, formatdict), html_template,
'text/html' 'text/html'
) )
# is there a pdf attachment to this mail? # is there a pdf attachment to this mail?
if attachment: if attachment:
msg.attach(attachment_filename, attachment, 'application/pdf') msg.attach(attachment_filename, attachment, 'application/pdf')
except Exception as e: except Exception as e:
logger.exception('exception while rendering email: {}'.format(e)) logger.exception('exception while rendering email: {}'.format(e))
return False return False
# send the email # send the email
msg.send() try:
msg.send(fail_silently=False)
except Exception as e:
logger.exception('exception while sending email: {}'.format(e))
return False
return True
def add_outgoing_email(
text_template,
formatdict,
subject,
to_recipients=[],
cc_recipients=[],
bcc_recipients=[],
html_template='',
sender='BornHack <info@bornhack.dk>',
attachment=None,
attachment_filename=''
):
""" adds an email to the outgoing queue
recipients is a list of to recipients
"""
text_template = render_to_string(text_template, formatdict)
if html_template:
html_template = render_to_string(html_template, formatdict)
if not isinstance(to_recipients, list):
to_recipients = [to_recipients]
for recipient in to_recipients:
try:
validate_email(recipient)
except ValidationError:
return False
email = OutgoingEmail.objects.create(
text_template=text_template,
html_template=html_template,
subject=subject,
sender=sender,
to_recipients=to_recipients,
cc_recipients=cc_recipients,
bcc_recipients=bcc_recipients
)
if attachment:
django_file = ContentFile(attachment)
django_file.name = attachment_filename
email.attachment.save(attachment_filename, django_file, save=True)
return True return True

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-04-30 12:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='OutgoingEmail',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('subject', models.CharField(max_length=500)),
('text_template', models.TextField()),
('html_template', models.TextField(blank=True)),
('recipient', models.CharField(max_length=500)),
('sender', models.CharField(max_length=500)),
('attachment', models.FileField(blank=True, upload_to='')),
('processed', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-21 16:08
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('utils', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='outgoingemail',
name='recipient',
),
]

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-21 17:32
from __future__ import unicode_literals
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('utils', '0002_remove_outgoingemail_recipient'),
]
operations = [
migrations.AddField(
model_name='outgoingemail',
name='bcc_recipients',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=500), blank=True, null=True, size=None),
),
migrations.AddField(
model_name='outgoingemail',
name='cc_recipients',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=500), blank=True, null=True, size=None),
),
migrations.AddField(
model_name='outgoingemail',
name='to_recipients',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=500), blank=True, null=True, size=None),
),
]

View file

View file

@ -1,4 +1,5 @@
import uuid import uuid
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib import messages from django.contrib import messages
from django.db import models from django.db import models
@ -68,3 +69,38 @@ class CampRelatedModel(CreatedUpdatedModel):
raise ValidationError('This camp is in read only mode.') raise ValidationError('This camp is in read only mode.')
super().delete(**kwargs) super().delete(**kwargs)
class OutgoingEmail(CreatedUpdatedModel):
subject = models.CharField(max_length=500)
text_template = models.TextField()
html_template = models.TextField(blank=True)
sender = models.CharField(max_length=500)
to_recipients = ArrayField(
models.CharField(max_length=500, blank=True),
null=True,
blank=True
)
cc_recipients = ArrayField(
models.CharField(max_length=500, blank=True),
null=True,
blank=True
)
bcc_recipients = ArrayField(
models.CharField(max_length=500, blank=True),
null=True,
blank=True
)
attachment = models.FileField(blank=True)
processed = models.BooleanField(default=False)
def __str__(self):
return 'OutgoingEmail Object id: {} '.format(self.id)
def clean(self):
if not self.to_recipients \
and not self.bcc_recipients \
and not self.cc_recipients:
raise ValidationError(
{'recipient': 'either to_recipient, bcc_recipient or cc_recipient required.'}
)

View file

@ -0,0 +1,43 @@
from .models import OutgoingEmail
from .email import _send_email
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('bornhack.%s' % __name__)
def do_work():
"""
The outgoing email worker sends emails added to the OutgoingEmail
queue.
"""
not_processed_email = OutgoingEmail.objects.filter(processed=False)
if len(not_processed_email) > 0:
logger.debug('about to process {} emails'.format(
len(not_processed_email))
)
for email in not_processed_email:
attachment = None
attachment_filename = ''
if email.attachment:
attachment = email.attachment.read()
attachment_filename = email.attachment.name
mail_send_success = _send_email(
text_template=email.text_template,
to_recipients=email.to_recipients,
subject=email.subject,
cc_recipients=email.cc_recipients,
bcc_recipients=email.bcc_recipients,
html_template=email.html_template,
attachment=attachment,
attachment_filename=attachment_filename
)
if mail_send_success:
email.processed = True
email.save()
logger.debug('successfully sent {}'.format(email))
else:
logger.error('unable to sent {}'.format(email))