diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6c71fb77fdbc..d794f638ea0c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index e506a52fd96d..2a19eab34695 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -8,6 +8,7 @@ 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 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 @@ def assert_update_forum_role_membership(self, unique_student_identifier, rolenam self.assertNotIn(rolename, user_roles) - @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase): """ @@ -1802,7 +1804,7 @@ def test_list_instructor_tasks_running(self, act): 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 @@ def test_list_background_email_tasks(self, act): 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 @@ def test_list_instructor_tasks_problem(self, act): 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 @@ def test_list_instructor_tasks_problem_student(self, act): 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 @@ def test_list_instructor_tasks_problem_student(self, act): 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") diff --git a/lms/djangoapps/instructor/tests/utils.py b/lms/djangoapps/instructor/tests/utils.py new file mode 100644 index 000000000000..8607fd84a669 --- /dev/null +++ b/lms/djangoapps/instructor/tests/utils.py @@ -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} diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 67e2eaa84c83..d6f26531cbe4 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -20,6 +20,8 @@ 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 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.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') diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index ef632e4b91bc..e20f2988a712 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -31,6 +31,8 @@ '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$', diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 43a4eeb12c5e..d9118d30163f 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -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 diff --git a/lms/djangoapps/instructor/views/instructor_task_helpers.py b/lms/djangoapps/instructor/views/instructor_task_helpers.py new file mode 100644 index 000000000000..4c89e6addd28 --- /dev/null +++ b/lms/djangoapps/instructor/views/instructor_task_helpers.py @@ -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 diff --git a/lms/static/coffee/src/instructor_dashboard/send_email.coffee b/lms/static/coffee/src/instructor_dashboard/send_email.coffee index 772dddad213f..d26ed4a2877e 100644 --- a/lms/static/coffee/src/instructor_dashboard/send_email.coffee +++ b/lms/static/coffee/src/instructor_dashboard/send_email.coffee @@ -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() diff --git a/lms/static/coffee/src/instructor_dashboard/util.coffee b/lms/static/coffee/src/instructor_dashboard/util.coffee index 839472ef73b9..101047465191 100644 --- a/lms/static/coffee/src/instructor_dashboard/util.coffee +++ b/lms/static/coffee/src/instructor_dashboard/util.coffee @@ -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 = $('').text(value['subject']).html() + return '

' + subject_text + '

' + +# Formats the created field for the email content history table +created_formatter = (row, cell, value, columnDef, dataContext) -> + if !value then return "

" + gettext("Unknown") + "

" else return '

' + value + '

' + +# Formats the number sent field for the email content history table +number_sent_formatter = (row, cell, value, columndDef, dataContext) -> + if !value then return "

" + gettext("Unknown") + "

" else return '

' + value + '

' + +# 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 = $ '
', class: 'slickgrid' + $table_emails_inner.append $table_placeholder + grid = new Slick.Grid($table_placeholder, table_data, columns, options) + $table_emails.append $ '
' + +# 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 = $('
', "aria-hidden": "true", class: "modal email-modal", id: "email_message_" + email_id) + $email_wrapper = $ '
', class: 'inner-wrapper email-content-wrapper' + $email_header = $ '
', class: 'email-content-header' + + # Add copy email body button + $email_header.append $('', type: "button", name: "copy-email-body-text", value: gettext("Copy Email To Editor"), id: "copy_email_" + email_id) + + $close_button = $ '', href: '#', class: "close-modal" + $close_button.append $ '', class: 'icon-remove' + $email_header.append $close_button + + # HTML escape the subject line + subject_text = $('').text(email_info.email['subject']).html() + $email_header.append $('

', class: "message-bold").html('' + gettext('Subject:') + ' ' + subject_text) + + $email_header.append $('

', class: "message-bold").html('' + gettext('Time Sent:') + ' ' + email_info.created) + $email_header.append $('

', class: "message-bold").html('' + gettext('Sent To:') + ' ' + email_info.sent_to) + $email_wrapper.append $email_header + + $email_wrapper.append $ '
' + + # Last, add email content section + $email_content = $ '
', class: 'email-content-message' + $email_content.append $('

', class: "message-bold").html("" + gettext("Message:") + "") + $message = $('
').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 diff --git a/lms/static/js/toggle_login_modal.js b/lms/static/js/toggle_login_modal.js index 28bec089881a..e8310716e349 100644 --- a/lms/static/js/toggle_login_modal.js +++ b/lms/static/js/toggle_login_modal.js @@ -17,7 +17,7 @@ closeButton: null, position: 'fixed' } - + if ($("#lean_overlay").length == 0) { var overlay = $("
"); $("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) { diff --git a/lms/static/sass/course/instructor/_email.scss b/lms/static/sass/course/instructor/_email.scss index bc38d64394d7..859a9733f85e 100644 --- a/lms/static/sass/course/instructor/_email.scss +++ b/lms/static/sass/course/instructor/_email.scss @@ -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; + } + } +} diff --git a/lms/templates/instructor/instructor_dashboard_2/send_email.html b/lms/templates/instructor/instructor_dashboard_2/send_email.html index 2e157af0476f..b44c87bc6c64 100644 --- a/lms/templates/instructor/instructor_dashboard_2/send_email.html +++ b/lms/templates/instructor/instructor_dashboard_2/send_email.html @@ -69,12 +69,27 @@

${_("Pending Instructor Tasks")}


%endif