diff --git a/lms/djangoapps/bulk_email/admin.py b/lms/djangoapps/bulk_email/admin.py
index 3b40290f5d3c..9d6926c91642 100644
--- a/lms/djangoapps/bulk_email/admin.py
+++ b/lms/djangoapps/bulk_email/admin.py
@@ -3,7 +3,8 @@
"""
from django.contrib import admin
-from bulk_email.models import CourseEmail, Optout
+from bulk_email.models import CourseEmail, Optout, CourseEmailTemplate
+from bulk_email.forms import CourseEmailTemplateForm
class CourseEmailAdmin(admin.ModelAdmin):
@@ -16,5 +17,33 @@ class OptoutAdmin(admin.ModelAdmin):
list_display = ('user', 'course_id')
+class CourseEmailTemplateAdmin(admin.ModelAdmin):
+ form = CourseEmailTemplateForm
+ fieldsets = (
+ (None, {
+ # make the HTML template display above the plain template:
+ 'fields': ('html_template', 'plain_template'),
+ 'description': '''
+Enter template to be used by course staff when sending emails to enrolled students.
+
+The HTML template is for HTML email, and may contain HTML markup. The plain template is
+for plaintext email. Both templates should contain the string '{{message_body}}' (with
+two curly braces on each side), to indicate where the email text is to be inserted.
+'''
+ }),
+ )
+ # Turn off the action bar (we have no bulk actions)
+ actions = None
+
+ def has_add_permission(self, request):
+ """Disables the ability to add new templates, as we want to maintain a Singleton."""
+ return False
+
+ def has_delete_permission(self, request, obj=None):
+ """Disables the ability to remove existing templates, as we want to maintain a Singleton."""
+ return False
+
+
admin.site.register(CourseEmail, CourseEmailAdmin)
admin.site.register(Optout, OptoutAdmin)
+admin.site.register(CourseEmailTemplate, CourseEmailTemplateAdmin)
diff --git a/lms/djangoapps/bulk_email/fixtures/course_email_template.json b/lms/djangoapps/bulk_email/fixtures/course_email_template.json
new file mode 100644
index 000000000000..427dfbb8aab4
--- /dev/null
+++ b/lms/djangoapps/bulk_email/fixtures/course_email_template.json
@@ -0,0 +1,10 @@
+[
+ {
+ "pk": 1,
+ "model": "bulk_email.courseemailtemplate",
+ "fields": {
+ "plain_template": "{{message_body}}\r\n----\r\nThis email was automatically sent from {platform_name}.\r\nYou are receiving this email at address {email} because you are enrolled in {course_title}\r\n(URL: {course_url} ).\r\nTo stop receiving email like this, update your account settings at {account_settings_url}.\r\n",
+ "html_template": "{{message_body}}\r\n
\r\n----
\r\nThis email was automatically sent from {platform_name}.
\r\nYou are receiving this email at address {email} because you are enrolled in {course_title}.
\r\nTo stop receiving email like this, update your course email settings here.
\r\n"
+ }
+ }
+]
diff --git a/lms/djangoapps/bulk_email/forms.py b/lms/djangoapps/bulk_email/forms.py
new file mode 100644
index 000000000000..2ccdd72d1668
--- /dev/null
+++ b/lms/djangoapps/bulk_email/forms.py
@@ -0,0 +1,42 @@
+import logging
+
+from django import forms
+from django.core.exceptions import ValidationError
+
+from bulk_email.models import CourseEmailTemplate, COURSE_EMAIL_MESSAGE_BODY_TAG
+
+log = logging.getLogger(__name__)
+
+
+class CourseEmailTemplateForm(forms.ModelForm):
+ """Form providing validation of CourseEmail templates."""
+
+ class Meta:
+ model = CourseEmailTemplate
+
+ def _validate_template(self, template):
+ """Check the template for required tags."""
+ index = template.find(COURSE_EMAIL_MESSAGE_BODY_TAG)
+ if index < 0:
+ msg = 'Missing tag: "{}"'.format(COURSE_EMAIL_MESSAGE_BODY_TAG)
+ log.warning(msg)
+ raise ValidationError(msg)
+ if template.find(COURSE_EMAIL_MESSAGE_BODY_TAG, index + 1) >= 0:
+ msg = 'Multiple instances of tag: "{}"'.format(COURSE_EMAIL_MESSAGE_BODY_TAG)
+ log.warning(msg)
+ raise ValidationError(msg)
+ # TODO: add more validation here, including the set of known tags
+ # for which values will be supplied. (Email will fail if the template
+ # uses tags for which values are not supplied.)
+
+ def clean_html_template(self):
+ """Validate the HTML template."""
+ template = self.cleaned_data["html_template"]
+ self._validate_template(template)
+ return template
+
+ def clean_plain_template(self):
+ """Validate the plaintext template."""
+ template = self.cleaned_data["plain_template"]
+ self._validate_template(template)
+ return template
diff --git a/lms/djangoapps/bulk_email/migrations/0006_add_course_email_template.py b/lms/djangoapps/bulk_email/migrations/0006_add_course_email_template.py
new file mode 100644
index 000000000000..e41234fcff3a
--- /dev/null
+++ b/lms/djangoapps/bulk_email/migrations/0006_add_course_email_template.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Rename 'to' field to 'to_option'
+ db.rename_column('bulk_email_courseemail', 'to', 'to_option')
+
+ # Adding model 'CourseEmailTemplate'
+ db.create_table('bulk_email_courseemailtemplate', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('html_template', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ('plain_template', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ))
+ db.send_create_signal('bulk_email', ['CourseEmailTemplate'])
+
+ def backwards(self, orm):
+ # Rename 'to_option' field back to 'to'
+ db.rename_column('bulk_email_courseemail', 'to_option', 'to')
+
+ # Deleting model 'CourseEmailTemplate'
+ db.delete_table('bulk_email_courseemailtemplate')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'bulk_email.courseemail': {
+ 'Meta': {'object_name': 'CourseEmail'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'html_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'sender': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'slug': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'subject': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
+ 'text_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'to_option': ('django.db.models.fields.CharField', [], {'default': "'myself'", 'max_length': '64'})
+ },
+ 'bulk_email.courseemailtemplate': {
+ 'Meta': {'object_name': 'CourseEmailTemplate'},
+ 'html_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'plain_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'bulk_email.optout': {
+ 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'Optout'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['bulk_email']
diff --git a/lms/djangoapps/bulk_email/migrations/0007_load_course_email_template.py b/lms/djangoapps/bulk_email/migrations/0007_load_course_email_template.py
new file mode 100644
index 000000000000..7ccaaf07f9bd
--- /dev/null
+++ b/lms/djangoapps/bulk_email/migrations/0007_load_course_email_template.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+from south.v2 import DataMigration
+
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Load data from fixture."
+ from django.core.management import call_command
+ call_command("loaddata", "course_email_template.json")
+
+ def backwards(self, orm):
+ "Perform a no-op to go backwards."
+ pass
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'bulk_email.courseemail': {
+ 'Meta': {'object_name': 'CourseEmail'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'html_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'sender': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'slug': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
+ 'subject': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
+ 'text_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'to_option': ('django.db.models.fields.CharField', [], {'default': "'myself'", 'max_length': '64'})
+ },
+ 'bulk_email.courseemailtemplate': {
+ 'Meta': {'object_name': 'CourseEmailTemplate'},
+ 'html_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'plain_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'bulk_email.optout': {
+ 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'Optout'},
+ 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['bulk_email']
+ symmetrical = True
diff --git a/lms/djangoapps/bulk_email/models.py b/lms/djangoapps/bulk_email/models.py
index 72c9569cc1ec..c7937fab5c6d 100644
--- a/lms/djangoapps/bulk_email/models.py
+++ b/lms/djangoapps/bulk_email/models.py
@@ -10,13 +10,13 @@
2. ./manage.py lms schemamigration bulk_email --auto description_of_your_change
3. Add the migration file created in edx-platform/lms/djangoapps/bulk_email/migrations/
-
-ASSUMPTIONS: modules have unique IDs, even across different module_types
-
"""
+import logging
from django.db import models
from django.contrib.auth.models import User
+log = logging.getLogger(__name__)
+
class Email(models.Model):
"""
@@ -33,6 +33,10 @@ class Email(models.Model):
class Meta: # pylint: disable=C0111
abstract = True
+SEND_TO_MYSELF = 'myself'
+SEND_TO_STAFF = 'staff'
+SEND_TO_ALL = 'all'
+
class CourseEmail(Email, models.Model):
"""
@@ -48,12 +52,12 @@ class CourseEmail(Email, models.Model):
# (student, staff, or instructor)
#
TO_OPTIONS = (
- ('myself', 'Myself'),
- ('staff', 'Staff and instructors'),
- ('all', 'All')
+ (SEND_TO_MYSELF, 'Myself'),
+ (SEND_TO_STAFF, 'Staff and instructors'),
+ (SEND_TO_ALL, 'All')
)
course_id = models.CharField(max_length=255, db_index=True)
- to_option = models.CharField(max_length=64, choices=TO_OPTIONS, default='myself')
+ to_option = models.CharField(max_length=64, choices=TO_OPTIONS, default=SEND_TO_MYSELF)
def __unicode__(self):
return self.subject
@@ -68,3 +72,82 @@ class Optout(models.Model):
class Meta: # pylint: disable=C0111
unique_together = ('user', 'course_id')
+
+
+# Defines the tag that must appear in a template, to indicate
+# the location where the email message body is to be inserted.
+COURSE_EMAIL_MESSAGE_BODY_TAG = '{{message_body}}'
+
+
+class CourseEmailTemplate(models.Model):
+ """
+ Stores templates for all emails to a course to use.
+
+ This is expected to be a singleton, to be shared across all courses.
+ Initialization takes place in a migration that in turn loads a fixture.
+ The admin console interface disables add and delete operations.
+ Validation is handled in the CourseEmailTemplateForm class.
+ """
+ html_template = models.TextField(null=True, blank=True)
+ plain_template = models.TextField(null=True, blank=True)
+
+ @staticmethod
+ def get_template():
+ """
+ Fetch the current template
+
+ If one isn't stored, an exception is thrown.
+ """
+ return CourseEmailTemplate.objects.get()
+
+ @staticmethod
+ def _render(format_string, message_body, context):
+ """
+ Create a text message using a template, message body and context.
+
+ Convert message body (`message_body`) into an email message
+ using the provided template. The template is a format string,
+ which is rendered using format() with the provided `context`
+ dict. Output is encoded as UTF-8 by default.
+
+ This doesn't insert user's text into template, until such time we can support
+ proper error handling due to errors in the message body (e.g. due to
+ the use of curly braces).
+
+ Instead, for now, we insert the message body *after* the substitutions
+ have been performed, so that anything in the message body that might
+ interfere will be innocently returned as-is.
+
+ Output is returned as a unicode string. It is not encoded as utf-8.
+ Such encoding is left to the email code, which will use the value
+ of settings.DEFAULT_CHARSET to encode the message.
+ """
+ # If we wanted to support substitution, we'd call:
+ # format_string = format_string.replace(COURSE_EMAIL_MESSAGE_BODY_TAG, message_body)
+ result = format_string.format(**context)
+ # Note that the body tag in the template will now have been
+ # "formatted", so we need to do the same to the tag being
+ # searched for.
+ message_body_tag = COURSE_EMAIL_MESSAGE_BODY_TAG.format()
+ result = result.replace(message_body_tag, message_body, 1)
+
+ # finally, return the result, without converting to an encoded byte array.
+ return result
+
+ def render_plaintext(self, plaintext, context):
+ """
+ Create plain text message.
+
+ Convert plain text body (`plaintext`) into plaintext email message using the
+ stored plain template and the provided `context` dict.
+ """
+ return CourseEmailTemplate._render(self.plain_template, plaintext, context)
+
+ def render_htmltext(self, htmltext, context):
+ """
+ Create HTML text message.
+
+ Convert HTML text body (`htmltext`) into HTML email message using the
+ stored HTML template and the provided `context` dict.
+ """
+ return CourseEmailTemplate._render(self.html_template, htmltext, context)
diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py
index 7afd286570dd..a0001d37fd77 100644
--- a/lms/djangoapps/bulk_email/tasks.py
+++ b/lms/djangoapps/bulk_email/tasks.py
@@ -15,11 +15,14 @@
from django.http import Http404
from celery import task, current_task
from celery.utils.log import get_task_logger
+from django.core.urlresolvers import reverse
-from bulk_email.models import CourseEmail, Optout
+from bulk_email.models import (
+ CourseEmail, Optout, CourseEmailTemplate,
+ SEND_TO_MYSELF, SEND_TO_STAFF, SEND_TO_ALL,
+)
from courseware.access import _course_staff_group_name, _course_instructor_group_name
from courseware.courses import get_course_by_id
-from mitxmako.shortcuts import render_to_string
log = get_task_logger(__name__)
@@ -47,9 +50,9 @@ def delegate_email_batches(email_id, to_option, course_id, course_url, user_id):
log.warning("Failed to get CourseEmail with id %s, retry %d", email_id, current_task.request.retries)
raise delegate_email_batches.retry(arg=[email_id, to_option, course_id, course_url, user_id], exc=exc)
- if to_option == "myself":
+ if to_option == SEND_TO_MYSELF:
recipient_qset = User.objects.filter(id=user_id)
- elif to_option == "all" or to_option == "staff":
+ elif to_option == SEND_TO_ALL or to_option == SEND_TO_STAFF:
staff_grpname = _course_staff_group_name(course.location)
staff_group, _ = Group.objects.get_or_create(name=staff_grpname)
staff_qset = staff_group.user_set.all()
@@ -58,7 +61,7 @@ def delegate_email_batches(email_id, to_option, course_id, course_url, user_id):
instructor_qset = instructor_group.user_set.all()
recipient_qset = staff_qset | instructor_qset
- if to_option == "all":
+ if to_option == SEND_TO_ALL:
enrollment_qset = User.objects.filter(courseenrollment__course_id=course_id,
courseenrollment__is_active=True)
recipient_qset = recipient_qset | enrollment_qset
@@ -72,7 +75,7 @@ def delegate_email_batches(email_id, to_option, course_id, course_url, user_id):
num_queries = int(math.ceil(float(total_num_emails) / float(settings.EMAILS_PER_QUERY)))
last_pk = recipient_qset[0].pk - 1
num_workers = 0
- for j in range(num_queries):
+ for _ in range(num_queries):
recipient_sublist = list(recipient_qset.order_by('pk').filter(pk__gt=last_pk)
.values('profile__name', 'email', 'pk')[:settings.EMAILS_PER_QUERY])
last_pk = recipient_sublist[-1]['pk']
@@ -95,7 +98,6 @@ def course_email(email_id, to_list, course_title, course_url, throttle=False):
being the only "to". Emails are sent multipart, in both plain
text and html.
"""
-
try:
msg = CourseEmail.objects.get(id=email_id)
except CourseEmail.DoesNotExist as exc:
@@ -116,41 +118,43 @@ def course_email(email_id, to_list, course_title, course_url, throttle=False):
course_title_no_quotes = re.sub(r'"', '', course_title)
from_addr = '"{0}" Course Staff <{1}>'.format(course_title_no_quotes, settings.DEFAULT_BULK_FROM_EMAIL)
+ course_email_template = CourseEmailTemplate.get_template()
+
try:
connection = get_connection()
connection.open()
num_sent = 0
num_error = 0
+ # define context values to use in all course emails:
email_context = {
'name': '',
'email': '',
'course_title': course_title,
- 'course_url': course_url
+ 'course_url': course_url,
+ 'account_settings_url': 'https://{}{}'.format(settings.SITE_NAME, reverse('dashboard')),
+ 'platform_name': settings.PLATFORM_NAME,
}
while to_list:
+ # update context with user-specific values:
email = to_list[-1]['email']
email_context['email'] = email
email_context['name'] = to_list[-1]['profile__name']
- html_footer = render_to_string(
- 'emails/email_footer.html',
- email_context
- )
+ # construct message content using templates and context:
+ plaintext_msg = course_email_template.render_plaintext(msg.text_message, email_context)
+ html_msg = course_email_template.render_htmltext(msg.html_message, email_context)
- plain_footer = render_to_string(
- 'emails/email_footer.txt',
- email_context
- )
+ # create email:
email_msg = EmailMultiAlternatives(
subject,
- msg.text_message + plain_footer.encode('utf-8'),
+ plaintext_msg,
from_addr,
[email],
connection=connection
)
- email_msg.attach_alternative(msg.html_message + html_footer.encode('utf-8'), 'text/html')
+ email_msg.attach_alternative(html_msg, 'text/html')
# Throttle if we tried a few times and got the rate limiter
if throttle or current_task.request.retries > 0:
diff --git a/lms/djangoapps/bulk_email/tests/test_course_optout.py b/lms/djangoapps/bulk_email/tests/test_course_optout.py
index 18f04cebc149..0adf11952701 100644
--- a/lms/djangoapps/bulk_email/tests/test_course_optout.py
+++ b/lms/djangoapps/bulk_email/tests/test_course_optout.py
@@ -4,6 +4,7 @@
import json
from django.core import mail
+from django.core.management import call_command
from django.core.urlresolvers import reverse
from django.conf import settings
from django.test.utils import override_settings
@@ -30,6 +31,9 @@ def setUp(self):
self.student = UserFactory.create()
CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id)
+ # load initial content (since we don't run migrations as part of tests):
+ call_command("loaddata", "course_email_template.json")
+
self.client.login(username=self.student.username, password="test")
def tearDown(self):
diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py
index b0cf8dc06e2d..86bcba7100cc 100644
--- a/lms/djangoapps/bulk_email/tests/test_email.py
+++ b/lms/djangoapps/bulk_email/tests/test_email.py
@@ -2,10 +2,11 @@
"""
Unit tests for sending course email
"""
-from django.test.utils import override_settings
from django.conf import settings
from django.core import mail
from django.core.urlresolvers import reverse
+from django.core.management import call_command
+from django.test.utils import override_settings
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, GroupFactory, CourseEnrollmentFactory
@@ -63,6 +64,9 @@ def setUp(self):
for student in self.students:
CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
+ # load initial content (since we don't run migrations as part of tests):
+ call_command("loaddata", "course_email_template.json")
+
self.client.login(username=self.instructor.username, password="test")
# Pull up email view on instructor dashboard
@@ -208,10 +212,8 @@ def test_unicode_message_send_to_all(self):
[self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students]
)
- self.assertIn(
- uni_message,
- mail.outbox[0].body
- )
+ message_body = mail.outbox[0].body
+ self.assertIn(uni_message, message_body)
def test_unicode_students_send_to_all(self):
"""
diff --git a/lms/djangoapps/bulk_email/tests/test_err_handling.py b/lms/djangoapps/bulk_email/tests/test_err_handling.py
index 606a0bef8888..e8874ea18eb9 100644
--- a/lms/djangoapps/bulk_email/tests/test_err_handling.py
+++ b/lms/djangoapps/bulk_email/tests/test_err_handling.py
@@ -4,6 +4,7 @@
from django.test.utils import override_settings
from django.conf import settings
+from django.core.management import call_command
from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
@@ -35,6 +36,9 @@ def setUp(self):
instructor = AdminFactory.create()
self.client.login(username=instructor.username, password="test")
+ # load initial content (since we don't run migrations as part of tests):
+ call_command("loaddata", "course_email_template.json")
+
self.smtp_server_thread = FakeSMTPServerThread('localhost', TEST_SMTP_PORT)
self.smtp_server_thread.start()