Merge pull request #1120 from kaedroho/kill-celery

No longer use celery for sending notifications
This commit is contained in:
Dave Cranwell 2015-03-31 12:10:52 +01:00
commit bbeb6cc319
8 changed files with 115 additions and 142 deletions

View file

@ -28,21 +28,6 @@ We recommend `Redis <http://redis.io/>`_ as a fast, persistent cache. Install Re
Without a persistent cache, Wagtail will recreate all compressable assets at each server start, e.g. when any files change under ``./manage.py runserver``.
Sending emails in the background using Celery
---------------------------------------------
Various actions in the Wagtail admin backend can trigger notification emails - for example, submitting a page for moderation. In Wagtail's default configuration, these are sent as part of the page request/response cycle, which means that web server threads can get tied up for long periods if many emails are being sent. To avoid this, Wagtail can be configured to do this as a background task, using `Celery <http://www.celeryproject.org/>`_ as a task queue. To install Celery, add ``django-celery`` to your requirements.txt. A sample configuration, using Redis as the queue backend, would look like::
import djcelery
djcelery.setup_loader()
CELERY_SEND_TASK_ERROR_EMAILS = True
BROKER_URL = 'redis://'
See the Celery documentation for instructions on running the worker process in development or production.
Search
------

View file

@ -13,19 +13,6 @@ TEMPLATE_DEBUG = False
COMPRESS_OFFLINE = True
# Send notification emails as a background task using Celery,
# to prevent this from blocking web server threads
# (requires the django-celery package):
# http://celery.readthedocs.org/en/latest/configuration.html
# import djcelery
#
# djcelery.setup_loader()
#
# CELERY_SEND_TASK_ERROR_EMAILS = True
# BROKER_URL = 'redis://'
# Use Redis as the cache backend for extra performance
# (requires the django-redis-cache package):
# http://wagtail.readthedocs.org/en/latest/howto/performance.html#cache

View file

@ -8,4 +8,3 @@ wagtail==1.0b1
# Recommended components to improve performance in production:
# django-redis==3.8.2
# django-celery==3.1.10

View file

@ -1,101 +0,0 @@
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q
from wagtail.wagtailcore.models import PageRevision, GroupPagePermission
from wagtail.wagtailusers.models import UserProfile
# The following will check to see if we can import task from celery -
# if not then we definitely haven't installed it
try:
from celery.decorators import task
NO_CELERY = False
except:
NO_CELERY = True
# However, we could have installed celery for other projects. So we will also
# check if we have defined the BROKER_URL setting. If not then definitely we
# haven't configured it.
if NO_CELERY or not hasattr(settings, 'BROKER_URL'):
# So if we enter here we will define a different "task" decorator that
# just returns the original function and sets its delay attribute to
# point to the original function: This way, the send_notification
# function will be actually called instead of the the
# send_notification.delay()
def task(f):
f.delay=f
return f
def users_with_page_permission(page, permission_type, include_superusers=True):
# Get user model
User = get_user_model()
# Find GroupPagePermission records of the given type that apply to this page or an ancestor
ancestors_and_self = list(page.get_ancestors()) + [page]
perm = GroupPagePermission.objects.filter(permission_type=permission_type, page__in=ancestors_and_self)
q = Q(groups__page_permissions=perm)
# Include superusers
if include_superusers:
q |= Q(is_superuser=True)
return User.objects.filter(is_active=True).filter(q).distinct()
@task
def send_notification(page_revision_id, notification, excluded_user_id):
# Get revision
revision = PageRevision.objects.get(id=page_revision_id)
# Get list of recipients
if notification == 'submitted':
# Get list of publishers
recipients = users_with_page_permission(revision.page, 'publish')
elif notification in ['rejected', 'approved']:
# Get submitter
recipients = [revision.user]
else:
return
# Get list of email addresses
email_addresses = [
recipient.email for recipient in recipients
if recipient.email and recipient.id != excluded_user_id and getattr(UserProfile.get_for_user(recipient), notification + '_notifications')
]
# Return if there are no email addresses
if not email_addresses:
return
# Get email subject and content
template = 'wagtailadmin/notifications/' + notification + '.html'
rendered_template = render_to_string(template, dict(revision=revision, settings=settings)).split('\n')
email_subject = rendered_template[0]
email_content = '\n'.join(rendered_template[1:])
# Get from email
if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'):
from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL
elif hasattr(settings, 'DEFAULT_FROM_EMAIL'):
from_email = settings.DEFAULT_FROM_EMAIL
else:
from_email = 'webmaster@localhost'
# Send email
send_mail(email_subject, email_content, from_email, email_addresses)
@task
def send_email_task(email_subject, email_content, email_addresses, from_email=None):
if not from_email:
if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'):
from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL
elif hasattr(settings, 'DEFAULT_FROM_EMAIL'):
from_email = settings.DEFAULT_FROM_EMAIL
else:
from_email = 'webmaster@localhost'
send_mail(email_subject, email_content, from_email, email_addresses)

View file

@ -1,10 +1,10 @@
from django.test import TestCase
from django.test import TestCase, override_settings
from django.core.urlresolvers import reverse
from django.core import mail
from wagtail.tests.utils import WagtailTestUtils
from wagtail.wagtailcore.models import Page
from wagtail.wagtailadmin.tasks import send_email_task
from wagtail.wagtailadmin.utils import send_mail
class TestHome(TestCase, WagtailTestUtils):
@ -56,15 +56,48 @@ class TestEditorHooks(TestCase, WagtailTestUtils):
self.assertContains(response, '<script src="/path/to/my/custom.js"></script>')
class TestSendEmailTask(TestCase):
class TestSendMail(TestCase):
def test_send_email(self):
send_email_task("Test subject", "Test content", ["nobody@email.com"], "test@email.com")
send_mail("Test subject", "Test content", ["nobody@email.com"], "test@email.com")
# Check that the email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "Test subject")
self.assertEqual(mail.outbox[0].body, "Test content")
self.assertEqual(mail.outbox[0].to, ["nobody@email.com"])
self.assertEqual(mail.outbox[0].from_email, "test@email.com")
@override_settings(WAGTAILADMIN_NOTIFICATION_FROM_EMAIL='anothertest@email.com')
def test_send_fallback_to_wagtailadmin_notification_from_email_setting(self):
send_mail("Test subject", "Test content", ["nobody@email.com"])
# Check that the email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "Test subject")
self.assertEqual(mail.outbox[0].body, "Test content")
self.assertEqual(mail.outbox[0].to, ["nobody@email.com"])
self.assertEqual(mail.outbox[0].from_email, "anothertest@email.com")
@override_settings(DEFAULT_FROM_EMAIL='yetanothertest@email.com')
def test_send_fallback_to_default_from_email_setting(self):
send_mail("Test subject", "Test content", ["nobody@email.com"])
# Check that the email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "Test subject")
self.assertEqual(mail.outbox[0].body, "Test content")
self.assertEqual(mail.outbox[0].to, ["nobody@email.com"])
self.assertEqual(mail.outbox[0].from_email, "yetanothertest@email.com")
def test_send_default_from_email(self):
send_mail("Test subject", "Test content", ["nobody@email.com"])
# Check that the email was sent
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, "Test subject")
self.assertEqual(mail.outbox[0].body, "Test content")
self.assertEqual(mail.outbox[0].to, ["nobody@email.com"])
self.assertEqual(mail.outbox[0].from_email, "webmaster@localhost")
class TestExplorerNavView(TestCase, WagtailTestUtils):

View file

@ -1,6 +1,13 @@
from django.template.loader import render_to_string
from django.core.mail import send_mail as django_send_mail
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Q
from modelcluster.fields import ParentalKey
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.models import Page, PageRevision, GroupPagePermission
from wagtail.wagtailusers.models import UserProfile
def get_object_usage(obj):
@ -34,3 +41,65 @@ def get_object_usage(obj):
)
return pages
def users_with_page_permission(page, permission_type, include_superusers=True):
# Get user model
User = get_user_model()
# Find GroupPagePermission records of the given type that apply to this page or an ancestor
ancestors_and_self = list(page.get_ancestors()) + [page]
perm = GroupPagePermission.objects.filter(permission_type=permission_type, page__in=ancestors_and_self)
q = Q(groups__page_permissions=perm)
# Include superusers
if include_superusers:
q |= Q(is_superuser=True)
return User.objects.filter(is_active=True).filter(q).distinct()
def send_mail(email_subject, email_content, email_addresses, from_email=None):
if not from_email:
if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'):
from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL
elif hasattr(settings, 'DEFAULT_FROM_EMAIL'):
from_email = settings.DEFAULT_FROM_EMAIL
else:
from_email = 'webmaster@localhost'
django_send_mail(email_subject, email_content, from_email, email_addresses)
def send_notification(page_revision_id, notification, excluded_user_id):
# Get revision
revision = PageRevision.objects.get(id=page_revision_id)
# Get list of recipients
if notification == 'submitted':
# Get list of publishers
recipients = users_with_page_permission(revision.page, 'publish')
elif notification in ['rejected', 'approved']:
# Get submitter
recipients = [revision.user]
else:
return
# Get list of email addresses
email_addresses = [
recipient.email for recipient in recipients
if recipient.email and recipient.id != excluded_user_id and getattr(UserProfile.get_for_user(recipient), notification + '_notifications')
]
# Return if there are no email addresses
if not email_addresses:
return
# Get email subject and content
template = 'wagtailadmin/notifications/' + notification + '.html'
rendered_template = render_to_string(template, dict(revision=revision, settings=settings)).split('\n')
email_subject = rendered_template[0]
email_content = '\n'.join(rendered_template[1:])
# Send email
send_mail(email_subject, email_content, email_addresses)

View file

@ -15,7 +15,8 @@ from django.db.models import Count
from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList
from wagtail.wagtailadmin.forms import SearchForm, CopyForm
from wagtail.wagtailadmin import tasks, signals
from wagtail.wagtailadmin.utils import send_notification
from wagtail.wagtailadmin import signals
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision, get_navigation_menu_items
@ -225,7 +226,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
messages.success(request, _("Page '{0}' published.").format(page.title))
elif is_submitting:
messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title))
tasks.send_notification.delay(page.get_latest_revision().id, 'submitted', request.user.id)
send_notification(page.get_latest_revision().id, 'submitted', request.user.id)
else:
messages.success(request, _("Page '{0}' created.").format(page.title))
@ -361,7 +362,7 @@ def edit(request, page_id):
messages.button(reverse('wagtailadmin_pages_view_draft', args=(page_id,)), _('View draft')),
messages.button(reverse('wagtailadmin_pages_edit', args=(page_id,)), _('Edit'))
])
tasks.send_notification.delay(page.get_latest_revision().id, 'submitted', request.user.id)
send_notification(page.get_latest_revision().id, 'submitted', request.user.id)
else:
messages.success(request, _("Page '{0}' updated.").format(page.title))
@ -782,7 +783,7 @@ def approve_moderation(request, revision_id):
if request.method == 'POST':
revision.approve_moderation()
messages.success(request, _("Page '{0}' published.").format(revision.page.title))
tasks.send_notification.delay(revision.id, 'approved', request.user.id)
send_notification(revision.id, 'approved', request.user.id)
return redirect('wagtailadmin_home')
@ -799,7 +800,7 @@ def reject_moderation(request, revision_id):
if request.method == 'POST':
revision.reject_moderation()
messages.success(request, _("Page '{0}' rejected for publication.").format(revision.page.title))
tasks.send_notification.delay(revision.id, 'rejected', request.user.id)
send_notification(revision.id, 'rejected', request.user.id)
return redirect('wagtailadmin_home')

View file

@ -16,7 +16,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from wagtail.wagtailcore.models import Page, Orderable, UserPagePermissionsProxy, get_page_types
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailadmin import tasks
from wagtail.wagtailadmin.utils import send_mail
from .forms import FormBuilder
@ -194,7 +194,7 @@ class AbstractEmailForm(AbstractForm):
if self.to_address:
content = '\n'.join([x[1].label + ': ' + form.data.get(x[0]) for x in form.fields.items()])
tasks.send_email_task.delay(self.subject, content, [self.to_address], self.from_address,)
send_mail(self.subject, content, [self.to_address], self.from_address,)
class Meta: