Instructors can view previously sent email content
Previously on the send email page of the instructor dashboard, instructors could only view task information about emails they've sent for their course in the past. In addition to this, I've now added the ability to see the content of all previously sent emails. A "Sent Email History" button has been added to the page. When clicked, a table displaying the subject line, number of emails sent, and date/time of submission for each previously sent email is created. An instructor can then click on any subject line to see the content of that email, displayed in a modal window that appears on the page. The window is also equipped with a "copy email to editor" button, which copies the emails contents to the tinyMCE editor, so that an instructor can easily resend an email that they've sent in the past.
This commit is contained in:
@@ -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
|
||||
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
|
||||
if set. LMS-2670
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
import requests
|
||||
import datetime
|
||||
import ddt
|
||||
import random
|
||||
from urllib import quote
|
||||
from django.test import TestCase
|
||||
from nose.tools import raises
|
||||
@@ -31,6 +32,8 @@ from student.tests.factories import UserFactory
|
||||
from courseware.tests.factories import StaffFactory, InstructorFactory, BetaTesterFactory
|
||||
from student.roles import CourseBetaTesterRole
|
||||
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 courseware.models import StudentModule
|
||||
@@ -1321,7 +1324,6 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
|
||||
self.assertNotIn(rolename, user_roles)
|
||||
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
@@ -1802,7 +1804,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
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
|
||||
response = self.client.get(url, {})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1821,7 +1823,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_background_email_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
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
|
||||
response = self.client.get(url, {})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -1840,7 +1842,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
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
|
||||
response = self.client.get(url, {
|
||||
'problem_location_str': self.problem_urlname,
|
||||
@@ -1861,7 +1863,7 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
act.return_value = self.tasks
|
||||
url = reverse('list_instructor_tasks', kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
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
|
||||
response = self.client.get(url, {
|
||||
'problem_location_str': self.problem_urlname,
|
||||
@@ -1879,6 +1881,104 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.assertEqual(actual_tasks, expected_tasks)
|
||||
|
||||
|
||||
@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_API_KEY="robot_api_key")
|
||||
|
||||
84
lms/djangoapps/instructor/tests/utils.py
Normal file
84
lms/djangoapps/instructor/tests/utils.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
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 _
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.utils.html import strip_tags
|
||||
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.courses import get_course_with_access, get_course_by_id
|
||||
@@ -36,7 +38,6 @@ from courseware.models import StudentModule
|
||||
from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user
|
||||
import instructor_task.api
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from instructor_task.views import get_task_completion_info
|
||||
from instructor_task.models import ReportStore
|
||||
import instructor.enrollment as enrollment
|
||||
from instructor.enrollment import (
|
||||
@@ -52,7 +53,7 @@ import instructor_analytics.distributions
|
||||
import instructor_analytics.csvs
|
||||
import csv
|
||||
|
||||
from submissions import api as sub_api # installed from the edx-submissions repository
|
||||
from submissions import api as sub_api # installed from the edx-submissions repository
|
||||
|
||||
from bulk_email.models import CourseEmail
|
||||
|
||||
@@ -610,9 +611,10 @@ def get_anon_ids(request, course_id): # pylint: disable=W0613
|
||||
Respond with 2-column CSV output of user-id, anonymized-user-id
|
||||
"""
|
||||
# TODO: the User.objects query and CSV generation here could be
|
||||
# centralized into instructor_analytics. Currently instructor_analytics
|
||||
# centralized into instructor_analytics. Currently instructor_analytics
|
||||
# has similar functionality but not quite what's needed.
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
def csv_response(filename, header, rows):
|
||||
"""Returns a CSV http response for the given header and rows (excel/utf-8)."""
|
||||
response = HttpResponse(mimetype='text/csv')
|
||||
@@ -850,45 +852,6 @@ def rescore_problem(request, course_id):
|
||||
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
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@@ -907,6 +870,24 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@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')
|
||||
|
||||
@@ -31,6 +31,8 @@ urlpatterns = patterns('', # nopep8
|
||||
'instructor.views.api.list_instructor_tasks', name="list_instructor_tasks"),
|
||||
url(r'^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$',
|
||||
'instructor.views.api.list_forum_members', name="list_forum_members"),
|
||||
url(r'^update_forum_role_membership$',
|
||||
|
||||
@@ -92,7 +92,6 @@ def instructor_dashboard_2(request, course_id):
|
||||
if course_mode_has_price:
|
||||
sections.append(_section_e_commerce(course_key, access))
|
||||
|
||||
|
||||
studio_url = None
|
||||
if is_studio_course:
|
||||
studio_url = get_cms_course_link(course)
|
||||
@@ -278,6 +277,9 @@ def _section_send_email(course_key, access, course):
|
||||
'email_background_tasks_url': reverse(
|
||||
'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
|
||||
|
||||
|
||||
113
lms/djangoapps/instructor/views/instructor_task_helpers.py
Normal file
113
lms/djangoapps/instructor/views/instructor_task_helpers.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
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
|
||||
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
|
||||
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
|
||||
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
|
||||
constructor: (@$container) ->
|
||||
@@ -21,9 +23,14 @@ class SendEmail
|
||||
@$btn_send = @$container.find("input[name='send']'")
|
||||
@$task_response = @$container.find(".request-response")
|
||||
@$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")
|
||||
@$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_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
|
||||
|
||||
@@ -83,10 +90,26 @@ class SendEmail
|
||||
else
|
||||
@$history_request_response_error.text gettext("There is no email history for this course.")
|
||||
# Enable the msg-warning css display
|
||||
$(".msg-warning").css({"display":"block"})
|
||||
@$history_request_response_error.css({"display":"block"})
|
||||
error: std_ajax_err =>
|
||||
@$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) ->
|
||||
console.warn msg
|
||||
@$task_response.empty()
|
||||
|
||||
@@ -119,6 +119,119 @@ create_task_list_table = ($table_tasks, tasks_data) ->
|
||||
$table_tasks.append $table_placeholder
|
||||
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.
|
||||
# Handles pausing and restarting.
|
||||
class IntervalManager
|
||||
@@ -178,4 +291,6 @@ if _?
|
||||
std_ajax_err: std_ajax_err
|
||||
IntervalManager: IntervalManager
|
||||
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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
closeButton: null,
|
||||
position: 'fixed'
|
||||
}
|
||||
|
||||
|
||||
if ($("#lean_overlay").length == 0) {
|
||||
var overlay = $("<div id='lean_overlay'></div>");
|
||||
$("body").append(overlay);
|
||||
@@ -52,6 +52,11 @@
|
||||
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_width = $(modal_id).outerWidth();
|
||||
|
||||
@@ -59,17 +64,28 @@
|
||||
$('#lean_overlay').fadeTo(200,o.overlay);
|
||||
|
||||
$('iframe', modal_id).attr('src', $('iframe', modal_id).data('src'));
|
||||
$(modal_id).css({
|
||||
'display' : 'block',
|
||||
'position' : o.position,
|
||||
'opacity' : 0,
|
||||
'z-index': 11000,
|
||||
'left' : 50 + '%',
|
||||
'margin-left' : -(modal_width/2) + "px",
|
||||
'top' : o.top + "px"
|
||||
})
|
||||
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({
|
||||
'position' : o.position,
|
||||
'opacity' : 0,
|
||||
'z-index': 11000,
|
||||
'left' : 50 + '%',
|
||||
'margin-left' : -(modal_width/2) + "px",
|
||||
'top' : o.top + "px"
|
||||
})
|
||||
}
|
||||
|
||||
$(modal_id).fadeTo(200,1);
|
||||
$(modal_id).show().fadeTo(200,1);
|
||||
$(modal_id).find(".notice").hide().html("");
|
||||
var notice = $(this).data('notice')
|
||||
if(notice !== undefined) {
|
||||
|
||||
@@ -20,9 +20,67 @@
|
||||
margin-top: 10px;
|
||||
line-height: 1.3;
|
||||
|
||||
ul {
|
||||
ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,12 +69,27 @@
|
||||
<hr>
|
||||
|
||||
<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>
|
||||
<br/>
|
||||
<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="task-history-email-table"></div>
|
||||
<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="task-history-email-table"></div>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user