create gradebook2

uses coffeescript, slickgrid, actual canvas APIs
reachable at /courses/x/gradebook2
does not include commenting or things external
to the grid like filtering and sorting options

Change-Id: I6967c2dbdd16f7ea4d8c1ad1995511d7c498226a
Reviewed-on: https://gerrit.instructure.com/4371
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Ryan Shaw 2011-06-27 13:43:54 -06:00
parent 6a1bddc9bc
commit 5456031ed6
36 changed files with 2007 additions and 28 deletions

View File

@ -0,0 +1,109 @@
class GradeCalculator
# each submission needs fields: score, points_possible, assignment_id, assignment_group_id
# to represent assignments that the student hasn't submitted, pass a
# submission with score == null
#
# each group needs fields: id, rules, group_weight
# rules is { drop_lowest: n, drop_highest: n, never_drop: [id...] }
#
# if weighting_scheme is "percent", group weights are used, otherwise no weighting is applied
@calculate: (submissions, groups, weighting_scheme) ->
result = {}
# NOTE: purposely using $.map because it can handle array or object, old gradebook sends array
# new gradebook sends object, needs jquery >1.6's version of $.map, since it can handle both
result.group_sums = $.map groups, (group) =>
group: group
current: @create_group_sum(group, submissions, true)
'final': @create_group_sum(group, submissions, false)
result.current = @calculate_total(result.group_sums, true, weighting_scheme)
result['final'] = @calculate_total(result.group_sums, false, weighting_scheme)
result
@create_group_sum: (group, submissions, ignore_ungraded) ->
sum = { submissions: [], score: 0, possible: 0, submission_count: 0 }
for submission in submissions
do (submission) =>
# handle if the submisison comes to us without an assignment_group_id
unless submission.assignment_group_id
assignment = $.detect(group.assignments, () -> submission.assignment_id == this.id)
if assignment
submission.assignment_group_id = group.id
# if submission doesn't have points possible, try to use assignment's
submission.points_possible ?= assignment?.points_possible
if submission.assignment_group_id == group.id
data = { submission: submission, score: 0, possible: 0, percent: 0, drop: false, submitted: false }
sum.submissions.push data
unless ignore_ungraded and (!submission.score || submission.score == '')
data.score = @parse submission.score
data.possible = @parse submission.points_possible
data.percent = @parse(data.score / data.possible)
data.submitted = (submission.score and submission.score != '')
sum.submission_count += 1 if data.submitted
# sort the submissions by assigned score
sum.submissions.sort (a,b) -> a.percent - b.percent
rules = $.extend({ drop_lowest: 0, drop_highest: 0, never_drop: [] }, group.rules)
dropped = 0
# drop the lowest and highest assignments
for lowOrHigh in ['low', 'high']
for data in sum.submissions
if !data.drop and rules["drop_#{lowOrHigh}est"] > 0 and $.inArray(data.assignment_id, rules.never_drop) == -1 and data.possible > 0 and data.submitted
data.drop = true
# TODO: do I want to do this, it actually modifies the passed in submission object but it
# it seems like the best way to tell it it should be dropped.
data.submission?.drop = true
rules["drop_#{lowOrHigh}est"] -= 1
dropped += 1
# if everything was dropped, un-drop the highest single submission
if dropped > 0 and dropped == sum.submission_count
sum.submissions[sum.submissions.length - 1].drop = false
# see TODO above
sum.submissions[sum.submissions.length - 1].submission?.drop = true
dropped -= 1
sum.submission_count -= dropped
sum.score += s.score for s in sum.submissions when !s.drop
sum.possible += s.possible for s in sum.submissions when !s.drop
sum
@calculate_total: (group_sums, ignore_ungraded, weighting_scheme) ->
data_idx = if ignore_ungraded then 'current' else 'final'
if weighting_scheme == 'percent'
score = 0.0
possible_weight_from_submissions = 0.0
total_possible_weight = 0.0
for data in group_sums when data.group.group_weight > 0
if data[data_idx].submission_count > 0
tally = data[data_idx].score / data[data_idx].possible
score += data.group.group_weight * tally
possible_weight_from_submissions += data.group.group_weight
total_possible_weight += data.group.group_weight
if ignore_ungraded and possible_weight_from_submissions < 1.0
possible = if total_possible_weight < 1.0 then total_possible_weight else 1.0
score = score * possible / possible_weight_from_submissions
{
score: score
possible: 1.0
}
else
{
score: @sum(data[data_idx].score for data in group_sums)
possible: @sum(data[data_idx].possible for data in group_sums)
}
@sum: (values) ->
result = 0
result += value for value in values
result
@parse: (score) ->
result = parseFloat score
if result && isFinite(result) then result else 0
window.INST.GradeCalculator = GradeCalculator

View File

@ -0,0 +1,322 @@
# This class both creates the slickgrid instance, and acts as the data source
# for that instance.
I18n.scoped 'gradebook2', (I18n) ->
this.Gradebook = class Gradebook
constructor: (@options) ->
@chunk_start = 0
@students = {}
@rows = []
@filterFn = (student) -> true
@sortFn = (student) -> student.display_name
@init()
@includeUngradedAssignments = false
init: () ->
if @options.assignment_groups
return @gotAssignmentGroups(@options.assignment_groups)
$.ajaxJSON( @options.assignment_groups_url, "GET", {}, @gotAssignmentGroups )
gotAssignmentGroups: (assignment_groups) =>
@assignment_groups = {}
@assignments = {}
for group in assignment_groups
$.htmlEscapeValues(group)
@assignment_groups[group.id] = group
for assignment in group.assignments
$.htmlEscapeValues(assignment)
assignment.due_at = $.parseFromISO(assignment.due_at) if assignment.due_at
@assignments[assignment.id] = assignment
if @options.sections
return @gotStudents(@options.sections)
$.ajaxJSON( @options.sections_and_students_url, "GET", {}, @gotStudents )
gotStudents: (sections) =>
@sections = {}
@rows = []
for section in sections
$.htmlEscapeValues(section)
@sections[section.id] = section
for student in section.students
$.htmlEscapeValues(student)
student.computed_current_score ||= 0
student.computed_final_score ||= 0
@students[student.id] = student
student.section = section
# fill in dummy submissions, so there's something there even if the
# student didn't submit anything for that assignment
for id, assignment of @assignments
student["assignment_#{id}"] ||= { assignment_id: id, user_id: student.id }
@rows.push(student)
@sections_enabled = sections.length > 1
for id, student of @students
student.display_name = "<div class='student-name'>#{student.name}</div>"
student.display_name += "<div class='student-section'>#{student.section.name}</div>" if @sections_enabled
@initGrid()
@buildRows()
@getSubmissionsChunk()
# filter, sort, and build the dataset for slickgrid to read from, then force
# a full redraw
buildRows: () ->
@rows.length = 0
sortables = {}
for id, student of @students
student.row = -1
if @filterFn(student)
@rows.push(student)
sortables[student.id] = @sortFn(student)
@rows.sort (a, b) ->
if sortables[a.id] < sortables[b.id] then -1
else if sortables[a.id] > sortables[b.id] then 1
else 0
student.row = i for student, i in @rows
@multiGrid.removeAllRows()
@multiGrid.updateRowCount()
@multiGrid.render()
sortBy: (sort) ->
@sortFn = switch sort
when "display_name" then (student) -> student.display_name
when "section" then (student) -> student.section.name
when "grade_desc" then (student) -> -student.computed_current_score
when "grade_asc" then (student) -> student.computed_current_score
this.buildRows()
getSubmissionsChunk: (student_id) ->
if @options.submissions
return this.gotSubmissionsChunk(@options.submissions)
students = @rows[@chunk_start...(@chunk_start+@options.chunk_size)]
params = {
student_ids: (student.id for student in students)
assignment_ids: (id for id, assignment of @assignments)
response_fields: ['user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id', 'grade_matches_current_submission']
}
if students.length > 0
$.ajaxJSON(@options.submissions_url, "GET", params, @gotSubmissionsChunk)
gotSubmissionsChunk: (student_submissions) =>
for data in student_submissions
student = @students[data.user_id]
student.submissionsAsArray = []
for submission in data.submissions
submission.submitted_at = $.parseFromISO(submission.submitted_at) if submission.submitted_at
student["assignment_#{submission.assignment_id}"] = submission
student.submissionsAsArray.push(submission)
student.loaded = true
@multiGrid.removeRow(student.row)
@calculateStudentGrade(student)
@multiGrid.render()
@chunk_start += @options.chunk_size
@getSubmissionsChunk()
cellFormatter: (row, col, submission) =>
if !@rows[row].loaded
@staticCellFormatter(row, col, '')
else if !submission?.grade
@staticCellFormatter(row, col, '-')
else
assignment = @assignments[submission.assignment_id]
if !assignment?
@staticCellFormatter(row, col, '')
else
if assignment.grading_type == 'points' && assignment.points_possible
SubmissionCell.out_of.formatter(row, col, submission, assignment)
else
(SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment)
staticCellFormatter: (row, col, val) =>
"<div class='cell-content gradebook-cell'>#{val}</div>"
groupTotalFormatter: (row, col, val, columnDef, student) =>
return '' unless val?
gradeToShow = val[if @includeUngradedAssignments then 'final' else 'current']
percentage = if columnDef.field == 'total_grade' then gradeToShow.score else (gradeToShow.score/gradeToShow.possible)*100
percentage = Math.round(percentage)
percentage = 0 if isNaN(percentage)
if !gradeToShow.possible then percentage = '-' else percentage+= "%"
res = """
<div class="gradebook-cell">
#{if columnDef.field == 'total_grade' then '' else '<div class="gradebook-tooltip">'+ gradeToShow.score + ' / ' + gradeToShow.possible + '</div>'}
#{percentage}
</div>
"""
res
calculateStudentGrade: (student) =>
if student.loaded
result = INST.GradeCalculator.calculate(student.submissionsAsArray, @assignment_groups, 'percent')
for group in result.group_sums
student["assignment_group_#{group.group.id}"] = {current: group.current, 'final': group['final']}
student["total_grade"] = {current: result.current, 'final': result['final']}
highlightColumn: (columnIndexOrEvent) =>
if isNaN(columnIndexOrEvent)
# then assume that columnIndexOrEvent is an event, so figure out which column
# it is based on its class name
match = columnIndexOrEvent.currentTarget.className.match(/c\d+/)
if match
columnIndexOrEvent = match.toString().replace('c', '')
@$grid.find('.slick-header-column:eq(' + columnIndexOrEvent + ')').addClass('hovered-column')
unhighlightColumns: () =>
@$grid.find('.hovered-column').removeClass('hovered-column')
showCommentDialog: =>
$('<div>TODO: show comments and stuff</div>').dialog()
return false
onGridInit: () ->
tooltipTexts = {}
@$grid = $('#gradebook_grid')
.fillWindowWithMe({
alsoResize: '#gradebook_students_grid',
onResize: () =>
@multiGrid.resizeCanvas()
})
.delegate('.slick-cell', 'mouseenter.gradebook focusin.gradebook', @highlightColumn)
.delegate('.slick-cell', 'mouseleave.gradebook focusout.gradebook', @unhighlightColumns)
.delegate('.gradebook-cell', 'hover.gradebook', -> $(this).toggleClass('hover'))
.delegate('.gradebook-cell-comment', 'click.gradebook', @showCommentDialog )
# # debugging stuff, remove
# events =
# onSort: null,
# onHeaderContextMenu: null,
# onHeaderClick: null,
# onClick: null,
# onDblClick: null,
# onContextMenu: null,
# onKeyDown: null,
# onAddNewRow: null,
# onValidationError: null,
# onViewportChanged: null,
# onSelectedRowsChanged: null,
# onColumnsReordered: null,
# onColumnsResized: null,
# onBeforeMoveRows: null,
# onMoveRows: null,
# # onCellChange: "Raised when cell has been edited. Args: row,cell,dataContext.",
# onBeforeEditCell : "Raised before a cell goes into edit mode. Return false to cancel. Args: row,cell,dataContext."
# onBeforeCellEditorDestroy: "Raised before a cell editor is destroyed. Args: current cell editor."
# onBeforeDestroy: "Raised just before the grid control is destroyed (part of the destroy() method)."
# onCurrentCellChanged: "Raised when the selected (active) cell changed. Args: {row:currentRow, cell:currentCell}."
# onCellRangeSelected: "Raised when a user selects a range of cells. Args: {from:{row,cell}, to:{row,cell}}."
# $.each events, (event, documentation) =>
# old = @multiGrid.grids[1][event]
# @multiGrid.grids[1][event] = () ->
# $.isFunction(old) && old.apply(this, arguments)
# console.log(event, documentation, arguments)
$('#grid-options').click (event) ->
event.preventDefault()
$('#sort_rows_dialog').dialog('close').dialog(width: 400, height: 300)
# set up row sorting options
$('#sort_rows_dialog .by_section').hide() unless @sections_enabled
$('#sort_rows_dialog button.sort_rows').click ->
gradebook.sortBy($(this).data('sort_by'))
$('#sort_rows_dialog').dialog('close')
initGrid: () ->
#this is used to figure out how wide to make each column
$widthTester = $('<span style="padding:10px" />').appendTo('#content')
testWidth = (text, minWidth) ->
Math.max($widthTester.text(text).outerWidth(), minWidth)
@columns = [{
id: 'student',
name: "<a href='javascript:void(0)' id='grid-options'>Options</a>",
field: 'display_name',
width: 150,
cssClass: "meta-cell"
},
{
id: 'secondary_identifier',
name: 'secondary ID',
field: 'secondary_identifier'
width: 100,
cssClass: "meta-cell secondary_identifier_cell"
}]
for id, assignment of @assignments when assignment.submission_types isnt "not_graded"
html = "<div class='assignment-name'>#{assignment.name}</div>"
html += "<div class='assignment-points-possible'>#{I18n.t 'points_out_of', "out of %{points_possible}", points_possible: assignment.points_possible}</div>" if assignment.points_possible?
outOfFormatter = assignment &&
assignment.grading_type == 'points' &&
assignment.points_possible? &&
SubmissionCell.out_of
minWidth = if outOfFormatter then 70 else 50
@columns.push
id: "assignment_#{id}"
field: "assignment_#{id}"
name: html
object: assignment
formatter: this.cellFormatter
editor: outOfFormatter ||
SubmissionCell[assignment.grading_type] ||
SubmissionCell
minWidth: minWidth,
maxWidth:200,
width: testWidth(assignment.name, minWidth)
for id, group of @assignment_groups
html = "#{group.name}"
html += "<div class='assignment-points-possible'>#{I18n.t 'percent_of_grade', "%{percentage} of grade", percentage: I18n.toPercentage(group.group_weight, precision: 0)}</div>" if group.group_weight?
@columns.push
id: "assignment_group_#{id}"
field: "assignment_group_#{id}"
formatter: @groupTotalFormatter
name: html
object: group
minWidth: 35,
maxWidth:200,
width: testWidth(group.name, 35)
cssClass: "meta-cell assignment-group-cell"
@columns.push
id: "total_grade"
field: "total_grade"
formatter: @groupTotalFormatter
name: "Total"
minWidth: 50,
maxWidth: 100,
width: testWidth("Total", 50)
cssClass: "total-cell"
$widthTester.remove()
options = $.extend({
enableCellNavigation: false
enableColumnReorder: false
enableAsyncPostRender: true
asyncPostRenderDelay: 1
autoEdit: true # whether to go into edit-mode as soon as you tab to a cell
rowHeight: 35
}, @options)
grids = [{
selector: '#gradebook_students_grid'
columns: @columns[0..1]
}, {
selector: '#gradebook_grid'
columns: @columns[2...@columns.length]
options:
enableCellNavigation: true
editable: true
syncColumnCellResize: true
}]
@multiGrid = new MultiGrid(@rows, options, grids, 1)
# this is the magic that actually updates group and final grades when you edit a cell
@multiGrid.grids[1].onCellChange = (row, col, student) =>
@calculateStudentGrade(student)
@multiGrid.parent_grid.onKeyDown = () =>
# TODO: start editing automatically when a number or letter is typed
false
@onGridInit?()

View File

@ -0,0 +1,29 @@
# this class coordinates multiple slick grids to behave as if they are
# different views on the same data -- if one scrolls vertically, the others
# scroll vertically as well.
this.MultiGrid = class MultiGrid
constructor: (data, default_options, grids, parent_grid) ->
@data = data
@parent_grid_idx = parent_grid
@grids = for grid_opts in grids
options = $.extend({}, default_options, grid_opts.options)
grid = new Slick.Grid(grid_opts.selector, @data, grid_opts.columns, options)
grid.multiview_grid_opts = grid_opts
grid_opts.$viewport = $(grid_opts.selector).find('.slick-viewport')
if grid_opts == grids[@parent_grid_idx]
@parent_grid = grid
else
grid_opts.$viewport.css('overflow-y', 'hidden')
grid
@parent_grid.onViewportChanged = () =>
for grid in @grids
if grid != @parent_grid
grid.multiview_grid_opts.$viewport[0].scrollTop =
@parent_grid.multiview_grid_opts.$viewport[0].scrollTop
grid.multiview_grid_opts.$viewport.trigger('scroll.slickgrid')
# simple delegation
for method in ['render', 'removeRow', 'removeAllRows', 'updateRowCount', 'autosizeColumns', 'resizeCanvas']
do (method) ->
MultiGrid::[method] = () ->
grid[method].apply(grid, arguments) for grid in @grids

View File

@ -0,0 +1,173 @@
this.SubmissionCell = class SubmissionCell
tooltipTexts = {}
constructor: (@opts) ->
@init()
init: () ->
submission = @opts.item[@opts.column.field]
@$wrapper = $(@cellWrapper('<input class="grade"/>')).appendTo(@opts.container)
@$input = @$wrapper.find('input').focus().select()
destroy: () ->
@$input.remove()
focus: () ->
@$input.focus()
loadValue: () ->
@val = @opts.item[@opts.column.field].grade || ""
@$input.val(@val)
@$input[0].defaultValue = @val
@$input.select()
serializeValue: () ->
@$input.val()
applyValue: (item, state) ->
item[@opts.column.field].grade = state
@wrapper?.remove()
@postValue(item, state)
# TODO: move selection down to the next row, same column
postValue: (item, state) ->
submission = item[@opts.column.field]
url = @opts.grid.getOptions().change_grade_url
url = url.replace(":assignment", submission.assignment_id).replace(":submission", submission.user_id)
$.ajaxJSON url, "PUT", { "submission[posted_grade]": state }
isValueChanged: () ->
@val != @$input.val()
validate: () ->
{ valid: true, msg: null }
@formatter: (row, col, submission, assignment) ->
this.prototype.cellWrapper(submission.grade, {submission: submission, assignment: assignment, editable: false})
# classes = []
# "<div class='cell-content gradebook-cell #{classes.join(' ')}'>#{submission.grade}</div>"
@imageForCell: (image_id) ->
$(image_id)[0].outerHTML
cellWrapper: (innerContents, options = {}) ->
opts = $.extend({}, {
innerContents: '',
classes: '',
editable: true
}, options)
opts.submission ||= @opts.item[@opts.column.field]
opts.assignment ||= @opts.column.object
specialClasses = SubmissionCell.classesBasedOnSubmission(opts.submission, opts.assignment)
tooltipText = $.map(specialClasses, (c)->
tooltipTexts[c] ?= $("#submission_tooltip_#{c}").text()
).join(', ')
"""
<div class="gradebook-cell #{ if opts.editable then 'gradebook-cell-editable focus' else ''} #{opts.classes} #{specialClasses.join(' ')}">
#{ if tooltipText then '<div class="gradebook-tooltip">'+ tooltipText + '</div>' else ''}
<a href="#" class="gradebook-cell-comment"><span class="gradebook-cell-comment-label">submission comments</span></a>
#{innerContents}
</div>
"""
@classesBasedOnSubmission: (submission={}, assignment={}) ->
classes = []
classes.push('resubmitted') if submission.grade_matches_current_submission == false
classes.push('late') if assignment.due_at && submission.submitted_at && (submission.submitted_at.timestamp > assignment.due_at.timestamp)
classes.push('dropped') if submission.drop
classes
class SubmissionCell.out_of extends SubmissionCell
init: () ->
submission = @opts.item[@opts.column.field]
@$wrapper = $(@cellWrapper("""
<div class="overflow-wrapper">
<div class="grade-and-outof-wrapper">
<input type="number" class="grade"/><span class="outof"><span class="divider">/</span>#{@opts.column.object.points_possible}</span>
</div>
</div>
""", { classes: 'gradebook-cell-out-of-formatter' })).appendTo(@opts.container)
@$input = @$wrapper.find('input').focus().select()
class SubmissionCell.pass_fail extends SubmissionCell
states = ['pass', 'fail', '']
classFromSubmission = (submission) ->
{ pass: 'pass', complete: 'pass', fail: 'fail', incomplete: 'fail' }[submission.grade] || ''
htmlFromSubmission: (options={}) ->
cssClass = classFromSubmission(options.submission)
SubmissionCell::cellWrapper("""
<a data-value="#{cssClass}" class="gradebook-checkbox gradebook-checkbox-#{cssClass} #{'editable' if options.editable}" href="#">#{cssClass}</a>
""", options)
# htmlFromSubmission = (submission, editable = false) ->
# cssClass = classFromSubmission(submission)
# """
# <div class="gradebook-cell #{SubmissionCell.classesBasedOnSubmission(submission).join(' ')}">
# <a href="#" class="gradebook-cell-comment"><span class="gradebook-cell-comment-label">submission comments</span></a>
# <a data-value="#{cssClass}" class="gradebook-checkbox gradebook-checkbox-#{cssClass} #{'editable' if editable}" href="#">#{cssClass}</a>
# </div>
# """
@formatter: (row, col, submission, assignment) ->
pass_fail::htmlFromSubmission({ submission, assignment, editable: false})
init: () ->
@$wrapper = $(@cellWrapper())
@$wrapper = $(@htmlFromSubmission({
submission: @opts.item[@opts.column.field],
assignment: @opts.column.object,
editable: true})
).appendTo(@opts.container)
@$input = @$wrapper.find('.gradebook-checkbox')
.bind('click keypress', (event) =>
event.preventDefault()
currentValue = @$input.data('value')
if currentValue is 'pass'
newValue = 'fail'
else if currentValue is 'fail'
newValue = ''
else
newValue = 'pass'
@transitionValue(newValue)
).focus()
destroy: () ->
@$wrapper.remove()
focus: () ->
@$input.focus()
transitionValue: (newValue) ->
@$input
.removeClass('gradebook-checkbox-pass gradebook-checkbox-fail')
.addClass('gradebook-checkbox-' + classFromSubmission(grade: newValue))
.data('value', newValue)
loadValue: () ->
@val = @opts.item[@opts.column.field].grade || ""
serializeValue: () ->
@$input.data('value')
applyValue: (item, state) ->
item[@opts.column.field].grade = state
this.postValue(item, state)
# TODO: move selection down to the next row, same column
postValue: (item, state) ->
submission = item[@opts.column.field]
url = @opts.grid.getOptions().change_grade_url
url = url.replace(":assignment", submission.assignment_id).replace(":submission", submission.user_id)
$.ajaxJSON url, "PUT", { "submission[posted_grade]": state }
@$input.effect('highlight') #TODO: also highlight the group total and the TotalScore
isValueChanged: () ->
@val != @$input.data('value')
class SubmissionCell.points extends SubmissionCell

View File

@ -43,19 +43,25 @@ class AssignmentGroupsController < ApplicationController
# "position": 7,
# "name": "group2",
# "id": 1,
# "assignments": [...]
# "group_weight": 20,
# "assignments": [...],
# "rules" : {...}
# },
# {
# "position": 10,
# "name": "group1",
# "id": 2,
# "assignments": [...]
# "group_weight": 20,
# "assignments": [...],
# "rules" : {...}
# },
# {
# "position": 12,
# "name": "group3",
# "id": 3,
# "assignments": [...]
# "group_weight": 60,
# "assignments": [...],
# "rules" : {...}
# }
# ]
def index
@ -73,7 +79,9 @@ class AssignmentGroupsController < ApplicationController
format.json {
hashes = @groups.map do |group|
hash = group.as_json(:include_root => false,
:only => %w(id name position))
:only => %w(id name position group_weight))
# note that 'rules_hash' gets to_jsoned as just 'rules' because that is what GradeCalculator expects.
hash['rules'] = group.rules_hash
if include_assignments
hash['assignments'] = group.assignments.active.map { |a| assignment_json(a, [], @context.user_is_teacher?(@current_user)) }
end

View File

@ -49,6 +49,7 @@ class AssignmentsApiController < ApplicationController
# "name": "some assignment",
# "points_possible": 12,
# "grading_type": "points",
# "due_at": "2011-05-26T23:59:00-06:00",
# "submission_types" : [
# "online_upload",
# "online_text_entry",

View File

@ -212,6 +212,17 @@ class CoursesController < ApplicationController
end
end
def api_student_enrollments(context)
enrollments = context.student_enrollments
enrollments.map do |e|
hash = e.user.as_json(:include_root => false, :only => STUDENT_API_FIELDS)
hash.merge!(
:secondary_identifier => e.user.secondary_identifier,
:computed_final_score => e.computed_final_score,
:computed_current_score => e.computed_current_score)
end
end
def destroy
@context = Course.find(params[:id])
if authorized_action(@context, @current_user, :delete)

View File

@ -0,0 +1,10 @@
class Gradebook2Controller < ApplicationController
before_filter :require_user_for_context
add_crumb("Gradebook") { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_grades_url }
before_filter { |c| c.active_tab = "grades" }
def show
if authorized_action(@context, @current_user, :manage_grades)
end
end
end

View File

@ -177,7 +177,7 @@ class GradebooksController < ApplicationController
end
end
end
def gradebook_init_json
# res = "{"
if params[:assignments]

View File

@ -29,6 +29,8 @@ class CourseSection < ActiveRecord::Base
belongs_to :enrollment_term
belongs_to :account
has_many :enrollments, :include => :user, :conditions => ['enrollments.workflow_state != ?', 'deleted'], :dependent => :destroy
has_many :student_enrollments, :class_name => 'StudentEnrollment', :conditions => ['enrollments.workflow_state != ? AND enrollments.workflow_state != ? AND enrollments.workflow_state != ? AND enrollments.workflow_state != ?', 'deleted', 'completed', 'rejected', 'inactive'], :include => :user
has_many :users, :through => :enrollments
has_many :course_account_associations
adheres_to_policy

View File

@ -1,3 +1,5 @@
@import environment.sass
=icon_link
:background-repeat no-repeat
:background-position left center
@ -237,20 +239,18 @@ a.no-underline,a.no-underline:hover, a.no-underline:focus
// atr is short for accessible text replacement
// these classes make it so you can have links like <a class="atr-reply">reply to this topic</a> and have only
// a reply.png icon show up.
=accessible_text_replacement
display: inline-block
text-indent: 999px
overflow: hidden
background-repeat: no-repeat
=atr_link
+accessible_text_replacement
width: 16px
height: 16px
.atr-reply
+accessible_text_replacement
+atr_link
background-image: url(/images/reply.png)
.atr-edit
+accessible_text_replacement
+atr_link
background-image: url(/images/edit.png)
.atr-delete
+accessible_text_replacement
+atr_link
background-image: url(/images/delete.png)

View File

@ -0,0 +1,281 @@
@import environment.sass
$cell_height: 33px
// overrides of the default slick.grid.css styles
#gradebook_grid
.slick-cell
border: 0
padding: 0
overflow: visible
&.editable
background: transparent
border: none
.gradebook2 #content
position: relative
padding: 0
#gradebook-grid-wrapper
position: relative
#gradebook_students_grid
float: left
width: 250px
// put some space at the bottom of the student names slickgrid to compensate
// for the lack of a scrollbar like the main grade grid so when you scroll
// to the bottom it can match the same scrollTop as the main grid.
.grid-canvas
padding-bottom: 50px
.student-name
color: #1b7eda
text-shadow: #fbf8f8 0 0 1px
.student-section
font-size: 10px
color: #aaa
.secondary_identifier_cell
color: #333
font-size: 10px
text-align: center
line-height: 35px
// override the default jquery ui top border
.slick-header.ui-state-default
border-top: 0
.slick-header-column
+vertical-gradiant(#f3f4f8, #dbdcde)
padding: 10px
font-size: 12px
text-align: center
font-weight: normal
// override jqueryUI style
&.ui-state-default
height: 30px
&.hovered-column
+vertical-gradiant(#e0ecf9, #bdd2e3)
.slick-column-name
font-weight: bold
color: #2f2a34
text-shadow: #fff 0 0 1px
.assignment-name
font-weight: normal
color: #1b7ecf
text-shadow: #fff 0 0 1px
//this is for the ellises
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
.assignment-points-possible
font-weight: normal
text-shadow: #84848a 0 0 1px
font-size: 10px
+opacity(0.5)
color: #2f2a34
#sort_rows_dialog
.button
margin-bottom: 5px
.odd .slick-cell
background-color: #f5f6f7
.even .slick-cell
background-color: #fcfcfc
.odd .meta-cell
background-color: #eff3f4
.even .meta-cell
background-color: #e6eaec
.odd .total-cell
background-color: #e0e6e8
.even .total-cell
background-color: #d7dee1
// overrides of jqueryUI theme
.slick-row.ui-state-active
color: inherit
background-image: none
font-weight: normal
// my styles
.slick-row.ui-state-active .slick-cell
background-color: #dfe9f2
.slick-row.ui-state-active .slick-cell.selected
+vertical-gradiant(#d7eaf9, #bedff6)
:background-color #CDF
.slick-cell div.cell-content
:text-align center
:vertical-align middle
.slick-cell.editable
border-color: #2fa1ff
.gradebook-tooltip
display: none
background-color: #444
color: #fff
+border-radius(3px)
padding: 5px 10px
z-index: 5
position: absolute
font-size: 0.8em
left: 0
top: -30px
&:after
border-color: #444 transparent
border-style: solid
border-width: 5px 5px 0
position: absolute
bottom: -5px
width: 0
left: 15px
content: ""
// make the tooltips for the top row pop-under so they fit in the grid
.slick-row[row="0"] &
bottom: -30px
top: auto
&:after
border-width: 0 5px 5px
bottom: auto
top: -5px
.gradebook-cell.hover &, .gradebook-cell.focus &, .slick-cell.selected &
display: block
.gradebook-cell
padding-top: 8px
height: $cell_height - 8px
position: relative
text-align: center
border: 1px solid transparent
border-right: 1px dotted silver
border-bottom-color: silver
background-repeat: repeat
&.late
background-image: url("/images/gradebook-late-indicator.png")
&.dropped
background-image: url("/images/gradebook-dropped-indicator.png")
&.resubmitted
background-image: url("/images/gradebook-resubmitted-indicator.png")
.gradebook-cell-comment
position: absolute
top: -1px
right: -1px
background: url("/images/gradebook-comments-sprite2.png") no-repeat 100% 0
height: 12px
width: 12px
visibility: hidden
z-index: 1 //needs to be above "normal but below tooltips"
overflow: hidden
&:hover,
&:focus
visibility: visible
background-position: 100% -88px !important
width: 17px
height: 17px
.gradebook-cell.with-comments &
visibility: visible
.gradebook-cell.focus &,
.gradebook-cell.hover &
visibility: visible
background-position: 100% -41px
width: 25px
height: 25px
.gradebook-cell-comment-label
+accessible_text_replacement
.gradebook-cell-editable
height: $cell_height - 1px -8px
padding-top: 8px
margin: 0
border: 0
border: 1px solid #35a5e5
background-color: #fff
box-shadow: 0 0 5px rgba(81, 203, 238, 1)
-webkit-box-shadow: 0 0 5px rgba(81, 203, 238, 1)
-moz-box-shadow: 0 0 5px rgba(81, 203, 238, 1)
.gradebook-cell
.grade
border: none
text-align: center
outline: none
font-size: 12px
width: 100%
padding: 0
margin: 0
background: none
.grade::-webkit-outer-spin-button
display: none
.gradebook-cell-out-of-formatter
padding-top: 0
height: $cell_height - 1px
.overflow-wrapper
overflow: hidden
position: relative
width: 100%
height: $cell_height - 1px
.grade-and-outof-wrapper
position: absolute
top: 50%
left: 50%
width: 400px
margin-left: -200px
margin-top: -10px //changeme
.divider
padding: 0 1px 0 2px
.outof, .grade
width: 200px
display: inline-block
background: transparent
.outof
+unselectable
text-align: left
color: #888
.grade
border: none
text-align: right
outline: none
font-size: 12px
.grade::-webkit-outer-spin-button
display: none
$gradebook_checkbox_width: 16px
.gradebook-checkbox
+accessible_text_replacement
display: block
position: absolute
left: 50%
top: 50%
width: $gradebook_checkbox_width
height: $gradebook_checkbox_width
margin-top: -$gradebook_checkbox_width/2
margin-left: -$gradebook_checkbox_width/2
background: url("/images/checkbox_sprite3.png") no-repeat 0 0
&.gradebook-checkbox-pass
background-position: -48px 0
&.gradebook-checkbox-fail
background-position: -64px 0
&.editable
&.gradebook-checkbox-pass
background-position: -16px 0
&.gradebook-checkbox-fail
background-position: -32px 0

View File

@ -35,4 +35,14 @@
background-image: -webkit-gradient(linear,left top,left bottom, color-stop(0, $topColor), color-stop(1, $bottomColor))
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#{$topColor}', EndColorStr='#{$bottomColor}')"
=accessible_text_replacement
display: inline-block
text-indent: 999px
overflow: hidden
background-repeat: no-repeat
=unselectable
-webkit-user-select: none
-moz-user-select: none
user-select: none

View File

@ -0,0 +1,50 @@
<%
content_for :page_title, "Gradebook - #{@context.name}"
@body_classes << "gradebook2"
@show_left_side = false
jammit_js :slickgrid, :gradebook2
jammit_css :slickgrid, :gradebook2
options = {
:chunk_size => 35,
:assignment_groups_url => api_v1_course_assignment_groups_url(@context, :include => [:assignments]),
:sections_and_students_url => api_v1_course_sections_url(@context, :include => [:students]),
:submissions_url => api_v1_course_student_submissions_url(@context, :grouped => '1'),
:change_grade_url => api_v1_course_assignment_submission_url(@context, ":assignment", ":submission"),
#:assignment_groups => @context.assignment_groups.all(:include => :assignments),
#:sections => @context.course_sections.all(:include => :users),
#:submissions => @context.submissions.all(),
}
%>
<div id="gradebook-toolbar">
</div>
<div id="gradebook-grid-wrapper">
<div id="gradebook_students_grid"></div>
<div id="gradebook_grid"></div>
</div>
<!-- gradebook images -->
<div style="display:none;">
<span id="submission_tooltip_dropped"><%= t 'dropped_for_grading', 'Dropped for grading purposes' %></span>
<span id="submission_tooltip_late"><%= t 'submitted_late', 'Submitted late' %></span>
<span id="submission_tooltip_resubmitted"><%= t 'resubmitted', 'Resubmitted since last graded' %></span>
<%= image_tag "pass.png", :id => "submission_entry_pass_image", :alt => "Pass", :title => "Pass", :class => "graded_icon" %>
<%= image_tag "pass.png", :id => "submission_entry_complete_image", :alt => "Complete", :title => "Complete", :class => "graded_icon" %>
<%= image_tag "fail.png", :id => "submission_entry_fail_image", :alt => "Fail", :title => "Fail", :class => "graded_icon" %>
<%= image_tag "fail.png", :id => "submission_entry_incomplete_image", :alt => "Incomplete", :title => "Incomplete", :class => "graded_icon" %>
</div>
<div id="gradebook_dialogs" style="display:none;">
<div id="sort_rows_dialog" title="Sort Gradebook Rows">
<button type="button" class="button sort_gradebook sort_rows" data-sort_by="display_name" title="By Student Name" style="width: 300px;">By Student Name</button>
<button type="button" class="button sort_gradebook sort_rows" data-sort_by="section" title="By Section Name" style="width: 300px;">By Section Name</button>
<button type="button" class="button sort_gradebook sort_rows by_grade" data-sort_by="grade_desc" title="By Total (Highest First)" style="width: 300px;">By Total (Highest First)</button>
<button type="button" class="button sort_gradebook sort_rows by_grade" data-sort_by="grade_asc" title="By Total (Lowest First)" style="width: 300px;">By Total (Lowest First)</button>
</div>
</div>
<% js_block do %>
<script>
new Gradebook(<%= options.to_json.html_safe %>);
</script>
<% end %>

View File

@ -69,7 +69,7 @@ javascripts:
- public/javascripts/jquery.metadata.js
- public/javascripts/gradebook-history.js
jobs:
- public/javascripts/jobs.js
- public/javascripts/compiled/jobs.js
profile:
- public/javascripts/profile.js
topics:
@ -124,9 +124,15 @@ javascripts:
- public/javascripts/email_lists.js
datagrid:
- public/javascripts/datagrid.js
gradebook2:
- public/javascripts/compiled/multi_grid.js
- public/javascripts/compiled/grade_calculator.js
- public/javascripts/compiled/gradebook2.js
- public/javascripts/compiled/submission_cell.js
gradebooks:
- public/javascripts/gradebooks.js
- public/javascripts/message_students.js
- public/javascripts/compiled/grade_calculator.js
attendance:
- public/javascripts/datagrid.js
- public/javascripts/attendance.js
@ -258,6 +264,8 @@ stylesheets:
gradebooks:
- public/stylesheets/compiled/gradebooks.css
- public/stylesheets/compiled/message_students.css
gradebook2:
- public/stylesheets/compiled/gradebook2.css
attendance:
- public/stylesheets/compiled/attendance.css
quizzes:

View File

@ -1,11 +1,12 @@
# Configure barista.
Barista.configure do |c|
c.add_preamble = false
# Change the root to use app/scripts
# c.root = Rails.root.join("app", "scripts")
# Change the output root, causing Barista to compile into public/coffeescripts
# c.output_root = Rails.root.join("public", "coffeescripts")
# Change the output root, causing Barista to compile into javascripts/compiled
c.output_root = Rails.root+'public/javascripts/compiled'
# Set the compiler

View File

@ -91,6 +91,8 @@ ActionController::Routing::Routes.draw do |map|
} do |gradebook|
gradebook.submissions_upload 'submissions_upload/:assignment_id', :controller => 'gradebooks', :action => 'submissions_zip_upload', :conditions => { :method => :post }
end
course.resource :gradebook2,
:controller => 'gradebook2'
course.attendance 'attendance', :controller => 'gradebooks', :action => 'attendance'
course.attendance_user 'attendance/:user_id', :controller => 'gradebooks', :action => 'attendance'
course.imports 'imports', :controller => 'content_imports', :action => 'intro'

View File

@ -19,7 +19,7 @@
module Api::V1::Assignment
def assignment_json(assignment, includes = [], show_admin_fields = false)
# no includes supported right now
hash = assignment.as_json(:include_root => false, :only => %w(id grading_type points_possible position))
hash = assignment.as_json(:include_root => false, :only => %w(id grading_type points_possible position due_at))
hash['name'] = assignment.title

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,187 @@
(function() {
var GradeCalculator;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
GradeCalculator = (function() {
function GradeCalculator() {}
GradeCalculator.calculate = function(submissions, groups, weighting_scheme) {
var result;
result = {};
result.group_sums = $.map(groups, __bind(function(group) {
return {
group: group,
current: this.create_group_sum(group, submissions, true),
'final': this.create_group_sum(group, submissions, false)
};
}, this));
result.current = this.calculate_total(result.group_sums, true, weighting_scheme);
result['final'] = this.calculate_total(result.group_sums, false, weighting_scheme);
return result;
};
GradeCalculator.create_group_sum = function(group, submissions, ignore_ungraded) {
var data, dropped, lowOrHigh, rules, s, submission, sum, _fn, _i, _j, _k, _l, _len, _len2, _len3, _len4, _len5, _m, _ref, _ref2, _ref3, _ref4, _ref5, _ref6;
sum = {
submissions: [],
score: 0,
possible: 0,
submission_count: 0
};
_fn = __bind(function(submission) {
var assignment, data, _ref;
if (!submission.assignment_group_id) {
assignment = $.detect(group.assignments, function() {
return submission.assignment_id === this.id;
});
if (assignment) {
submission.assignment_group_id = group.id;
if ((_ref = submission.points_possible) != null) {
_ref;
} else {
submission.points_possible = assignment != null ? assignment.points_possible : void 0;
};
}
}
if (submission.assignment_group_id === group.id) {
data = {
submission: submission,
score: 0,
possible: 0,
percent: 0,
drop: false,
submitted: false
};
sum.submissions.push(data);
if (!(ignore_ungraded && (!submission.score || submission.score === ''))) {
data.score = this.parse(submission.score);
data.possible = this.parse(submission.points_possible);
data.percent = this.parse(data.score / data.possible);
data.submitted = submission.score && submission.score !== '';
if (data.submitted) {
return sum.submission_count += 1;
}
}
}
}, this);
for (_i = 0, _len = submissions.length; _i < _len; _i++) {
submission = submissions[_i];
_fn(submission);
}
sum.submissions.sort(function(a, b) {
return a.percent - b.percent;
});
rules = $.extend({
drop_lowest: 0,
drop_highest: 0,
never_drop: []
}, group.rules);
dropped = 0;
_ref = ['low', 'high'];
for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) {
lowOrHigh = _ref[_j];
_ref2 = sum.submissions;
for (_k = 0, _len3 = _ref2.length; _k < _len3; _k++) {
data = _ref2[_k];
if (!data.drop && rules["drop_" + lowOrHigh + "est"] > 0 && $.inArray(data.assignment_id, rules.never_drop) === -1 && data.possible > 0 && data.submitted) {
data.drop = true;
if ((_ref3 = data.submission) != null) {
_ref3.drop = true;
}
rules["drop_" + lowOrHigh + "est"] -= 1;
dropped += 1;
}
}
}
if (dropped > 0 && dropped === sum.submission_count) {
sum.submissions[sum.submissions.length - 1].drop = false;
if ((_ref4 = sum.submissions[sum.submissions.length - 1].submission) != null) {
_ref4.drop = true;
}
dropped -= 1;
}
sum.submission_count -= dropped;
_ref5 = sum.submissions;
for (_l = 0, _len4 = _ref5.length; _l < _len4; _l++) {
s = _ref5[_l];
if (!s.drop) {
sum.score += s.score;
}
}
_ref6 = sum.submissions;
for (_m = 0, _len5 = _ref6.length; _m < _len5; _m++) {
s = _ref6[_m];
if (!s.drop) {
sum.possible += s.possible;
}
}
return sum;
};
GradeCalculator.calculate_total = function(group_sums, ignore_ungraded, weighting_scheme) {
var data, data_idx, possible, possible_weight_from_submissions, score, tally, total_possible_weight, _i, _len;
data_idx = ignore_ungraded ? 'current' : 'final';
if (weighting_scheme === 'percent') {
score = 0.0;
possible_weight_from_submissions = 0.0;
total_possible_weight = 0.0;
for (_i = 0, _len = group_sums.length; _i < _len; _i++) {
data = group_sums[_i];
if (data.group.group_weight > 0) {
if (data[data_idx].submission_count > 0) {
tally = data[data_idx].score / data[data_idx].possible;
score += data.group.group_weight * tally;
possible_weight_from_submissions += data.group.group_weight;
}
total_possible_weight += data.group.group_weight;
}
}
if (ignore_ungraded && possible_weight_from_submissions < 1.0) {
possible = total_possible_weight < 1.0 ? total_possible_weight : 1.0;
score = score * possible / possible_weight_from_submissions;
}
return {
score: score,
possible: 1.0
};
} else {
return {
score: this.sum((function() {
var _j, _len2, _results;
_results = [];
for (_j = 0, _len2 = group_sums.length; _j < _len2; _j++) {
data = group_sums[_j];
_results.push(data[data_idx].score);
}
return _results;
})()),
possible: this.sum((function() {
var _j, _len2, _results;
_results = [];
for (_j = 0, _len2 = group_sums.length; _j < _len2; _j++) {
data = group_sums[_j];
_results.push(data[data_idx].possible);
}
return _results;
})())
};
}
};
GradeCalculator.sum = function(values) {
var result, value, _i, _len;
result = 0;
for (_i = 0, _len = values.length; _i < _len; _i++) {
value = values[_i];
result += value;
}
return result;
};
GradeCalculator.parse = function(score) {
var result;
result = parseFloat(score);
if (result && isFinite(result)) {
return result;
} else {
return 0;
}
};
return GradeCalculator;
})();
window.INST.GradeCalculator = GradeCalculator;
}).call(this);

View File

@ -0,0 +1,424 @@
(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
I18n.scoped('gradebook2', function(I18n) {
var Gradebook;
return this.Gradebook = Gradebook = (function() {
function Gradebook(options) {
this.options = options;
this.showCommentDialog = __bind(this.showCommentDialog, this);
this.unhighlightColumns = __bind(this.unhighlightColumns, this);
this.highlightColumn = __bind(this.highlightColumn, this);
this.calculateStudentGrade = __bind(this.calculateStudentGrade, this);
this.groupTotalFormatter = __bind(this.groupTotalFormatter, this);
this.staticCellFormatter = __bind(this.staticCellFormatter, this);
this.cellFormatter = __bind(this.cellFormatter, this);
this.gotSubmissionsChunk = __bind(this.gotSubmissionsChunk, this);
this.gotStudents = __bind(this.gotStudents, this);
this.gotAssignmentGroups = __bind(this.gotAssignmentGroups, this);
this.chunk_start = 0;
this.students = {};
this.rows = [];
this.filterFn = function(student) {
return true;
};
this.sortFn = function(student) {
return student.display_name;
};
this.init();
this.includeUngradedAssignments = false;
}
Gradebook.prototype.init = function() {
if (this.options.assignment_groups) {
return this.gotAssignmentGroups(this.options.assignment_groups);
}
return $.ajaxJSON(this.options.assignment_groups_url, "GET", {}, this.gotAssignmentGroups);
};
Gradebook.prototype.gotAssignmentGroups = function(assignment_groups) {
var assignment, group, _i, _j, _len, _len2, _ref;
this.assignment_groups = {};
this.assignments = {};
for (_i = 0, _len = assignment_groups.length; _i < _len; _i++) {
group = assignment_groups[_i];
$.htmlEscapeValues(group);
this.assignment_groups[group.id] = group;
_ref = group.assignments;
for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) {
assignment = _ref[_j];
$.htmlEscapeValues(assignment);
if (assignment.due_at) {
assignment.due_at = $.parseFromISO(assignment.due_at);
}
this.assignments[assignment.id] = assignment;
}
}
if (this.options.sections) {
return this.gotStudents(this.options.sections);
}
return $.ajaxJSON(this.options.sections_and_students_url, "GET", {}, this.gotStudents);
};
Gradebook.prototype.gotStudents = function(sections) {
var assignment, id, section, student, _i, _j, _len, _len2, _name, _ref, _ref2, _ref3;
this.sections = {};
this.rows = [];
for (_i = 0, _len = sections.length; _i < _len; _i++) {
section = sections[_i];
$.htmlEscapeValues(section);
this.sections[section.id] = section;
_ref = section.students;
for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) {
student = _ref[_j];
$.htmlEscapeValues(student);
student.computed_current_score || (student.computed_current_score = 0);
student.computed_final_score || (student.computed_final_score = 0);
this.students[student.id] = student;
student.section = section;
_ref2 = this.assignments;
for (id in _ref2) {
assignment = _ref2[id];
student[_name = "assignment_" + id] || (student[_name] = {
assignment_id: id,
user_id: student.id
});
}
this.rows.push(student);
}
}
this.sections_enabled = sections.length > 1;
_ref3 = this.students;
for (id in _ref3) {
student = _ref3[id];
student.display_name = "<div class='student-name'>" + student.name + "</div>";
if (this.sections_enabled) {
student.display_name += "<div class='student-section'>" + student.section.name + "</div>";
}
}
this.initGrid();
this.buildRows();
return this.getSubmissionsChunk();
};
Gradebook.prototype.buildRows = function() {
var i, id, sortables, student, _len, _ref, _ref2;
this.rows.length = 0;
sortables = {};
_ref = this.students;
for (id in _ref) {
student = _ref[id];
student.row = -1;
if (this.filterFn(student)) {
this.rows.push(student);
sortables[student.id] = this.sortFn(student);
}
}
this.rows.sort(function(a, b) {
if (sortables[a.id] < sortables[b.id]) {
return -1;
} else if (sortables[a.id] > sortables[b.id]) {
return 1;
} else {
return 0;
}
});
_ref2 = this.rows;
for (i = 0, _len = _ref2.length; i < _len; i++) {
student = _ref2[i];
student.row = i;
}
this.multiGrid.removeAllRows();
this.multiGrid.updateRowCount();
return this.multiGrid.render();
};
Gradebook.prototype.sortBy = function(sort) {
this.sortFn = (function() {
switch (sort) {
case "display_name":
return function(student) {
return student.display_name;
};
case "section":
return function(student) {
return student.section.name;
};
case "grade_desc":
return function(student) {
return -student.computed_current_score;
};
case "grade_asc":
return function(student) {
return student.computed_current_score;
};
}
})();
return this.buildRows();
};
Gradebook.prototype.getSubmissionsChunk = function(student_id) {
var assignment, id, params, student, students;
if (this.options.submissions) {
return this.gotSubmissionsChunk(this.options.submissions);
}
students = this.rows.slice(this.chunk_start, this.chunk_start + this.options.chunk_size);
params = {
student_ids: (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = students.length; _i < _len; _i++) {
student = students[_i];
_results.push(student.id);
}
return _results;
})(),
assignment_ids: (function() {
var _ref, _results;
_ref = this.assignments;
_results = [];
for (id in _ref) {
assignment = _ref[id];
_results.push(id);
}
return _results;
}).call(this),
response_fields: ['user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id', 'grade_matches_current_submission']
};
if (students.length > 0) {
return $.ajaxJSON(this.options.submissions_url, "GET", params, this.gotSubmissionsChunk);
}
};
Gradebook.prototype.gotSubmissionsChunk = function(student_submissions) {
var data, student, submission, _i, _j, _len, _len2, _ref;
for (_i = 0, _len = student_submissions.length; _i < _len; _i++) {
data = student_submissions[_i];
student = this.students[data.user_id];
student.submissionsAsArray = [];
_ref = data.submissions;
for (_j = 0, _len2 = _ref.length; _j < _len2; _j++) {
submission = _ref[_j];
if (submission.submitted_at) {
submission.submitted_at = $.parseFromISO(submission.submitted_at);
}
student["assignment_" + submission.assignment_id] = submission;
student.submissionsAsArray.push(submission);
}
student.loaded = true;
this.multiGrid.removeRow(student.row);
this.calculateStudentGrade(student);
}
this.multiGrid.render();
this.chunk_start += this.options.chunk_size;
return this.getSubmissionsChunk();
};
Gradebook.prototype.cellFormatter = function(row, col, submission) {
var assignment;
if (!this.rows[row].loaded) {
return this.staticCellFormatter(row, col, '');
} else if (!(submission != null ? submission.grade : void 0)) {
return this.staticCellFormatter(row, col, '-');
} else {
assignment = this.assignments[submission.assignment_id];
if (!(assignment != null)) {
return this.staticCellFormatter(row, col, '');
} else {
if (assignment.grading_type === 'points' && assignment.points_possible) {
return SubmissionCell.out_of.formatter(row, col, submission, assignment);
} else {
return (SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment);
}
}
}
};
Gradebook.prototype.staticCellFormatter = function(row, col, val) {
return "<div class='cell-content gradebook-cell'>" + val + "</div>";
};
Gradebook.prototype.groupTotalFormatter = function(row, col, val, columnDef, student) {
var gradeToShow, percentage, res;
if (val == null) {
return '';
}
gradeToShow = val[this.includeUngradedAssignments ? 'final' : 'current'];
percentage = columnDef.field === 'total_grade' ? gradeToShow.score : (gradeToShow.score / gradeToShow.possible) * 100;
percentage = Math.round(percentage);
if (isNaN(percentage)) {
percentage = 0;
}
if (!gradeToShow.possible) {
percentage = '-';
} else {
percentage += "%";
}
res = "<div class=\"gradebook-cell\">\n " + (columnDef.field === 'total_grade' ? '' : '<div class="gradebook-tooltip">' + gradeToShow.score + ' / ' + gradeToShow.possible + '</div>') + "\n " + percentage + "\n</div>";
return res;
};
Gradebook.prototype.calculateStudentGrade = function(student) {
var group, result, _i, _len, _ref;
if (student.loaded) {
result = INST.GradeCalculator.calculate(student.submissionsAsArray, this.assignment_groups, 'percent');
_ref = result.group_sums;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
group = _ref[_i];
student["assignment_group_" + group.group.id] = {
current: group.current,
'final': group['final']
};
}
return student["total_grade"] = {
current: result.current,
'final': result['final']
};
}
};
Gradebook.prototype.highlightColumn = function(columnIndexOrEvent) {
var match;
if (isNaN(columnIndexOrEvent)) {
match = columnIndexOrEvent.currentTarget.className.match(/c\d+/);
if (match) {
columnIndexOrEvent = match.toString().replace('c', '');
}
}
return this.$grid.find('.slick-header-column:eq(' + columnIndexOrEvent + ')').addClass('hovered-column');
};
Gradebook.prototype.unhighlightColumns = function() {
return this.$grid.find('.hovered-column').removeClass('hovered-column');
};
Gradebook.prototype.showCommentDialog = function() {
$('<div>TODO: show comments and stuff</div>').dialog();
return false;
};
Gradebook.prototype.onGridInit = function() {
var tooltipTexts;
tooltipTexts = {};
this.$grid = $('#gradebook_grid').fillWindowWithMe({
alsoResize: '#gradebook_students_grid',
onResize: __bind(function() {
return this.multiGrid.resizeCanvas();
}, this)
}).delegate('.slick-cell', 'mouseenter.gradebook focusin.gradebook', this.highlightColumn).delegate('.slick-cell', 'mouseleave.gradebook focusout.gradebook', this.unhighlightColumns).delegate('.gradebook-cell', 'hover.gradebook', function() {
return $(this).toggleClass('hover');
}).delegate('.gradebook-cell-comment', 'click.gradebook', this.showCommentDialog);
$('#grid-options').click(function(event) {
event.preventDefault();
return $('#sort_rows_dialog').dialog('close').dialog({
width: 400,
height: 300
});
});
if (!this.sections_enabled) {
$('#sort_rows_dialog .by_section').hide();
}
return $('#sort_rows_dialog button.sort_rows').click(function() {
gradebook.sortBy($(this).data('sort_by'));
return $('#sort_rows_dialog').dialog('close');
});
};
Gradebook.prototype.initGrid = function() {
var $widthTester, assignment, grids, group, html, id, minWidth, options, outOfFormatter, testWidth, _ref, _ref2;
$widthTester = $('<span style="padding:10px" />').appendTo('#content');
testWidth = function(text, minWidth) {
return Math.max($widthTester.text(text).outerWidth(), minWidth);
};
this.columns = [
{
id: 'student',
name: "<a href='javascript:void(0)' id='grid-options'>Options</a>",
field: 'display_name',
width: 150,
cssClass: "meta-cell"
}, {
id: 'secondary_identifier',
name: 'secondary ID',
field: 'secondary_identifier',
width: 100,
cssClass: "meta-cell secondary_identifier_cell"
}
];
_ref = this.assignments;
for (id in _ref) {
assignment = _ref[id];
if (assignment.submission_types !== "not_graded") {
html = "<div class='assignment-name'>" + assignment.name + "</div>";
if (assignment.points_possible != null) {
html += "<div class='assignment-points-possible'>" + (I18n.t('points_out_of', "out of %{points_possible}", {
points_possible: assignment.points_possible
})) + "</div>";
}
outOfFormatter = assignment && assignment.grading_type === 'points' && (assignment.points_possible != null) && SubmissionCell.out_of;
minWidth = outOfFormatter ? 70 : 50;
this.columns.push({
id: "assignment_" + id,
field: "assignment_" + id,
name: html,
object: assignment,
formatter: this.cellFormatter,
editor: outOfFormatter || SubmissionCell[assignment.grading_type] || SubmissionCell,
minWidth: minWidth,
maxWidth: 200,
width: testWidth(assignment.name, minWidth)
});
}
}
_ref2 = this.assignment_groups;
for (id in _ref2) {
group = _ref2[id];
html = "" + group.name;
if (group.group_weight != null) {
html += "<div class='assignment-points-possible'>" + (I18n.t('percent_of_grade', "%{percentage} of grade", {
percentage: I18n.toPercentage(group.group_weight, {
precision: 0
})
})) + "</div>";
}
this.columns.push({
id: "assignment_group_" + id,
field: "assignment_group_" + id,
formatter: this.groupTotalFormatter,
name: html,
object: group,
minWidth: 35,
maxWidth: 200,
width: testWidth(group.name, 35),
cssClass: "meta-cell assignment-group-cell"
});
}
this.columns.push({
id: "total_grade",
field: "total_grade",
formatter: this.groupTotalFormatter,
name: "Total",
minWidth: 50,
maxWidth: 100,
width: testWidth("Total", 50),
cssClass: "total-cell"
});
$widthTester.remove();
options = $.extend({
enableCellNavigation: false,
enableColumnReorder: false,
enableAsyncPostRender: true,
asyncPostRenderDelay: 1,
autoEdit: true,
rowHeight: 35
}, this.options);
grids = [
{
selector: '#gradebook_students_grid',
columns: this.columns.slice(0, 2)
}, {
selector: '#gradebook_grid',
columns: this.columns.slice(2, this.columns.length),
options: {
enableCellNavigation: true,
editable: true,
syncColumnCellResize: true
}
}
];
this.multiGrid = new MultiGrid(this.rows, options, grids, 1);
this.multiGrid.grids[1].onCellChange = __bind(function(row, col, student) {
return this.calculateStudentGrade(student);
}, this);
this.multiGrid.parent_grid.onKeyDown = __bind(function() {
return false;
}, this);
return typeof this.onGridInit === "function" ? this.onGridInit() : void 0;
};
return Gradebook;
})();
});
}).call(this);

View File

@ -1,7 +1,3 @@
/* DO NOT MODIFY. This file was compiled Wed, 22 Jun 2011 20:46:48 GMT from
* /Users/jon/Dropbox/git/canvas-lms/app/coffeescripts/jobs.coffee
*/
(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }

View File

@ -0,0 +1,57 @@
(function() {
var MultiGrid, method, _fn, _i, _len, _ref;
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.MultiGrid = MultiGrid = (function() {
function MultiGrid(data, default_options, grids, parent_grid) {
var grid, grid_opts, options;
this.data = data;
this.parent_grid_idx = parent_grid;
this.grids = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = grids.length; _i < _len; _i++) {
grid_opts = grids[_i];
options = $.extend({}, default_options, grid_opts.options);
grid = new Slick.Grid(grid_opts.selector, this.data, grid_opts.columns, options);
grid.multiview_grid_opts = grid_opts;
grid_opts.$viewport = $(grid_opts.selector).find('.slick-viewport');
if (grid_opts === grids[this.parent_grid_idx]) {
this.parent_grid = grid;
} else {
grid_opts.$viewport.css('overflow-y', 'hidden');
}
_results.push(grid);
}
return _results;
}).call(this);
this.parent_grid.onViewportChanged = __bind(function() {
var grid, _i, _len, _ref, _results;
_ref = this.grids;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
grid = _ref[_i];
_results.push(grid !== this.parent_grid ? (grid.multiview_grid_opts.$viewport[0].scrollTop = this.parent_grid.multiview_grid_opts.$viewport[0].scrollTop, grid.multiview_grid_opts.$viewport.trigger('scroll.slickgrid')) : void 0);
}
return _results;
}, this);
}
return MultiGrid;
})();
_ref = ['render', 'removeRow', 'removeAllRows', 'updateRowCount', 'autosizeColumns', 'resizeCanvas'];
_fn = function(method) {
return MultiGrid.prototype[method] = function() {
var grid, _j, _len2, _ref2, _results;
_ref2 = this.grids;
_results = [];
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
grid = _ref2[_j];
_results.push(grid[method].apply(grid, arguments));
}
return _results;
};
};
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
method = _ref[_i];
_fn(method);
}
}).call(this);

View File

@ -0,0 +1,225 @@
(function() {
var SubmissionCell;
var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor;
child.__super__ = parent.prototype;
return child;
}, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.SubmissionCell = SubmissionCell = (function() {
var tooltipTexts;
tooltipTexts = {};
function SubmissionCell(opts) {
this.opts = opts;
this.init();
}
SubmissionCell.prototype.init = function() {
var submission;
submission = this.opts.item[this.opts.column.field];
this.$wrapper = $(this.cellWrapper('<input class="grade"/>')).appendTo(this.opts.container);
return this.$input = this.$wrapper.find('input').focus().select();
};
SubmissionCell.prototype.destroy = function() {
return this.$input.remove();
};
SubmissionCell.prototype.focus = function() {
return this.$input.focus();
};
SubmissionCell.prototype.loadValue = function() {
this.val = this.opts.item[this.opts.column.field].grade || "";
this.$input.val(this.val);
this.$input[0].defaultValue = this.val;
return this.$input.select();
};
SubmissionCell.prototype.serializeValue = function() {
return this.$input.val();
};
SubmissionCell.prototype.applyValue = function(item, state) {
var _ref;
item[this.opts.column.field].grade = state;
if ((_ref = this.wrapper) != null) {
_ref.remove();
}
return this.postValue(item, state);
};
SubmissionCell.prototype.postValue = function(item, state) {
var submission, url;
submission = item[this.opts.column.field];
url = this.opts.grid.getOptions().change_grade_url;
url = url.replace(":assignment", submission.assignment_id).replace(":submission", submission.user_id);
return $.ajaxJSON(url, "PUT", {
"submission[posted_grade]": state
});
};
SubmissionCell.prototype.isValueChanged = function() {
return this.val !== this.$input.val();
};
SubmissionCell.prototype.validate = function() {
return {
valid: true,
msg: null
};
};
SubmissionCell.formatter = function(row, col, submission, assignment) {
return this.prototype.cellWrapper(submission.grade, {
submission: submission,
assignment: assignment,
editable: false
});
};
SubmissionCell.imageForCell = function(image_id) {
return $(image_id)[0].outerHTML;
};
SubmissionCell.prototype.cellWrapper = function(innerContents, options) {
var opts, specialClasses, tooltipText;
if (options == null) {
options = {};
}
opts = $.extend({}, {
innerContents: '',
classes: '',
editable: true
}, options);
opts.submission || (opts.submission = this.opts.item[this.opts.column.field]);
opts.assignment || (opts.assignment = this.opts.column.object);
specialClasses = SubmissionCell.classesBasedOnSubmission(opts.submission, opts.assignment);
tooltipText = $.map(specialClasses, function(c) {
var _ref;
return (_ref = tooltipTexts[c]) != null ? _ref : tooltipTexts[c] = $("#submission_tooltip_" + c).text();
}).join(', ');
return "<div class=\"gradebook-cell " + (opts.editable ? 'gradebook-cell-editable focus' : '') + " " + opts.classes + " " + (specialClasses.join(' ')) + "\">\n " + (tooltipText ? '<div class="gradebook-tooltip">' + tooltipText + '</div>' : '') + "\n <a href=\"#\" class=\"gradebook-cell-comment\"><span class=\"gradebook-cell-comment-label\">submission comments</span></a>\n " + innerContents + "\n</div>";
};
SubmissionCell.classesBasedOnSubmission = function(submission, assignment) {
var classes;
if (submission == null) {
submission = {};
}
if (assignment == null) {
assignment = {};
}
classes = [];
if (submission.grade_matches_current_submission === false) {
classes.push('resubmitted');
}
if (assignment.due_at && submission.submitted_at && (submission.submitted_at.timestamp > assignment.due_at.timestamp)) {
classes.push('late');
}
if (submission.drop) {
classes.push('dropped');
}
return classes;
};
return SubmissionCell;
})();
SubmissionCell.out_of = (function() {
__extends(out_of, SubmissionCell);
function out_of() {
out_of.__super__.constructor.apply(this, arguments);
}
out_of.prototype.init = function() {
var submission;
submission = this.opts.item[this.opts.column.field];
this.$wrapper = $(this.cellWrapper("<div class=\"overflow-wrapper\">\n <div class=\"grade-and-outof-wrapper\">\n <input type=\"number\" class=\"grade\"/><span class=\"outof\"><span class=\"divider\">/</span>" + this.opts.column.object.points_possible + "</span>\n </div>\n</div>", {
classes: 'gradebook-cell-out-of-formatter'
})).appendTo(this.opts.container);
return this.$input = this.$wrapper.find('input').focus().select();
};
return out_of;
})();
SubmissionCell.pass_fail = (function() {
var classFromSubmission, states;
__extends(pass_fail, SubmissionCell);
function pass_fail() {
pass_fail.__super__.constructor.apply(this, arguments);
}
states = ['pass', 'fail', ''];
classFromSubmission = function(submission) {
return {
pass: 'pass',
complete: 'pass',
fail: 'fail',
incomplete: 'fail'
}[submission.grade] || '';
};
pass_fail.prototype.htmlFromSubmission = function(options) {
var cssClass;
if (options == null) {
options = {};
}
cssClass = classFromSubmission(options.submission);
return SubmissionCell.prototype.cellWrapper("<a data-value=\"" + cssClass + "\" class=\"gradebook-checkbox gradebook-checkbox-" + cssClass + " " + (options.editable ? 'editable' : void 0) + "\" href=\"#\">" + cssClass + "</a>", options);
};
pass_fail.formatter = function(row, col, submission, assignment) {
return pass_fail.prototype.htmlFromSubmission({
submission: submission,
assignment: assignment,
editable: false
});
};
pass_fail.prototype.init = function() {
this.$wrapper = $(this.cellWrapper());
this.$wrapper = $(this.htmlFromSubmission({
submission: this.opts.item[this.opts.column.field],
assignment: this.opts.column.object,
editable: true
})).appendTo(this.opts.container);
return this.$input = this.$wrapper.find('.gradebook-checkbox').bind('click keypress', __bind(function(event) {
var currentValue, newValue;
event.preventDefault();
currentValue = this.$input.data('value');
if (currentValue === 'pass') {
newValue = 'fail';
} else if (currentValue === 'fail') {
newValue = '';
} else {
newValue = 'pass';
}
return this.transitionValue(newValue);
}, this)).focus();
};
pass_fail.prototype.destroy = function() {
return this.$wrapper.remove();
};
pass_fail.prototype.focus = function() {
return this.$input.focus();
};
pass_fail.prototype.transitionValue = function(newValue) {
return this.$input.removeClass('gradebook-checkbox-pass gradebook-checkbox-fail').addClass('gradebook-checkbox-' + classFromSubmission({
grade: newValue
})).data('value', newValue);
};
pass_fail.prototype.loadValue = function() {
return this.val = this.opts.item[this.opts.column.field].grade || "";
};
pass_fail.prototype.serializeValue = function() {
return this.$input.data('value');
};
pass_fail.prototype.applyValue = function(item, state) {
item[this.opts.column.field].grade = state;
return this.postValue(item, state);
};
pass_fail.prototype.postValue = function(item, state) {
var submission, url;
submission = item[this.opts.column.field];
url = this.opts.grid.getOptions().change_grade_url;
url = url.replace(":assignment", submission.assignment_id).replace(":submission", submission.user_id);
$.ajaxJSON(url, "PUT", {
"submission[posted_grade]": state
});
return this.$input.effect('highlight');
};
pass_fail.prototype.isValueChanged = function() {
return this.val !== this.$input.data('value');
};
return pass_fail;
})();
SubmissionCell.points = (function() {
__extends(points, SubmissionCell);
function points() {
points.__super__.constructor.apply(this, arguments);
}
return points;
})();
}).call(this);

View File

@ -77,6 +77,54 @@
} while (prefix && $('#' + id).length);
return id;
};
// Return the first value which passes a truth test
$.detect = function(collection, callback) {
var result;
$.each(collection, function(index, value) {
if (callback.call(value, index, collection)) {
result = value;
return false; // we found it, break the $.each() loop iteration by returning false
}
});
return result;
};
// this is just pulled from jquery 1.6 because jquery 1.5 could not do .map on an object
$.map = function (elems, callback, arg) {
var value, key, ret = [],
i = 0,
length = elems.length,
// jquery objects are treated as arrays
isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ((length > 0 && elems[0] && elems[length - 1]) || length === 0 || jQuery.isArray(elems));
// Go through the array, translating each of the items to their
if (isArray) {
for (; i < length; i++) {
value = callback(elems[i], i, arg);
if (value != null) {
ret[ret.length] = value;
}
}
// Go through every key on the object,
} else {
for (key in elems) {
value = callback(elems[key], key, arg);
if (value != null) {
ret[ret.length] = value;
}
}
}
// Flatten any nested arrays
return ret.concat.apply([], ret);
}
// Intercepts the default form submission process. Uses the form tag's
// current action and method attributes to know where to submit to.
@ -3315,20 +3363,26 @@
};
// this is used if you want to fill the browser window with something inside #content but you want to also leave the footer and header on the page.
$.fn.fillWindowWithMe = function(){
var $this = $(this),
$.fn.fillWindowWithMe = function(options){
var opts = $.extend({minHeight: 400}, options),
$this = $(this),
$wrapper_container = $('#wrapper-container'),
$main = $('#main'),
$not_right_side = $('#not_right_side'),
$window = $(window);
$window = $(window),
$toResize = $(this).add(opts.alsoResize);
function fillWindowWithThisElement(){
$this.height(0);
$toResize.height(0);
var spaceLeftForThis = $window.height()
- ($wrapper_container.offset().top + $wrapper_container.height())
+ ($main.height() - $not_right_side.height());
+ ($main.height() - $not_right_side.height()),
newHeight = Math.max(400, spaceLeftForThis);
$this.height( Math.max(400, spaceLeftForThis) );
$toResize.height(newHeight);
if ($.isFunction(opts.onResize)) {
opts.onResize.call($this, newHeight);
}
}
fillWindowWithThisElement();
$window

View File

@ -40,16 +40,22 @@ describe AssignmentGroupsController, :type => :integration do
'id' => group2.id,
'name' => 'group2',
'position' => 7,
'rules' => {},
'group_weight' => 0
},
{
'id' => group1.id,
'name' => 'group1',
'position' => 10,
'rules' => {},
'group_weight' => 0
},
{
'id' => group3.id,
'name' => 'group3',
'position' => 12,
'rules' => {},
'group_weight' => 0
},
]
end
@ -58,8 +64,10 @@ describe AssignmentGroupsController, :type => :integration do
course_with_teacher(:active_all => true)
group1 = @course.assignment_groups.create!(:name => 'group1')
group1.update_attribute(:position, 10)
group1.update_attribute(:group_weight, 40)
group2 = @course.assignment_groups.create!(:name => 'group2')
group2.update_attribute(:position, 7)
group2.update_attribute(:group_weight, 60)
a1 = @course.assignments.create!(:title => "test1", :assignment_group => group1, :points_possible => 10)
a2 = @course.assignments.create!(:title => "test2", :assignment_group => group1, :points_possible => 12)
@ -82,9 +90,12 @@ describe AssignmentGroupsController, :type => :integration do
'id' => group2.id,
'name' => 'group2',
'position' => 7,
'rules' => {},
'group_weight' => 60,
'assignments' => [
{
'id' => a3.id,
'due_at' => nil,
'name' => 'test3',
'position' => 1,
'points_possible' => 8,
@ -113,6 +124,7 @@ describe AssignmentGroupsController, :type => :integration do
},
{
'id' => a4.id,
'due_at' => nil,
'name' => 'test4',
'position' => 2,
'points_possible' => 9,
@ -128,9 +140,12 @@ describe AssignmentGroupsController, :type => :integration do
'id' => group1.id,
'name' => 'group1',
'position' => 10,
'rules' => {},
'group_weight' => 40,
'assignments' => [
{
'id' => a1.id,
'due_at' => nil,
'name' => 'test1',
'position' => 1,
'points_possible' => 10,
@ -142,6 +157,7 @@ describe AssignmentGroupsController, :type => :integration do
},
{
'id' => a2.id,
'due_at' => nil,
'name' => 'test2',
'position' => 2,
'points_possible' => 12,

View File

@ -79,6 +79,7 @@ describe AssignmentsApiController, :type => :integration do
'use_rubric_for_grading' => true,
'free_form_criterion_comments' => true,
'needs_grading_count' => 0,
'due_at' => nil,
'submission_types' => [
"online_upload",
"online_text_entry",
@ -145,6 +146,7 @@ describe AssignmentsApiController, :type => :integration do
'points_possible' => 12,
'grading_type' => 'points',
'needs_grading_count' => 0,
'due_at' => nil,
'submission_types' => [
'none',
],
@ -181,6 +183,7 @@ describe AssignmentsApiController, :type => :integration do
'points_possible' => 15,
'grading_type' => 'points',
'needs_grading_count' => 0,
'due_at' => nil,
'submission_types' => [
'none',
],