diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 6c68b7fed65d..43a1434f94d9 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -38,6 +38,10 @@ import analytics.distributions import analytics.csvs +from bulk_email.models import CourseEmail +from html_to_text import html_to_text +from bulk_email import tasks + log = logging.getLogger(__name__) @@ -665,6 +669,44 @@ def extract_user_info(user): return JsonResponse(response_payload) +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_level('staff') +@require_query_params(send_to="sending to whom", subject="subject line", message="message text") +def send_email(request, course_id): + """ + Send an email to self, staff, or everyone involved in a course. + Query Paramaters: + - 'send_to' specifies what group the email should be sent to + - 'subject' specifies email's subject + - 'message' specifies email's content + """ + course = get_course_by_id(course_id) + has_instructor_access = has_access(request.user, course, 'instructor') + send_to = request.GET.get("send_to") + subject = request.GET.get("subject") + message = request.GET.get("message") + text_message = html_to_text(message) + if not has_instructor_access: + return HttpResponseForbidden("Operation requires instructor access.") + email = CourseEmail( + course_id = course_id, + sender=request.user, + to_option=send_to, + subject=subject, + html_message=message, + text_message=text_message + ) + email.save() + tasks.delegate_email_batches.delay( + email.id, + request.user.id + ) + response_payload = { + 'course_id': course_id, + } + return JsonResponse(response_payload) + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @require_level('staff') @@ -728,7 +770,6 @@ def update_forum_role_membership(request, course_id): } 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 8c67c24a7748..dcd67be4b9ad 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -2,7 +2,6 @@ Instructor API endpoint urls. """ - from django.conf.urls import patterns, url urlpatterns = patterns('', # nopep8 @@ -32,4 +31,6 @@ 'instructor.views.api.update_forum_role_membership', name="update_forum_role_membership"), url(r'^proxy_legacy_analytics$', 'instructor.views.api.proxy_legacy_analytics', name="proxy_legacy_analytics"), + url(r'^send_email$', + 'instructor.views.api.send_email', name="send_email") ) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 9a1bea222ea6..c4f6601c756d 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -9,21 +9,27 @@ from django.core.urlresolvers import reverse from django.utils.html import escape from django.http import Http404 +from django.conf import settings +from xmodule_modifiers import wrap_xmodule +from xmodule.html_module import HtmlDescriptor +from xmodule.modulestore import MONGO_MODULESTORE_TYPE +from xmodule.modulestore.django import modulestore +from xblock.field_data import DictFieldData +from xblock.fields import ScopeIds from courseware.access import has_access from courseware.courses import get_course_by_id from django_comment_client.utils import has_forum_access from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR -from xmodule.modulestore.django import modulestore from student.models import CourseEnrollment - @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course = get_course_by_id(course_id, depth=None) + is_studio_course = modulestore().get_modulestore_type(course_id) == MONGO_MODULESTORE_TYPE access = { 'admin': request.user.is_staff, @@ -42,9 +48,12 @@ def instructor_dashboard_2(request, course_id): _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id), - _section_analytics(course_id), + _section_analytics(course_id) ] + if settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and is_studio_course: + sections.append(_section_send_email(course_id,access,course)) + context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}), @@ -136,6 +145,18 @@ def _section_data_download(course_id): } return section_data +def _section_send_email(course_id, access, course): + """ Provide data for the corresponding bulk email section """ + html_module = HtmlDescriptor(course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, None)) + section_data = { + 'section_key': 'send_email', + 'section_display_name': _('Email'), + 'access': access, + 'send_email': reverse('send_email',kwargs={'course_id': course_id}), + 'editor': wrap_xmodule(html_module.get_html, html_module, 'xmodule_edit.html')() + } + return section_data + def _section_analytics(course_id): """ Provide data for the corresponding dashboard section """ diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 749a7004aaee..23fa4dd69886 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -57,12 +57,10 @@ from xblock.field_data import DictFieldData from xblock.fields import ScopeIds - from bulk_email.models import CourseEmail from html_to_text import html_to_text from bulk_email import tasks - log = logging.getLogger(__name__) # internal commands for managing forum roles: diff --git a/lms/static/coffee/src/instructor_dashboard/analytics.coffee b/lms/static/coffee/src/instructor_dashboard/analytics.coffee index d53b511e1ca2..018b7e9c57c0 100644 --- a/lms/static/coffee/src/instructor_dashboard/analytics.coffee +++ b/lms/static/coffee/src/instructor_dashboard/analytics.coffee @@ -1,8 +1,11 @@ -# Analytics Section +### +Analytics Section + +imports from other modules. +wrap in (-> ... apply) to defer evaluation +such that the value can be defined later than this assignment (file load order). +### -# imports from other modules. -# wrap in (-> ... apply) to defer evaluation -# such that the value can be defined later than this assignment (file load order). plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments diff --git a/lms/static/coffee/src/instructor_dashboard/course_info.coffee b/lms/static/coffee/src/instructor_dashboard/course_info.coffee index d48c7ba87312..19f9ce9707b2 100644 --- a/lms/static/coffee/src/instructor_dashboard/course_info.coffee +++ b/lms/static/coffee/src/instructor_dashboard/course_info.coffee @@ -1,10 +1,13 @@ -# Course Info Section -# This is the implementation of the simplest section -# of the instructor dashboard. +### +Course Info Section +This is the implementation of the simplest section +of the instructor dashboard. + +imports from other modules. +wrap in (-> ... apply) to defer evaluation +such that the value can be defined later than this assignment (file load order). +### -# imports from other modules. -# wrap in (-> ... apply) to defer evaluation -# such that the value can be defined later than this assignment (file load order). plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index cfd3534e0424..7e7a804b5ca5 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -1,8 +1,11 @@ -# Data Download Section +### +Data Download Section + +imports from other modules. +wrap in (-> ... apply) to defer evaluation +such that the value can be defined later than this assignment (file load order). +### -# imports from other modules. -# wrap in (-> ... apply) to defer evaluation -# such that the value can be defined later than this assignment (file load order). plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments diff --git a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee index a7c803f8acaa..c645fcf67e51 100644 --- a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee +++ b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee @@ -1,26 +1,31 @@ -# Instructor Dashboard Tab Manager -# The instructor dashboard is broken into sections. -# Only one section is visible at a time, -# and is responsible for its own functionality. -# -# NOTE: plantTimeout (which is just setTimeout from util.coffee) -# is used frequently in the instructor dashboard to isolate -# failures. If one piece of code under a plantTimeout fails -# then it will not crash the rest of the dashboard. -# -# NOTE: The instructor dashboard currently does not -# use backbone. Just lots of jquery. This should be fixed. -# -# NOTE: Server endpoints in the dashboard are stored in -# the 'data-endpoint' attribute of relevant html elements. -# The urls are rendered there by a template. -# -# NOTE: For an example of what a section object should look like -# see course_info.coffee - -# imports from other modules -# wrap in (-> ... apply) to defer evaluation -# such that the value can be defined later than this assignment (file load order). +### +Instructor Dashboard Tab Manager + +The instructor dashboard is broken into sections. + +Only one section is visible at a time, + and is responsible for its own functionality. + +NOTE: plantTimeout (which is just setTimeout from util.coffee) + is used frequently in the instructor dashboard to isolate + failures. If one piece of code under a plantTimeout fails + then it will not crash the rest of the dashboard. + +NOTE: The instructor dashboard currently does not + use backbone. Just lots of jquery. This should be fixed. + +NOTE: Server endpoints in the dashboard are stored in + the 'data-endpoint' attribute of relevant html elements. + The urls are rendered there by a template. + +NOTE: For an example of what a section object should look like + see course_info.coffee + +imports from other modules +wrap in (-> ... apply) to defer evaluation +such that the value can be defined later than this assignment (file load order). +### + plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments @@ -156,6 +161,9 @@ setup_instructor_dashboard_sections = (idash_content) -> , constructor: window.InstructorDashboard.sections.StudentAdmin $element: idash_content.find ".#{CSS_IDASH_SECTION}#student_admin" + , + constructor: window.InstructorDashboard.sections.Email + $element: idash_content.find ".#{CSS_IDASH_SECTION}#send_email" , constructor: window.InstructorDashboard.sections.Analytics $element: idash_content.find ".#{CSS_IDASH_SECTION}#analytics" diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee index a50cd2c3dd37..54b04be5db6a 100644 --- a/lms/static/coffee/src/instructor_dashboard/membership.coffee +++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee @@ -1,8 +1,11 @@ -# Membership Section +### +Membership Section + +imports from other modules. +wrap in (-> ... apply) to defer evaluation +such that the value can be defined later than this assignment (file load order). +### -# imports from other modules. -# wrap in (-> ... apply) to defer evaluation -# such that the value can be defined later than this assignment (file load order). plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments diff --git a/lms/static/coffee/src/instructor_dashboard/send_email.coffee b/lms/static/coffee/src/instructor_dashboard/send_email.coffee new file mode 100644 index 000000000000..af509a7d525e --- /dev/null +++ b/lms/static/coffee/src/instructor_dashboard/send_email.coffee @@ -0,0 +1,74 @@ +### +Email Section + +imports from other modules. +wrap in (-> ... apply) to defer evaluation +such that the value can be defined later than this assignment (file load order). +### + +plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments +std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments + +class SendEmail + constructor: (@$container) -> + # gather elements + @$emailEditor = XModule.loadModule($('.xmodule_edit')); + @$send_to = @$container.find("select[name='send_to']'") + @$subject = @$container.find("input[name='subject']'") + @$btn_send = @$container.find("input[name='send']'") + @$task_response = @$container.find(".request-response") + @$request_response_error = @$container.find(".request-response-error") + + # attach click handlers + + @$btn_send.click => + + send_data = + action: 'send' + send_to: @$send_to.val() + subject: @$subject.val() + message: @$emailEditor.save()['data'] + + $.ajax + dataType: 'json' + url: @$btn_send.data 'endpoint' + data: send_data + success: (data) => @display_response gettext('Your email was successfully queued for sending.') + error: std_ajax_err => @fail_with_error gettext('Error sending email.') + + fail_with_error: (msg) -> + console.warn msg + @$task_response.empty() + @$request_response_error.empty() + @$request_response_error.text gettext(msg) + + display_response: (data_from_server) -> + @$task_response.empty() + @$request_response_error.empty() + @$task_response.text(gettext('Your email was successfully queued for sending.')) + + +# Email Section +class Email + # enable subsections. + constructor: (@$section) -> + # attach self to html + # so that instructor_dashboard.coffee can find this object + # to call event handlers like 'onClickTitle' + @$section.data 'wrapper', @ + + # isolate # initialize SendEmail subsection + plantTimeout 0, => new SendEmail @$section.find '.send-email' + + # handler for when the section title is clicked. + onClickTitle: -> + + +# export for use +# create parent namespaces if they do not already exist. +# abort if underscore can not be found. +if _? + _.defaults window, InstructorDashboard: {} + _.defaults window.InstructorDashboard, sections: {} + _.defaults window.InstructorDashboard.sections, + Email: Email diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee index 7e40eb98d4a3..d5f38cf23f33 100644 --- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee +++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee @@ -1,8 +1,11 @@ -# Student Admin Section +### +Student Admin Section + +imports from other modules. +wrap in (-> ... apply) to defer evaluation +such that the value can be defined later than this assignment (file load order). +### -# imports from other modules. -# wrap in (-> ... apply) to defer evaluation -# such that the value can be defined later than this assignment (file load order). plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments plantInterval = -> window.InstructorDashboard.util.plantInterval.apply this, arguments std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 61dab3ef1c8c..684a314f051c 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -110,7 +110,6 @@ section.instructor-dashboard-content-2 { } } - .instructor-dashboard-wrapper-2 section.idash-section#course_info { .course-errors-wrapper { margin-top: 2em; diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index d3fc66d5c639..4ae09301db66 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -18,6 +18,12 @@ + + + + + + <%static:js group='module-descriptor-js'/> %block> ## NOTE that instructor is set as the active page so that the instructor button lights up, even though this is the instructor_2 page. diff --git a/lms/templates/instructor/instructor_dashboard_2/send_email.html b/lms/templates/instructor/instructor_dashboard_2/send_email.html new file mode 100644 index 000000000000..68fd0938a176 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/send_email.html @@ -0,0 +1,43 @@ +<%! from django.utils.translation import ugettext as _ %> +<%page args="section_data"/> + + + +