Commit 65f7b098 by Sarina Canelake

Bulk Email: Add design styling

Switch to using decorators; refactor and cleanup tests.
parent 3b32d421
...@@ -27,6 +27,7 @@ from django_future.csrf import ensure_csrf_cookie ...@@ -27,6 +27,7 @@ from django_future.csrf import ensure_csrf_cookie
from django.utils.http import cookie_date from django.utils.http import cookie_date
from django.utils.http import base36_to_int from django.utils.http import base36_to_int
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from ratelimitbackend.exceptions import RateLimitException from ratelimitbackend.exceptions import RateLimitException
...@@ -68,8 +69,7 @@ Article = namedtuple('Article', 'title url author image deck publication publish ...@@ -68,8 +69,7 @@ Article = namedtuple('Article', 'title url author image deck publication publish
def csrf_token(context): def csrf_token(context):
''' A csrf token that can be included in a form. """A csrf token that can be included in a form."""
'''
csrf_token = context.get('csrf_token', '') csrf_token = context.get('csrf_token', '')
if csrf_token == 'NOTPROVIDED': if csrf_token == 'NOTPROVIDED':
return '' return ''
...@@ -82,12 +82,12 @@ def csrf_token(context): ...@@ -82,12 +82,12 @@ def csrf_token(context):
# This means that it should always return the same thing for anon # This means that it should always return the same thing for anon
# users. (in particular, no switching based on query params allowed) # users. (in particular, no switching based on query params allowed)
def index(request, extra_context={}, user=None): def index(request, extra_context={}, user=None):
''' """
Render the edX main page. Render the edX main page.
extra_context is used to allow immediate display of certain modal windows, eg signup, extra_context is used to allow immediate display of certain modal windows, eg signup,
as used by external_auth. as used by external_auth.
''' """
# The course selection work is done in courseware.courses. # The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
...@@ -411,7 +411,7 @@ def accounts_login(request, error=""): ...@@ -411,7 +411,7 @@ def accounts_login(request, error=""):
# Need different levels of logging # Need different levels of logging
@ensure_csrf_cookie @ensure_csrf_cookie
def login_user(request, error=""): def login_user(request, error=""):
''' AJAX request to log in the user. ''' """AJAX request to log in the user."""
if 'email' not in request.POST or 'password' not in request.POST: if 'email' not in request.POST or 'password' not in request.POST:
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'value': _('There was an error receiving your login information. Please email us.')})) # TODO: User error message 'value': _('There was an error receiving your login information. Please email us.')})) # TODO: User error message
...@@ -494,11 +494,11 @@ def login_user(request, error=""): ...@@ -494,11 +494,11 @@ def login_user(request, error=""):
@ensure_csrf_cookie @ensure_csrf_cookie
def logout_user(request): def logout_user(request):
''' """
HTTP request to log out the user. Redirects to marketing page. HTTP request to log out the user. Redirects to marketing page.
Deletes both the CSRF and sessionid cookies so the marketing Deletes both the CSRF and sessionid cookies so the marketing
site can determine the logged in state of the user site can determine the logged in state of the user
''' """
# We do not log here, because we have a handler registered # We do not log here, because we have a handler registered
# to perform logging on successful logouts. # to perform logging on successful logouts.
logout(request) logout(request)
...@@ -512,8 +512,7 @@ def logout_user(request): ...@@ -512,8 +512,7 @@ def logout_user(request):
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def change_setting(request): def change_setting(request):
''' JSON call to change a profile setting: Right now, location """JSON call to change a profile setting: Right now, location"""
'''
# TODO (vshnayder): location is no longer used # TODO (vshnayder): location is no longer used
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
if 'location' in request.POST: if 'location' in request.POST:
...@@ -581,10 +580,10 @@ def _do_create_account(post_vars): ...@@ -581,10 +580,10 @@ def _do_create_account(post_vars):
@ensure_csrf_cookie @ensure_csrf_cookie
def create_account(request, post_override=None): def create_account(request, post_override=None):
''' """
JSON call to create new edX account. JSON call to create new edX account.
Used by form in signup_modal.html, which is included into navigation.html Used by form in signup_modal.html, which is included into navigation.html
''' """
js = {'success': False} js = {'success': False}
post_vars = post_override if post_override else request.POST post_vars = post_override if post_override else request.POST
...@@ -818,10 +817,10 @@ def begin_exam_registration(request, course_id): ...@@ -818,10 +817,10 @@ def begin_exam_registration(request, course_id):
@ensure_csrf_cookie @ensure_csrf_cookie
def create_exam_registration(request, post_override=None): def create_exam_registration(request, post_override=None):
''' """
JSON call to create a test center exam registration. JSON call to create a test center exam registration.
Called by form in test_center_register.html Called by form in test_center_register.html
''' """
post_vars = post_override if post_override else request.POST post_vars = post_override if post_override else request.POST
# first determine if we need to create a new TestCenterUser, or if we are making any update # first determine if we need to create a new TestCenterUser, or if we are making any update
...@@ -974,8 +973,7 @@ def auto_auth(request): ...@@ -974,8 +973,7 @@ def auto_auth(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def activate_account(request, key): def activate_account(request, key):
''' When link in activation e-mail is clicked """When link in activation e-mail is clicked"""
'''
r = Registration.objects.filter(activation_key=key) r = Registration.objects.filter(activation_key=key)
if len(r) == 1: if len(r) == 1:
user_logged_in = request.user.is_authenticated() user_logged_in = request.user.is_authenticated()
...@@ -1010,7 +1008,7 @@ def activate_account(request, key): ...@@ -1010,7 +1008,7 @@ def activate_account(request, key):
@ensure_csrf_cookie @ensure_csrf_cookie
def password_reset(request): def password_reset(request):
''' Attempts to send a password reset e-mail. ''' """ Attempts to send a password reset e-mail. """
if request.method != "POST": if request.method != "POST":
raise Http404 raise Http404
...@@ -1032,9 +1030,9 @@ def password_reset_confirm_wrapper( ...@@ -1032,9 +1030,9 @@ def password_reset_confirm_wrapper(
uidb36=None, uidb36=None,
token=None, token=None,
): ):
''' A wrapper around django.contrib.auth.views.password_reset_confirm. """ A wrapper around django.contrib.auth.views.password_reset_confirm.
Needed because we want to set the user as active at this step. Needed because we want to set the user as active at this step.
''' """
# cribbed from django.contrib.auth.views.password_reset_confirm # cribbed from django.contrib.auth.views.password_reset_confirm
try: try:
uid_int = base36_to_int(uidb36) uid_int = base36_to_int(uidb36)
...@@ -1076,8 +1074,8 @@ def reactivation_email_for_user(user): ...@@ -1076,8 +1074,8 @@ def reactivation_email_for_user(user):
@ensure_csrf_cookie @ensure_csrf_cookie
def change_email_request(request): def change_email_request(request):
''' AJAX call from the profile page. User wants a new e-mail. """ AJAX call from the profile page. User wants a new e-mail.
''' """
## Make sure it checks for existing e-mail conflicts ## Make sure it checks for existing e-mail conflicts
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise Http404 raise Http404
...@@ -1132,9 +1130,9 @@ def change_email_request(request): ...@@ -1132,9 +1130,9 @@ def change_email_request(request):
@ensure_csrf_cookie @ensure_csrf_cookie
@transaction.commit_manually @transaction.commit_manually
def confirm_email_change(request, key): def confirm_email_change(request, key):
''' User requested a new e-mail. This is called when the activation """ User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update link is clicked. We confirm with the old e-mail, and update
''' """
try: try:
try: try:
pec = PendingEmailChange.objects.get(activation_key=key) pec = PendingEmailChange.objects.get(activation_key=key)
...@@ -1191,7 +1189,7 @@ def confirm_email_change(request, key): ...@@ -1191,7 +1189,7 @@ def confirm_email_change(request, key):
@ensure_csrf_cookie @ensure_csrf_cookie
def change_name_request(request): def change_name_request(request):
''' Log a request for a new name. ''' """ Log a request for a new name. """
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise Http404 raise Http404
...@@ -1215,7 +1213,7 @@ def change_name_request(request): ...@@ -1215,7 +1213,7 @@ def change_name_request(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def pending_name_changes(request): def pending_name_changes(request):
''' Web page which allows staff to approve or reject name changes. ''' """ Web page which allows staff to approve or reject name changes. """
if not request.user.is_staff: if not request.user.is_staff:
raise Http404 raise Http404
...@@ -1231,7 +1229,7 @@ def pending_name_changes(request): ...@@ -1231,7 +1229,7 @@ def pending_name_changes(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def reject_name_change(request): def reject_name_change(request):
''' JSON: Name change process. Course staff clicks 'reject' on a given name change ''' """ JSON: Name change process. Course staff clicks 'reject' on a given name change """
if not request.user.is_staff: if not request.user.is_staff:
raise Http404 raise Http404
...@@ -1269,32 +1267,31 @@ def accept_name_change_by_id(id): ...@@ -1269,32 +1267,31 @@ def accept_name_change_by_id(id):
@ensure_csrf_cookie @ensure_csrf_cookie
def accept_name_change(request): def accept_name_change(request):
''' JSON: Name change process. Course staff clicks 'accept' on a given name change """ JSON: Name change process. Course staff clicks 'accept' on a given name change
We used this during the prototype but now we simply record name changes instead We used this during the prototype but now we simply record name changes instead
of manually approving them. Still keeping this around in case we want to go of manually approving them. Still keeping this around in case we want to go
back to this approval method. back to this approval method.
''' """
if not request.user.is_staff: if not request.user.is_staff:
raise Http404 raise Http404
return accept_name_change_by_id(int(request.POST['id'])) return accept_name_change_by_id(int(request.POST['id']))
@require_POST
@login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def change_email_settings(request): def change_email_settings(request):
"""Modify logged-in user's setting for receiving emails from a course.""" """Modify logged-in user's setting for receiving emails from a course."""
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
user = request.user user = request.user
if not user.is_authenticated():
return HttpResponseForbidden()
course_id = request.POST.get("course_id") course_id = request.POST.get("course_id")
receive_emails = request.POST.get("receive_emails") receive_emails = request.POST.get("receive_emails")
if receive_emails: if receive_emails:
Optout.objects.filter(email=user.email, course_id=course_id).delete() optout_object = Optout.objects.filter(email=user.email, course_id=course_id)
if optout_object:
optout_object.delete()
log.info(u"User {0} ({1}) opted to receive emails from course {2}".format(user.username, user.email, course_id)) log.info(u"User {0} ({1}) opted to receive emails from course {2}".format(user.username, user.email, course_id))
track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard') track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard')
else: else:
......
...@@ -33,7 +33,6 @@ class Migration(SchemaMigration): ...@@ -33,7 +33,6 @@ class Migration(SchemaMigration):
# Adding unique constraint on 'Optout', fields ['email', 'course_id'] # Adding unique constraint on 'Optout', fields ['email', 'course_id']
db.create_unique('bulk_email_optout', ['email', 'course_id']) db.create_unique('bulk_email_optout', ['email', 'course_id'])
def backwards(self, orm): def backwards(self, orm):
# Removing unique constraint on 'Optout', fields ['email', 'course_id'] # Removing unique constraint on 'Optout', fields ['email', 'course_id']
db.delete_unique('bulk_email_optout', ['email', 'course_id']) db.delete_unique('bulk_email_optout', ['email', 'course_id'])
...@@ -44,7 +43,6 @@ class Migration(SchemaMigration): ...@@ -44,7 +43,6 @@ class Migration(SchemaMigration):
# Deleting model 'Optout' # Deleting model 'Optout'
db.delete_table('bulk_email_optout') db.delete_table('bulk_email_optout')
models = { models = {
'auth.group': { 'auth.group': {
'Meta': {'object_name': 'Group'}, 'Meta': {'object_name': 'Group'},
...@@ -102,4 +100,4 @@ class Migration(SchemaMigration): ...@@ -102,4 +100,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['bulk_email'] complete_apps = ['bulk_email']
\ No newline at end of file
...@@ -10,6 +10,7 @@ class Email(models.Model): ...@@ -10,6 +10,7 @@ class Email(models.Model):
Abstract base class for common information for an email. Abstract base class for common information for an email.
""" """
sender = models.ForeignKey(User, default=1, blank=True, null=True) sender = models.ForeignKey(User, default=1, blank=True, null=True)
# The unique hash for this email. Used to quickly look up an email (see `tasks.py`)
hash = models.CharField(max_length=128, db_index=True) hash = models.CharField(max_length=128, db_index=True)
subject = models.CharField(max_length=128, blank=True) subject = models.CharField(max_length=128, blank=True)
html_message = models.TextField(null=True, blank=True) html_message = models.TextField(null=True, blank=True)
...@@ -24,10 +25,20 @@ class CourseEmail(Email, models.Model): ...@@ -24,10 +25,20 @@ class CourseEmail(Email, models.Model):
""" """
Stores information for an email to a course. Stores information for an email to a course.
""" """
TO_OPTIONS = (('myself', 'Myself'), # Three options for sending that we provide from the instructor dashboard:
('staff', 'Staff and instructors'), # * Myself: This sends an email to the staff member that is composing the email.
('all', 'All') #
) # * Staff and instructors: This sends an email to anyone in the staff group and
# anyone in the instructor group
#
# * All: This sends an email to anyone enrolled in the course, with any role
# (student, staff, or instructor)
#
TO_OPTIONS = (
('myself', 'Myself'),
('staff', 'Staff and instructors'),
('all', 'All')
)
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
to = models.CharField(max_length=64, choices=TO_OPTIONS, default='myself') to = models.CharField(max_length=64, choices=TO_OPTIONS, default='myself')
......
...@@ -31,7 +31,7 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_ ...@@ -31,7 +31,7 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_
get the mail, chopping up into batches of settings.EMAILS_PER_TASK size, get the mail, chopping up into batches of settings.EMAILS_PER_TASK size,
and queueing up worker jobs. and queueing up worker jobs.
`to_option` is {'students', 'staff', or 'all'} `to_option` is {'myself', 'staff', or 'all'}
Returns the number of batches (workers) kicked off. Returns the number of batches (workers) kicked off.
""" """
...@@ -49,7 +49,8 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_ ...@@ -49,7 +49,8 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_
if to_option == "myself": if to_option == "myself":
recipient_qset = User.objects.filter(id=user_id).values('profile__name', 'email') recipient_qset = User.objects.filter(id=user_id).values('profile__name', 'email')
else:
elif to_option == "all" or to_option == "staff":
staff_grpname = _course_staff_group_name(course.location) staff_grpname = _course_staff_group_name(course.location)
staff_group, _ = Group.objects.get_or_create(name=staff_grpname) staff_group, _ = Group.objects.get_or_create(name=staff_grpname)
staff_qset = staff_group.user_set.values('profile__name', 'email') staff_qset = staff_group.user_set.values('profile__name', 'email')
...@@ -66,8 +67,12 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_ ...@@ -66,8 +67,12 @@ def delegate_email_batches(hash_for_msg, to_option, course_id, course_url, user_
recipient_qset = recipient_qset | enrollment_qset recipient_qset = recipient_qset | enrollment_qset
recipient_qset = recipient_qset.distinct() recipient_qset = recipient_qset.distinct()
else:
log.error("Unexpected bulk email TO_OPTION found: %s", to_option)
raise Exception("Unexpected bulk email TO_OPTION found: {0}".format(to_option))
recipient_list = list(recipient_qset) recipient_list = list(recipient_qset)
total_num_emails = recipient_qset.count() total_num_emails = len(recipient_list)
num_workers = int(math.ceil(float(total_num_emails) / float(settings.EMAILS_PER_TASK))) num_workers = int(math.ceil(float(total_num_emails) / float(settings.EMAILS_PER_TASK)))
chunk = int(math.ceil(float(total_num_emails) / float(num_workers))) chunk = int(math.ceil(float(total_num_emails) / float(num_workers)))
...@@ -97,7 +102,7 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False ...@@ -97,7 +102,7 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
(plaintext, err_from_stderr) = process.communicate(input=msg.html_message.encode('utf-8')) # use lynx to get plaintext (plaintext, err_from_stderr) = process.communicate(input=msg.html_message.encode('utf-8')) # use lynx to get plaintext
course_title_no_quotes = re.sub(r'"', '', course_title) course_title_no_quotes = re.sub(r'"', '', course_title)
from_addr = '"%s" Course Staff <%s>' % (course_title_no_quotes, settings.DEFAULT_BULK_FROM_EMAIL) from_addr = '"{0}" Course Staff <{1}>'.format(course_title_no_quotes, settings.DEFAULT_BULK_FROM_EMAIL)
if err_from_stderr: if err_from_stderr:
log.info(err_from_stderr) log.info(err_from_stderr)
...@@ -108,20 +113,33 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False ...@@ -108,20 +113,33 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
num_sent = 0 num_sent = 0
num_error = 0 num_error = 0
email_context = {
'name': '',
'email': '',
'course_title': course_title,
'course_url': course_url
}
while to_list: while to_list:
(name, email) = to_list[-1].values() (name, email) = to_list[-1].values()
html_footer = render_to_string('emails/email_footer.html', email_context['name'] = name
{'name': name, email_context['email'] = email
'email': email,
'course_title': course_title, html_footer = render_to_string(
'course_url': course_url}) 'emails/email_footer.html',
plain_footer = render_to_string('emails/email_footer.txt', email_context
{'name': name, )
'email': email, plain_footer = render_to_string(
'course_title': course_title, 'emails/email_footer.txt',
'course_url': course_url}) email_context
)
email_msg = EmailMultiAlternatives(subject, plaintext + plain_footer.encode('utf-8'), from_addr, [email], connection=connection)
email_msg = EmailMultiAlternatives(
subject,
plaintext + plain_footer.encode('utf-8'),
from_addr,
[email],
connection=connection
)
email_msg.attach_alternative(msg.html_message + html_footer.encode('utf-8'), 'text/html') email_msg.attach_alternative(msg.html_message + html_footer.encode('utf-8'), 'text/html')
if throttle or current_task.request.retries > 0: # throttle if we tried a few times and got the rate limiter if throttle or current_task.request.retries > 0: # throttle if we tried a few times and got the rate limiter
...@@ -132,11 +150,12 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False ...@@ -132,11 +150,12 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
log.info('Email with hash ' + hash_for_msg + ' sent to ' + email) log.info('Email with hash ' + hash_for_msg + ' sent to ' + email)
num_sent += 1 num_sent += 1
except SMTPDataError as exc: except SMTPDataError as exc:
#According to SMTP spec, we'll retry error codes in the 4xx range. 5xx range indicates hard failure # According to SMTP spec, we'll retry error codes in the 4xx range. 5xx range indicates hard failure
if exc.smtp_code >= 400 and exc.smtp_code < 500: if exc.smtp_code >= 400 and exc.smtp_code < 500:
raise exc # this will cause the outer handler to catch the exception and retry the entire task # This will cause the outer handler to catch the exception and retry the entire task
raise exc
else: else:
#this will fall through and not retry the message, since it will be popped # This will fall through and not retry the message, since it will be popped
log.warning('Email with hash ' + hash_for_msg + ' not delivered to ' + email + ' due to error: ' + exc.smtp_error) log.warning('Email with hash ' + hash_for_msg + ' not delivered to ' + email + ' due to error: ' + exc.smtp_error)
num_error += 1 num_error += 1
...@@ -146,8 +165,18 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False ...@@ -146,8 +165,18 @@ def course_email(hash_for_msg, to_list, course_title, course_url, throttle=False
return course_email_result(num_sent, num_error) return course_email_result(num_sent, num_error)
except (SMTPDataError, SMTPConnectError, SMTPServerDisconnected) as exc: except (SMTPDataError, SMTPConnectError, SMTPServerDisconnected) as exc:
#error caught here cause the email to be retried. The entire task is actually retried without popping the list # Error caught here cause the email to be retried. The entire task is actually retried without popping the list
raise course_email.retry(arg=[hash_for_msg, to_list, course_title, course_url, current_task.request.retries > 0], exc=exc, countdown=(2 ** current_task.request.retries) * 15) raise course_email.retry(
arg=[
hash_for_msg,
to_list,
course_title,
course_url,
current_task.request.retries > 0
],
exc=exc,
countdown=(2 ** current_task.request.retries) * 15
)
# This string format code is wrapped in this function to allow mocking for a unit test # This string format code is wrapped in this function to allow mocking for a unit test
......
...@@ -4,12 +4,16 @@ Unit tests for student optouts from course email ...@@ -4,12 +4,16 @@ Unit tests for student optouts from course email
import json import json
from django.core import mail from django.core import mail
from django.test.utils import override_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings
from django.test.utils import override_settings
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from mock import patch
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
...@@ -27,6 +31,24 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -27,6 +31,24 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.client.login(username=self.student.username, password="test") self.client.login(username=self.student.username, password="test")
def navigate_to_email_view(self):
"""Navigate to the instructor dash's email view"""
# Pull up email view on instructor dashboard
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.get(url)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
self.assertTrue(email_link in response.content)
# Select the Email view of the instructor dash
session = self.client.session
session['idash_mode'] = 'Email'
session.save()
response = self.client.get(url)
selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>'
self.assertTrue(selected_email_link in response.content)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_optout_course(self): def test_optout_course(self):
""" """
Make sure student does not receive course email after opting out. Make sure student does not receive course email after opting out.
...@@ -36,15 +58,24 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -36,15 +58,24 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.assertEquals(json.loads(response.content), {'success': True}) self.assertEquals(json.loads(response.content), {'success': True})
self.client.logout() self.client.logout()
self.client.login(username=self.instructor.username, password="test") self.client.login(username=self.instructor.username, password="test")
self.navigate_to_email_view()
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'}) test_email = {
'action': 'Send email',
'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
response = self.client.post(url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.") self.assertContains(response, "Your email was successfully queued for sending.")
#assert that self.student.email not in mail.to, outbox should be empty # Assert that self.student.email not in mail.to, outbox should be empty
self.assertEqual(len(mail.outbox), 0) self.assertEqual(len(mail.outbox), 0)
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_optin_course(self): def test_optin_course(self):
""" """
Make sure student receives course email after opting in. Make sure student receives course email after opting in.
...@@ -54,13 +85,22 @@ class TestOptoutCourseEmails(ModuleStoreTestCase): ...@@ -54,13 +85,22 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
self.assertEquals(json.loads(response.content), {'success': True}) self.assertEquals(json.loads(response.content), {'success': True})
self.client.logout() self.client.logout()
self.client.login(username=self.instructor.username, password="test") self.client.login(username=self.instructor.username, password="test")
self.navigate_to_email_view()
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'}) test_email = {
'action': 'Send email',
'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
response = self.client.post(url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.") self.assertContains(response, "Your email was successfully queued for sending.")
#assert that self.student.email in mail.to # Assert that self.student.email in mail.to
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(len(mail.outbox[0].to), 1) self.assertEqual(len(mail.outbox[0].to), 1)
self.assertEquals(mail.outbox[0].to[0], self.student.email) self.assertEquals(mail.outbox[0].to[0], self.student.email)
...@@ -3,52 +3,79 @@ Unit tests for sending course email ...@@ -3,52 +3,79 @@ Unit tests for sending course email
""" """
from django.test.utils import override_settings 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.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory, GroupFactory, CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, GroupFactory, CourseEnrollmentFactory
from django.core import mail
from bulk_email.tasks import delegate_email_batches, course_email from bulk_email.tasks import delegate_email_batches, course_email
from bulk_email.models import CourseEmail from bulk_email.models import CourseEmail
from mock import patch
STAFF_COUNT = 3 STAFF_COUNT = 3
STUDENT_COUNT = 10 STUDENT_COUNT = 10
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestEmail(ModuleStoreTestCase): class TestEmailSendFromDashboard(ModuleStoreTestCase):
""" """
Test that emails send correctly. Test that emails send correctly.
""" """
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def setUp(self): def setUp(self):
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.instructor = UserFactory.create(username="instructor", email="robot+instructor@edx.org") self.instructor = UserFactory.create(username="instructor", email="robot+instructor@edx.org")
#Create instructor group for course # Create instructor group for course
instructor_group = GroupFactory.create(name="instructor_MITx/999/Robot_Super_Course") instructor_group = GroupFactory.create(name="instructor_MITx/999/Robot_Super_Course")
instructor_group.user_set.add(self.instructor) instructor_group.user_set.add(self.instructor)
#create staff # Create staff
self.staff = [UserFactory() for _ in xrange(STAFF_COUNT)] self.staff = [UserFactory() for _ in xrange(STAFF_COUNT)]
staff_group = GroupFactory() staff_group = GroupFactory()
for staff in self.staff: for staff in self.staff:
staff_group.user_set.add(staff) # pylint: disable=E1101 staff_group.user_set.add(staff) # pylint: disable=E1101
#create students # Create students
self.students = [UserFactory() for _ in xrange(STUDENT_COUNT)] self.students = [UserFactory() for _ in xrange(STUDENT_COUNT)]
for student in self.students: for student in self.students:
CourseEnrollmentFactory.create(user=student, course_id=self.course.id) CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
self.client.login(username=self.instructor.username, password="test") self.client.login(username=self.instructor.username, password="test")
# Pull up email view on instructor dashboard
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
response = self.client.get(self.url)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
self.assertTrue(email_link in response.content)
# Select the Email view of the instructor dash
session = self.client.session
session['idash_mode'] = 'Email'
session.save()
response = self.client.get(self.url)
selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>'
self.assertTrue(selected_email_link in response.content)
def test_send_to_self(self): def test_send_to_self(self):
""" """
Make sure email send to myself goes to myself. Make sure email send to myself goes to myself.
""" """
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) # Now we know we have pulled up the instructor dash's email view
response = self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'}) # (in the setUp method), we can test sending an email.
test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
response = self.client.post(self.url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.") self.assertContains(response, "Your email was successfully queued for sending.")
...@@ -61,8 +88,15 @@ class TestEmail(ModuleStoreTestCase): ...@@ -61,8 +88,15 @@ class TestEmail(ModuleStoreTestCase):
""" """
Make sure email send to staff and instructors goes there. Make sure email send to staff and instructors goes there.
""" """
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) # Now we know we have pulled up the instructor dash's email view
response = self.client.post(url, {'action': 'Send email', 'to_option': 'staff', 'subject': 'test subject for staff', 'message': 'test message for subject'}) # (in the setUp method), we can test sending an email.
test_email = {
'action': 'Send email',
'to_option': 'staff',
'subject': 'test subject for staff',
'message': 'test message for subject'
}
response = self.client.post(self.url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.") self.assertContains(response, "Your email was successfully queued for sending.")
...@@ -73,24 +107,35 @@ class TestEmail(ModuleStoreTestCase): ...@@ -73,24 +107,35 @@ class TestEmail(ModuleStoreTestCase):
""" """
Make sure email send to all goes there. Make sure email send to all goes there.
""" """
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) # Now we know we have pulled up the instructor dash's email view
response = self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'}) # (in the setUp method), we can test sending an email.
test_email = {
'action': 'Send email',
'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
response = self.client.post(self.url, test_email)
self.assertContains(response, "Your email was successfully queued for sending.") self.assertContains(response, "Your email was successfully queued for sending.")
self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students)) self.assertEquals(len(mail.outbox), 1 + len(self.staff) + len(self.students))
self.assertItemsEqual([e.to[0] for e in mail.outbox], [self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students]) self.assertItemsEqual([e.to[0] for e in mail.outbox], [self.instructor.email] + [s.email for s in self.staff] + [s.email for s in self.students])
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestEmailSendExceptions(ModuleStoreTestCase):
"""
Test that exceptions are handled correctly.
"""
def test_get_course_exc(self): def test_get_course_exc(self):
""" # Make sure delegate_email_batches handles Http404 exception from get_course_by_id.
Make sure delegate_email_batches handles Http404 exception from get_course_by_id.
"""
with self.assertRaises(Exception): with self.assertRaises(Exception):
delegate_email_batches("_", "_", "blah/blah/blah", "_", "_") delegate_email_batches("_", "_", "blah/blah/blah", "_", "_")
def test_no_course_email_obj(self): def test_no_course_email_obj(self):
""" # Make sure course_email handles CourseEmail.DoesNotExist exception.
Make sure course_email handles CourseEmail.DoesNotExist exception.
"""
with self.assertRaises(CourseEmail.DoesNotExist): with self.assertRaises(CourseEmail.DoesNotExist):
course_email("dummy hash", [], "_", "_", False) course_email("dummy hash", [], "_", "_", False)
...@@ -5,10 +5,12 @@ Unit tests for handling email sending errors ...@@ -5,10 +5,12 @@ Unit tests for handling email sending errors
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory
from bulk_email.tests.smtp_server_thread import FakeSMTPServerThread from bulk_email.tests.smtp_server_thread import FakeSMTPServerThread
from mock import patch from mock import patch
...@@ -17,9 +19,13 @@ from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError ...@@ -17,9 +19,13 @@ from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError
TEST_SMTP_PORT = 1025 TEST_SMTP_PORT = 1025
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend', EMAIL_HOST='localhost', EMAIL_PORT=TEST_SMTP_PORT) @override_settings(
MODULESTORE=TEST_DATA_MONGO_MODULESTORE,
EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend',
EMAIL_HOST='localhost',
EMAIL_PORT=TEST_SMTP_PORT
)
class TestEmailErrors(ModuleStoreTestCase): class TestEmailErrors(ModuleStoreTestCase):
""" """
Test that errors from sending email are handled properly. Test that errors from sending email are handled properly.
""" """
...@@ -32,6 +38,8 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -32,6 +38,8 @@ class TestEmailErrors(ModuleStoreTestCase):
self.smtp_server_thread = FakeSMTPServerThread('localhost', TEST_SMTP_PORT) self.smtp_server_thread = FakeSMTPServerThread('localhost', TEST_SMTP_PORT)
self.smtp_server_thread.start() self.smtp_server_thread.start()
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
def tearDown(self): def tearDown(self):
self.smtp_server_thread.stop() self.smtp_server_thread.stop()
...@@ -40,9 +48,20 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -40,9 +48,20 @@ class TestEmailErrors(ModuleStoreTestCase):
""" """
Test that celery handles transient SMTPDataErrors by retrying. Test that celery handles transient SMTPDataErrors by retrying.
""" """
self.smtp_server_thread.server.set_errtype("DATA", "454 Throttling failure: Daily message quota exceeded.") self.smtp_server_thread.server.set_errtype(
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) "DATA",
self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'}) "454 Throttling failure: Daily message quota exceeded."
)
test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
self.client.post(self.url, test_email)
# Test that we retry upon hitting a 4xx error
self.assertTrue(retry.called) self.assertTrue(retry.called)
(_, kwargs) = retry.call_args (_, kwargs) = retry.call_args
exc = kwargs['exc'] exc = kwargs['exc']
...@@ -54,16 +73,26 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -54,16 +73,26 @@ class TestEmailErrors(ModuleStoreTestCase):
""" """
Test that celery handles permanent SMTPDataErrors by failing and not retrying. Test that celery handles permanent SMTPDataErrors by failing and not retrying.
""" """
self.smtp_server_thread.server.set_errtype("DATA", "554 Message rejected: Email address is not verified.") self.smtp_server_thread.server.set_errtype(
"DATA",
"554 Message rejected: Email address is not verified."
)
students = [UserFactory() for _ in xrange(settings.EMAILS_PER_TASK)] students = [UserFactory() for _ in xrange(settings.EMAILS_PER_TASK)]
for student in students: for student in students:
CourseEnrollmentFactory.create(user=student, course_id=self.course.id) CourseEnrollmentFactory.create(user=student, course_id=self.course.id)
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) test_email = {
self.client.post(url, {'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all'}) 'action': 'Send email',
self.assertFalse(retry.called) 'to_option': 'all',
'subject': 'test subject for all',
'message': 'test message for all'
}
self.client.post(self.url, test_email)
#test that after the failed email, the rest send successfully # We shouldn't retry when hitting a 5xx error
self.assertFalse(retry.called)
# Test that after the rejected email, the rest still successfully send
((sent, fail), _) = result.call_args ((sent, fail), _) = result.call_args
self.assertEquals(fail, 1) self.assertEquals(fail, 1)
self.assertEquals(sent, settings.EMAILS_PER_TASK - 1) self.assertEquals(sent, settings.EMAILS_PER_TASK - 1)
...@@ -73,9 +102,18 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -73,9 +102,18 @@ class TestEmailErrors(ModuleStoreTestCase):
""" """
Test that celery handles SMTPServerDisconnected by retrying. Test that celery handles SMTPServerDisconnected by retrying.
""" """
self.smtp_server_thread.server.set_errtype("DISCONN", "Server disconnected, please try again later.") self.smtp_server_thread.server.set_errtype(
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) "DISCONN",
self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'}) "Server disconnected, please try again later."
)
test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
self.client.post(self.url, test_email)
self.assertTrue(retry.called) self.assertTrue(retry.called)
(_, kwargs) = retry.call_args (_, kwargs) = retry.call_args
exc = kwargs['exc'] exc = kwargs['exc']
...@@ -86,10 +124,17 @@ class TestEmailErrors(ModuleStoreTestCase): ...@@ -86,10 +124,17 @@ class TestEmailErrors(ModuleStoreTestCase):
""" """
Test that celery handles SMTPConnectError by retrying. Test that celery handles SMTPConnectError by retrying.
""" """
#SMTP reply is already specified in fake SMTP Channel created # SMTP reply is already specified in fake SMTP Channel created
self.smtp_server_thread.server.set_errtype("CONN") self.smtp_server_thread.server.set_errtype("CONN")
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
self.client.post(url, {'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself'}) test_email = {
'action': 'Send email',
'to_option': 'myself',
'subject': 'test subject for myself',
'message': 'test message for myself'
}
self.client.post(self.url, test_email)
self.assertTrue(retry.called) self.assertTrue(retry.called)
(_, kwargs) = retry.call_args (_, kwargs) = retry.call_args
exc = kwargs['exc'] exc = kwargs['exc']
......
...@@ -26,18 +26,23 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase): ...@@ -26,18 +26,23 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
instructor = AdminFactory.create() instructor = AdminFactory.create()
self.client.login(username=instructor.username, password="test") self.client.login(username=instructor.username, password="test")
# URL for instructor dash
self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
# URL for email view
self.email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True}) @patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True})
def test_email_flag_true(self): def test_email_flag_true(self):
response = self.client.get(reverse('instructor_dashboard', response = self.client.get(self.url)
kwargs={'course_id': self.course.id})) self.assertTrue(self.email_link in response.content)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
self.assertTrue(email_link in response.content)
# Select the Email view of the instructor dash
session = self.client.session session = self.client.session
session['idash_mode'] = 'Email' session['idash_mode'] = 'Email'
session.save() session.save()
response = self.client.get(reverse('instructor_dashboard', response = self.client.get(self.url)
kwargs={'course_id': self.course.id}))
# Ensure we've selected the view properly and that the send_to field is present.
selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>' selected_email_link = '<a href="#" onclick="goto(\'Email\')" class="selectedmode">Email</a>'
self.assertTrue(selected_email_link in response.content) self.assertTrue(selected_email_link in response.content)
send_to_label = '<label for="id_to">Send to:</label>' send_to_label = '<label for="id_to">Send to:</label>'
...@@ -45,7 +50,5 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase): ...@@ -45,7 +50,5 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
@patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False}) @patch.dict(settings.MITX_FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False})
def test_email_flag_false(self): def test_email_flag_false(self):
response = self.client.get(reverse('instructor_dashboard', response = self.client.get(self.url)
kwargs={'course_id': self.course.id})) self.assertFalse(self.email_link in response.content)
email_link = '<a href="#" onclick="goto(\'Email\')" class="None">Email</a>'
self.assertFalse(email_link in response.content)
...@@ -83,6 +83,7 @@ def instructor_dashboard(request, course_id): ...@@ -83,6 +83,7 @@ def instructor_dashboard(request, course_id):
forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR) forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR)
msg = '' msg = ''
email_msg = ''
to_option = None to_option = None
subject = None subject = None
html_message = '' html_message = ''
...@@ -717,10 +718,9 @@ def instructor_dashboard(request, course_id): ...@@ -717,10 +718,9 @@ def instructor_dashboard(request, course_id):
tasks.delegate_email_batches.delay(email.hash, email.to, course_id, course_url, request.user.id) tasks.delegate_email_batches.delay(email.hash, email.to, course_id, course_url, request.user.id)
if to_option == "all": if to_option == "all":
msg = "<font color='green'>Your email was successfully queued for sending. Please note that for large public classe\ email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending. Please note that for large public classes (~10k), it may take 1-2 hours to send all emails.</p></div>'
s (~10k), it may take 1-2 hours to send all emails.</font>"
else: else:
msg = "<font color='green'>Your email was successfully queued for sending.</font>" email_msg = '<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending.</p></div>'
#---------------------------------------- #----------------------------------------
# psychometrics # psychometrics
...@@ -809,6 +809,7 @@ s (~10k), it may take 1-2 hours to send all emails.</font>" ...@@ -809,6 +809,7 @@ s (~10k), it may take 1-2 hours to send all emails.</font>"
'datatable': datatable, 'datatable': datatable,
'course_stats': course_stats, 'course_stats': course_stats,
'msg': msg, 'msg': msg,
'email_msg': email_msg,
'modeflag': {idash_mode: 'selectedmode'}, 'modeflag': {idash_mode: 'selectedmode'},
'to_option': to_option, # email 'to_option': to_option, # email
'subject': subject, # email 'subject': subject, # email
......
...@@ -17,5 +17,56 @@ ...@@ -17,5 +17,56 @@
@extend .top-header; @extend .top-header;
} }
} }
// form fields
.list-fields {
list-style: none;
margin: 0;
padding: 0;
.field {
margin-bottom: 20px;
padding: 0;
&:last-child {
margin-bottom: 0;
}
}
}
// system feedback - messages
.msg {
border-radius: 1px;
padding: 10px 15px;
margin-bottom: 20px;
.copy {
font-weight: 600;
}
}
.msg-confirm {
border-top: 2px solid green;
background: tint(green,90%);
.copy {
color: green;
}
}
.list-advice {
list-style: none;
padding: 0;
margin: 20px 0;
.item {
font-weight: 600;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
} }
...@@ -443,38 +443,51 @@ function goto( mode) ...@@ -443,38 +443,51 @@ function goto( mode)
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Email'): %if modeflag.get('Email'):
<p> %if email_msg:
<label for="id_to">Send to:</label> <p></p><p>${email_msg}</p>
<select id="id_to" name="to_option">
<option value="myself">Myself</option>
%if to_option == "staff":
<option value="staff" selected="selected">Staff and instructors</option>
%else:
<option value="staff">Staff and instructors</option>
%endif
%if to_option == "all":
<option value="all" selected="selected">All (students, staff and instructors)</option>
%else:
<option value="all">All (students, staff and instructors)</option>
%endif
</select>
<label for="id_subject">Subject: </label>
%if subject:
<input type="text" id="id_subject" name="subject" maxlength="100" size="75" value="${subject}">
%else:
<input type="text" id="id_subject" name="subject" maxlength="100" size="75">
%endif %endif
<label>Message:</label>
<div class="email-editor"> <ul class="list-fields">
${editor} <li class="field">
</div> <label for="id_to">${_("Send to:")}</label>
<input type="hidden" name="message" value=""> <select id="id_to" name="to_option">
</p> <option value="myself">${_("Myself")}</option>
%if to_option == "staff":
<option value="staff" selected="selected">${_("Staff and instructors")}</option>
%else:
<option value="staff">${_("Staff and instructors")}</option>
%endif
%if to_option == "all":
<option value="all" selected="selected">${_("All (students, staff and instructors)")}</option>
%else:
<option value="all">${_("All (students, staff and instructors)")}</option>
%endif
</select>
</li>
<li class="field">
<label for="id_subject">${_("Subject: ")}</label>
%if subject:
<input type="text" id="id_subject" name="subject" maxlength="100" size="75" value="${subject}">
%else:
<input type="text" id="id_subject" name="subject" maxlength="100" size="75">
%endif
</li>
<li class="field">
<label>Message:</label>
<div class="email-editor">
${editor}
</div>
<input type="hidden" name="message" value="">
</li>
</ul>
<div class="submit-email-action"> <div class="submit-email-action">
Please try not to email students more than once a day. Important things to consider before sending: ${_("Please try not to email students more than once a day. Important things to consider before sending:")}
<ul> <ul class="list-advice">
<li>Have you read over the email to make sure it says everything you want to say?</li> <li class="item">${_("Have you read over the email to make sure it says everything you want to say?")}</li>
<li>Have you sent the email to yourself first to make sure you're happy with how it's displayed?</li> <li class="item">${_("Have you sent the email to yourself first to make sure you're happy with how it's displayed?")}</li>
</ul> </ul>
<input type="submit" name="action" value="Send email"> <input type="submit" name="action" value="Send email">
</div> </div>
...@@ -520,7 +533,7 @@ function goto( mode) ...@@ -520,7 +533,7 @@ function goto( mode)
%if analytics_results.get("StudentsDropoffPerDay"): %if analytics_results.get("StudentsDropoffPerDay"):
<p> <p>
${_("Student activity day by day")} ${_("Student activity day by day")}
(${analytics_results["StudentsDropoffPerDay"]['time']}) (${analytics_results["StudentsDropoffPerDay"]['time']})
</p> </p>
<div> <div>
......
...@@ -306,7 +306,7 @@ ...@@ -306,7 +306,7 @@
% endif % endif
% endif % endif
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">${_('Unregister')}</a> <a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">${_('Unregister')}</a>
<a href="#email-settings-modal" class="email-settings" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" data-optout="${course.id in course_optouts}">Email Settings</a> <a href="#email-settings-modal" class="email-settings" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" data-optout="${course.id in course_optouts}">${_('Email Settings')}</a>
</section> </section>
</article> </article>
...@@ -343,13 +343,13 @@ ...@@ -343,13 +343,13 @@
<section id="email-settings-modal" class="modal"> <section id="email-settings-modal" class="modal">
<div class="inner-wrapper"> <div class="inner-wrapper">
<header> <header>
<h2>Email Settings for <span id="email_settings_course_number"></span></h2> <h2>${_('Email Settings for {course_number}').format(course_number='<span id="email_settings_course_number"></span>')}</h2>
<hr/> <hr/>
</header> </header>
<form id="email_settings_form" method="post"> <form id="email_settings_form" method="post">
<input name="course_id" id="email_settings_course_id" type="hidden" /> <input name="course_id" id="email_settings_course_id" type="hidden" />
<label>Receive course emails <input type="checkbox" id="receive_emails" name="receive_emails" /></label> <label>${_("Receive course emails")} <input type="checkbox" id="receive_emails" name="receive_emails" /></label>
<div class="submit"> <div class="submit">
<input type="submit" id="submit" value="Save Settings" /> <input type="submit" id="submit" value="Save Settings" />
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment