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,
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,
......@@ -1880,6 +1882,104 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
@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")
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 _
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')
......@@ -910,6 +873,24 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a
@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')
def list_instructor_tasks(request, course_id):
"""
List instructor tasks.
......
......@@ -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
......
"""
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"
})
$(modal_id).fadeTo(200,1);
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).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
......
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