diff --git a/lms/static/coffee/src/instructor_dashboard/analytics.coffee b/lms/static/coffee/src/instructor_dashboard/analytics.coffee index e586a2400a..b4deb93f8a 100644 --- a/lms/static/coffee/src/instructor_dashboard/analytics.coffee +++ b/lms/static/coffee/src/instructor_dashboard/analytics.coffee @@ -1,17 +1,15 @@ -log = -> console.log.apply console, arguments -plantTimeout = (ms, cb) -> setTimeout cb, ms - -std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) -> - console.warn """ajax error - textStatus: #{textStatus} - errorThrown: #{errorThrown}""" - handler.apply this, arguments +# 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). +plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments +std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments +# Analytics Section class Analytics constructor: (@$section) -> - log "setting up instructor dashboard section - analytics" - + # gather elements @$display = @$section.find '.distribution-display' @$display_text = @$display.find '.distribution-display-text' @$display_graph = @$display.find '.distribution-display-graph' @@ -21,20 +19,23 @@ class Analytics @populate_selector => @$distribution_select.change => @on_selector_change() - reset_display: -> @$display_text.empty() @$display_graph.empty() @$display_table.empty() @$request_response_error.empty() - + # fetch and list available distributions + # `cb` is a callback to be run after populate_selector: (cb) -> @get_profile_distributions [], + # on error, print to console and dom. error: std_ajax_err => @$request_response_error.text "Error getting available distributions." success: (data) => + # replace loading text in drop-down with "-- Select Distribution --" @$distribution_select.find('option').eq(0).text "-- Select Distribution --" + # add all fetched available features to drop-down for feature in data.available_features opt = $ '', text: data.display_names[feature] @@ -43,14 +44,13 @@ class Analytics @$distribution_select.append opt + # call callback if one was supplied cb?() - + # display data on_selector_change: -> - # log 'changeargs', arguments opt = @$distribution_select.children('option:selected') feature = opt.data 'feature' - log "distribution selected: #{feature}" @reset_display() return unless feature @@ -64,7 +64,7 @@ class Analytics @$display_text.text 'Error fetching data' else if feature_res.type is 'EASY_CHOICE' - # setup SlickGrid + # display on SlickGrid options = enableCellNavigation: true enableColumnReorder: false @@ -89,7 +89,6 @@ class Analytics table_placeholder = $ '
', class: 'slickgrid' @$display_table.append table_placeholder grid = new Slick.Grid(table_placeholder, grid_data, columns, options) - # grid.autosizeColumns() else if feature is 'year_of_birth' graph_placeholder = $ '', class: 'year-of-birth' @$display_graph.append graph_placeholder @@ -104,7 +103,9 @@ class Analytics @$display_text.text 'Unavailable Metric\n' + JSON.stringify(feature_res) - # handler can be either a callback for success or a mapping e.g. {success: ->, error: ->, complete: ->} + # fetch distribution data from server. + # `handler` can be either a callback for success + # or a mapping e.g. {success: ->, error: ->, complete: ->} get_profile_distributions: (featurelist, handler) -> settings = dataType: 'json' @@ -119,7 +120,9 @@ class Analytics $.ajax settings -# exports +# 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: {} diff --git a/lms/static/coffee/src/instructor_dashboard/course_info.coffee b/lms/static/coffee/src/instructor_dashboard/course_info.coffee index 9eecc48499..d48c7ba873 100644 --- a/lms/static/coffee/src/instructor_dashboard/course_info.coffee +++ b/lms/static/coffee/src/instructor_dashboard/course_info.coffee @@ -1,21 +1,32 @@ -log = -> console.log.apply console, arguments -plantTimeout = (ms, cb) -> setTimeout cb, ms +# 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). +plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments +std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments + +# A typical section object. +# constructed with $section, a jquery object +# which holds the section body container. class CourseInfo constructor: (@$section) -> - log "setting up instructor dashboard section - course info" - @$section.data 'wrapper', @ - @$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').eq(0) - @$course_error_toggle_text = @$course_error_toggle.find('h2').eq(0) + @$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') + @$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' @@ -24,7 +35,9 @@ class CourseInfo @$course_errors_wrapper.addClass 'open' -# exports +# 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: {} diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index 6dbd91c66e..cfd3534e04 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -1,32 +1,40 @@ -log = -> console.log.apply console, arguments -plantTimeout = (ms, cb) -> setTimeout cb, ms +# Data Download Section -std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) -> - console.warn """ajax error - textStatus: #{textStatus} - errorThrown: #{errorThrown}""" - handler.apply this, arguments +# 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 +# Data Download Section class DataDownload constructor: (@$section) -> - log "setting up instructor dashboard section - data download" - + # gather elements @$display = @$section.find '.data-display' @$display_text = @$display.find '.data-display-text' @$display_table = @$display.find '.data-display-table' @$request_response_error = @$display.find '.request-response-error' + @$list_studs_btn = @$section.find("input[name='list-profiles']'") + @$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") - $list_studs_btn = @$section.find("input[name='list-profiles']'") - $list_studs_btn.click (e) => - log "fetching student list" - url = $list_studs_btn.data('endpoint') + # attach click handlers + + # this handler binds to both the download + # and the csv button + @$list_studs_btn.click (e) => + url = @$list_studs_btn.data 'endpoint' + + # handle csv special case if $(e.target).data 'csv' + # redirect the document to the csv file. url += '/csv' location.href = url else @clear_display() @$display_table.text 'Loading...' + + # fetch user list $.ajax dataType: 'json' url: url @@ -36,7 +44,7 @@ class DataDownload success: (data) => @clear_display() - # setup SlickGrid + # display on a SlickGrid options = enableCellNavigation: true enableColumnReorder: false @@ -50,10 +58,9 @@ class DataDownload grid = new Slick.Grid($table_placeholder, grid_data, columns, options) # grid.autosizeColumns() - $grade_config_btn = @$section.find("input[name='dump-gradeconf']'") - $grade_config_btn.click (e) => - log "fetching grading config" - url = $grade_config_btn.data('endpoint') + @$grade_config_btn.click (e) => + url = @$grade_config_btn.data 'endpoint' + # display html from grading config endpoint $.ajax dataType: 'json' url: url @@ -71,7 +78,9 @@ class DataDownload @$request_response_error.empty() -# exports +# 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: {} diff --git a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee index 705bf630d7..1deaccb092 100644 --- a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee +++ b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee @@ -1,70 +1,83 @@ # 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 -log = -> console.log.apply console, arguments -plantTimeout = (ms, cb) -> setTimeout cb, ms - - -# # intercepts a jquery method -# # calls the original method after callback -# intercept_jquery_method = (method_name, callback) -> -# original = jQuery.fn[method_name] -# jQuery.fn[method_name] = -> -# callback.apply this, arguments -# original.apply this, arguments - - -# intercept_jquery_method 'on', (event_name) -> -# this.addClass "has-event-handler-for-#{event_name}" - +# 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-' - -# once we're ready, check if this page has the instructor dashboard +# once we're ready, check if this page is the instructor dashboard $ => instructor_dashboard_content = $ ".#{CSS_INSTRUCTOR_CONTENT}" - if instructor_dashboard_content.length != 0 - log "setting up instructor dashboard" + console.log 'checking if we are on the instructor dashboard' + if instructor_dashboard_content.length > 0 + console.log 'we are on the instructor dashboard' setup_instructor_dashboard instructor_dashboard_content setup_instructor_dashboard_sections instructor_dashboard_content -# enable links +# enable navigation bar +# handles hiding and showing sections setup_instructor_dashboard = (idash_content) => + # clickable section titles links = idash_content.find(".#{CSS_INSTRUCTOR_NAV}").find('a') - # setup section header click handlers + for link in ($ link for link in links) link.click (e) -> e.preventDefault() - # deactivate (styling) all sections - idash_content.find(".#{CSS_IDASH_SECTION}").removeClass CSS_ACTIVE_SECTION - idash_content.find(".#{CSS_INSTRUCTOR_NAV}").children().removeClass CSS_ACTIVE_SECTION - # find paired section + # deactivate all link & section styles + idash_content.find(".#{CSS_INSTRUCTOR_NAV}").children().removeClass CSS_ACTIVE_SECTION + 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 (styling) active - section.addClass CSS_ACTIVE_SECTION + # activate link & section styling $(this).addClass CSS_ACTIVE_SECTION + section.addClass CSS_ACTIVE_SECTION # tracking # analytics.pageview "instructor_#{section_name}" - # write deep link + # deep linking + # write to url location.hash = "#{HASH_LINK_PREFIX}#{section_name}" - log "clicked section #{section_name}" plantTimeout 0, -> section.data('wrapper')?.onClickTitle?() # plantTimeout 0, -> section.data('wrapper')?.onExit?() - # recover deep link from url - # click default or go to section specified by hash + + # activate an initial section by programmatically clicking on it. + # check for a deep-link, or click the first link. if (new RegExp "^#{HASH_LINK_PREFIX}").test location.hash rmatch = (new RegExp "^#{HASH_LINK_PREFIX}(.*)").exec location.hash section_name = rmatch[1] @@ -78,11 +91,10 @@ setup_instructor_dashboard = (idash_content) => -# call setup handlers for each section +# enable sections setup_instructor_dashboard_sections = (idash_content) -> - log "setting up instructor dashboard sections" - # fault isolation - # an error thrown in one section will not block other sections from exectuing + # see fault isolation NOTE at top of file. + # an error thrown in one section will not block other sections from exectuing. plantTimeout 0, -> new window.InstructorDashboard.sections.CourseInfo idash_content.find ".#{CSS_IDASH_SECTION}#course_info" plantTimeout 0, -> new window.InstructorDashboard.sections.DataDownload idash_content.find ".#{CSS_IDASH_SECTION}#data_download" plantTimeout 0, -> new window.InstructorDashboard.sections.Membership idash_content.find ".#{CSS_IDASH_SECTION}#membership" diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee index 9b01262b85..e1e68d739e 100644 --- a/lms/static/coffee/src/instructor_dashboard/membership.coffee +++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee @@ -1,191 +1,200 @@ -log = -> console.log.apply console, arguments -plantTimeout = (ms, cb) -> setTimeout cb, ms +# Membership Section -std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) -> - console.warn """ajax error - textStatus: #{textStatus} - errorThrown: #{errorThrown}""" - handler.apply this, arguments +# 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 +# Wrapper for the batch enrollment subsection. +# This object handles buttons, success and failure reporting, +# and server communication. class BatchEnrollment constructor: (@$container) -> - log "setting up instructor dashboard subsection - batch enrollment" + # gather elements + @$emails_input = @$container.find("textarea[name='student-emails']'") + @$btn_enroll = @$container.find("input[name='enroll']'") + @$btn_unenroll = @$container.find("input[name='unenroll']'") + @$checkbox_autoenroll = @$container.find("input[name='auto-enroll']'") + @$task_response = @$container.find(".request-response") + @$request_response_error = @$container.find(".request-response-error") - $emails_input = @$container.find("textarea[name='student-emails']'") - $btn_enroll = @$container.find("input[name='enroll']'") - $btn_unenroll = @$container.find("input[name='unenroll']'") - $checkbox_autoenroll = @$container.find("input[name='auto-enroll']'") - $task_response = @$container.find(".request-response") - $request_response_error = @$container.find(".request-response-error") + # attach click handlers - $emails_input.click -> log 'click $emails_input' - $btn_enroll.click -> log 'click $btn_enroll' - $btn_unenroll.click -> log 'click $btn_unenroll' - - $btn_enroll.click -> + @$btn_enroll.click => send_data = action: 'enroll' - emails: $emails_input.val() - auto_enroll: $checkbox_autoenroll.is(':checked') + emails: @$emails_input.val() + auto_enroll: @$checkbox_autoenroll.is(':checked') $.ajax dataType: 'json' - url: $btn_enroll.data 'endpoint' + url: @$btn_enroll.data 'endpoint' data: send_data - success: (data) -> display_response(data) - error: std_ajax_err -> fail_with_error "Error enrolling/unenrolling students." + success: (data) => @display_response data + error: std_ajax_err => @fail_with_error "Error enrolling/unenrolling students." - $btn_unenroll.click -> + @$btn_unenroll.click => send_data = action: 'unenroll' - emails: $emails_input.val() - auto_enroll: $checkbox_autoenroll.is(':checked') + emails: @$emails_input.val() + auto_enroll: @$checkbox_autoenroll.is(':checked') $.ajax dataType: 'json' - url: $btn_unenroll.data 'endpoint' + url: @$btn_unenroll.data 'endpoint' data: send_data - success: (data) -> display_response(data) - error: std_ajax_err -> fail_with_error "Error enrolling/unenrolling students." + success: (data) => @display_response data + error: std_ajax_err => @fail_with_error "Error enrolling/unenrolling students." - fail_with_error = (msg) -> - console.warn msg - $task_response.empty() - $request_response_error.empty() - $request_response_error.text msg + fail_with_error: (msg) -> + console.warn msg + @$task_response.empty() + @$request_response_error.empty() + @$request_response_error.text msg - display_response = (data_from_server) -> - $task_response.empty() - $request_response_error.empty() + display_response: (data_from_server) -> + @$task_response.empty() + @$request_response_error.empty() - # these results arrays contain student_results - # only populated arrays will be rendered + # these results arrays contain student_results + # only populated arrays will be rendered + # + # students for which there was an error during the action + errors = [] + # students who are now enrolled in the course + enrolled = [] + # students who are now allowed to enroll in the course + allowed = [] + # students who will be autoenrolled on registration + autoenrolled = [] + # students who are now not enrolled in the course + notenrolled = [] + + # categorize student results into the above arrays. + for student_results in data_from_server.results + # for a successful action. + # student_results is of the form { + # "email": "jd405@edx.org", + # "before": { + # "enrollment": true, + # "auto_enroll": false, + # "user": true, + # "allowed": false + # } + # "after": { + # "enrollment": true, + # "auto_enroll": false, + # "user": true, + # "allowed": false + # }, + # } # - # students for which there was an error during the action - errors = [] - # students who are now enrolled in the course - enrolled = [] - # students who are now allowed to enroll in the course - allowed = [] - # students who will be autoenrolled on registration - autoenrolled = [] - # students who are now not enrolled in the course - notenrolled = [] + # for an action error. + # student_results is of the form { + # 'email': email, + # 'error': True, + # } - # categorize student results into the above arrays. - for student_results in data_from_server.results - # for a successful action. - # student_results is of the form { - # "email": "jd405@edx.org", - # "before": { - # "enrollment": true, - # "auto_enroll": false, - # "user": true, - # "allowed": false - # } - # "after": { - # "enrollment": true, - # "auto_enroll": false, - # "user": true, - # "allowed": false - # }, - # } - # - # for an action error. - # student_results is of the form { - # 'email': email, - # 'error': True, - # } - - if student_results.error != undefined - errors.push student_results - else if student_results.after.enrollment - enrolled.push student_results - else if student_results.after.allowed - if student_results.after.auto_enroll - autoenrolled.push student_results - else - allowed.push student_results - else if not student_results.after.enrollment - notenrolled.push student_results + if student_results.error + errors.push student_results + else if student_results.after.enrollment + enrolled.push student_results + else if student_results.after.allowed + if student_results.after.auto_enroll + autoenrolled.push student_results else - console.warn 'student results not reported to user' - console.warn student_results + allowed.push student_results + else if not student_results.after.enrollment + notenrolled.push student_results + else + console.warn 'student results not reported to user' + console.warn student_results - # render populated result arrays - render_list = (label, emails) -> - log emails - task_res_section = $ '', class: 'request-res-section' - task_res_section.append $ '', text: label - email_list = $ '