Commit 07990336 by alisan617 Committed by GitHub

Merge pull request #13681 from edx/alisan/instructor-dashboard-coffeescript

Convert instructor dashboard coffeeScript to js
parents e1a0072b 5fe397f8
......@@ -1319,10 +1319,7 @@ discussion_vendor_js = [
]
notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.js'))
instructor_dash_js = (
sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/instructor_dashboard/**/*.js')) +
sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/instructor_dashboard/**/*.js'))
)
instructor_dash_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/instructor_dashboard/**/*.js'))
verify_student_js = [
'js/sticky_filter.js',
......
describe 'AutoEnrollment', ->
beforeEach ->
loadFixtures 'coffee/fixtures/autoenrollment.html'
@autoenrollment = new AutoEnrollmentViaCsv $('.auto_enroll_csv')
it 'binds the ajax call and the result will be success', ->
spyOn($, "ajax").and.callFake((params) =>
params.success({row_errors: [], general_errors: [], warnings: []})
{always: ->}
)
# mock the render_notification_view which returns the html (since we are only using the existing notification model)
@autoenrollment.render_notification_view = jasmine.createSpy("render_notification_view(type, title, message, details) spy").and.callFake =>
return '<div><div class="message message-confirmation"><h3 class="message-title">Success</h3><div class="message-copy"><p>All accounts were created successfully.</p></div></div><div>'
submitCallback = jasmine.createSpy().and.returnValue()
@autoenrollment.$student_enrollment_form.submit(submitCallback)
@autoenrollment.$enrollment_signup_button.click()
expect($('.results .message-copy').text()).toEqual('All accounts were created successfully.')
expect(submitCallback).toHaveBeenCalled()
it 'binds the ajax call and the result will be error', ->
spyOn($, "ajax").and.callFake((params) =>
params.success({
row_errors: [{
'username': 'testuser1',
'email': 'testemail1@email.com',
'response': 'Username already exists'
}],
general_errors: [{
'response': 'cannot read the line 2'
}],
warnings: []
})
{always: ->}
)
# mock the render_notification_view which returns the html (since we are only using the existing notification model)
@autoenrollment.render_notification_view = jasmine.createSpy("render_notification_view(type, title, message, details) spy").and.callFake =>
return '<div><div class="message message-error"><h3 class="message-title">Errors</h3><div class="message-copy"><p>The following errors were generated:</p><ul class="list-summary summary-items"><li class="summary-item">cannot read the line 2</li><li class="summary-item">testuser1 (testemail1@email.com): (Username already exists)</li></ul></div></div></div>'
submitCallback = jasmine.createSpy().and.returnValue()
@autoenrollment.$student_enrollment_form.submit(submitCallback)
@autoenrollment.$enrollment_signup_button.click()
expect($('.results .list-summary').text()).toEqual('cannot read the line 2testuser1 (testemail1@email.com): (Username already exists)');
expect(submitCallback).toHaveBeenCalled()
it 'binds the ajax call and the result will be warnings', ->
spyOn($, "ajax").and.callFake((params) =>
params.success({
row_errors: [],
general_errors: [],
warnings: [{
'username': 'user1',
'email': 'user1email',
'response': 'email is in valid'
}]
})
{always: ->}
)
# mock the render_notification_view which returns the html (since we are only using the existing notification model)
@autoenrollment.render_notification_view = jasmine.createSpy("render_notification_view(type, title, message, details) spy").and.callFake =>
return '<div><div class="message message-warning"><h3 class="message-title">Warnings</h3><div class="message-copy"><p>The following warnings were generated:</p><ul class="list-summary summary-items"><li class="summary-item">user1 (user1email): (email is in valid)</li></ul></div></div></div>'
submitCallback = jasmine.createSpy().and.returnValue()
@autoenrollment.$student_enrollment_form.submit(submitCallback)
@autoenrollment.$enrollment_signup_button.click()
expect($('.results .list-summary').text()).toEqual('user1 (user1email): (email is in valid)')
expect(submitCallback).toHaveBeenCalled()
\ No newline at end of file
describe "Bulk Email Queueing", ->
beforeEach ->
testSubject = "Test Subject"
testBody = "Hello, World! This is a test email message!"
loadFixtures 'coffee/fixtures/send_email.html'
@send_email = new SendEmail $('.send-email')
@send_email.$subject.val(testSubject)
@send_email.$send_to.first().prop("checked", true)
@send_email.$emailEditor =
save: ->
{"data": testBody}
@ajax_params = {
type: "POST",
dataType: "json",
url: undefined,
data: {
action: "send",
send_to: JSON.stringify([@send_email.$send_to.first().val()]),
subject: testSubject,
message: testBody,
},
success: jasmine.any(Function),
error: jasmine.any(Function),
}
it 'cannot send an email with no target', ->
spyOn(window, "alert")
spyOn($, "ajax")
for target in @send_email.$send_to
target.checked = false
@send_email.$btn_send.click()
expect(window.alert).toHaveBeenCalledWith("Your message must have at least one target.")
expect($.ajax).not.toHaveBeenCalled()
it 'cannot send an email with no subject', ->
spyOn(window, "alert")
spyOn($, "ajax")
@send_email.$subject.val("")
@send_email.$btn_send.click()
expect(window.alert).toHaveBeenCalledWith("Your message must have a subject.")
expect($.ajax).not.toHaveBeenCalled()
it 'cannot send an email with no message', ->
spyOn(window, "alert")
spyOn($, "ajax")
@send_email.$emailEditor =
save: ->
{"data": ""}
@send_email.$btn_send.click()
expect(window.alert).toHaveBeenCalledWith("Your message cannot be blank.")
expect($.ajax).not.toHaveBeenCalled()
it 'can send a simple message to a single target', ->
spyOn($, "ajax").and.callFake((params) =>
params.success()
)
@send_email.$btn_send.click()
expect($('.msg-confirm').text()).toEqual('Your email message was successfully queued for sending. In courses with a large number of learners, email messages to learners might take up to an hour to be sent.')
expect($.ajax).toHaveBeenCalledWith(@ajax_params)
it 'can send a simple message to a multiple targets', ->
spyOn($, "ajax").and.callFake((params) =>
params.success()
)
@ajax_params.data.send_to = JSON.stringify(target.value for target in @send_email.$send_to)
for target in @send_email.$send_to
target.checked = true
@send_email.$btn_send.click()
expect($('.msg-confirm').text()).toEqual('Your email message was successfully queued for sending. In courses with a large number of learners, email messages to learners might take up to an hour to be sent.')
expect($.ajax).toHaveBeenCalledWith(@ajax_params)
it 'can handle an error result from the bulk email api', ->
spyOn($, "ajax").and.callFake((params) =>
params.error()
)
spyOn(console, "warn")
@send_email.$btn_send.click()
expect($('.request-response-error').text()).toEqual('Error sending email.')
expect(console.warn).toHaveBeenCalled()
it 'selecting all learners disables cohort selections', ->
@send_email.$send_to.filter("[value='learners']").click
@send_email.$cohort_targets.each ->
expect(this.disabled).toBe(true)
@send_email.$send_to.filter("[value='learners']").click
@send_email.$cohort_targets.each ->
expect(this.disabled).toBe(false)
it 'selected targets are listed after "send to:"', ->
@send_email.$send_to.click
$('input[name="send_to"]:checked+label').each ->
expect($('.send_to_list'.text())).toContain(this.innerText.replace(/\s*\n.*/g,''))
###
Course Info 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).
###
# Load utilities
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
# A typical section object.
# constructed with $section, a jquery object
# which holds the section body container.
class CourseInfo
constructor: (@$section) ->
# attach self to html so that instructor_dashboard.coffee can find
# this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# gather elements
@instructor_tasks = new (PendingInstructorTasks()) @$section
@$course_errors_wrapper = @$section.find '.course-errors-wrapper'
# if there are errors
if @$course_errors_wrapper.length
@$course_error_toggle = @$course_errors_wrapper.find '.toggle-wrapper'
@$course_error_toggle_text = @$course_error_toggle.find 'h2'
@$course_error_visibility_wrapper = @$course_errors_wrapper.find '.course-errors-visibility-wrapper'
@$course_errors = @$course_errors_wrapper.find '.course-error'
# append "(34)" to the course errors label
@$course_error_toggle_text.text @$course_error_toggle_text.text() + " (#{@$course_errors.length})"
# toggle .open class on errors
# to show and hide them.
@$course_error_toggle.click (e) =>
e.preventDefault()
if @$course_errors_wrapper.hasClass 'open'
@$course_errors_wrapper.removeClass 'open'
else
@$course_errors_wrapper.addClass 'open'
# handler for when the section title is clicked.
onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
onExit: -> @instructor_tasks.task_poller.stop()
# export for use
# create parent namespaces if they do not already exist.
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
CourseInfo: CourseInfo
###
E-Commerce Section
###
# Load utilities
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
ReportDownloads = -> window.InstructorDashboard.util.ReportDownloads
class ECommerce
# E-Commerce Section
constructor: (@$section) ->
# attach self to html so that instructor_dashboard.coffee can find
# this object to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# gather elements
@$list_sale_csv_btn = @$section.find("input[name='list-sale-csv']")
@$list_order_sale_csv_btn = @$section.find("input[name='list-order-sale-csv']")
@$download_company_name = @$section.find("input[name='download_company_name']")
@$active_company_name = @$section.find("input[name='active_company_name']")
@$spent_company_name = @$section.find('input[name="spent_company_name"]')
@$download_coupon_codes = @$section.find('input[name="download-coupon-codes-csv"]')
@$download_registration_codes_form = @$section.find("form#download_registration_codes")
@$active_registration_codes_form = @$section.find("form#active_registration_codes")
@$spent_registration_codes_form = @$section.find("form#spent_registration_codes")
@$reports = @$section.find '.reports-download-container'
@$reports_request_response = @$reports.find '.request-response'
@$reports_request_response_error = @$reports.find '.request-response-error'
@report_downloads = new (ReportDownloads()) @$section
@instructor_tasks = new (PendingInstructorTasks()) @$section
@$error_msg = @$section.find('#error-msg')
# attach click handlers
# this handler binds to both the download
# and the csv button
@$list_sale_csv_btn.click (e) =>
url = @$list_sale_csv_btn.data 'endpoint'
url += '/csv'
location.href = url
@$list_order_sale_csv_btn.click (e) =>
url = @$list_order_sale_csv_btn.data 'endpoint'
location.href = url
@$download_coupon_codes.click (e) =>
url = @$download_coupon_codes.data 'endpoint'
location.href = url
@$download_registration_codes_form.submit (e) =>
@$error_msg.attr('style', 'display: none')
return true
@$active_registration_codes_form.submit (e) =>
@$error_msg.attr('style', 'display: none')
return true
@$spent_registration_codes_form.submit (e) =>
@$error_msg.attr('style', 'display: none')
return true
# handler for when the section title is clicked.
onClickTitle: ->
@clear_display()
@instructor_tasks.task_poller.start()
@report_downloads.downloads_poller.start()
# handler for when the section is closed
onExit: ->
@clear_display()
@instructor_tasks.task_poller.stop()
@report_downloads.downloads_poller.stop()
clear_display: ->
@$error_msg.attr('style', 'display: none')
@$download_company_name.val('')
@$reports_request_response.empty()
@$reports_request_response_error.empty()
@$active_company_name.val('')
@$spent_company_name.val('')
isInt = (n) -> return n % 1 == 0;
# Clear any generated tables, warning messages, etc.
# export for use
# create parent namespaces if they do not already exist.
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
ECommerce: ECommerce
###
Extensions 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
# Extensions Section
class Extensions
constructor: (@$section) ->
# attach self to html
# so that instructor_dashboard.coffee can find this object
# to call event handlers like 'onClickTitle'
@$section.data 'wrapper', @
# Gather buttons
@$change_due_date = @$section.find("input[name='change-due-date']")
@$reset_due_date = @$section.find("input[name='reset-due-date']")
@$show_unit_extensions = @$section.find("input[name='show-unit-extensions']")
@$show_student_extensions = @$section.find("input[name='show-student-extensions']")
# Gather notification areas
@$section.find(".request-response").hide()
@$section.find(".request-response-error").hide()
# Gather grid elements
$grid_display = @$section.find '.data-display'
@$grid_text = $grid_display.find '.data-display-text'
@$grid_table = $grid_display.find '.data-display-table'
# Click handlers
@$change_due_date.click =>
@clear_display()
@$student_input = @$section.find("#set-extension input[name='student']")
@$url_input = @$section.find("#set-extension select[name='url']")
@$due_datetime_input = @$section.find("#set-extension input[name='due_datetime']")
send_data =
student: @$student_input.val()
url: @$url_input.val()
due_datetime: @$due_datetime_input.val()
$.ajax
type: 'POST'
dataType: 'json'
url: @$change_due_date.data 'endpoint'
data: send_data
success: (data) => @display_response "set-extension", data
error: (xhr) => @fail_with_error "set-extension", "Error changing due date", xhr
@$reset_due_date.click =>
@clear_display()
@$student_input = @$section.find("#reset-extension input[name='student']")
@$url_input = @$section.find("#reset-extension select[name='url']")
send_data =
student: @$student_input.val()
url: @$url_input.val()
$.ajax
type: 'POST'
dataType: 'json'
url: @$reset_due_date.data 'endpoint'
data: send_data
success: (data) => @display_response "reset-extension", data
error: (xhr) => @fail_with_error "reset-extension", "Error reseting due date", xhr
@$show_unit_extensions.click =>
@clear_display()
@$grid_table.text 'Loading'
@$url_input = @$section.find("#view-granted-extensions select[name='url']")
url = @$show_unit_extensions.data 'endpoint'
send_data =
url: @$url_input.val()
$.ajax
type: 'POST'
dataType: 'json'
url: url
data: send_data
error: (xhr) => @fail_with_error "view-granted-extensions", "Error getting due dates", xhr
success: (data) => @display_grid data
@$show_student_extensions.click =>
@clear_display()
@$grid_table.text 'Loading'
url = @$show_student_extensions.data 'endpoint'
@$student_input = @$section.find("#view-granted-extensions input[name='student']")
send_data =
student: @$student_input.val()
$.ajax
type: 'POST'
dataType: 'json'
url: url
data: send_data
error: (xhr) => @fail_with_error "view-granted-extensions", "Error getting due dates", xhr
success: (data) => @display_grid data
# handler for when the section title is clicked.
onClickTitle: ->
fail_with_error: (id, msg, xhr) ->
$task_error = @$section.find("#" + id + " .request-response-error")
$task_response = @$section.find("#" + id + " .request-response")
@clear_display()
data = $.parseJSON xhr.responseText
msg += ": " + data['error']
console.warn msg
$task_response.empty()
$task_error.empty()
$task_error.text msg
$task_error.show()
display_response: (id, data) ->
$task_error = @$section.find("#" + id + " .request-response-error")
$task_response = @$section.find("#" + id + " .request-response")
$task_error.empty().hide()
$task_response.empty().text data
$task_response.show()
display_grid: (data) ->
@clear_display()
@$grid_text.text data.title
# display on a SlickGrid
options =
enableCellNavigation: true
enableColumnReorder: false
forceFitColumns: true
columns = ({id: col, field: col, name: col} for col in data.header)
grid_data = data.data
$table_placeholder = $ '<div/>', class: 'slickgrid', style: 'min-height: 400px'
@$grid_table.append $table_placeholder
grid = new Slick.Grid($table_placeholder, grid_data, columns, options)
clear_display: ->
@$grid_text.empty()
@$grid_table.empty()
@$section.find(".request-response-error").empty().hide()
@$section.find(".request-response").empty().hide()
# 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,
Extensions: Extensions
###
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
# CSS classes
CSS_INSTRUCTOR_CONTENT = 'instructor-dashboard-content-2'
CSS_ACTIVE_SECTION = 'active-section'
CSS_IDASH_SECTION = 'idash-section'
CSS_INSTRUCTOR_NAV = 'instructor-nav'
# prefix for deep-linking
HASH_LINK_PREFIX = '#view-'
$active_section = null
# helper class for queueing and fault isolation.
# Will execute functions marked by waiter.after only after all functions marked by
# waiter.waitFor have been called.
# To guarantee this functionality, waitFor and after must be called
# before the functions passed to waitFor are called.
class SafeWaiter
constructor: ->
@after_handlers = []
@waitFor_handlers = []
@fired = false
after: (f) ->
if @fired
f()
else
@after_handlers.push f
waitFor: (f) ->
return if @fired
@waitFor_handlers.push f
# wrap the function so that it notifies the waiter
# and can fire the after handlers.
=>
@waitFor_handlers = @waitFor_handlers.filter (g) -> g isnt f
if @waitFor_handlers.length is 0
@fired = true
@after_handlers.map (cb) -> plantTimeout 0, cb
f.apply this, arguments
# waiter for dashboard sections.
# Will only execute after all sections have at least attempted to load.
# This is here to facilitate section constructors isolated by setTimeout
# while still being able to interact with them under the guarantee
# that the sections will be initialized at call time.
sections_have_loaded = new SafeWaiter
# once we're ready, check if this page is the instructor dashboard
$ =>
instructor_dashboard_content = $ ".#{CSS_INSTRUCTOR_CONTENT}"
if instructor_dashboard_content.length > 0
setup_instructor_dashboard instructor_dashboard_content
setup_instructor_dashboard_sections instructor_dashboard_content
# enable navigation bar
# handles hiding and showing sections
setup_instructor_dashboard = (idash_content) =>
# clickable section titles
$links = idash_content.find(".#{CSS_INSTRUCTOR_NAV}").find('.btn-link')
# attach link click handlers
$links.each (i, link) ->
$(link).click (e) ->
e.preventDefault()
# deactivate all link & section styles
idash_content.find(".#{CSS_INSTRUCTOR_NAV} li").children().removeClass CSS_ACTIVE_SECTION
idash_content.find(".#{CSS_INSTRUCTOR_NAV} li").children().attr('aria-pressed', 'false')
idash_content.find(".#{CSS_IDASH_SECTION}").removeClass CSS_ACTIVE_SECTION
# discover section paired to link
section_name = $(this).data 'section'
$section = idash_content.find "##{section_name}"
# activate link & section styling
$(this).addClass CSS_ACTIVE_SECTION
$(this).attr('aria-pressed','true')
$section.addClass CSS_ACTIVE_SECTION
# tracking
analytics.pageview "instructor_section:#{section_name}"
# deep linking
# write to url
location.hash = "#{HASH_LINK_PREFIX}#{section_name}"
sections_have_loaded.after ->
$section.data('wrapper').onClickTitle()
# call onExit handler if exiting a section to a different section.
unless $section.is $active_section
$active_section?.data('wrapper')?.onExit?()
$active_section = $section
# TODO enable onExit handler
# activate an initial section by 'clicking' on it.
# check for a deep-link, or click the first link.
click_first_link = ->
link = $links.eq(0)
link.click()
if (new RegExp "^#{HASH_LINK_PREFIX}").test location.hash
rmatch = (new RegExp "^#{HASH_LINK_PREFIX}(.*)").exec location.hash
section_name = rmatch[1]
link = $links.filter "[data-section='#{section_name}']"
if link.length == 1
link.click()
else
click_first_link()
else
click_first_link()
# enable sections
setup_instructor_dashboard_sections = (idash_content) ->
sections_to_initialize = [
constructor: window.InstructorDashboard.sections.CourseInfo
$element: idash_content.find ".#{CSS_IDASH_SECTION}#course_info"
,
constructor: window.InstructorDashboard.sections.DataDownload
$element: idash_content.find ".#{CSS_IDASH_SECTION}#data_download"
,
constructor: window.InstructorDashboard.sections.ECommerce
$element: idash_content.find ".#{CSS_IDASH_SECTION}#e-commerce"
,
constructor: window.InstructorDashboard.sections.Membership
$element: idash_content.find ".#{CSS_IDASH_SECTION}#membership"
,
constructor: window.InstructorDashboard.sections.StudentAdmin
$element: idash_content.find ".#{CSS_IDASH_SECTION}#student_admin"
,
constructor: window.InstructorDashboard.sections.Extensions
$element: idash_content.find ".#{CSS_IDASH_SECTION}#extensions"
,
constructor: window.InstructorDashboard.sections.Email
$element: idash_content.find ".#{CSS_IDASH_SECTION}#send_email"
,
constructor: window.InstructorDashboard.sections.InstructorAnalytics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#instructor_analytics"
,
constructor: window.InstructorDashboard.sections.Metrics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#metrics"
,
constructor: window.InstructorDashboard.sections.CohortManagement
$element: idash_content.find ".#{CSS_IDASH_SECTION}#cohort_management"
,
constructor: window.InstructorDashboard.sections.Certificates
$element: idash_content.find ".#{CSS_IDASH_SECTION}#certificates"
]
# proctoring can be feature disabled
if edx.instructor_dashboard.proctoring != undefined
sections_to_initialize = sections_to_initialize.concat [
constructor: edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView
$element: idash_content.find ".#{CSS_IDASH_SECTION}#special_exams"
,
constructor: edx.instructor_dashboard.proctoring.ProctoredExamAttemptView
$element: idash_content.find ".#{CSS_IDASH_SECTION}#special_exams"
]
sections_to_initialize.map ({constructor, $element}) ->
# See fault isolation NOTE at top of file.
# If an error is thrown in one section, it will not stop other sections from exectuing.
plantTimeout 0, sections_have_loaded.waitFor ->
new constructor $element
# METRICS 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
#Metrics Section
class Metrics
constructor: (@$section) ->
@$section.data 'wrapper', @
# 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,
Metrics: Metrics
###
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).
###
# Load utilities
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
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
KeywordValidator = -> window.InstructorDashboard.util.KeywordValidator
class @SendEmail
constructor: (@$container) ->
# gather elements
@$emailEditor = XBlock.initializeBlock($('.xblock-studio_view'));
@$send_to = @$container.find("input[name='send_to']")
@$cohort_targets = @$send_to.filter('[value^="cohort:"]')
@$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")
@$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
@$btn_send.click =>
subject = @$subject.val()
body = @$emailEditor.save()['data']
targets = []
@$send_to.filter(':checked').each ->
targets.push(this.value)
if subject == ""
alert gettext("Your message must have a subject.")
else if body == ""
alert gettext("Your message cannot be blank.")
else if targets.length == 0
alert gettext("Your message must have at least one target.")
else
# Validation for keyword substitution
validation = KeywordValidator().validate_string body
if not validation.is_valid
message = gettext("There are invalid keywords in your email. Check the following keywords and try again.")
message += "\n" + validation.invalid_keywords.join('\n')
alert message
return
display_target = (value) ->
if value == "myself"
gettext("Yourself")
else if value == "staff"
gettext("Everyone who has staff privileges in this course")
else if value == "learners"
gettext("All learners who are enrolled in this course")
else
gettext("All learners in the {cohort_name} cohort").replace('{cohort_name}', value.slice(value.indexOf(':')+1))
success_message = gettext("Your email message was successfully queued for sending. In courses with a large number of learners, email messages to learners might take up to an hour to be sent.")
confirm_message = gettext("You are sending an email message with the subject {subject} to the following recipients.")
for target in targets
confirm_message += "\n-" + display_target(target)
confirm_message += "\n\n" + gettext("Is this OK?")
full_confirm_message = confirm_message.replace('{subject}', subject)
if confirm full_confirm_message
send_data =
action: 'send'
send_to: JSON.stringify(targets)
subject: subject
message: body
$.ajax
type: 'POST'
dataType: 'json'
url: @$btn_send.data 'endpoint'
data: send_data
success: (data) =>
@display_response success_message
error: std_ajax_err =>
@fail_with_error gettext('Error sending email.')
else
@task_response.empty()
@$request_response_error.empty()
# list task history for email
@$btn_task_history_email.click =>
url = @$btn_task_history_email.data 'endpoint'
$.ajax
type: 'POST'
dataType: 'json'
url: url
success: (data) =>
if data.tasks.length
create_task_list_table @$table_task_history_email, data.tasks
else
@$history_request_response_error.text gettext("There is no email history for this course.")
# Enable the msg-warning css display
@$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
type: 'POST'
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.")
@$send_to.change =>
# Ensure invalid combinations are disabled
if $('input#target_learners:checked').length
# If all is selected, cohorts can't be
@$cohort_targets.each ->
this.checked = false
this.disabled = true
true
else
@$cohort_targets.each ->
this.disabled = false
true
# Also, keep the sent_to_list div updated
targets = []
$('input[name="send_to"]:checked+label').each ->
# Only use the first line, even if a subheading is present
targets.push(this.innerText.replace(/\s*\n.*/g,''))
$(".send_to_list").text(gettext("Send to:") + " " + targets.join(", "))
fail_with_error: (msg) ->
console.warn msg
@$task_response.empty()
@$request_response_error.empty()
@$request_response_error.text msg
$(".msg-confirm").css({"display":"none"})
display_response: (data_from_server) ->
@$task_response.empty()
@$request_response_error.empty()
@$task_response.text(data_from_server)
$(".msg-confirm").css({"display":"block"})
# 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'
@instructor_tasks = new (PendingInstructorTasks()) @$section
# handler for when the section title is clicked.
onClickTitle: -> @instructor_tasks.task_poller.start()
# handler for when the section is closed
onExit: -> @instructor_tasks.task_poller.stop()
# export for use
# create parent namespaces if they do not already exist.
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
Email: Email
(function() {
'use strict';
var InstructorDashboardCourseInfo, PendingInstructorTasks;
PendingInstructorTasks = function() {
return window.InstructorDashboard.util.PendingInstructorTasks;
};
InstructorDashboardCourseInfo = (function() {
function CourseInfo($section) {
var courseInfo = this;
this.$section = $section;
this.$section.data('wrapper', this);
this.instructor_tasks = new (PendingInstructorTasks())(this.$section);
this.$course_errors_wrapper = this.$section.find('.course-errors-wrapper');
if (this.$course_errors_wrapper.length) {
this.$course_error_toggle = this.$course_errors_wrapper.find('.toggle-wrapper');
this.$course_error_toggle_text = this.$course_error_toggle.find('h2');
this.$course_errors = this.$course_errors_wrapper.find('.course-error');
this.$course_error_toggle_text.text(
this.$course_error_toggle_text.text() + (" (' + this.$course_errors.length + ')")
);
this.$course_error_toggle.click(function(e) {
e.preventDefault();
if (courseInfo.$course_errors_wrapper.hasClass('open')) {
return courseInfo.$course_errors_wrapper.removeClass('open');
} else {
return courseInfo.$course_errors_wrapper.addClass('open');
}
});
}
}
CourseInfo.prototype.onClickTitle = function() {
return this.instructor_tasks.task_poller.start();
};
CourseInfo.prototype.onExit = function() {
return this.instructor_tasks.task_poller.stop();
};
return CourseInfo;
}());
window.InstructorDashboard.sections.CourseInfo = InstructorDashboardCourseInfo;
}).call(this);
/* globals _ */
(function() {
'use strict';
var ECommerce, PendingInstructorTasks, ReportDownloads;
PendingInstructorTasks = function() {
return window.InstructorDashboard.util.PendingInstructorTasks;
};
ReportDownloads = function() {
return window.InstructorDashboard.util.ReportDownloads;
};
ECommerce = (function() {
function eCommerce($section) {
var eCom = this;
this.$section = $section;
this.$section.data('wrapper', this);
this.$list_sale_csv_btn = this.$section.find("input[name='list-sale-csv']");
this.$list_order_sale_csv_btn = this.$section.find("input[name='list-order-sale-csv']");
this.$download_company_name = this.$section.find("input[name='download_company_name']");
this.$active_company_name = this.$section.find("input[name='active_company_name']");
this.$spent_company_name = this.$section.find('input[name="spent_company_name"]');
this.$download_coupon_codes = this.$section.find('input[name="download-coupon-codes-csv"]');
this.$download_registration_codes_form = this.$section.find('form#download_registration_codes');
this.$active_registration_codes_form = this.$section.find('form#active_registration_codes');
this.$spent_registration_codes_form = this.$section.find('form#spent_registration_codes');
this.$reports = this.$section.find('.reports-download-container');
this.$reports_request_response = this.$reports.find('.request-response');
this.$reports_request_response_error = this.$reports.find('.request-response-error');
this.report_downloads = new (ReportDownloads())(this.$section);
this.instructor_tasks = new (PendingInstructorTasks())(this.$section);
this.$error_msg = this.$section.find('#error-msg');
this.$list_sale_csv_btn.click(function() {
location.href = eCom.$list_sale_csv_btn.data('endpoint') + '/csv';
return location.href;
});
this.$list_order_sale_csv_btn.click(function() {
location.href = eCom.$list_order_sale_csv_btn.data('endpoint');
return location.href;
});
this.$download_coupon_codes.click(function() {
location.href = eCom.$download_coupon_codes.data('endpoint');
return location.href;
});
this.$download_registration_codes_form.submit(function() {
eCom.$error_msg.attr('style', 'display: none');
return true;
});
this.$active_registration_codes_form.submit(function() {
eCom.$error_msg.attr('style', 'display: none');
return true;
});
this.$spent_registration_codes_form.submit(function() {
eCom.$error_msg.attr('style', 'display: none');
return true;
});
}
eCommerce.prototype.onClickTitle = function() {
this.clear_display();
this.instructor_tasks.task_poller.start();
return this.report_downloads.downloads_poller.start();
};
eCommerce.prototype.onExit = function() {
this.clear_display();
this.instructor_tasks.task_poller.stop();
return this.report_downloads.downloads_poller.stop();
};
eCommerce.prototype.clear_display = function() {
this.$error_msg.attr('style', 'display: none');
this.$download_company_name.val('');
this.$reports_request_response.empty();
this.$reports_request_response_error.empty();
this.$active_company_name.val('');
return this.$spent_company_name.val('');
};
return eCommerce;
}());
_.defaults(window, {
InstructorDashboard: {}
});
_.defaults(window.InstructorDashboard, {
sections: {}
});
_.defaults(window.InstructorDashboard.sections, {
ECommerce: ECommerce
});
}).call(this);
/* globals _ */
(function() {
'use strict';
var Extensions;
Extensions = (function() {
function extensions($section) {
var $gridDisplay,
ext = this;
this.$section = $section;
this.$section.data('wrapper', this);
this.$change_due_date = this.$section.find("input[name='change-due-date']");
this.$reset_due_date = this.$section.find("input[name='reset-due-date']");
this.$show_unit_ext = this.$section.find("input[name='show-unit-extensions']");
this.$show_student_ext = this.$section.find("input[name='show-student-extensions']");
this.$section.find('.request-response').hide();
this.$section.find('.request-response-error').hide();
$gridDisplay = this.$section.find('.data-display');
this.$grid_text = $gridDisplay.find('.data-display-text');
this.$grid_table = $gridDisplay.find('.data-display-table');
this.$change_due_date.click(function() {
var sendData;
ext.clear_display();
ext.$student_input = ext.$section.find("#set-extension input[name='student']");
ext.$url_input = ext.$section.find("#set-extension select[name='url']");
ext.$due_datetime_input = ext.$section.find("#set-extension input[name='due_datetime']");
sendData = {
student: ext.$student_input.val(),
url: ext.$url_input.val(),
due_datetime: ext.$due_datetime_input.val()
};
return $.ajax({
type: 'POST',
dataType: 'json',
url: ext.$change_due_date.data('endpoint'),
data: sendData,
success: function(data) {
return ext.display_response('set-extension', data);
},
error: function(xhr) {
return ext.fail_with_error('set-extension', 'Error changing due date', xhr);
}
});
});
this.$reset_due_date.click(function() {
var sendData;
ext.clear_display();
ext.$student_input = ext.$section.find("#reset-extension input[name='student']");
ext.$url_input = ext.$section.find("#reset-extension select[name='url']");
sendData = {
student: ext.$student_input.val(),
url: ext.$url_input.val()
};
return $.ajax({
type: 'POST',
dataType: 'json',
url: ext.$reset_due_date.data('endpoint'),
data: sendData,
success: function(data) {
return ext.display_response('reset-extension', data);
},
error: function(xhr) {
return ext.fail_with_error('reset-extension', 'Error reseting due date', xhr);
}
});
});
this.$show_unit_ext.click(function() {
var sendData, url;
ext.clear_display();
ext.$grid_table.text('Loading');
ext.$url_input = ext.$section.find("#view-granted-extensions select[name='url']");
url = ext.$show_unit_ext.data('endpoint');
sendData = {
url: ext.$url_input.val()
};
return $.ajax({
type: 'POST',
dataType: 'json',
url: url,
data: sendData,
error: function(xhr) {
return ext.fail_with_error('view-granted-extensions', 'Error getting due dates', xhr);
},
success: function(data) {
return ext.display_grid(data);
}
});
});
this.$show_student_ext.click(function() {
var sendData, url;
ext.clear_display();
ext.$grid_table.text('Loading');
url = ext.$show_student_ext.data('endpoint');
ext.$student_input = ext.$section.find("#view-granted-extensions input[name='student']");
sendData = {
student: ext.$student_input.val()
};
return $.ajax({
type: 'POST',
dataType: 'json',
url: url,
data: sendData,
error: function(xhr) {
return ext.fail_with_error('view-granted-extensions', 'Error getting due dates', xhr);
},
success: function(data) {
return ext.display_grid(data);
}
});
});
}
extensions.prototype.onClickTitle = function() {};
extensions.prototype.fail_with_error = function(id, msg, xhr) {
var $taskError, $taskResponse, data,
message = msg;
$taskError = this.$section.find('#' + id + ' .request-response-error');
$taskResponse = this.$section.find('#' + id + ' .request-response');
this.clear_display();
data = $.parseJSON(xhr.responseText);
message += ': ' + data.error;
$taskResponse.empty();
$taskError.empty();
$taskError.text(message);
return $taskError.show();
};
extensions.prototype.display_response = function(id, data) {
var $taskError, $taskResponse;
$taskError = this.$section.find('#' + id + ' .request-response-error');
$taskResponse = this.$section.find('#' + id + ' .request-response');
$taskError.empty().hide();
$taskResponse.empty().text(data);
return $taskResponse.show();
};
extensions.prototype.display_grid = function(data) {
var $tablePlaceholder, col, columns, gridData, options;
this.clear_display();
this.$grid_text.text(data.title);
options = {
enableCellNavigation: true,
enableColumnReorder: false,
forceFitColumns: true
};
columns = (function() {
var i, len, ref, results;
ref = data.header;
results = [];
for (i = 0, len = ref.length; i < len; i++) {
col = ref[i];
results.push({
id: col,
field: col,
name: col
});
}
return results;
}());
gridData = data.data;
$tablePlaceholder = $('<div/>', {
class: 'slickgrid',
style: 'min-height: 400px'
});
this.$grid_table.append($tablePlaceholder);
return new window.Slick.Grid($tablePlaceholder, gridData, columns, options);
};
extensions.prototype.clear_display = function() {
this.$grid_text.empty();
this.$grid_table.empty();
this.$section.find('.request-response-error').empty().hide();
return this.$section.find('.request-response').empty().hide();
};
return extensions;
}());
_.defaults(window, {
InstructorDashboard: {}
});
_.defaults(window.InstructorDashboard, {
sections: {}
});
_.defaults(window.InstructorDashboard.sections, {
Extensions: Extensions
});
}).call(this);
/*
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).
*/
(function() {
'use strict';
var $activeSection,
CSS_ACTIVE_SECTION, CSS_IDASH_SECTION, CSS_INSTRUCTOR_CONTENT, CSS_INSTRUCTOR_NAV, HASH_LINK_PREFIX,
SafeWaiter, plantTimeout, sectionsHaveLoaded, setupInstructorDashboard,
setupInstructorDashboardSections;
plantTimeout = function() {
return window.InstructorDashboard.util.plantTimeout.apply(this, arguments);
};
CSS_INSTRUCTOR_CONTENT = 'instructor-dashboard-content-2';
CSS_ACTIVE_SECTION = 'active-section';
CSS_IDASH_SECTION = 'idash-section';
CSS_INSTRUCTOR_NAV = 'instructor-nav';
HASH_LINK_PREFIX = '#view-';
$activeSection = null;
SafeWaiter = (function() {
function safeWaiter() {
this.after_handlers = [];
this.waitFor_handlers = [];
this.fired = false;
}
safeWaiter.prototype.afterFor = function(f) {
if (this.fired) {
return f();
} else {
return this.after_handlers.push(f);
}
};
safeWaiter.prototype.waitFor = function(f) {
var safeWait = this;
if (!this.fired) {
this.waitFor_handlers.push(f);
return function() {
safeWait.waitFor_handlers = safeWait.waitFor_handlers.filter(function(g) {
return g !== f;
});
if (safeWait.waitFor_handlers.length === 0) {
safeWait.fired = true;
safeWait.after_handlers.map(function(cb) {
return plantTimeout(0, cb);
});
}
return f.apply(safeWait, arguments);
};
} else {
return false;
}
};
return safeWaiter;
}());
sectionsHaveLoaded = new SafeWaiter;
$(function() {
var $instructorDashboardContent;
$instructorDashboardContent = $('.' + CSS_INSTRUCTOR_CONTENT);
if ($instructorDashboardContent.length > 0) {
setupInstructorDashboard($instructorDashboardContent);
return setupInstructorDashboardSections($instructorDashboardContent);
}
return setupInstructorDashboardSections($instructorDashboardContent);
});
setupInstructorDashboard = function(idashContent) {
var $links, clickFirstLink, link, rmatch, sectionName;
$links = idashContent.find('.' + CSS_INSTRUCTOR_NAV).find('.btn-link');
$links.each(function(i, linkItem) {
return $(linkItem).click(function(e) {
var $section, itemSectionName, ref;
e.preventDefault();
idashContent.find('.' + CSS_INSTRUCTOR_NAV + ' li').children().removeClass(CSS_ACTIVE_SECTION);
idashContent.find('.' + CSS_INSTRUCTOR_NAV + ' li').children().attr('aria-pressed', 'false');
idashContent.find('.' + CSS_IDASH_SECTION).removeClass(CSS_ACTIVE_SECTION);
itemSectionName = $(this).data('section');
$section = idashContent.find('#' + itemSectionName);
$(this).addClass(CSS_ACTIVE_SECTION);
$(this).attr('aria-pressed', 'true');
$section.addClass(CSS_ACTIVE_SECTION);
window.analytics.pageview('instructor_section:' + itemSectionName);
location.hash = '' + HASH_LINK_PREFIX + itemSectionName;
sectionsHaveLoaded.afterFor(function() {
return $section.data('wrapper').onClickTitle();
});
if (!$section.is($activeSection)) {
if ($activeSection != null) {
ref = $activeSection.data('wrapper') != null;
if (ref) {
if (typeof ref.onExit === 'function') {
ref.onExit();
}
}
}
}
$activeSection = $section;
return $activeSection;
});
});
clickFirstLink = function() {
var firstLink;
firstLink = $links.eq(0);
return firstLink.click();
};
if ((new RegExp('^' + HASH_LINK_PREFIX)).test(location.hash)) {
rmatch = (new RegExp('^' + HASH_LINK_PREFIX + '(.*)')).exec(location.hash);
sectionName = rmatch[1];
link = $links.filter("[data-section='" + sectionName + "']");
if (link.length === 1) {
return link.click();
} else {
return clickFirstLink();
}
} else {
return clickFirstLink();
}
};
setupInstructorDashboardSections = function(idashContent) {
var sectionsToInitialize;
sectionsToInitialize = [
{
constructor: window.InstructorDashboard.sections.CourseInfo,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#course_info')
}, {
constructor: window.InstructorDashboard.sections.DataDownload,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#data_download')
}, {
constructor: window.InstructorDashboard.sections.ECommerce,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#e-commerce')
}, {
constructor: window.InstructorDashboard.sections.Membership,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#membership')
}, {
constructor: window.InstructorDashboard.sections.StudentAdmin,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#student_admin')
}, {
constructor: window.InstructorDashboard.sections.Extensions,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#extensions')
}, {
constructor: window.InstructorDashboard.sections.Email,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#send_email')
}, {
constructor: window.InstructorDashboard.sections.InstructorAnalytics,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#instructor_analytics')
}, {
constructor: window.InstructorDashboard.sections.Metrics,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#metrics')
}, {
constructor: window.InstructorDashboard.sections.CohortManagement,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#cohort_management')
}, {
constructor: window.InstructorDashboard.sections.Certificates,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#certificates')
}
];
if (edx.instructor_dashboard.proctoring !== void 0) {
sectionsToInitialize = sectionsToInitialize.concat([
{
constructor: edx.instructor_dashboard.proctoring.ProctoredExamAllowanceView,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#special_exams')
}, {
constructor: edx.instructor_dashboard.proctoring.ProctoredExamAttemptView,
$element: idashContent.find('.' + CSS_IDASH_SECTION + '#special_exams')
}
]);
}
return sectionsToInitialize.map(function(_arg) {
var $element, constructor;
constructor = _arg.constructor;
$element = _arg.$element;
return plantTimeout(0, sectionsHaveLoaded.waitFor(function() {
return new constructor($element);
}));
});
};
}).call(this);
(function() {
'use strict';
var Metrics;
Metrics = (function() {
function metrics($section) {
this.$section = $section;
this.$section.data('wrapper', this);
}
metrics.prototype.onClickTitle = function() {};
return metrics;
}());
window.InstructorDashboard.sections.Metrics = Metrics;
}).call(this);
/* global define */
define(['jquery',
'coffee/src/instructor_dashboard/data_download',
'js/instructor_dashboard/data_download',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'slick.grid'],
function($, DataDownload, AjaxHelpers) {
......
// Generated by CoffeeScript 1.6.1
(function() {
'use strict';
describe('AutoEnrollment', function() {
beforeEach(function() {
loadFixtures('../../fixtures/autoenrollment.html');
this.autoenrollment = new window.AutoEnrollmentViaCsv($('.auto_enroll_csv'));
return this.autoenrollment;
});
it('binds the ajax call and the result will be success', function() {
var submitCallback;
spyOn($, 'ajax').and.callFake(function(params) {
params.success({
row_errors: [],
general_errors: [],
warnings: []
});
return {
always: function() {}
};
});
this.autoenrollment.render_notification_view = jasmine.createSpy(
'render_notification_view(type, title, message, details) spy').and.callFake(function() {
return '<div><div class="message message-confirmation"><h3 class="message-title">Success</h3><div class="message-copy"><p>All accounts were created successfully.</p></div></div><div>'; // eslint-disable-line max-len
});
submitCallback = jasmine.createSpy().and.returnValue();
this.autoenrollment.$student_enrollment_form.submit(submitCallback);
this.autoenrollment.$enrollment_signup_button.click();
expect($('.results .message-copy').text()).toEqual('All accounts were created successfully.');
return expect(submitCallback).toHaveBeenCalled();
});
it('binds the ajax call and the result will be error', function() {
var submitCallback;
spyOn($, 'ajax').and.callFake(function(params) {
params.success({
row_errors: [
{
username: 'testuser1',
email: 'testemail1@email.com',
response: 'Username already exists'
}
],
general_errors: [
{
response: 'cannot read the line 2'
}
],
warnings: []
});
return {
always: function() {}
};
});
this.autoenrollment.render_notification_view = jasmine.createSpy(
'render_notification_view(type, title, message, details) spy').and.callFake(function() {
return '<div><div class="message message-error"><h3 class="message-title">Errors</h3><div class="message-copy"><p>The following errors were generated:</p><ul class="list-summary summary-items"><li class="summary-item">cannot read the line 2</li><li class="summary-item">testuser1 (testemail1@email.com): (Username already exists)</li></ul></div></div></div>'; // eslint-disable-line max-len
});
submitCallback = jasmine.createSpy().and.returnValue();
this.autoenrollment.$student_enrollment_form.submit(submitCallback);
this.autoenrollment.$enrollment_signup_button.click();
expect($('.results .list-summary').text()).toEqual('cannot read the line 2testuser1 (testemail1@email.com): (Username already exists)'); // eslint-disable-line max-len
return expect(submitCallback).toHaveBeenCalled();
});
return it('binds the ajax call and the result will be warnings', function() {
var submitCallback;
spyOn($, 'ajax').and.callFake(function(params) {
params.success({
row_errors: [],
general_errors: [],
warnings: [
{
username: 'user1',
email: 'user1email',
response: 'email is in valid'
}
]
});
return {
always: function() {}
};
});
this.autoenrollment.render_notification_view = jasmine.createSpy(
'render_notification_view(type, title, message, details) spy').and.callFake(function() {
return '<div><div class="message message-warning"><h3 class="message-title">Warnings</h3><div class="message-copy"><p>The following warnings were generated:</p><ul class="list-summary summary-items"><li class="summary-item">user1 (user1email): (email is in valid)</li></ul></div></div></div>'; // eslint-disable-line max-len
});
submitCallback = jasmine.createSpy().and.returnValue();
this.autoenrollment.$student_enrollment_form.submit(submitCallback);
this.autoenrollment.$enrollment_signup_button.click();
expect($('.results .list-summary').text()).toEqual('user1 (user1email): (email is in valid)');
return expect(submitCallback).toHaveBeenCalled();
});
});
}).call(this);
(function() {
'use strict';
describe('Bulk Email Queueing', function() {
beforeEach(function() {
var testBody, testSubject;
testSubject = 'Test Subject';
testBody = 'Hello, World! This is a test email message!';
loadFixtures('../../fixtures/send_email.html');
this.send_email = new window.SendEmail($('.send-email'));
this.send_email.$subject.val(testSubject);
this.send_email.$send_to.first().prop('checked', true);
this.send_email.$emailEditor = {
save: function() {
return {
data: testBody
};
}
};
this.ajax_params = {
type: 'POST',
dataType: 'json',
url: void 0,
data: {
action: 'send',
send_to: JSON.stringify([this.send_email.$send_to.first().val()]),
subject: testSubject,
message: testBody
},
success: jasmine.any(Function),
error: jasmine.any(Function)
};
return this.ajax_params;
});
it('cannot send an email with no target', function() {
var target, i, len, ref;
spyOn(window, 'alert');
spyOn($, 'ajax');
ref = this.send_email.$send_to;
for (i = 0, len = ref.length; i < len; i++) {
target = ref[i];
target.checked = false;
}
this.send_email.$btn_send.click();
expect(window.alert).toHaveBeenCalledWith('Your message must have at least one target.');
return expect($.ajax).not.toHaveBeenCalled();
});
it('cannot send an email with no subject', function() {
spyOn(window, 'alert');
spyOn($, 'ajax');
this.send_email.$subject.val('');
this.send_email.$btn_send.click();
expect(window.alert).toHaveBeenCalledWith('Your message must have a subject.');
return expect($.ajax).not.toHaveBeenCalled();
});
it('cannot send an email with no message', function() {
spyOn(window, 'alert');
spyOn($, 'ajax');
this.send_email.$emailEditor = {
save: function() {
return {
data: ''
};
}
};
this.send_email.$btn_send.click();
expect(window.alert).toHaveBeenCalledWith('Your message cannot be blank.');
return expect($.ajax).not.toHaveBeenCalled();
});
it('can send a simple message to a single target', function() {
spyOn($, 'ajax').and.callFake(function(params) {
return params.success();
});
this.send_email.$btn_send.click();
expect($('.msg-confirm').text()).toEqual('Your email message was successfully queued for sending. In courses with a large number of learners, email messages to learners might take up to an hour to be sent.'); // eslint-disable-line max-len
return expect($.ajax).toHaveBeenCalledWith(this.ajax_params);
});
it('can send a simple message to a multiple targets', function() {
var target, i, len, ref;
spyOn($, 'ajax').and.callFake(function(params) {
return params.success();
});
this.ajax_params.data.send_to = JSON.stringify((function() {
var j, len1, ref1, results;
ref1 = this.send_email.$send_to;
results = [];
for (j = 0, len1 = ref.length; j < len1; j++) {
target = ref1[j];
results.push(target.value);
}
return results;
}).call(this));
ref = this.send_email.$send_to;
for (i = 0, len = ref.length; i < len; i++) {
target = ref[i];
target.checked = true;
}
this.send_email.$btn_send.click();
expect($('.msg-confirm').text()).toEqual('Your email message was successfully queued for sending. In courses with a large number of learners, email messages to learners might take up to an hour to be sent.'); // eslint-disable-line max-len
return expect($.ajax).toHaveBeenCalledWith(this.ajax_params);
});
it('can handle an error result from the bulk email api', function() {
spyOn($, 'ajax').and.callFake(function(params) {
return params.error();
});
spyOn(console, 'warn');
this.send_email.$btn_send.click();
return expect($('.request-response-error').text()).toEqual('Error sending email.');
});
it('selecting all learners disables cohort selections', function() {
this.send_email.$send_to.filter("[value='learners']").click();
this.send_email.$cohort_targets.each(function() {
return expect(this.disabled).toBe(true);
});
this.send_email.$send_to.filter("[value='learners']").click();
return this.send_email.$cohort_targets.each(function() {
return expect(this.disabled).toBe(false);
});
});
return it('selected targets are listed after "send to:"', function() {
this.send_email.$send_to.click();
return $('input[name="send_to"]:checked+label').each(function() {
return expect($('.send_to_list'.text())).toContain(this.innerText.replace(/\s*\n.*/g, ''));
});
});
});
}).call(this);
......@@ -54,7 +54,7 @@
mathjax: '//cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_SVG&delayStartupUntil=configured', // eslint-disable-line max-len
'youtube': '//www.youtube.com/player_api?noext',
'coffee/src/ajax_prefix': 'xmodule_js/common_static/coffee/src/ajax_prefix',
'coffee/src/instructor_dashboard/student_admin': 'coffee/src/instructor_dashboard/student_admin',
'js/instructor_dashboard/student_admin': 'js/instructor_dashboard/student_admin',
'xmodule_js/common_static/js/test/add_ajax_prefix': 'xmodule_js/common_static/js/test/add_ajax_prefix',
'xblock/lms.runtime.v1': 'lms/js/xblock/lms.runtime.v1',
'xblock': 'common/js/xblock',
......@@ -283,8 +283,8 @@
exports: 'AjaxPrefix',
deps: ['coffee/src/ajax_prefix']
},
'coffee/src/instructor_dashboard/util': {
exports: 'coffee/src/instructor_dashboard/util',
'js/instructor_dashboard/util': {
exports: 'js/instructor_dashboard/util',
deps: ['jquery', 'underscore', 'slick.core', 'slick.grid'],
init: function() {
// Set global variables that the util code is expecting to be defined
......@@ -298,9 +298,9 @@
});
}
},
'coffee/src/instructor_dashboard/student_admin': {
exports: 'coffee/src/instructor_dashboard/student_admin',
deps: ['jquery', 'underscore', 'coffee/src/instructor_dashboard/util', 'string_utils']
'js/instructor_dashboard/student_admin': {
exports: 'js/instructor_dashboard/student_admin',
deps: ['jquery', 'underscore', 'js/instructor_dashboard/util', 'string_utils']
},
'js/instructor_dashboard/certificates': {
exports: 'js/instructor_dashboard/certificates',
......
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