Merge remote-tracking branch 'origin/master' into feature/vik/oe-ui

Conflicts:
	common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
This commit is contained in:
Vik Paruchuri
2013-08-15 11:36:11 -04:00
228 changed files with 6307 additions and 5545 deletions

View File

@@ -6,60 +6,32 @@
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) ->
@$section.data 'wrapper', @
# gather elements
@$display = @$section.find '.distribution-display'
@$display_text = @$display.find '.distribution-display-text'
@$display_graph = @$display.find '.distribution-display-graph'
@$display_table = @$display.find '.distribution-display-table'
@$distribution_select = @$section.find 'select#distributions'
@$request_response_error = @$display.find '.request-response-error'
@populate_selector => @$distribution_select.change => @on_selector_change()
class ProfileDistributionWidget
constructor: ({@$container, @feature, @title, @endpoint}) ->
# render template
template_params =
title: @title
feature: @feature
endpoint: @endpoint
template_html = $("#profile-distribution-widget-template").text()
@$container.html Mustache.render template_html, template_params
reset_display: ->
@$display_text.empty()
@$display_graph.empty()
@$display_table.empty()
@$request_response_error.empty()
@$container.find('.display-errors').empty()
@$container.find('.display-text').empty()
@$container.find('.display-graph').empty()
@$container.find('.display-table').empty()
# fetch and list available distributions
# `cb` is a callback to be run after
populate_selector: (cb) ->
# ask for no particular distribution to get list of available distribuitions.
@get_profile_distributions undefined,
# 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 = $ '<option/>',
text: data.feature_display_names[feature]
data:
feature: feature
@$distribution_select.append opt
# call callback if one was supplied
cb?()
show_error: (msg) ->
@$container.find('.display-errors').text msg
# display data
on_selector_change: ->
opt = @$distribution_select.children('option:selected')
feature = opt.data 'feature'
load: ->
@reset_display()
# only proceed if there is a feature attached to the selected option.
return unless feature
@get_profile_distributions feature,
error: std_ajax_err => @$request_response_error.text "Error getting distribution for '#{feature}'."
@get_profile_distributions @feature,
error: std_ajax_err => @show_error "Error fetching distribution."
success: (data) =>
feature_res = data.feature_results
if feature_res.type is 'EASY_CHOICE'
@@ -70,27 +42,27 @@ class Analytics
forceFitColumns: true
columns = [
id: feature
field: feature
name: feature
id: @feature
field: @feature
name: data.feature_display_names[@feature]
,
id: 'count'
field: 'count'
name: 'Count'
]
grid_data = _.map feature_res.data, (value, key) ->
grid_data = _.map feature_res.data, (value, key) =>
datapoint = {}
datapoint[feature] = feature_res.choices_display_names[key]
datapoint[@feature] = feature_res.choices_display_names[key]
datapoint['count'] = value
datapoint
table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append table_placeholder
@$container.find('.display-table').append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
else if feature_res.feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'year-of-birth'
@$display_graph.append graph_placeholder
graph_placeholder = $ '<div/>', class: 'graph-placeholder'
@$container.find('.display-graph').append graph_placeholder
graph_data = _.map feature_res.data, (value, key) -> [parseInt(key), value]
@@ -99,7 +71,7 @@ class Analytics
]
else
console.warn("unable to show distribution #{feature_res.type}")
@$display_text.text 'Unavailable Metric Display\n' + JSON.stringify(feature_res)
@show_error 'Unavailable metric display.'
# fetch distribution data from server.
# `handler` can be either a callback for success
@@ -107,7 +79,7 @@ class Analytics
get_profile_distributions: (feature, handler) ->
settings =
dataType: 'json'
url: @$distribution_select.data 'endpoint'
url: @endpoint
data: feature: feature
if typeof handler is 'function'
@@ -117,13 +89,138 @@ class Analytics
$.ajax settings
# slickgrid's layout collapses when rendered
# in an invisible div. use this method to reload
# the AuthList widget
refresh: ->
@on_selector_change()
# handler for when the section title is clicked.
class GradeDistributionDisplay
constructor: ({@$container, @endpoint}) ->
template_params = {}
template_html = $('#grade-distributions-widget-template').text()
@$container.html Mustache.render template_html, template_params
@$problem_selector = @$container.find '.problem-selector'
reset_display: ->
@$container.find('.display-errors').empty()
@$container.find('.display-text').empty()
@$container.find('.display-graph').empty()
show_error: (msg) ->
@$container.find('.display-errors').text msg
load: ->
@get_grade_distributions
error: std_ajax_err => @show_error "Error fetching grade distributions."
success: (data) =>
@$container.find('.last-updated').text "Last Updated: #{data.time}"
# populate selector
@$problem_selector.empty()
for {module_id, grade_info} in data.data
I4X_PROBLEM = /i4x:\/\/.*\/.*\/problem\/(.*)/
label = (I4X_PROBLEM.exec module_id)?[1]
label ?= module_id
@$problem_selector.append $ '<option/>',
text: label
data:
module_id: module_id
grade_info: grade_info
@$problem_selector.change =>
$opt = @$problem_selector.children('option:selected')
return unless $opt.length > 0
@reset_display()
@render_distribution
module_id: $opt.data 'module_id'
grade_info: $opt.data 'grade_info'
# one-time first selection of first list item.
@$problem_selector.change()
render_distribution: ({module_id, grade_info}) ->
$display_graph = @$container.find('.display-graph')
graph_data = grade_info.map ({grade, max_grade, num_students}) -> [grade, num_students]
total_students = _.reduce ([0].concat grade_info),
(accum, {grade, max_grade, num_students}) -> accum + num_students
# show total students
@$container.find('.display-text').text "#{total_students} students scored."
# render to graph
graph_placeholder = $ '<div/>', class: 'graph-placeholder'
$display_graph.append graph_placeholder
graph_data = graph_data
$.plot graph_placeholder, [
data: graph_data
bars: show: true
color: '#1d9dd9'
]
# `handler` can be either a callback for success
# or a mapping e.g. {success: ->, error: ->, complete: ->}
#
# the data passed to the success handler takes this form:
# {
# "aname": "ProblemGradeDistribution",
# "time": "2013-07-31T20:25:56+00:00",
# "course_id": "MITx/6.002x/2013_Spring",
# "options": {
# "course_id": "MITx/6.002x/2013_Spring",
# "_id": "6fudge2b49somedbid1e1",
# "data": [
# {
# "module_id": "i4x://MITx/6.002x/problem/Capacitors_and_Energy_Storage",
# "grade_info": [
# {
# "grade": 0.0,
# "max_grade": 100.0,
# "num_students": 3
# }, ... for each grade number between 0 and max_grade
# ],
# }
get_grade_distributions: (handler) ->
settings =
dataType: 'json'
url: @endpoint
data: aname: 'ProblemGradeDistribution'
if typeof handler is 'function'
_.extend settings, success: handler
else
_.extend settings, handler
$.ajax settings
# Analytics Section
class Analytics
constructor: (@$section) ->
@$section.data 'wrapper', @
@$pd_containers = @$section.find '.profile-distribution-widget-container'
@$gd_containers = @$section.find '.grade-distributions-widget-container'
@pdws = _.map (@$pd_containers), (container) =>
new ProfileDistributionWidget
$container: $(container)
feature: $(container).data 'feature'
title: $(container).data 'title'
endpoint: $(container).data 'endpoint'
@gdws = _.map (@$gd_containers), (container) =>
new GradeDistributionDisplay
$container: $(container)
endpoint: $(container).data 'endpoint'
refresh: ->
for pdw in @pdws
pdw.load()
for gdw in @gdws
gdw.load()
onClickTitle: ->
@refresh()

View File

@@ -33,6 +33,47 @@ 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}"
@@ -45,10 +86,11 @@ $ =>
# handles hiding and showing sections
setup_instructor_dashboard = (idash_content) =>
# clickable section titles
links = idash_content.find(".#{CSS_INSTRUCTOR_NAV}").find('a')
$links = idash_content.find(".#{CSS_INSTRUCTOR_NAV}").find('a')
for link in ($ link for link in links)
link.click (e) ->
# attach link click handlers
$links.each (i, link) ->
$(link).click (e) ->
e.preventDefault()
# deactivate all link & section styles
@@ -57,11 +99,11 @@ setup_instructor_dashboard = (idash_content) =>
# discover section paired to link
section_name = $(this).data 'section'
section = idash_content.find "##{section_name}"
$section = idash_content.find "##{section_name}"
# activate link & section styling
$(this).addClass CSS_ACTIVE_SECTION
section.addClass CSS_ACTIVE_SECTION
$section.addClass CSS_ACTIVE_SECTION
# tracking
analytics.pageview "instructor_section:#{section_name}"
@@ -70,24 +112,29 @@ setup_instructor_dashboard = (idash_content) =>
# write to url
location.hash = "#{HASH_LINK_PREFIX}#{section_name}"
plantTimeout 0, -> section.data('wrapper')?.onClickTitle?()
# plantTimeout 0, -> section.data('wrapper')?.onExit?()
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 = $links.eq(0)
link.click()
link.data('wrapper')?.onClickTitle?()
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}']"
link = $links.filter "[data-section='#{section_name}']"
if link.length == 1
link.click()
link.data('wrapper')?.onClickTitle?()
else
click_first_link()
else
@@ -97,10 +144,25 @@ setup_instructor_dashboard = (idash_content) =>
# enable sections
setup_instructor_dashboard_sections = (idash_content) ->
# 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"
plantTimeout 0, -> new window.InstructorDashboard.sections.StudentAdmin idash_content.find ".#{CSS_IDASH_SECTION}#student_admin"
plantTimeout 0, -> new window.InstructorDashboard.sections.Analytics idash_content.find ".#{CSS_IDASH_SECTION}#analytics"
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.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.Analytics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#analytics"
]
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

View File

@@ -463,6 +463,8 @@ class Membership
text: auth_list.$container.data 'display-name'
data:
auth_list: auth_list
if @auth_lists.length is 0
@$list_selector.hide()
@$list_selector.change =>
$opt = @$list_selector.children('option:selected')

View File

@@ -6,6 +6,8 @@
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
load_IntervalManager = -> window.InstructorDashboard.util.IntervalManager
# wrap window.confirm
# display `msg`
@@ -102,8 +104,13 @@ class StudentAdmin
@$request_response_error_all = @$section.find ".course-specific-container .request-response-error"
# start polling for task list
# if the list is in the DOM
if @$table_running_tasks.length > 0
@start_refresh_running_task_poll_loop()
# reload every 20 seconds.
TASK_LIST_POLL_INTERVAL = 20000
@reload_running_tasks_list()
@task_poller = new (load_IntervalManager()) TASK_LIST_POLL_INTERVAL, =>
@reload_running_tasks_list()
# attach click handlers
@@ -255,7 +262,6 @@ class StudentAdmin
create_task_list_table @$table_task_history_all, data.tasks
error: std_ajax_err => @$request_response_error_all.text "Error listing task history for this student and problem."
reload_running_tasks_list: =>
list_endpoint = @$table_running_tasks.data 'endpoint'
$.ajax
@@ -264,12 +270,6 @@ class StudentAdmin
success: (data) => create_task_list_table @$table_running_tasks, data.tasks
error: std_ajax_err => console.warn "error listing all instructor tasks"
start_refresh_running_task_poll_loop: ->
@reload_running_tasks_list()
if @$section.hasClass 'active-section'
# poll every 20 seconds
plantTimeout 20000, => @start_refresh_running_task_poll_loop()
# wraps a function, but first clear the error displays
clear_errors_then: (cb) ->
@$request_response_error_single.empty()
@@ -278,14 +278,10 @@ class StudentAdmin
cb?.apply this, arguments
# handler for when the section title is clicked.
onClickTitle: ->
if @$table_running_tasks.length > 0
@start_refresh_running_task_poll_loop()
onClickTitle: -> @task_poller?.start()
# handler for when the section is closed
# not working yet.
# onExit: ->
# clearInterval @reload_running_task_list_slot
onExit: -> @task_poller?.stop()
# export for use

View File

@@ -5,6 +5,7 @@
plantTimeout = (ms, cb) -> setTimeout cb, ms
plantInterval = (ms, cb) -> setInterval cb, ms
# standard ajax error wrapper
#
# wraps a `handler` function so that first
@@ -16,6 +17,26 @@ std_ajax_err = (handler) -> (jqXHR, textStatus, errorThrown) ->
handler.apply this, arguments
# Helper class for managing the execution of interval tasks.
# Handles pausing and restarting.
class IntervalManager
# Create a manager which will call `fn`
# after a call to .start every `ms` milliseconds.
constructor: (@ms, @fn) ->
@intervalID = null
# Start or restart firing every `ms` milliseconds.
# Soes not fire immediately.
start: ->
if @intervalID is null
@intervalID = setInterval @fn, @ms
# Pause firing.
stop: ->
clearInterval @intervalID
@intervalID = null
# export for use
# create parent namespaces if they do not already exist.
# abort if underscore can not be found.
@@ -25,3 +46,4 @@ if _?
plantTimeout: plantTimeout
plantInterval: plantInterval
std_ajax_err: std_ajax_err
IntervalManager: IntervalManager