Commit 3af43cde by Sarina Canelake

Merge pull request #4451 from Stanford-Online/njdupoux/email-content-history

Instructors can view previously sent email content
parents 672c2a94 5791fd10
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
LMS: Instructors can request and see content of previous bulk emails sent in the instructor dashboard.
Studio: New advanced setting "invitation_only" for courses. This setting overrides the enrollment start/end dates Studio: New advanced setting "invitation_only" for courses. This setting overrides the enrollment start/end dates
if set. LMS-2670 if set. LMS-2670
......
...@@ -8,6 +8,7 @@ import json ...@@ -8,6 +8,7 @@ import json
import requests import requests
import datetime import datetime
import ddt import ddt
import random
from urllib import quote from urllib import quote
from django.test import TestCase from django.test import TestCase
from nose.tools import raises from nose.tools import raises
...@@ -31,6 +32,8 @@ from student.tests.factories import UserFactory ...@@ -31,6 +32,8 @@ from student.tests.factories import UserFactory
from courseware.tests.factories import StaffFactory, InstructorFactory, BetaTesterFactory from courseware.tests.factories import StaffFactory, InstructorFactory, BetaTesterFactory
from student.roles import CourseBetaTesterRole from student.roles import CourseBetaTesterRole
from microsite_configuration import microsite from microsite_configuration import microsite
from util.date_utils import get_default_time_display
from instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo
from student.models import CourseEnrollment, CourseEnrollmentAllowed from student.models import CourseEnrollment, CourseEnrollmentAllowed
from courseware.models import StudentModule from courseware.models import StudentModule
...@@ -1321,7 +1324,6 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase ...@@ -1321,7 +1324,6 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
self.assertNotIn(rolename, user_roles) self.assertNotIn(rolename, user_roles)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase): class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
...@@ -1802,7 +1804,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -1802,7 +1804,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
act.return_value = self.tasks act.return_value = self.tasks
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
mock_factory = MockCompletionInfo() mock_factory = MockCompletionInfo()
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info:
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
response = self.client.get(url, {}) response = self.client.get(url, {})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -1821,7 +1823,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -1821,7 +1823,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
act.return_value = self.tasks act.return_value = self.tasks
url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
mock_factory = MockCompletionInfo() mock_factory = MockCompletionInfo()
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info:
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
response = self.client.get(url, {}) response = self.client.get(url, {})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -1840,7 +1842,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -1840,7 +1842,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
act.return_value = self.tasks act.return_value = self.tasks
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
mock_factory = MockCompletionInfo() mock_factory = MockCompletionInfo()
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info:
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
response = self.client.get(url, { response = self.client.get(url, {
'problem_location_str': self.problem_urlname, 'problem_location_str': self.problem_urlname,
...@@ -1861,7 +1863,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -1861,7 +1863,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
act.return_value = self.tasks act.return_value = self.tasks
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
mock_factory = MockCompletionInfo() mock_factory = MockCompletionInfo()
with patch('instructor.views.api.get_task_completion_info') as mock_completion_info: with patch('instructor.views.instructor_task_helpers.get_task_completion_info') as mock_completion_info:
mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info mock_completion_info.side_effect = mock_factory.mock_get_task_completion_info
response = self.client.get(url, { response = self.client.get(url, {
'problem_location_str': self.problem_urlname, 'problem_location_str': self.problem_urlname,
...@@ -1880,6 +1882,104 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -1880,6 +1882,104 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@patch.object(instructor_task.api, 'get_instructor_task_history')
class TestInstructorEmailContentList(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test the instructor email content history endpoint.
"""
def setUp(self):
self.course = CourseFactory.create()
self.instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=self.instructor.username, password='test')
def tearDown(self):
"""
Undo all patches.
"""
patch.stopall()
def setup_fake_email_info(self, num_emails):
""" Initialize the specified number of fake emails """
self.tasks = {}
self.emails = {}
self.emails_info = {}
for email_id in range(num_emails):
num_sent = random.randint(1, 15401)
self.tasks[email_id] = FakeContentTask(email_id, num_sent, 'expected')
self.emails[email_id] = FakeEmail(email_id)
self.emails_info[email_id] = FakeEmailInfo(self.emails[email_id], num_sent)
def get_matching_mock_email(self, *args, **kwargs):
""" Returns the matching mock emails for the given id """
email_id = kwargs.get('id', 0)
return self.emails[email_id]
def get_email_content_response(self, num_emails, task_history_request):
""" Calls the list_email_content endpoint and returns the repsonse """
self.setup_fake_email_info(num_emails)
task_history_request.return_value = self.tasks.values()
url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()})
with patch('instructor.views.api.CourseEmail.objects.get') as mock_email_info:
mock_email_info.side_effect = self.get_matching_mock_email
response = self.client.get(url, {})
self.assertEqual(response.status_code, 200)
return response
def test_content_list_one_email(self, task_history_request):
""" Test listing of bulk emails when email list has one email """
response = self.get_email_content_response(1, task_history_request)
self.assertTrue(task_history_request.called)
email_info = json.loads(response.content)['emails']
# Emails list should have one email
self.assertEqual(len(email_info), 1)
# Email content should be what's expected
expected_message = self.emails[0].html_message
returned_email_info = email_info[0]
received_message = returned_email_info[u'email'][u'html_message']
self.assertEqual(expected_message, received_message)
def test_content_list_no_emails(self, task_history_request):
""" Test listing of bulk emails when email list empty """
response = self.get_email_content_response(0, task_history_request)
self.assertTrue(task_history_request.called)
email_info = json.loads(response.content)['emails']
# Emails list should be empty
self.assertEqual(len(email_info), 0)
def test_content_list_email_content_many(self, task_history_request):
""" Test listing of bulk emails sent large amount of emails """
response = self.get_email_content_response(50, task_history_request)
self.assertTrue(task_history_request.called)
expected_email_info = [email_info.to_dict() for email_info in self.emails_info.values()]
actual_email_info = json.loads(response.content)['emails']
self.assertEqual(len(actual_email_info), 50)
for exp_email, act_email in zip(expected_email_info, actual_email_info):
self.assertDictEqual(exp_email, act_email)
self.assertEqual(actual_email_info, expected_email_info)
def test_list_email_content_error(self, task_history_request):
""" Test handling of error retrieving email """
self.invalid_task = FakeContentTask(0, 0, 'test')
self.invalid_task.make_invalid_input()
task_history_request.return_value = [self.invalid_task]
url = reverse('list_email_content', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(url, {})
self.assertEqual(response.status_code, 200)
self.assertTrue(task_history_request.called)
returned_email_info = json.loads(response.content)['emails']
self.assertEqual(len(returned_email_info), 1)
returned_info = returned_email_info[0]
for info in ['created', 'sent_to', 'email', 'number_sent']:
self.assertEqual(returned_info[info], None)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
@override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/") @override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/")
@override_settings(ANALYTICS_API_KEY="robot_api_key") @override_settings(ANALYTICS_API_KEY="robot_api_key")
class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCase): class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCase):
......
"""
Utilities for instructor unit tests
"""
import datetime
import json
import random
from django.utils.timezone import utc
from util.date_utils import get_default_time_display
class FakeInfo(object):
"""Parent class for faking objects used in tests"""
FEATURES = []
def __init__(self):
for feature in self.FEATURES:
setattr(self, feature, u'expected')
def to_dict(self):
""" Returns a dict representation of the object """
return {key: getattr(self, key) for key in self.FEATURES}
class FakeContentTask(FakeInfo):
""" Fake task info needed for email content list """
FEATURES = [
'task_input',
'task_output',
]
def __init__(self, email_id, num_sent, sent_to):
super(FakeContentTask, self).__init__()
self.task_input = {'email_id': email_id, 'to_option': sent_to}
self.task_input = json.dumps(self.task_input)
self.task_output = {'total': num_sent}
self.task_output = json.dumps(self.task_output)
def make_invalid_input(self):
"""Corrupt the task input field to test errors"""
self.task_input = "THIS IS INVALID JSON"
class FakeEmail(FakeInfo):
""" Corresponding fake email for a fake task """
FEATURES = [
'subject',
'html_message',
'id',
'created',
]
def __init__(self, email_id):
super(FakeEmail, self).__init__()
self.id = unicode(email_id)
# Select a random data for create field
year = random.choice(range(1950, 2000))
month = random.choice(range(1, 12))
day = random.choice(range(1, 28))
hour = random.choice(range(0, 23))
minute = random.choice(range(0, 59))
self.created = datetime.datetime(year, month, day, hour, minute, tzinfo=utc)
class FakeEmailInfo(FakeInfo):
""" Fake email information object """
FEATURES = [
u'created',
u'sent_to',
u'email',
u'number_sent'
]
EMAIL_FEATURES = [
u'subject',
u'html_message',
u'id'
]
def __init__(self, fake_email, num_sent):
super(FakeEmailInfo, self).__init__()
self.created = get_default_time_display(fake_email.created)
self.number_sent = num_sent
fake_email_dict = fake_email.to_dict()
self.email = {feature: fake_email_dict[feature] for feature in self.EMAIL_FEATURES}
...@@ -20,6 +20,8 @@ from django.utils.translation import ugettext as _ ...@@ -20,6 +20,8 @@ from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.utils.html import strip_tags from django.utils.html import strip_tags
from util.json_request import JsonResponse from util.json_request import JsonResponse
from util.date_utils import get_default_time_display
from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import get_course_with_access, get_course_by_id from courseware.courses import get_course_with_access, get_course_by_id
...@@ -36,7 +38,6 @@ from courseware.models import StudentModule ...@@ -36,7 +38,6 @@ from courseware.models import StudentModule
from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user
import instructor_task.api import instructor_task.api
from instructor_task.api_helper import AlreadyRunningError from instructor_task.api_helper import AlreadyRunningError
from instructor_task.views import get_task_completion_info
from instructor_task.models import ReportStore from instructor_task.models import ReportStore
import instructor.enrollment as enrollment import instructor.enrollment as enrollment
from instructor.enrollment import ( from instructor.enrollment import (
...@@ -613,6 +614,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613 ...@@ -613,6 +614,7 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613
# centralized into instructor_analytics. Currently instructor_analytics # centralized into instructor_analytics. Currently instructor_analytics
# has similar functionality but not quite what's needed. # has similar functionality but not quite what's needed.
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
def csv_response(filename, header, rows): def csv_response(filename, header, rows):
"""Returns a CSV http response for the given header and rows (excel/utf-8).""" """Returns a CSV http response for the given header and rows (excel/utf-8)."""
response = HttpResponse(mimetype='text/csv') response = HttpResponse(mimetype='text/csv')
...@@ -850,45 +852,6 @@ def rescore_problem(request, course_id): ...@@ -850,45 +852,6 @@ def rescore_problem(request, course_id):
return JsonResponse(response_payload) return JsonResponse(response_payload)
def extract_task_features(task):
"""
Convert task to dict for json rendering.
Expects tasks have the following features:
* task_type (str, type of task)
* task_input (dict, input(s) to the task)
* task_id (str, celery id of the task)
* requester (str, username who submitted the task)
* task_state (str, state of task eg PROGRESS, COMPLETED)
* created (datetime, when the task was completed)
* task_output (optional)
"""
# Pull out information from the task
features = ['task_type', 'task_input', 'task_id', 'requester', 'task_state']
task_feature_dict = {feature: str(getattr(task, feature)) for feature in features}
# Some information (created, duration, status, task message) require additional formatting
task_feature_dict['created'] = task.created.isoformat()
# Get duration info, if known
duration_sec = 'unknown'
if hasattr(task, 'task_output') and task.task_output is not None:
try:
task_output = json.loads(task.task_output)
except ValueError:
log.error("Could not parse task output as valid json; task output: %s", task.task_output)
else:
if 'duration_ms' in task_output:
duration_sec = int(task_output['duration_ms'] / 1000.0)
task_feature_dict['duration_sec'] = duration_sec
# Get progress status message & success information
success, task_message = get_task_completion_info(task)
status = _("Complete") if success else _("Incomplete")
task_feature_dict['status'] = status
task_feature_dict['task_message'] = task_message
return task_feature_dict
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff') @require_level('staff')
...@@ -910,6 +873,24 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a ...@@ -910,6 +873,24 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff') @require_level('staff')
def list_email_content(requests, course_id):
"""
List the content of bulk emails sent
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
task_type = 'bulk_course_email'
# First get tasks list of bulk emails sent
emails = instructor_task.api.get_instructor_task_history(course_id, task_type=task_type)
response_payload = {
'emails': map(extract_email_features, emails),
}
return JsonResponse(response_payload)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def list_instructor_tasks(request, course_id): def list_instructor_tasks(request, course_id):
""" """
List instructor tasks. List instructor tasks.
......
...@@ -31,6 +31,8 @@ urlpatterns = patterns('', # nopep8 ...@@ -31,6 +31,8 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.list_instructor_tasks', name="list_instructor_tasks"), 'instructor.views.api.list_instructor_tasks', name="list_instructor_tasks"),
url(r'^list_background_email_tasks$', url(r'^list_background_email_tasks$',
'instructor.views.api.list_background_email_tasks', name="list_background_email_tasks"), 'instructor.views.api.list_background_email_tasks', name="list_background_email_tasks"),
url(r'^list_email_content$',
'instructor.views.api.list_email_content', name="list_email_content"),
url(r'^list_forum_members$', url(r'^list_forum_members$',
'instructor.views.api.list_forum_members', name="list_forum_members"), 'instructor.views.api.list_forum_members', name="list_forum_members"),
url(r'^update_forum_role_membership$', url(r'^update_forum_role_membership$',
......
...@@ -92,7 +92,6 @@ def instructor_dashboard_2(request, course_id): ...@@ -92,7 +92,6 @@ def instructor_dashboard_2(request, course_id):
if course_mode_has_price: if course_mode_has_price:
sections.append(_section_e_commerce(course_key, access)) sections.append(_section_e_commerce(course_key, access))
studio_url = None studio_url = None
if is_studio_course: if is_studio_course:
studio_url = get_cms_course_link(course) studio_url = get_cms_course_link(course)
...@@ -278,6 +277,9 @@ def _section_send_email(course_key, access, course): ...@@ -278,6 +277,9 @@ def _section_send_email(course_key, access, course):
'email_background_tasks_url': reverse( 'email_background_tasks_url': reverse(
'list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()} 'list_background_email_tasks', kwargs={'course_id': course_key.to_deprecated_string()}
), ),
'email_content_history_url': reverse(
'list_email_content', kwargs={'course_id': course_key.to_deprecated_string()}
),
} }
return section_data return section_data
......
"""
A collection of helper utility functions for working with instructor
tasks.
"""
import json
import logging
from util.date_utils import get_default_time_display
from bulk_email.models import CourseEmail
from django.utils.translation import ugettext as _
from instructor_task.views import get_task_completion_info
log = logging.getLogger(__name__)
def email_error_information():
"""
Returns email information marked as None, used in event email
cannot be loaded
"""
expected_info = [
'created',
'sent_to',
'email',
'number_sent'
]
return {info: None for info in expected_info}
def extract_email_features(email_task):
"""
From the given task, extract email content information
Expects that the given task has the following attributes:
* task_input (dict containing email_id and to_option)
* task_output (optional, dict containing total emails sent)
With this information, gets the corresponding email object from the
bulk emails table, and loads up a dict containing the following:
* created, the time the email was sent displayed in default time display
* sent_to, the group the email was delivered to
* email, dict containing the subject, id, and html_message of an email
* number_sent, int number of emails sent
If task_input cannot be loaded, then the email cannot be loaded
and None is returned for these fields.
"""
# Load the task input info to get email id
try:
task_input_information = json.loads(email_task.task_input)
except ValueError:
log.error("Could not parse task input as valid json; task input: %s", email_task.task_input)
return email_error_information()
email = CourseEmail.objects.get(id=task_input_information['email_id'])
creation_time = get_default_time_display(email.created)
email_feature_dict = {'created': creation_time, 'sent_to': task_input_information['to_option']}
features = ['subject', 'html_message', 'id']
email_info = {feature: unicode(getattr(email, feature)) for feature in features}
# Pass along email as an object with the information we desire
email_feature_dict['email'] = email_info
number_sent = None
if hasattr(email_task, 'task_output') and email_task.task_output is not None:
try:
task_output = json.loads(email_task.task_output)
except ValueError:
log.error("Could not parse task output as valid json; task output: %s", email_task.task_output)
else:
if 'total' in task_output:
number_sent = int(task_output['total'])
email_feature_dict['number_sent'] = number_sent
return email_feature_dict
def extract_task_features(task):
"""
Convert task to dict for json rendering.
Expects tasks have the following features:
* task_type (str, type of task)
* task_input (dict, input(s) to the task)
* task_id (str, celery id of the task)
* requester (str, username who submitted the task)
* task_state (str, state of task eg PROGRESS, COMPLETED)
* created (datetime, when the task was completed)
* task_output (optional)
"""
# Pull out information from the task
features = ['task_type', 'task_input', 'task_id', 'requester', 'task_state']
task_feature_dict = {feature: str(getattr(task, feature)) for feature in features}
# Some information (created, duration, status, task message) require additional formatting
task_feature_dict['created'] = task.created.isoformat()
# Get duration info, if known
duration_sec = 'unknown'
if hasattr(task, 'task_output') and task.task_output is not None:
try:
task_output = json.loads(task.task_output)
except ValueError:
log.error("Could not parse task output as valid json; task output: %s", task.task_output)
else:
if 'duration_ms' in task_output:
duration_sec = int(task_output['duration_ms'] / 1000.0)
task_feature_dict['duration_sec'] = duration_sec
# Get progress status message & success information
success, task_message = get_task_completion_info(task)
status = _("Complete") if success else _("Incomplete")
task_feature_dict['status'] = status
task_feature_dict['task_message'] = task_message
return task_feature_dict
...@@ -11,6 +11,8 @@ plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, argum ...@@ -11,6 +11,8 @@ plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, argum
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
create_task_list_table = -> window.InstructorDashboard.util.create_task_list_table.apply this, arguments create_task_list_table = -> window.InstructorDashboard.util.create_task_list_table.apply this, arguments
create_email_content_table = -> window.InstructorDashboard.util.create_email_content_table.apply this, arguments
create_email_message_views = -> window.InstructorDashboard.util.create_email_message_views.apply this, arguments
class SendEmail class SendEmail
constructor: (@$container) -> constructor: (@$container) ->
...@@ -21,9 +23,14 @@ class SendEmail ...@@ -21,9 +23,14 @@ class SendEmail
@$btn_send = @$container.find("input[name='send']'") @$btn_send = @$container.find("input[name='send']'")
@$task_response = @$container.find(".request-response") @$task_response = @$container.find(".request-response")
@$request_response_error = @$container.find(".request-response-error") @$request_response_error = @$container.find(".request-response-error")
@$content_request_response_error = @$container.find(".content-request-response-error")
@$history_request_response_error = @$container.find(".history-request-response-error") @$history_request_response_error = @$container.find(".history-request-response-error")
@$btn_task_history_email = @$container.find("input[name='task-history-email']'") @$btn_task_history_email = @$container.find("input[name='task-history-email']'")
@$btn_task_history_email_content = @$container.find("input[name='task-history-email-content']'")
@$table_task_history_email = @$container.find(".task-history-email-table") @$table_task_history_email = @$container.find(".task-history-email-table")
@$table_email_content_history = @$container.find(".content-history-email-table")
@$email_content_table_inner = @$container.find(".content-history-table-inner")
@$email_messages_wrapper = @$container.find(".email-messages-wrapper")
# attach click handlers # attach click handlers
...@@ -83,10 +90,26 @@ class SendEmail ...@@ -83,10 +90,26 @@ class SendEmail
else else
@$history_request_response_error.text gettext("There is no email history for this course.") @$history_request_response_error.text gettext("There is no email history for this course.")
# Enable the msg-warning css display # Enable the msg-warning css display
$(".msg-warning").css({"display":"block"}) @$history_request_response_error.css({"display":"block"})
error: std_ajax_err => error: std_ajax_err =>
@$history_request_response_error.text gettext("There was an error obtaining email task history for this course.") @$history_request_response_error.text gettext("There was an error obtaining email task history for this course.")
# List content history for emails sent
@$btn_task_history_email_content.click =>
url = @$btn_task_history_email_content.data 'endpoint'
$.ajax
dataType: 'json'
url : url
success: (data) =>
if data.emails.length
create_email_content_table @$table_email_content_history, @$email_content_table_inner, data.emails
create_email_message_views @$email_messages_wrapper, data.emails
else
@$content_request_response_error.text gettext("There is no email history for this course.")
@$content_request_response_error.css({"display":"block"})
error: std_ajax_err =>
@$content_request_response_error.text gettext("There was an error obtaining email content history for this course.")
fail_with_error: (msg) -> fail_with_error: (msg) ->
console.warn msg console.warn msg
@$task_response.empty() @$task_response.empty()
......
...@@ -119,6 +119,119 @@ create_task_list_table = ($table_tasks, tasks_data) -> ...@@ -119,6 +119,119 @@ create_task_list_table = ($table_tasks, tasks_data) ->
$table_tasks.append $table_placeholder $table_tasks.append $table_placeholder
grid = new Slick.Grid($table_placeholder, table_data, columns, options) grid = new Slick.Grid($table_placeholder, table_data, columns, options)
# Formats the subject field for email content history table
subject_formatter = (row, cell, value, columnDef, dataContext) ->
if !value then return gettext("An error occurred retrieving your email. Please try again later, and contact technical support if the problem persists.")
subject_text = $('<span>').text(value['subject']).html()
return '<p><a href="#email_message_' + value['id']+ '" id="email_message_' + value['id'] + '_trig">' + subject_text + '</a></p>'
# Formats the created field for the email content history table
created_formatter = (row, cell, value, columnDef, dataContext) ->
if !value then return "<p>" + gettext("Unknown") + "</p>" else return '<p>' + value + '</p>'
# Formats the number sent field for the email content history table
number_sent_formatter = (row, cell, value, columndDef, dataContext) ->
if !value then return "<p>" + gettext("Unknown") + "</p>" else return '<p>' + value + '</p>'
# Creates a table to display the content of bulk course emails
# sent in the past
create_email_content_table = ($table_emails, $table_emails_inner, email_data) ->
$table_emails_inner.empty()
$table_emails.show()
options =
enableCellNavigation: true
enableColumnReorder: false
autoHeight: true
rowHeight: 50
forceFitColumns: true
columns = [
id: 'email'
field: 'email'
name: gettext('Subject')
minWidth: 80
cssClass: "email-content-cell"
formatter: subject_formatter
,
id: 'created'
field: 'created'
name: gettext('Time Sent')
minWidth: 80
cssClass: "email-content-cell"
formatter: created_formatter
,
id: 'number_sent'
field: 'number_sent'
name: gettext('Number Sent')
minwidth: 100
maxWidth: 150
cssClass: "email-content-cell"
formatter: number_sent_formatter
,
]
table_data = email_data
$table_placeholder = $ '<div/>', class: 'slickgrid'
$table_emails_inner.append $table_placeholder
grid = new Slick.Grid($table_placeholder, table_data, columns, options)
$table_emails.append $ '<br/>'
# Creates the modal windows linked to each email in the email history
# Displayed when instructor clicks an email's subject in the content history table
create_email_message_views = ($messages_wrapper, emails) ->
$messages_wrapper.empty()
for email_info in emails
# If some error occured, bail out
if !email_info.email then return
# Create hidden section for modal window
email_id = email_info.email['id']
$message_content = $('<section>', "aria-hidden": "true", class: "modal email-modal", id: "email_message_" + email_id)
$email_wrapper = $ '<div>', class: 'inner-wrapper email-content-wrapper'
$email_header = $ '<div>', class: 'email-content-header'
# Add copy email body button
$email_header.append $('<input>', type: "button", name: "copy-email-body-text", value: gettext("Copy Email To Editor"), id: "copy_email_" + email_id)
$close_button = $ '<a>', href: '#', class: "close-modal"
$close_button.append $ '<i>', class: 'icon-remove'
$email_header.append $close_button
# HTML escape the subject line
subject_text = $('<span>').text(email_info.email['subject']).html()
$email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Subject:') + '</em> ' + subject_text)
$email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Time Sent:') + '</em> ' + email_info.created)
$email_header.append $('<h2>', class: "message-bold").html('<em>' + gettext('Sent To:') + '</em> ' + email_info.sent_to)
$email_wrapper.append $email_header
$email_wrapper.append $ '<hr>'
# Last, add email content section
$email_content = $ '<div>', class: 'email-content-message'
$email_content.append $('<h2>', class: "message-bold").html("<em>" + gettext("Message:") + "</em>")
$message = $('<div>').html(email_info.email['html_message'])
$email_content.append $message
$email_wrapper.append $email_content
$message_content.append $email_wrapper
$messages_wrapper.append $message_content
# Setup buttons to open modal window and copy an email message
$('#email_message_' + email_info.email['id'] + '_trig').leanModal({closeButton: ".close-modal", copyEmailButton: "#copy_email_" + email_id})
setup_copy_email_button(email_id, email_info.email['html_message'], email_info.email['subject'])
# Helper method to set click handler for modal copy email button
setup_copy_email_button = (email_id, html_message, subject) ->
$("#copy_email_" + email_id).click =>
editor = tinyMCE.get("mce_0")
editor.setContent(html_message)
$('#id_subject').val(subject)
# Helper class for managing the execution of interval tasks. # Helper class for managing the execution of interval tasks.
# Handles pausing and restarting. # Handles pausing and restarting.
class IntervalManager class IntervalManager
...@@ -178,4 +291,6 @@ if _? ...@@ -178,4 +291,6 @@ if _?
std_ajax_err: std_ajax_err std_ajax_err: std_ajax_err
IntervalManager: IntervalManager IntervalManager: IntervalManager
create_task_list_table: create_task_list_table create_task_list_table: create_task_list_table
create_email_content_table: create_email_content_table
create_email_message_views: create_email_message_views
PendingInstructorTasks: PendingInstructorTasks PendingInstructorTasks: PendingInstructorTasks
...@@ -52,6 +52,11 @@ ...@@ -52,6 +52,11 @@
close_modal(modal_id, e); close_modal(modal_id, e);
}); });
// To enable closing of email modal when copy button hit
$(o.copyEmailButton).click(function(e) {
close_modal(modal_id, e);
});
var modal_height = $(modal_id).outerHeight(); var modal_height = $(modal_id).outerHeight();
var modal_width = $(modal_id).outerWidth(); var modal_width = $(modal_id).outerWidth();
...@@ -59,8 +64,18 @@ ...@@ -59,8 +64,18 @@
$('#lean_overlay').fadeTo(200,o.overlay); $('#lean_overlay').fadeTo(200,o.overlay);
$('iframe', modal_id).attr('src', $('iframe', modal_id).data('src')); $('iframe', modal_id).attr('src', $('iframe', modal_id).data('src'));
if ($(modal_id).hasClass("email-modal")){
$(modal_id).css({
'width' : 80 + '%',
'height' : 80 + '%',
'position' : o.position,
'opacity' : 0,
'z-index' : 11000,
'left' : 10 + '%',
'top' : 10 + '%'
})
} else {
$(modal_id).css({ $(modal_id).css({
'display' : 'block',
'position' : o.position, 'position' : o.position,
'opacity' : 0, 'opacity' : 0,
'z-index': 11000, 'z-index': 11000,
...@@ -68,8 +83,9 @@ ...@@ -68,8 +83,9 @@
'margin-left' : -(modal_width/2) + "px", 'margin-left' : -(modal_width/2) + "px",
'top' : o.top + "px" 'top' : o.top + "px"
}) })
}
$(modal_id).fadeTo(200,1); $(modal_id).show().fadeTo(200,1);
$(modal_id).find(".notice").hide().html(""); $(modal_id).find(".notice").hide().html("");
var notice = $(this).data('notice') var notice = $(this).data('notice')
if(notice !== undefined) { if(notice !== undefined) {
......
...@@ -26,3 +26,61 @@ ...@@ -26,3 +26,61 @@
} }
} }
.email-background{
.content-history-email-table {
display: none;
}
.email-content-wrapper {
min-height: 100%;
background: #f5f5f5;
hr {
width: 90%;
margin-left: 5%;
margin-top: 0;
}
}
.message-bold em {
font-weight: bold;
font-style: normal;
}
.email-content-header {
padding: 20px 5%;
h2 {
text-align: left;
padding-top: 10px;
margin: 0;
}
input {
margin-top: 15px;
float: right;
}
}
.email-content-message {
padding: 5px 5% 40px 5%;
}
.email-modal {
overflow: auto;
color: $black;
}
.email-content-cell {
p {
padding: 15px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
a:hover {
font-weight: bold;
}
}
}
...@@ -70,11 +70,26 @@ ...@@ -70,11 +70,26 @@
<div class="vert-left email-background" id="section-task-history"> <div class="vert-left email-background" id="section-task-history">
<h2> ${_("Email Task History")} </h2> <h2> ${_("Email Task History")} </h2>
<div>
<p>${_("To see the content of all previously sent emails, click this button:")}</p>
<br/>
<input type="button" name="task-history-email-content" value="${_("Sent Email History")}" data-endpoint="${ section_data['email_content_history_url'] }" >
<div class="content-request-response-error msg msg-warning copy"></div>
<p>
<div class="content-history-email-table">
<p><em>${_("To read an email, click its subject.")}</em></p>
<br/>
<div class="content-history-table-inner"></div>
</div>
<div class="email-messages-wrapper"></div>
</div>
<div>
<p>${_("To see the status for all bulk email tasks ever submitted for this course, click on this button:")}</p> <p>${_("To see the status for all bulk email tasks ever submitted for this course, click on this button:")}</p>
<br/> <br/>
<input type="button" name="task-history-email" value="${_("Show Email Task History")}" data-endpoint="${ section_data['email_background_tasks_url'] }" > <input type="button" name="task-history-email" value="${_("Show Email Task History")}" data-endpoint="${ section_data['email_background_tasks_url'] }" >
<div class="history-request-response-error msg msg-warning copy"></div> <div class="history-request-response-error msg msg-warning copy"></div>
<div class="task-history-email-table"></div> <div class="task-history-email-table"></div>
</div>
</div> </div>
%endif %endif
......
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