diff --git a/lms/djangoapps/bulk_email/__init__.py b/lms/djangoapps/bulk_email/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/bulk_email/models.py b/lms/djangoapps/bulk_email/models.py new file mode 100644 index 000000000000..fb2536137b11 --- /dev/null +++ b/lms/djangoapps/bulk_email/models.py @@ -0,0 +1,19 @@ +from django.db import models +from django.contrib.auth.models import User + +class Email(models.Model): + sender = models.ForeignKey(User, default=1, blank=True, null=True) + hash = models.CharField(max_length=128, db_index=True) + subject = models.CharField(max_length=128, blank=True) + html_message = models.TextField(null=True, blank=True) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + class Meta: + abstract = True + +class CourseEmail(Email, models.Model): + course_id = models.CharField(max_length=255, db_index=True) + to = models.CharField(max_length=64) + + def __unicode__(self): + return self.subject diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py new file mode 100644 index 000000000000..157d59ba0fbc --- /dev/null +++ b/lms/djangoapps/bulk_email/tasks.py @@ -0,0 +1,53 @@ +from celery import task +from django.conf import settings +from django.contrib.auth.models import User +from bulk_email.models import * + +import math + +EMAILS_PER_WORKER=getattr(settings, 'EMAILS_PER_WORKER', 10) + +@task() +def delegate_emails(hash_for_msg, recipient, course): + ''' + Delegates emails by querying for the list of recipients who should + get the mail, chopping up into batches of EMAILS_PER_WORKER size, + and queueing up worker jobs. + + Recipient is {'students', 'staff', or 'all'} + + Returns the number of batches (workers) kicked off. + ''' + + recipient_qset = User.objects.all() + if recipient == "students": + #get student list + pass + elif recipient == "staff": + #get staff list + pass + else: + #everyone + pass + recipient_list = list(recipient_qset) + + total_num_emails = recipient_qset.count() + num_workers=int(math.ceil(float(total_num_emails)/float(EMAILS_PER_WORKER))) + chunk=int(math.ceil(float(total_num_emails)/float(num_workers))) + + for i in range(num_workers): + to_list=recipient_list[i*chunk:i*chunk+chunk] + course_email.delay(hash_for_msg, to_list, False, course) + return num_workers + + +@task(default_retry_delay=15, max_retries=5) +def course_email(hash_for_msg, to_list, course, throttle=False): + """ + Takes a subject and an html formatted email and sends it from + sender to all addresses in the to_list, with each recipient + being the only "to". Emails are sent multipart, in both plain + text and html. + """ + + msg = CourseEmail.objects.get(hash=hash_for_msg) diff --git a/lms/djangoapps/bulk_email/tests/__init__.py b/lms/djangoapps/bulk_email/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/bulk_email/tests/tests.py b/lms/djangoapps/bulk_email/tests/tests.py new file mode 100644 index 000000000000..396a9d50633d --- /dev/null +++ b/lms/djangoapps/bulk_email/tests/tests.py @@ -0,0 +1,74 @@ +""" +Unit tests for email feature in instructor dashboard + +Based on (and depends on) unit tests for courseware. +""" + +from django.test.utils import override_settings + +# Need access to internal func to put users in the right group +from django.contrib.auth.models import Group + +from django.conf import settings +from django.core.urlresolvers import reverse + +from courseware.access import _course_staff_group_name +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE, get_user +from xmodule.modulestore.django import modulestore +import xmodule.modulestore.django + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) +class TestInstructorDashboardEmailView(LoginEnrollmentTestCase): + ''' + Check for email view displayed with flag + ''' + + def setUp(self): + xmodule.modulestore.django._MODULESTORES = {} + + self.toy = modulestore().get_course("edX/toy/2012_Fall") + + # Create instructor account + self.instructor = 'view@test.com' + self.password = 'foo' + self.create_account('u1', self.instructor, self.password) + self.activate_user(self.instructor) + + group_name = _course_staff_group_name(self.toy.location) + g = Group.objects.create(name=group_name) + g.user_set.add(get_user(self.instructor)) + + self.logout() + self.login(self.instructor, self.password) + self.enroll(self.toy) + + def test_email_flag_true(self): + oldEmailFlag = settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] + settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True + response = self.client.get(reverse('instructor_dashboard', + kwargs={'course_id': self.toy.id})) + email_link = 'Email' + self.assertTrue(email_link in response.content) + + session = self.client.session + session['idash_mode'] = 'Email' + session.save() + response = self.client.get(reverse('instructor_dashboard', + kwargs={'course_id': self.toy.id})) + selected_email_link = 'Email' + self.assertTrue(selected_email_link in response.content) + send_to_label = '' + self.assertTrue(send_to_label in response.content) + + del self.client.session['idash_mode'] + settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = oldEmailFlag + + def test_email_flag_false(self): + oldEmailFlag = settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] + settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = False + response = self.client.get(reverse('instructor_dashboard', + kwargs={'course_id': self.toy.id})) + email_link = 'Email' + self.assertFalse(email_link in response.content) + + settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = oldEmailFlag diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f221d35a1b8d..39d44f7ffd3f 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -41,6 +41,11 @@ from .offline_gradecalc import student_grades, offline_grades_available +from bulk_email.models import CourseEmail +import datetime +from hashlib import md5 +from bulk_email.tasks import delegate_emails + log = logging.getLogger(__name__) # internal commands for managing forum roles: @@ -586,7 +591,24 @@ def getdat(u): ret = _do_enroll_students(course, course_id, students, overload=overload) datatable = ret['datatable'] - + #---------------------------------------- + # email + + elif action == 'Send email': + to = request.POST.get("to") + subject = request.POST.get("subject") + html_message = request.POST.get("message") + + email = CourseEmail(course_id=course_id, + sender=request.user, + to=to, + subject=subject, + html_message=html_message, + hash=md5((html_message+subject+datetime.datetime.isoformat(datetime.datetime.now())).encode('utf-8')).hexdigest()) + email.save() + + delegate_emails(email.hash, email.to, course) + #---------------------------------------- # psychometrics diff --git a/lms/envs/common.py b/lms/envs/common.py index b2c6f15c393f..123285e1303f 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -95,6 +95,8 @@ # analytics experiments 'ENABLE_INSTRUCTOR_ANALYTICS': False, + 'ENABLE_INSTRUCTOR_EMAIL': False, + # Flip to True when the YouTube iframe API breaks (again) 'USE_YOUTUBE_OBJECT_API': False, @@ -694,6 +696,7 @@ 'psychometrics', 'licenses', 'course_groups', + 'bulk_email', #For the wiki 'wiki', # The new django-wiki from benjaoming diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 17bce9399198..ccf85e519c87 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -28,6 +28,7 @@ MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True +MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True WIKI_ENABLED = True diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 8f84109626c9..f1f304830a34 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -9,6 +9,7 @@ + %block> @@ -111,6 +112,9 @@
+ + + + + + +
++ +
+%endif + ##-----------------------------------------------------------------------------