Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand Down Expand Up @@ -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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameters

- '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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scope of this may not just be limited to this PR (since from a cursory glance it appears the rest of the new dashboard works this way), but I'm pretty sure that we don't want to be passing the message in a GET parameter. That seems like a lot of data to stuff into a URL. I think this should naturally be a POST, no?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's some nginx documentation on the size limits of http request headers:
http://wiki.nginx.org/NginxHttpCoreModule#large_client_header_buffers
It seems to me that it's not impossible to exceed 8K for HTML formatted email messages, so I think it has to be POST.

text_message = html_to_text(message)
if not has_instructor_access:
return HttpResponseForbidden("Operation requires instructor access.")
email = CourseEmail(
course_id = course_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't put spaces around this equal sign

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a short function, but there's a lot to test here. I'd start by thinking about possible paths:

  1. User is logged in and has instructor access; course exists (happy path)
  2. User does not have instructor access.
  3. User is not logged in.
  4. Course does not exist.

In addition, there are some unhappy paths to test:

  1. Request is missing the "send to" parameter.
  2. Request is missing the "subject" parameter.
  3. Request is missing the "message" parameter.

I'd also expect at least one test with non-ASCII unicode in each of the user-specified fields (this could be your happy-path test). In Python, using unicode incorrectly can cause exceptions, which means users will see a 500 error. Ned gave a great talk about this: see http://nedbatchelder.com/text/unipain.html

Since this code involves JavaScript / Python integration, I'd expect one happy-path test at the UI level. This is something I can help with, since we don't have any existing UI-level tests for the instuctor dash.


@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
Expand Down Expand Up @@ -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')
Expand Down
3 changes: 2 additions & 1 deletion lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
Instructor API endpoint urls.
"""


from django.conf.urls import patterns, url

urlpatterns = patterns('', # nopep8
Expand Down Expand Up @@ -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")
)
27 changes: 24 additions & 3 deletions lms/djangoapps/instructor/views/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pep 8 here.


context = {
'course': course,
'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}),
Expand Down Expand Up @@ -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}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put a space after the comma

'editor': wrap_xmodule(html_module.get_html, html_module, 'xmodule_edit.html')()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should look at what @cpennington did with the editor in https://github.com/edx/edx-platform/pull/945/files (legacy.py) and copy that logic instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well... Except that that logic only works if you've got the changes from my branch.

Sadly, whether you need to change this or not depends on whether you merge first or whether #945 does.

}
return section_data


def _section_analytics(course_id):
""" Provide data for the corresponding dashboard section """
Expand Down
2 changes: 0 additions & 2 deletions lms/djangoapps/instructor/views/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 7 additions & 4 deletions lms/static/coffee/src/instructor_dashboard/analytics.coffee
Original file line number Diff line number Diff line change
@@ -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

Expand Down
15 changes: 9 additions & 6 deletions lms/static/coffee/src/instructor_dashboard/course_info.coffee
Original file line number Diff line number Diff line change
@@ -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

Expand Down
11 changes: 7 additions & 4 deletions lms/static/coffee/src/instructor_dashboard/data_download.coffee
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"
Expand Down
11 changes: 7 additions & 4 deletions lms/static/coffee/src/instructor_dashboard/membership.coffee
Original file line number Diff line number Diff line change
@@ -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

Expand Down
74 changes: 74 additions & 0 deletions lms/static/coffee/src/instructor_dashboard/send_email.coffee
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could make it a POST here.

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
11 changes: 7 additions & 4 deletions lms/static/coffee/src/instructor_dashboard/student_admin.coffee
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion lms/static/sass/course/instructor/_instructor_2.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ section.instructor-dashboard-content-2 {
}
}


.instructor-dashboard-wrapper-2 section.idash-section#course_info {
.course-errors-wrapper {
margin-top: 2em;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
<script type="text/javascript" src="${static.url('js/vendor/slick.grid.js')}"></script>
<link rel="stylesheet" href="${static.url('css/vendor/slickgrid/smoothness/jquery-ui-1.8.16.custom.css')}">
<link rel="stylesheet" href="${static.url('css/vendor/slickgrid/slick.grid.css')}">
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/htmlmixed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/codemirror-compressed.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/tiny_mce/tiny_mce.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/tiny_mce/jquery.tinymce.js')}"></script>
<%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.
Expand Down
Loading