Merge pull request #4451 from Stanford-Online/njdupoux/email-content-history
Instructors can view previously sent email content
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