Merge branch 'dev/weighted-grading' into master

Fixes CNVS-35214

Change-Id: I55a1d4a02ed2640ad3f147e2c5534ef8f747c2f4
This commit is contained in:
Neil Gupta 2017-03-03 10:59:36 -06:00
commit ec51d5843a
152 changed files with 4585 additions and 2615 deletions

View File

@ -3,10 +3,11 @@ define [
'underscore'
'i18n!grading_periods'
'jsx/shared/helpers/dateHelper'
'compiled/api/gradingPeriodsApi'
'axios'
'jsx/shared/CheatDepaginator'
'jquery.instructure_misc_helpers'
], ($, _, I18n, DateHelper, axios, Depaginate) ->
], ($, _, I18n, DateHelper, gradingPeriodsApi, axios, Depaginate) ->
listUrl = () =>
ENV.GRADING_PERIOD_SETS_URL
@ -17,26 +18,15 @@ define [
$.replaceTags(ENV.GRADING_PERIOD_SET_UPDATE_URL, 'id', id)
serializeSet = (set) =>
grading_period_set: { title: set.title },
grading_period_set: { title: set.title, weighted: set.weighted },
enrollment_term_ids: set.enrollmentTermIDs
deserializePeriods = (periods) =>
_.map periods, (period) ->
{
id: period.id.toString()
title: period.title
startDate: new Date(period.start_date)
endDate: new Date(period.end_date)
# TODO: After the close_date data fixup has run, this can become:
# `closeDate: new Date(period.close_date)`
closeDate: new Date(period.close_date || period.end_date)
}
baseDeserializeSet = (set) ->
{
id: set.id.toString()
title: gradingPeriodSetTitle(set)
gradingPeriods: deserializePeriods(set.grading_periods)
weighted: set.weighted || false
gradingPeriods: gradingPeriodsApi.deserializePeriods(set.grading_periods)
permissions: set.permissions
createdAt: new Date(set.created_at)
}
@ -46,7 +36,7 @@ define [
set.title.trim()
else
createdAt = DateHelper.formatDateForDisplay(new Date(set.created_at))
I18n.t("Set created %{createdAt}", { createdAt: createdAt });
I18n.t('Set created %{createdAt}', { createdAt })
deserializeSet = (set) ->
newSet = baseDeserializeSet(set)
@ -57,29 +47,23 @@ define [
_.flatten _.map setGroups, (group) ->
_.map group.grading_period_sets, (set) -> baseDeserializeSet(set)
deserializeSet: deserializeSet
list: () ->
promise = new Promise (resolve, reject) =>
Depaginate(listUrl())
.then (response) ->
resolve(deserializeSets(response))
.fail (error) ->
reject(error)
.then (response) ->
resolve(deserializeSets(response))
.fail (error) ->
reject(error)
promise
create: (set) ->
promise = new Promise (resolve, reject) =>
axios.post(createUrl(), serializeSet(set))
.then (response) ->
resolve(deserializeSet(response.data.grading_period_set))
.catch (error) ->
reject(error)
promise
axios.post(createUrl(), serializeSet(set))
.then (response) ->
deserializeSet(response.data.grading_period_set)
update: (set) ->
promise = new Promise (resolve, reject) =>
axios.patch(updateUrl(set.id), serializeSet(set))
.then (response) ->
resolve(set)
.catch (error) ->
reject(error)
promise
axios.patch(updateUrl(set.id), serializeSet(set))
.then (response) ->
set

View File

@ -15,6 +15,7 @@ define [
start_date: period.startDate
end_date: period.endDate
close_date: period.closeDate
weight: period.weight
}
grading_periods: serialized
@ -25,11 +26,10 @@ define [
title: period.title
startDate: new Date(period.start_date)
endDate: new Date(period.end_date)
# TODO: After the close_date data fixup has run, this can become:
# `closeDate: new Date(period.close_date)`
closeDate: new Date(period.close_date || period.end_date)
closeDate: new Date(period.close_date)
isLast: period.is_last
isClosed: period.is_closed
weight: period.weight
}
batchUpdate: (setId, periods) ->

View File

@ -7,7 +7,6 @@ require [
ReactDOM.render(
TabContainerFactory(
multipleGradingPeriodsEnabled: ENV.MULTIPLE_GRADING_PERIODS
readOnly: ENV.GRADING_PERIODS_READ_ONLY
urls:
enrollmentTermsURL: ENV.ENROLLMENT_TERMS_URL

View File

@ -95,7 +95,7 @@ require [
# kick it all off
assignmentGroups.fetch(reset: true).then ->
app.filterResults() if ENV.MULTIPLE_GRADING_PERIODS_ENABLED
app.filterResults() if ENV.HAS_GRADING_PERIODS
if ENV.PERMISSIONS.manage
assignmentGroups.loadModuleNames()
else

View File

@ -4,8 +4,7 @@ require [
'jsx/grading/CourseTabContainer'
], (React, ReactDOM, CourseTabContainer) ->
CourseTabContainerFactory = React.createFactory CourseTabContainer
mgpEnabled = ENV.MULTIPLE_GRADING_PERIODS
ReactDOM.render(
CourseTabContainerFactory(multipleGradingPeriodsEnabled: mgpEnabled),
CourseTabContainerFactory(hasGradingPeriods: ENV.HAS_GRADING_PERIODS),
document.getElementById("react_grading_tabs")
)

View File

@ -17,7 +17,7 @@ define [
pointRatio: ( ->
"#{I18n.n @get('student.total_grade.score')} / #{I18n.n @get('student.total_grade.possible')}"
).property("weighted_groups", "student.total_grade.score", "student.total_grade.possible")
).property("weighted_grades", "student.total_grade.score", "student.total_grade.possible")
letterGrade:(->
GradingSchemeHelper.scoreToGrade(@get('percent'), @get('gradingStandard'))
@ -26,7 +26,7 @@ define [
showGrade: Ember.computed.bool('student.total_grade.possible')
showPoints:(->
!!(!@get("weighted_groups") && @get("student.total_grade"))
).property("weighted_groups","student.total_grade")
!!(!@get("weighted_grades") && @get("student.total_grade"))
).property("weighted_grades","student.total_grade")
showLetterGrade: Ember.computed.bool("gradingStandard")

View File

@ -32,19 +32,23 @@ define [
'compiled/AssignmentDetailsDialog'
'compiled/AssignmentMuter'
'jsx/gradebook/CourseGradeCalculator'
'jsx/gradebook/EffectiveDueDates'
'compiled/gradebook/OutcomeGradebookGrid'
'../../shared/components/ic_submission_download_dialog_component'
'str/htmlEscape'
'compiled/models/grade_summary/CalculationMethodContent'
'jsx/gradebook/SubmissionStateMap'
'compiled/api/gradingPeriodsApi'
'compiled/api/gradingPeriodSetsApi'
'jsx/gradezilla/individual-gradebook/components/GradebookSelector'
'jquery.instructure_date_and_time'
], ($, React, ReactDOM, ajax, round, userSettings, fetchAllPages, parseLinkHeader,
I18n, Ember, _, tz, AssignmentDetailsDialog, AssignmentMuter,
CourseGradeCalculator, outcomeGrid, ic_submission_download_dialog,
htmlEscape, CalculationMethodContent, SubmissionStateMap, GradingPeriodsAPI,
GradebookSelector) ->
], (
$, React, ReactDOM,
ajax, round, userSettings, fetchAllPages, parseLinkHeader, I18n, Ember, _, tz, AssignmentDetailsDialog,
AssignmentMuter, CourseGradeCalculator, EffectiveDueDates, outcomeGrid, ic_submission_download_dialog,
htmlEscape, CalculationMethodContent, SubmissionStateMap, GradingPeriodsApi, GradingPeriodSetsApi,
GradebookSelector
) ->
{ get, set, setProperties } = Ember
@ -117,17 +121,33 @@ define [
submissionsUrl: get(window, 'ENV.GRADEBOOK_OPTIONS.submissions_url')
mgpEnabled: get(window, 'ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled')
has_grading_periods: get(window, 'ENV.GRADEBOOK_OPTIONS.has_grading_periods')
gradingPeriods:
(->
periods = get(window, 'ENV.GRADEBOOK_OPTIONS.active_grading_periods')
deserializedPeriods = GradingPeriodsApi.deserializePeriods(periods)
optionForAllPeriods =
id: '0', title: I18n.t("all_grading_periods", "All Grading Periods")
_.compact([optionForAllPeriods].concat(deserializedPeriods))
)()
getGradingPeriodSet: ->
grading_period_set = get(window, 'ENV.GRADEBOOK_OPTIONS.grading_period_set')
if grading_period_set
GradingPeriodSetsApi.deserializeSet(grading_period_set)
else
null
gradingPeriods: (->
periods = get(window, 'ENV.GRADEBOOK_OPTIONS.active_grading_periods')
deserializedPeriods = GradingPeriodsAPI.deserializePeriods(periods)
deserializedPeriods = GradingPeriodsApi.deserializePeriods(periods)
optionForAllPeriods =
id: '0', title: I18n.t("all_grading_periods", "All Grading Periods")
_.compact([optionForAllPeriods].concat(deserializedPeriods))
)()
lastGeneratedCsvLabel: do () =>
lastGeneratedCsvLabel: do () =>
if get(window, 'ENV.GRADEBOOK_OPTIONS.gradebook_csv_progress')
gradebook_csv_export_date = get(window, 'ENV.GRADEBOOK_OPTIONS.gradebook_csv_progress.progress.updated_at')
I18n.t('Download Scores Generated on %{date}',
@ -333,12 +353,26 @@ define [
id != userId
set(assignment, 'assignment_visibility', filteredVisibilities)
calculate: (submissionsArray) ->
CourseGradeCalculator.calculate submissionsArray, @assignmentGroupsHash(), @get('weightingScheme')
calculate: (student) ->
submissions = @submissionsForStudent(student)
assignmentGroups = @assignmentGroupsHash()
weightingScheme = @get('weightingScheme')
gradingPeriodSet = @getGradingPeriodSet()
effectiveDueDates = @get('effectiveDueDates.content')
hasGradingPeriods = gradingPeriodSet and effectiveDueDates
CourseGradeCalculator.calculate(
submissions,
assignmentGroups,
weightingScheme,
gradingPeriodSet if hasGradingPeriods,
EffectiveDueDates.scopeToUser(effectiveDueDates, student.id) if hasGradingPeriods
)
submissionsForStudent: (student) ->
allSubmissions = (value for key, value of student when key.match /^assignment_(?!group)/)
return allSubmissions unless @get('mgpEnabled')
return allSubmissions unless @get('has_grading_periods')
selectedPeriodID = @get('selectedGradingPeriod.id')
return allSubmissions if !selectedPeriodID or selectedPeriodID == '0'
@ -348,23 +382,32 @@ define [
calculateStudentGrade: (student) ->
if student.isLoaded
finalOrCurrent = if @get('includeUngradedAssignments') then 'final' else 'current'
result = @calculate(@submissionsForStudent(student))
for group in result.group_sums
set(student, "assignment_group_#{group.group.id}", group[finalOrCurrent])
for submissionData in group[finalOrCurrent].submissions
set(submissionData.submission, 'drop', submissionData.drop)
result = result[finalOrCurrent]
grades = @calculate(student)
percent = round (result.score / result.possible * 100), 2
selectedPeriodID = @get('selectedGradingPeriod.id')
if selectedPeriodID && selectedPeriodID != '0'
grades = grades.gradingPeriods[selectedPeriodID]
finalOrCurrent = if @get('includeUngradedAssignments') then 'final' else 'current'
for assignmentGroupId, grade of grades.assignmentGroups
set(student, "assignment_group_#{assignmentGroupId}", grade[finalOrCurrent])
for submissionData in grade[finalOrCurrent].submissions
set(submissionData.submission, 'drop', submissionData.drop)
grades = grades[finalOrCurrent]
percent = round (grades.score / grades.possible * 100), 2
percent = 0 if isNaN(percent)
setProperties student,
total_grade: result
total_grade: grades
total_percent: I18n.n(percent, percentage: true)
calculateAllGrades: (->
@get('students').forEach (student) => @calculateStudentGrade student
).observes('includeUngradedAssignments','groupsAreWeighted', 'assignment_groups.@each.group_weight')
).observes(
'includeUngradedAssignments','groupsAreWeighted', 'assignment_groups.@each.group_weight',
'effectiveDueDates.isLoaded'
)
sectionSelectDefaultLabel: I18n.t "all_sections", "All Sections"
studentSelectDefaultLabel: I18n.t "no_student", "No Student Selected"
@ -378,7 +421,7 @@ define [
fetchAssignmentGroups: (->
params = { exclude_response_fields: ['in_closed_grading_period'] }
gpId = @get('selectedGradingPeriod.id')
if @get('mgpEnabled') && gpId != '0'
if @get('has_grading_periods') && gpId != '0'
params.grading_period_id = gpId
@set('assignment_groups', [])
@set('assignmentsFromGroups', [])
@ -475,16 +518,17 @@ define [
)
displayPointTotals: (->
if @get("groupsAreWeighted")
false
else
@get("showTotalAsPoints")
).property('groupsAreWeighted', 'showTotalAsPoints')
@get('showTotalAsPoints') and not @get('gradesAreWeighted')
).property('gradesAreWeighted', 'showTotalAsPoints')
groupsAreWeighted: (->
@get("weightingScheme") == "percent"
).property("weightingScheme")
gradesAreWeighted: (->
@get('groupsAreWeighted') or !!@getGradingPeriodSet()?.weighted
).property('weightingScheme')
updateShowTotalAs: (->
@set "showTotalAsPoints", @get("displayPointTotals")
ajax.request(
@ -493,7 +537,7 @@ define [
url: ENV.GRADEBOOK_OPTIONS.setting_update_url
data:
show_total_grade_as_points: @get("displayPointTotals"))
).observes('showTotalAsPoints', 'groupsAreWeighted')
).observes('showTotalAsPoints', 'gradesAreWeighted')
studentColumnData: {}
@ -705,7 +749,7 @@ define [
populateSubmissionStateMap: (->
map = new SubmissionStateMap(
gradingPeriodsEnabled: !!@mgpEnabled
hasGradingPeriods: !!@has_grading_periods
selectedGradingPeriodID: @get('selectedGradingPeriod.id') || '0'
isAdmin: ENV.current_user_roles && _.contains(ENV.current_user_roles, "admin")
)

View File

@ -1,4 +1,4 @@
{{#if mgpEnabled}}
{{#if has_grading_periods}}
<div class="row">
<div class="span4">
<label for="grading_period_select" class="text-right-responsive">
@ -18,4 +18,4 @@
}}
</div>
</div>
{{/if}}
{{/if}}

View File

@ -35,7 +35,7 @@
{{
final-grade
student=selectedStudent
weighted_groups=groupsAreWeighted
weighted_grades=gradesAreWeighted
gradingStandard=ENV.GRADEBOOK_OPTIONS.grading_standard
}}
@ -55,4 +55,4 @@
{{/if}}
</div>
</div>
</div>
</div>

View File

@ -17,8 +17,7 @@ define [
App = startApp()
@component = App.GradingCellComponent.create()
ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled = true
ENV.GRADEBOOK_OPTIONS.latest_end_date_of_admin_created_grading_periods_in_the_past = "2013-10-01T10:00:00Z"
ENV.GRADEBOOK_OPTIONS.has_grading_periods = true
ENV.current_user_roles = []
setType = (type) =>

View File

@ -5,10 +5,15 @@ define [
'../start_app'
'ember'
'../shared_ajax_fixtures'
'spec/jsx/gradebook/GradeCalculatorSpecHelper'
'../../controllers/screenreader_gradebook_controller'
'compiled/userSettings'
'jsx/gradebook/CourseGradeCalculator'
'vendor/jquery.ba-tinypubsub'
], ($, _, ajax, startApp, Ember, fixtures, SRGBController, userSettings) ->
], (
$, _, ajax, startApp, Ember, fixtures, GradeCalculatorSpecHelper, SRGBController, userSettings,
CourseGradeCalculator
) ->
workAroundRaceCondition = ->
ajax.request()
@ -19,6 +24,13 @@ define [
clone = (obj) ->
Ember.copy obj, true
createExampleGrades = GradeCalculatorSpecHelper.createCourseGradesWithGradingPeriods
createExampleGradingPeriodSet = ->
id: '1501'
gradingPeriods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
setup = (isDraftState=false, sortOrder='assignment_group') ->
fixtures.create()
@contextGetStub = sinon.stub(userSettings, 'contextGet')
@ -51,7 +63,6 @@ define [
teardown: ->
teardown.call this
test 'calculates students properly', ->
equal @srgb.get('students.length'), 10
equal @srgb.get('students.firstObject').name, fixtures.students[0].user.name
@ -85,19 +96,19 @@ define [
test 'displayName is hiddenName when hideStudentNames is true', ->
@srgb.set('hideStudentNames', true)
equal @srgb.get('displayName'), "hiddenName"
equal @srgb.get('displayName'), 'hiddenName'
@srgb.set('hideStudentNames', false)
equal @srgb.get('displayName'), "name"
equal @srgb.get('displayName'), 'name'
test 'displayPointTotals is false when groups are weighted even if showTotalAsPoints is true', ->
test 'displayPointTotals is false when grades are weighted even if showTotalAsPoints is true', ->
Ember.run =>
@srgb.set('showTotalAsPoints', true)
@srgb.set('groupsAreWeighted', true)
@srgb.set('gradesAreWeighted', true)
equal @srgb.get('displayPointTotals'), false
test 'displayPointTotals is toggled by showTotalAsPoints when groups are unweighted', ->
test 'displayPointTotals is toggled by showTotalAsPoints when grades are unweighted', ->
Ember.run =>
@srgb.set('groupsAreWeighted', false)
@srgb.set('gradesAreWeighted', false)
@srgb.set('showTotalAsPoints', true)
equal @srgb.get('displayPointTotals'), true
@srgb.set('showTotalAsPoints', false)
@ -161,18 +172,75 @@ define [
equal @srgb.get('assignments.lastObject.name'), 'Drink Water'
start()
QUnit.module 'screenreader_gradebook_controller#gradesAreWeighted',
setup: ->
setup.call this
teardown: ->
teardown.call this
test 'is true when the grading period set is weighted', ->
gradingPeriodSet = createExampleGradingPeriodSet()
gradingPeriodSet.weighted = true
@stub(@srgb, 'getGradingPeriodSet').returns(gradingPeriodSet)
Ember.run =>
@srgb.set('groupsAreWeighted', false)
equal @srgb.get('gradesAreWeighted'), true
test 'is true when groupsAreWeighted is true', ->
gradingPeriodSet = createExampleGradingPeriodSet()
gradingPeriodSet.weighted = false
@stub(@srgb, 'getGradingPeriodSet').returns(gradingPeriodSet)
Ember.run =>
@srgb.set('groupsAreWeighted', true)
equal @srgb.get('gradesAreWeighted'), true
test 'is false when assignment groups are not weighted and the grading period set is not weighted', ->
gradingPeriodSet = createExampleGradingPeriodSet()
gradingPeriodSet.weighted = false
@stub(@srgb, 'getGradingPeriodSet').returns(gradingPeriodSet)
Ember.run =>
@srgb.set('groupsAreWeighted', false)
equal @srgb.get('gradesAreWeighted'), false
test 'is false when assignment groups are not weighted and the grading period set is not defined', ->
@stub(@srgb, 'getGradingPeriodSet').returns(null)
Ember.run =>
@srgb.set('groupsAreWeighted', false)
equal @srgb.get('gradesAreWeighted'), false
QUnit.module '#getGradingPeriodSet',
setup: ->
setup.call this
teardown: ->
teardown.call this
test 'normalizes the grading period set from the env', ->
ENV.GRADEBOOK_OPTIONS.grading_period_set =
id: '1501'
grading_periods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
gradingPeriodSet = @srgb.getGradingPeriodSet()
deepEqual(gradingPeriodSet.id, '1501')
equal(gradingPeriodSet.gradingPeriods.length, 2)
deepEqual(_.map(gradingPeriodSet.gradingPeriods, 'id'), ['701', '702'])
test 'sets grading period set to null when not defined in the env', ->
gradingPeriodSet = @srgb.getGradingPeriodSet()
deepEqual(gradingPeriodSet, null)
QUnit.module '#submissionsForStudent',
setupThis: (options = {}) ->
effectiveDueDates = Ember.ObjectProxy.create(
content: {
1: { 1: { grading_period_id: "1" } },
2: { 1: { grading_period_id: "2" } }
1: { 1: { grading_period_id: '1' } },
2: { 1: { grading_period_id: '2' } }
}
)
defaults = {
mgpEnabled: false,
"selectedGradingPeriod.id": null,
has_grading_periods: false,
'selectedGradingPeriod.id': null,
effectiveDueDates
}
self = _.defaults options, defaults
@ -181,40 +249,41 @@ define [
setup: ->
@student =
id: "1"
assignment_1: { assignment_id: "1", user_id: "1", name: "yolo" }
assignment_2: { assignment_id: "2", user_id: "1", name: "froyo" }
id: '1'
assignment_1: { assignment_id: '1', user_id: '1', name: 'yolo' }
assignment_2: { assignment_id: '2', user_id: '1', name: 'froyo' }
setup.call this
teardown: ->
teardown.call this
test "returns all submissions for the student (multiple grading periods disabled)", ->
test 'returns all submissions for the student when there are no grading periods', ->
self = @setupThis()
submissions = @srgb.submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["1", "2"]
propEqual _.pluck(submissions, 'assignment_id'), ['1', '2']
test "returns all submissions if 'All Grading Periods' is selected", ->
test 'returns all submissions if "All Grading Periods" is selected', ->
self = @setupThis(
mgpEnabled: true,
"selectedGradingPeriod.id": "0",
has_grading_periods: true,
'selectedGradingPeriod.id': '0'
)
submissions = @srgb.submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["1", "2"]
propEqual _.pluck(submissions, 'assignment_id'), ['1', '2']
test "only returns submissions due for the student in the selected grading period", ->
test 'only returns submissions due for the student in the selected grading period', ->
self = @setupThis(
mgpEnabled: true,
"selectedGradingPeriod.id": "2"
has_grading_periods: true,
'selectedGradingPeriod.id': '2'
)
submissions = @srgb.submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["2"]
propEqual _.pluck(submissions, 'assignment_id'), ['2']
QUnit.module 'screenreader_gradebook_controller: with selected student',
setup: ->
setup.call this
@stub(@srgb, 'calculateStudentGrade')
@completeSetup = =>
workAroundRaceCondition().then =>
Ember.run =>
@ -232,7 +301,7 @@ define [
start()
asyncTest 'assignments excludes any due for the selected student in a different grading period', ->
@srgb.mgpEnabled = true
@srgb.has_grading_periods = true
@completeSetup().then =>
deepEqual(@srgb.get('assignments').mapBy('id'), ['3'])
start()
@ -350,8 +419,8 @@ define [
id: '21'
name: 'Unpublished Assignment'
points_possible: 10
grading_type: "percent"
submission_types: ["none"]
grading_type: 'percent'
submission_types: ['none']
due_at: null
position: 6
assignment_group_id:'4'
@ -370,24 +439,7 @@ define [
calc_stub = {
group_sums: [
{
final:
possible: 100
score: 50
submission_count: 10
weight: 50
submissions: []
current:
possible: 100
score: 20
submission_count: 5
weight: 50
submissions:[]
group:
id: "1"
}
]
assignmentGroups: {}
final:
possible: 100
score: 90
@ -398,24 +450,7 @@ define [
calc_stub_with_0_possible = {
group_sums: [
{
final:
possible: 0
score: 50
submission_count: 10
weight: 50
submissions: []
current:
possible: 0
score: 20
submission_count: 5
weight: 50
submissions:[]
group:
id: "1"
}
]
assignmentGroups: {}
final:
possible: 0
score: 0
@ -457,6 +492,125 @@ define [
equal @srgb.get('students.firstObject.total_percent'), '0%'
start()
QUnit.module 'screenreader_gradebook_controller: calculate',
setupThis:(options = {}) ->
assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }]
submissions = [{ assignment_id: 201, score: 10 }]
assignmentGroupsHash = { 301: { id: 301, group_weight: 60, rules: {}, assignments } }
gradingPeriodSet =
id: '1501'
gradingPeriods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
props = _.defaults options,
weightingScheme: 'points'
getGradingPeriodSet: () -> gradingPeriodSet
'effectiveDueDates.content': { 201: { 101: { grading_period_id: '701' } } }
_.extend {}, props,
get: (attr) -> props[attr]
submissionsForStudent: () -> submissions
assignmentGroupsHash: () -> assignmentGroupsHash
setup: ->
@calculate = SRGBController.prototype.calculate
test 'calculates grades using properties from the gradebook', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns('expected')
grades = @calculate.call(self, id: '101', loaded: true)
equal(grades, 'expected')
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroupsHash())
equal(args[2], self.get('weightingScheme'))
equal(args[3], self.getGradingPeriodSet())
test 'scopes effective due dates to the user', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate')
@calculate.call(self, id: '101', loaded: true)
dueDates = CourseGradeCalculator.calculate.getCall(0).args[4]
deepEqual(dueDates, 201: { grading_period_id: '701' })
test 'calculates grades without grading period data when grading period set is null', ->
self = @setupThis(getGradingPeriodSet: -> null)
@stub(CourseGradeCalculator, 'calculate')
@calculate.call(self, id: '101', loaded: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroupsHash())
equal(args[2], self.get('weightingScheme'))
equal(typeof args[3], 'undefined')
equal(typeof args[4], 'undefined')
test 'calculates grades without grading period data when effective due dates are not defined', ->
self = @setupThis('effectiveDueDates.content': null)
@stub(CourseGradeCalculator, 'calculate')
@calculate.call(self, id: '101', loaded: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroupsHash())
equal(args[2], self.get('weightingScheme'))
equal(typeof args[3], 'undefined')
equal(typeof args[4], 'undefined')
QUnit.module 'screenreader_gradebook_controller: calculateStudentGrade',
setupThis:(options = {}) ->
assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }]
submissions = [{ assignment_id: 201, score: 10 }]
assignmentGroupsHash = { 301: { id: 301, group_weight: 60, rules: {}, assignments } }
gradingPeriodSet =
id: '1501'
gradingPeriods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
props = _.defaults options,
weightingScheme: 'points'
getGradingPeriodSet: () -> gradingPeriodSet
calculate: () -> CourseGradeCalculator.calculate()
'effectiveDueDates.content': { 201: { 101: { grading_period_id: '701' } } }
'selectedGradingPeriod.id': '0'
_.extend {}, props,
get: (attr) -> props[attr]
submissionsForStudent: () -> submissions
assignmentGroupsHash: () -> assignmentGroupsHash
setup: ->
@calculateStudentGrade = SRGBController.prototype.calculateStudentGrade
test 'stores the current grade on the student when not including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(includeUngradedAssignments: false)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = Ember.Object.create(id: '101', loaded: true)
student.set('isLoaded', true)
@calculateStudentGrade.call(self, student)
equal(student.total_grade, exampleGrades.current)
test 'stores the final grade on the student when including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(includeUngradedAssignments: true)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = Ember.Object.create(id: '101', loaded: true)
student.set('isLoaded', true)
@calculateStudentGrade.call(self, student)
equal(student.total_grade, exampleGrades.final)
test 'stores the current grade from the selected grading period when not including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis('selectedGradingPeriod.id': 701, includeUngradedAssignments: false)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = Ember.Object.create(id: '101', loaded: true)
student.set('isLoaded', true)
@calculateStudentGrade.call(self, student)
equal(student.total_grade, exampleGrades.gradingPeriods[701].current)
test 'stores the final grade from the selected grading period when including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis('selectedGradingPeriod.id': 701, includeUngradedAssignments: true)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = Ember.Object.create(id: '101', loaded: true)
student.set('isLoaded', true)
@calculateStudentGrade.call(self, student)
equal(student.total_grade, exampleGrades.gradingPeriods[701].final)
QUnit.module 'screenreader_gradebook_controller: notes computed props',
setup: ->
@ -519,20 +673,20 @@ define [
Ember.run =>
@srgb.set('showNotesColumn', true)
@srgb.set('shouldCreateNotes', false)
deepEqual @srgb.get('notesParams'), "column[hidden]": false
deepEqual @srgb.get('notesParams'), 'column[hidden]': false
Ember.run =>
@srgb.set('showNotesColumn', false)
@srgb.set('shouldCreateNotes', false)
deepEqual @srgb.get('notesParams'), "column[hidden]": true
deepEqual @srgb.get('notesParams'), 'column[hidden]': true
Ember.run =>
@srgb.set('showNotesColumn', true)
@srgb.set('shouldCreateNotes', true)
deepEqual @srgb.get('notesParams'),
"column[title]": "Notes"
"column[position]": 1
"column[teacher_notes]": true
'column[title]': 'Notes'
'column[position]': 1
'column[teacher_notes]': true
test 'notesVerb', ->
Ember.run =>
@ -552,13 +706,14 @@ define [
teardown.call this
test 'calculates invalidGroupsWarningPhrases properly', ->
equal @srgb.get('invalidGroupsWarningPhrases'), "Note: Score does not include assignments from the group Invalid AG because it has no points possible."
equal @srgb.get('invalidGroupsWarningPhrases'),
'Note: Score does not include assignments from the group Invalid AG because it has no points possible.'
test 'sets showInvalidGroupWarning to false if groups are not weighted', ->
Ember.run =>
@srgb.set('weightingScheme', "equal")
@srgb.set('weightingScheme', 'equal')
equal @srgb.get('showInvalidGroupWarning'), false
@srgb.set('weightingScheme', "percent")
@srgb.set('weightingScheme', 'percent')
equal @srgb.get('showInvalidGroupWarning'), true

View File

@ -3,11 +3,6 @@ define ['../main', 'ember'], (Application, Ember) ->
App = null
Ember.run.join ->
App = Application.create
LOG_ACTIVE_GENERATION: yes
LOG_MODULE_RESOLVER: yes
LOG_TRANSITIONS: yes
LOG_TRANSITIONS_INTERNAL: yes
LOG_VIEW_LOOKUPS: yes
rootElement: '#fixtures'
App.Router.reopen history: 'none'
App.setupForTesting()

View File

@ -1,4 +1,21 @@
# This class both creates the slickgrid instance, and acts as the data source for that instance.
#
# Copyright (C) 2011 - 2017 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'jquery'
'underscore'
@ -18,6 +35,7 @@ define [
'i18n!gradebook'
'compiled/gradebook/GradebookTranslations'
'jsx/gradebook/CourseGradeCalculator'
'jsx/gradebook/EffectiveDueDates'
'jsx/gradebook/GradingSchemeHelper'
'compiled/userSettings'
'spin.js'
@ -41,6 +59,7 @@ define [
'compiled/gradebook/GradebookKeyboardNav'
'jsx/gradebook/shared/helpers/assignmentHelper'
'compiled/api/gradingPeriodsApi'
'compiled/api/gradingPeriodSetsApi'
'jst/_avatar' #needed by row_student_name
'jquery.ajaxJSON'
'jquery.instructure_date_and_time'
@ -60,13 +79,13 @@ define [
], (
$, _, Backbone, tz, DataLoader, React, ReactDOM, LongTextEditor, KeyboardNavDialog, KeyboardNavTemplate, Slick,
TotalColumnHeaderView, round, InputFilterView, i18nObj, I18n, GRADEBOOK_TRANSLATIONS, CourseGradeCalculator,
GradingSchemeHelper, UserSettings, Spinner, SubmissionDetailsDialog, AssignmentGroupWeightsDialog,
GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell, GradebookHeaderMenu, NumberCompare, natcompare,
htmlEscape, PostGradesStore, PostGradesApp, SubmissionStateMap, ColumnHeaderTemplate, GroupTotalCellTemplate,
RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, assignmentHelper,
GradingPeriodsAPI
EffectiveDueDates, GradingSchemeHelper, UserSettings, Spinner, SubmissionDetailsDialog,
AssignmentGroupWeightsDialog, GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell,
GradebookHeaderMenu, NumberCompare, natcompare, htmlEscape, PostGradesStore, PostGradesApp, SubmissionStateMap,
ColumnHeaderTemplate, GroupTotalCellTemplate, RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView,
GradebookKeyboardNav, assignmentHelper, GradingPeriodsApi, GradingPeriodSetsApi
) ->
# This class both creates the slickgrid instance, and acts as the data source for that instance.
class Gradebook
columnWidths =
assignment:
@ -102,11 +121,15 @@ define [
@options.settings['show_inactive_enrollments'] == "true"
@totalColumnInFront = UserSettings.contextGet 'total_column_in_front'
@numberOfFrozenCols = if @totalColumnInFront then 3 else 2
@gradingPeriodsEnabled = @options.multiple_grading_periods_enabled
@gradingPeriods = GradingPeriodsAPI.deserializePeriods(@options.active_grading_periods)
@hasGradingPeriods = @options.has_grading_periods
@gradingPeriods = GradingPeriodsApi.deserializePeriods(@options.active_grading_periods)
if @options.grading_period_set
@gradingPeriodSet = GradingPeriodSetsApi.deserializeSet(@options.grading_period_set)
else
@gradingPeriodSet = null
@gradingPeriodToShow = @getGradingPeriodToShow()
@submissionStateMap = new SubmissionStateMap
gradingPeriodsEnabled: @gradingPeriodsEnabled
hasGradingPeriods: @hasGradingPeriods
selectedGradingPeriodID: @gradingPeriodToShow
isAdmin: _.contains(ENV.current_user_roles, "admin")
@gradebookColumnSizeSettings = @options.gradebook_column_size_settings
@ -120,7 +143,7 @@ define [
$.subscribe 'currentGradingPeriod/change', @updateCurrentGradingPeriod
assignmentGroupsParams = { exclude_response_fields: @fieldsToExcludeFromAssignments }
if @gradingPeriodsEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
if @hasGradingPeriods && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
$.extend(assignmentGroupsParams, {grading_period_id: @gradingPeriodToShow})
$('li.external-tools-dialog > a[data-url], button.external-tools-dialog').on 'click keyclick', (event) ->
@ -135,7 +158,7 @@ define [
submissionParams =
response_fields: ['id', 'user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id', 'grade_matches_current_submission', 'attachments', 'late', 'workflow_state', 'excused']
exclude_response_fields: ['preview_url']
submissionParams['grading_period_id'] = @gradingPeriodToShow if @gradingPeriodsEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
submissionParams['grading_period_id'] = @gradingPeriodToShow if @hasGradingPeriods && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
dataLoader = DataLoader.loadGradebookData(
assignmentGroupsURL: @options.assignment_groups_url
assignmentGroupsParams: assignmentGroupsParams
@ -235,7 +258,7 @@ define [
_.contains(activePeriodIds, gradingPeriodId)
getGradingPeriodToShow: () =>
return null unless @gradingPeriodsEnabled
return null unless @hasGradingPeriods
currentPeriodId = UserSettings.contextGet('gradebook_current_grading_period')
if currentPeriodId && (@isAllGradingPeriods(currentPeriodId) || @gradingPeriodIsActive(currentPeriodId))
currentPeriodId
@ -719,7 +742,7 @@ define [
templateOpts.warning = @totalGradeWarning
templateOpts.lastColumn = true
templateOpts.showPointsNotPercent = @displayPointTotals()
templateOpts.hideTooltip = @weightedGroups() and not @totalGradeWarning
templateOpts.hideTooltip = @weightedGrades() and not @totalGradeWarning
GroupTotalCellTemplate templateOpts
htmlContentFormatter: (row, col, val, columnDef, student) ->
@ -732,7 +755,7 @@ define [
submissionsForStudent: (student) =>
allSubmissions = (value for key, value of student when key.match /^assignment_(?!group)/)
return allSubmissions unless @gradingPeriodsEnabled
return allSubmissions unless @hasGradingPeriods
return allSubmissions if !@gradingPeriodToShow or @isAllGradingPeriods(@gradingPeriodToShow)
_.filter allSubmissions, (submission) =>
@ -741,17 +764,26 @@ define [
calculateStudentGrade: (student) =>
if student.loaded and student.initialized
finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current'
result = CourseGradeCalculator.calculate(
hasGradingPeriods = @gradingPeriodSet and @effectiveDueDates
grades = CourseGradeCalculator.calculate(
@submissionsForStudent(student),
@assignmentGroups,
@options.group_weighting_scheme
@options.group_weighting_scheme,
@gradingPeriodSet if hasGradingPeriods,
EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if hasGradingPeriods
)
for group in result.group_sums
student["assignment_group_#{group.group.id}"] = group[finalOrCurrent]
for submissionData in group[finalOrCurrent].submissions
if @gradingPeriodToShow && !@isAllGradingPeriods(@gradingPeriodToShow)
grades = grades.gradingPeriods[@gradingPeriodToShow]
finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current'
for assignmentGroupId, grade of grades.assignmentGroups
student["assignment_group_#{assignmentGroupId}"] = grade[finalOrCurrent]
for submissionData in grade[finalOrCurrent].submissions
submissionData.submission.drop = submissionData.drop
student["total_grade"] = result[finalOrCurrent]
student["total_grade"] = grades[finalOrCurrent]
@addDroppedClass(student)
@ -1044,7 +1076,7 @@ define [
initHeader: =>
@drawSectionSelectButton() if @sections_enabled
@drawGradingPeriodSelectButton() if @gradingPeriodsEnabled
@drawGradingPeriodSelectButton() if @hasGradingPeriods
$settingsMenu = $('.gradebook_dropdown')
showConcludedEnrollmentsEl = $settingsMenu.find("#show_concluded_enrollments")
@ -1217,11 +1249,11 @@ define [
weightedGroups: =>
@options.group_weighting_scheme == "percent"
weightedGrades: =>
@options.group_weighting_scheme == "percent" || @gradingPeriodSet?.weighted || false
displayPointTotals: =>
if @weightedGroups()
false
else
@options.show_total_grade_as_points
@options.show_total_grade_as_points and not @weightedGrades()
switchTotalDisplay: =>
@options.show_total_grade_as_points = not @options.show_total_grade_as_points
@ -1667,7 +1699,7 @@ define [
currentPeriodId == "0"
hideAggregateColumns: ->
return false unless @gradingPeriodsEnabled
return false unless @hasGradingPeriods
return false if @options.all_grading_periods_totals
selectedPeriodId = @getGradingPeriodToShow()
@isAllGradingPeriods(selectedPeriodId)

View File

@ -29,12 +29,14 @@ define [
'jst/KeyboardNavDialog'
'vendor/slickgrid'
'compiled/api/gradingPeriodsApi'
'compiled/api/gradingPeriodSetsApi'
'compiled/util/round'
'compiled/views/InputFilterView'
'i18nObj'
'i18n!gradezilla'
'compiled/gradezilla/GradebookTranslations'
'jsx/gradebook/CourseGradeCalculator'
'jsx/gradebook/EffectiveDueDates'
'jsx/gradebook/GradingSchemeHelper'
'compiled/userSettings'
'spin.js'
@ -79,15 +81,16 @@ define [
'compiled/jquery.kylemenu'
'compiled/jquery/fixDialogButtons'
'jsx/context_cards/StudentContextCardTrigger'
], ($, _, Backbone, tz, DataLoader, React, ReactDOM, LongTextEditor, KeyboardNavDialog, KeyboardNavTemplate, Slick,
GradingPeriodsAPI, round, InputFilterView, i18nObj, I18n, GRADEBOOK_TRANSLATIONS, CourseGradeCalculator,
GradingSchemeHelper, UserSettings, Spinner, SubmissionDetailsDialog, AssignmentGroupWeightsDialog,
GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell, GradebookHeaderMenu, NumberCompare, natcompare,
htmlEscape, AssignmentColumnHeader, AssignmentGroupColumnHeader, StudentColumnHeader, TotalGradeColumnHeader,
GradebookMenu, ViewOptionsMenu, ActionMenu, PostGradesStore, PostGradesApp, SubmissionStateMap,
GroupTotalCellTemplate, RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav,
assignmentHelper ) ->
], (
$, _, Backbone, tz, DataLoader, React, ReactDOM, LongTextEditor, KeyboardNavDialog, KeyboardNavTemplate, Slick,
GradingPeriodsApi, GradingPeriodSetsApi, round, InputFilterView, i18nObj, I18n, GRADEBOOK_TRANSLATIONS,
CourseGradeCalculator, EffectiveDueDates, GradingSchemeHelper, UserSettings, Spinner, SubmissionDetailsDialog,
AssignmentGroupWeightsDialog, GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell,
GradebookHeaderMenu, NumberCompare, natcompare, htmlEscape, AssignmentColumnHeader, AssignmentGroupColumnHeader,
StudentColumnHeader, TotalGradeColumnHeader, GradebookMenu, ViewOptionsMenu, ActionMenu, PostGradesStore,
PostGradesApp, SubmissionStateMap, GroupTotalCellTemplate, RowStudentNameTemplate, SectionMenuView,
GradingPeriodMenuView, GradebookKeyboardNav, assignmentHelper
) ->
renderComponent = (reactClass, mountPoint, props = {}, children = null) ->
component = React.createElement(reactClass, props, children)
ReactDOM.render(component, mountPoint)
@ -127,11 +130,15 @@ define [
@options.settings['show_inactive_enrollments'] == "true"
@totalColumnInFront = UserSettings.contextGet 'total_column_in_front'
@numberOfFrozenCols = if @totalColumnInFront then 3 else 2
@gradingPeriodsEnabled = @options.multiple_grading_periods_enabled
@gradingPeriods = GradingPeriodsAPI.deserializePeriods(@options.active_grading_periods)
@hasGradingPeriods = @options.has_grading_periods
@gradingPeriods = GradingPeriodsApi.deserializePeriods(@options.active_grading_periods)
if @options.grading_period_set
@gradingPeriodSet = GradingPeriodSetsApi.deserializeSet(@options.grading_period_set)
else
@gradingPeriodSet = null
@gradingPeriodToShow = @getGradingPeriodToShow()
@submissionStateMap = new SubmissionStateMap
gradingPeriodsEnabled: @gradingPeriodsEnabled
hasGradingPeriods: @hasGradingPeriods
selectedGradingPeriodID: @gradingPeriodToShow
isAdmin: _.contains(ENV.current_user_roles, "admin")
@gradebookColumnSizeSettings = @options.gradebook_column_size_settings
@ -145,7 +152,7 @@ define [
$.subscribe 'currentGradingPeriod/change', @updateCurrentGradingPeriod
assignmentGroupsParams = { exclude_response_fields: @fieldsToExcludeFromAssignments }
if @gradingPeriodsEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
if @hasGradingPeriods && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
$.extend(assignmentGroupsParams, {grading_period_id: @gradingPeriodToShow})
$('li.external-tools-dialog > a[data-url], button.external-tools-dialog').on 'click keyclick', (event) ->
@ -158,7 +165,7 @@ define [
submissionParams =
response_fields: ['id', 'user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id', 'grade_matches_current_submission', 'attachments', 'late', 'workflow_state', 'excused']
exclude_response_fields: ['preview_url']
submissionParams['grading_period_id'] = @gradingPeriodToShow if @gradingPeriodsEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
submissionParams['grading_period_id'] = @gradingPeriodToShow if @hasGradingPeriods && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
dataLoader = DataLoader.loadGradebookData(
assignmentGroupsURL: @options.assignment_groups_url
assignmentGroupsParams: assignmentGroupsParams
@ -270,7 +277,7 @@ define [
_.contains(activePeriodIds, gradingPeriodId)
getGradingPeriodToShow: () =>
return null unless @gradingPeriodsEnabled
return null unless @hasGradingPeriods
currentPeriodId = UserSettings.contextGet('gradebook_current_grading_period')
if currentPeriodId && (@isAllGradingPeriods(currentPeriodId) || @gradingPeriodIsActive(currentPeriodId))
currentPeriodId
@ -737,7 +744,7 @@ define [
templateOpts.warning = @totalGradeWarning
templateOpts.lastColumn = true
templateOpts.showPointsNotPercent = @displayPointTotals()
templateOpts.hideTooltip = @weightedGroups() and not @totalGradeWarning
templateOpts.hideTooltip = @weightedGrades() and not @totalGradeWarning
GroupTotalCellTemplate templateOpts
htmlContentFormatter: (row, col, val, columnDef, student) ->
@ -750,7 +757,7 @@ define [
submissionsForStudent: (student) =>
allSubmissions = (value for key, value of student when key.match /^assignment_(?!group)/)
return allSubmissions unless @gradingPeriodsEnabled
return allSubmissions unless @hasGradingPeriods
return allSubmissions if !@gradingPeriodToShow or @isAllGradingPeriods(@gradingPeriodToShow)
_.filter allSubmissions, (submission) =>
@ -759,17 +766,26 @@ define [
calculateStudentGrade: (student) =>
if student.loaded and student.initialized
finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current'
result = CourseGradeCalculator.calculate(
hasGradingPeriods = @gradingPeriodSet and @effectiveDueDates
grades = CourseGradeCalculator.calculate(
@submissionsForStudent(student),
@assignmentGroups,
@options.group_weighting_scheme
@options.group_weighting_scheme,
@gradingPeriodSet if hasGradingPeriods,
EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if hasGradingPeriods
)
for group in result.group_sums
student["assignment_group_#{group.group.id}"] = group[finalOrCurrent]
for submissionData in group[finalOrCurrent].submissions
if @gradingPeriodToShow && !@isAllGradingPeriods(@gradingPeriodToShow)
grades = grades.gradingPeriods[@gradingPeriodToShow]
finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current'
for assignmentGroupId, grade of grades.assignmentGroups
student["assignment_group_#{assignmentGroupId}"] = grade[finalOrCurrent]
for submissionData in grade[finalOrCurrent].submissions
submissionData.submission.drop = submissionData.drop
student["total_grade"] = result[finalOrCurrent]
student["total_grade"] = grades[finalOrCurrent]
@addDroppedClass(student)
@ -1062,7 +1078,7 @@ define [
initHeader: =>
@renderGradebookMenus()
@drawSectionSelectButton() if @sections_enabled
@drawGradingPeriodSelectButton() if @gradingPeriodsEnabled
@drawGradingPeriodSelectButton() if @hasGradingPeriods
$settingsMenu = $('.gradebook_dropdown')
showConcludedEnrollmentsEl = $settingsMenu.find("#show_concluded_enrollments")
@ -1185,11 +1201,11 @@ define [
weightedGroups: =>
@options.group_weighting_scheme == "percent"
weightedGrades: =>
@options.group_weighting_scheme == "percent" || @gradingPeriodSet?.weighted || false
displayPointTotals: =>
if @weightedGroups()
false
else
@options.show_total_grade_as_points
@options.show_total_grade_as_points and not @weightedGrades()
switchTotalDisplay: =>
@options.show_total_grade_as_points = not @options.show_total_grade_as_points
@ -1648,7 +1664,7 @@ define [
currentPeriodId == "0"
hideAggregateColumns: ->
return false unless @gradingPeriodsEnabled
return false unless @hasGradingPeriods
return false if @options.all_grading_periods_totals
selectedPeriodId = @getGradingPeriodToShow()
@isAllGradingPeriods(selectedPeriodId)

View File

@ -30,7 +30,7 @@ define [
constructor: (params) ->
@dateRange = params['date_range']
@data = params['data']
@multipleGradingPeriodsEnabled = params.multipleGradingPeriodsEnabled
@hasGradingPeriods = params.hasGradingPeriods
@gradingPeriods = params.gradingPeriods
@userIsAdmin = params.userIsAdmin
@dueDateRequired = params.postToSIS && ENV.DUE_DATE_REQUIRED_FOR_ACCOUNT
@ -79,7 +79,7 @@ define [
dueDateRequired: @dueDateRequired,
}
if @multipleGradingPeriodsEnabled && !@userIsAdmin && @data.persisted == false
if @hasGradingPeriods && !@userIsAdmin && @data.persisted == false
datetimesToValidate.push {
date: dueAt,
range: "grading_period_range",

View File

@ -159,7 +159,7 @@ define [
dateValidator = new DateValidator(
date_range: _.extend({}, validRange)
data: data
multipleGradingPeriodsEnabled: !!ENV.MULTIPLE_GRADING_PERIODS_ENABLED
hasGradingPeriods: !!ENV.HAS_GRADING_PERIODS
gradingPeriods: GradingPeriodsAPI.deserializePeriods(ENV.active_grading_periods)
userIsAdmin: @currentUserIsAdmin(),
data

View File

@ -44,7 +44,7 @@ define [
defaultSectionId: @model.defaultDueDateSectionId,
selectedGroupSetId: @model.assignment.get("group_category_id"),
gradingPeriods: @gradingPeriods,
multipleGradingPeriodsEnabled: @multipleGradingPeriodsEnabled,
hasGradingPeriods: @hasGradingPeriods,
isOnlyVisibleToOverrides: @model.assignment.isOnlyVisibleToOverrides(),
dueAt: tz.parse(@model.assignment.get("due_at"))
})
@ -53,7 +53,7 @@ define [
gradingPeriods: GradingPeriodsAPI.deserializePeriods(ENV.active_grading_periods)
multipleGradingPeriodsEnabled: !!ENV.MULTIPLE_GRADING_PERIODS_ENABLED
hasGradingPeriods: !!ENV.HAS_GRADING_PERIODS
validateBeforeSave: (data, errors) =>
return errors unless data
@ -69,7 +69,7 @@ define [
dateValidator = new DateValidator({
date_range: _.extend({}, ENV.VALID_DATE_RANGE)
data: override
multipleGradingPeriodsEnabled: @multipleGradingPeriodsEnabled
hasGradingPeriods: @hasGradingPeriods
gradingPeriods: @gradingPeriods
userIsAdmin: _.contains(ENV.current_user_roles, "admin"),
postToSIS: @options.postToSIS || data.postToSIS == '1'

View File

@ -104,7 +104,7 @@ define [
filterResults: =>
term = $('#search_term').val()
gradingPeriod = null
if ENV.MULTIPLE_GRADING_PERIODS_ENABLED
if ENV.HAS_GRADING_PERIODS
gradingPeriodIndex = $("#grading_period_selector").val()
gradingPeriod = @gradingPeriods[parseInt(gradingPeriodIndex)] if gradingPeriodIndex != "all"
@saveSelectedGradingPeriod(gradingPeriod)

View File

@ -235,11 +235,10 @@ class ApplicationController < ActionController::Base
end
helper_method :k12?
def multiple_grading_periods?
account_and_grading_periods_allowed? ||
context_grading_periods_enabled?
def grading_periods?
!!@context.try(:grading_periods?)
end
helper_method :multiple_grading_periods?
helper_method :grading_periods?
def master_courses?
@domain_root_account && @domain_root_account.feature_enabled?(:master_courses)
@ -277,18 +276,6 @@ class ApplicationController < ActionController::Base
end
private :tool_dimensions
def account_and_grading_periods_allowed?
@context.is_a?(Account) &&
@context.feature_allowed?(:multiple_grading_periods)
end
private :account_and_grading_periods_allowed?
def context_grading_periods_enabled?
@context.present? &&
@context.feature_enabled?(:multiple_grading_periods)
end
private :context_grading_periods_enabled?
# Reject the request by halting the execution of the current handler
# and returning a helpful error message (and HTTP status code).
#
@ -2050,7 +2037,7 @@ class ApplicationController < ActionController::Base
},
:POST_TO_SIS => Assignment.sis_grade_export_enabled?(@context),
:PERMISSIONS => permissions,
:MULTIPLE_GRADING_PERIODS_ENABLED => @context.feature_enabled?(:multiple_grading_periods),
:HAS_GRADING_PERIODS => @context.grading_periods?,
:VALID_DATE_RANGE => CourseDateRange.new(@context),
:assignment_menu_tools => external_tools_display_hashes(:assignment_menu),
:discussion_topic_menu_tools => external_tools_display_hashes(:discussion_topic_menu),
@ -2061,7 +2048,7 @@ class ApplicationController < ActionController::Base
conditional_release_js_env(includes: :active_rules)
if @context.feature_enabled?(:multiple_grading_periods)
if @context.grading_periods?
js_env(:active_grading_periods => GradingPeriod.json_for(@context, @current_user))
end
end

View File

@ -37,7 +37,7 @@ class AssignmentGroupsApiController < ApplicationController
#
# @argument grading_period_id [Integer]
# The id of the grading period in which assignment groups are being requested
# (Requires the Multiple Grading Periods account feature turned on)
# (Requires grading periods to exist on the account)
#
# @returns AssignmentGroup
def show
@ -45,7 +45,7 @@ class AssignmentGroupsApiController < ApplicationController
includes = Array(params[:include])
override_dates = value_to_boolean(params[:override_assignment_dates] || true)
assignments = @assignment_group.visible_assignments(@current_user)
if params[:grading_period_id].present? && multiple_grading_periods?
if params[:grading_period_id].present?
assignments = GradingPeriod.for(@context).find_by(id: params[:grading_period_id]).assignments(assignments)
end
if assignments.any? && includes.include?('submission')

View File

@ -115,14 +115,14 @@ class AssignmentGroupsController < ApplicationController
#
# @argument grading_period_id [Integer]
# The id of the grading period in which assignment groups are being requested
# (Requires the Multiple Grading Periods feature turned on.)
# (Requires grading periods to exist.)
#
# @argument scope_assignments_to_student [Boolean]
# If true, all assignments returned will apply to the current user in the
# specified grading period. If assignments apply to other students in the
# specified grading period, but not the current user, they will not be
# returned. (Requires the grading_period_id argument and the Multiple Grading
# Periods feature turned on. In addition, the current user must be a student.)
# returned. (Requires the grading_period_id argument and grading periods to
# exist. In addition, the current user must be a student.)
#
# @returns [AssignmentGroup]
def index
@ -295,8 +295,7 @@ class AssignmentGroupsController < ApplicationController
end
def filter_by_grading_period?
return false if all_grading_periods_selected?
params[:grading_period_id].present? && multiple_grading_periods?
params[:grading_period_id].present? && !all_grading_periods_selected?
end
def all_grading_periods_selected?
@ -390,7 +389,7 @@ class AssignmentGroupsController < ApplicationController
assignments = assignments.with_student_submission_count.all
if params[:grading_period_id].present? && multiple_grading_periods?
if filter_by_grading_period?
assignments = filter_assignments_by_grading_period(assignments, context)
end
@ -427,7 +426,7 @@ class AssignmentGroupsController < ApplicationController
end
def can_reorder_assignments?(assignments, group)
return true unless @context.feature_enabled?(:multiple_grading_periods)
return true unless @context.grading_periods?
return true if @context.account_membership_allows(@current_user)
effective_due_dates = EffectiveDueDates.for_course(@context, assignments)

View File

@ -443,7 +443,7 @@ class AssignmentsController < ApplicationController
GROUP_CATEGORIES: group_categories,
HAS_GRADED_SUBMISSIONS: @assignment.graded_submissions_exist?,
KALTURA_ENABLED: !!feature_enabled?(:kaltura),
MULTIPLE_GRADING_PERIODS_ENABLED: @context.feature_enabled?(:multiple_grading_periods),
HAS_GRADING_PERIODS: @context.grading_periods?,
PLAGIARISM_DETECTION_PLATFORM: @context.root_account.feature_enabled?(:plagiarism_detection_platform),
POST_TO_SIS: post_to_sis,
SIS_NAME: AssignmentUtil.post_to_sis_friendly_name(@assignment),
@ -475,7 +475,7 @@ class AssignmentsController < ApplicationController
hash[:SELECTED_CONFIG_TOOL_ID] = selected_tool ? selected_tool.id : nil
hash[:SELECTED_CONFIG_TOOL_TYPE] = selected_tool ? selected_tool.class.to_s : nil
if @context.feature_enabled?(:multiple_grading_periods)
if @context.grading_periods?
hash[:active_grading_periods] = GradingPeriod.json_for(@context, @current_user)
end
append_sis_data(hash)

View File

@ -359,14 +359,14 @@ class CoursesController < ApplicationController
# - "current_grading_period_scores": Optional information to include with
# each Course. When current_grading_period_scores is given and total_scores
# is given, any student enrollments will also include the fields
# 'multiple_grading_periods_enabled',
# 'has_grading_periods',
# 'totals_for_all_grading_periods_option', 'current_grading_period_title',
# 'current_grading_period_id', current_period_computed_current_score',
# 'current_period_computed_final_score',
# 'current_period_computed_current_grade', and
# 'current_period_computed_final_grade' (see Enrollment documentation for
# more information on these fields). In addition, when this argument is
# passed, the course will have a 'multiple_grading_periods_enabled' attribute
# passed, the course will have a 'has_grading_periods' attribute
# on it. This argument is ignored if the course is configured to hide final
# grades or if the total_scores argument is not included.
# - "term": Optional information to include with each Course. When
@ -474,14 +474,14 @@ class CoursesController < ApplicationController
# - "current_grading_period_scores": Optional information to include with
# each Course. When current_grading_period_scores is given and total_scores
# is given, any student enrollments will also include the fields
# 'multiple_grading_periods_enabled',
# 'has_grading_periods',
# 'totals_for_all_grading_periods_option', 'current_grading_period_title',
# 'current_grading_period_id', current_period_computed_current_score',
# 'current_period_computed_final_score',
# 'current_period_computed_current_grade', and
# 'current_period_computed_final_grade' (see Enrollment documentation for
# more information on these fields). In addition, when this argument is
# passed, the course will have a 'multiple_grading_periods_enabled' attribute
# passed, the course will have a 'has_grading_periods' attribute
# on it. This argument is ignored if the course is configured to hide final
# grades or if the total_scores argument is not included.
# - "term": Optional information to include with each Course. When
@ -2474,9 +2474,9 @@ class CoursesController < ApplicationController
# @API Get effective due dates
# For each assignment in the course, returns each assigned student's ID
# and their corresponding due date along with some Multiple Grading Periods
# data. Returns a collection with keys representing assignment IDs and values
# as a collection containing keys representing student IDs and values representing
# and their corresponding due date along with some grading period data.
# Returns a collection with keys representing assignment IDs and values as a
# collection containing keys representing student IDs and values representing
# the student's effective due_at, the grading_period_id of which the due_at falls
# in, and whether or not the grading period is closed (in_closed_grading_period)
#
@ -2805,7 +2805,7 @@ class CoursesController < ApplicationController
end
def can_change_group_weighting_scheme?
return true unless @course.feature_enabled?(:multiple_grading_periods)
return true unless @course.grading_periods?
return true if @course.account_membership_allows(@current_user)
!@course.any_assignment_in_closed_grading_period?
end

View File

@ -448,7 +448,7 @@ class DiscussionTopicsController < ApplicationController
GROUP_CATEGORIES: categories.
reject(&:student_organized?).
map { |category| { id: category.id, name: category.name } },
MULTIPLE_GRADING_PERIODS_ENABLED: @context.feature_enabled?(:multiple_grading_periods),
HAS_GRADING_PERIODS: @context.grading_periods?,
SECTION_LIST: sections.map { |section| { id: section.id, name: section.name } }
}
@ -479,7 +479,7 @@ class DiscussionTopicsController < ApplicationController
js_hash[:CANCEL_TO] = cancel_redirect_url
append_sis_data(js_hash)
if @context.feature_enabled?(:multiple_grading_periods)
if @context.grading_periods?
gp_context = @context.is_a?(Group) ? @context.context : @context
js_hash[:active_grading_periods] = GradingPeriod.json_for(gp_context, @current_user)
end

View File

@ -207,8 +207,8 @@
# "example": "B-",
# "type": "string"
# },
# "multiple_grading_periods_enabled": {
# "description": "optional: Indicates whether the course the enrollment belongs to has the Multiple Grading Periods feature enabled. (applies only to student enrollments, and only available in course endpoints)",
# "has_grading_periods": {
# "description": "optional: Indicates whether the course the enrollment belongs to has grading periods set up. (applies only to student enrollments, and only available in course endpoints)",
# "example": true,
# "type": "boolean"
# },
@ -218,32 +218,32 @@
# "type": "boolean"
# },
# "current_grading_period_title": {
# "description": "optional: The name of the currently active grading period, if one exists. If the course the enrollment belongs to does not have Multiple Grading Periods enabled, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "description": "optional: The name of the currently active grading period, if one exists. If the course the enrollment belongs to does not have grading periods, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "example": "Fall Grading Period",
# "type": "string"
# },
# "current_grading_period_id": {
# "description": "optional: The id of the currently active grading period, if one exists. If the course the enrollment belongs to does not have Multiple Grading Periods enabled, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "description": "optional: The id of the currently active grading period, if one exists. If the course the enrollment belongs to does not have grading periods, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "example": 5,
# "type": "integer"
# },
# "current_period_computed_current_score": {
# "description": "optional: The student's score in the course for the current grading period, ignoring ungraded assignments. If the course the enrollment belongs to does not have Multiple Grading Periods enabled, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "description": "optional: The student's score in the course for the current grading period, ignoring ungraded assignments. If the course the enrollment belongs to does not have grading periods, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "example": 95.80,
# "type": "number"
# },
# "current_period_computed_final_score": {
# "description": "optional: The student's score in the course for the current grading period, including ungraded assignments with a score of 0. If the course the enrollment belongs to does not have Multiple Grading Periods enabled, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "description": "optional: The student's score in the course for the current grading period, including ungraded assignments with a score of 0. If the course the enrollment belongs to does not have grading periods, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "example": 85.25,
# "type": "number"
# },
# "current_period_computed_current_grade": {
# "description": "optional: The letter grade equivalent of current_period_computed_current_score, if available. If the course the enrollment belongs to does not have Multiple Grading Periods enabled, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "description": "optional: The letter grade equivalent of current_period_computed_current_score, if available. If the course the enrollment belongs to does not have grading periods, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "example": "A",
# "type": "string"
# },
# "current_period_computed_final_grade": {
# "description": "optional: The letter grade equivalent of current_period_computed_final_score, if available. If the course the enrollment belongs to does not have Multiple Grading Periods enabled, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "description": "optional: The letter grade equivalent of current_period_computed_final_score, if available. If the course the enrollment belongs to does not have grading periods, or if no currently active grading period exists, the value will be null. (applies only to student enrollments, and only available in course endpoints)",
# "example": "B",
# "type": "string"
# }
@ -262,7 +262,6 @@ class EnrollmentsApiController < ApplicationController
:inactive_role => 'Cannot create an enrollment with this role because it is inactive.',
:base_type_mismatch => 'The specified type must match the base type for the role',
:concluded_course => 'Can\'t add an enrollment to a concluded course.',
:multiple_grading_periods_disabled => 'Multiple Grading Periods feature is disabled. Cannot filter by grading_period_id with this feature disabled',
:insufficient_sis_permissions => 'Insufficient permissions to filter by SIS fields'
}
@ -371,20 +370,10 @@ class EnrollmentsApiController < ApplicationController
if params[:grading_period_id].present?
if @context.is_a? User
unless @context.account.feature_enabled?(:multiple_grading_periods)
render_create_errors([@@errors[:multiple_grading_periods_disabled]])
return false
end
grading_period = @context.courses.lazy.map do |course|
GradingPeriod.for(course).find_by(id: params[:grading_period_id])
end.detect(&:present?)
else
unless multiple_grading_periods?
render_create_errors([@@errors[:multiple_grading_periods_disabled]])
return false
end
grading_period = GradingPeriod.for(@context).find_by(id: params[:grading_period_id])
end

View File

@ -1,11 +0,0 @@
module Filters::GradingPeriods
def check_feature_flag
unless multiple_grading_periods?
if api_request?
render json: {message: t('Page not found')}, status: :not_found
else
render status: 404, template: "shared/errors/404_message"
end
end
end
end

View File

@ -36,7 +36,7 @@ class GradebooksController < ApplicationController
MAX_POST_GRADES_TOOLS = 10
def grade_summary
set_current_grading_period if multiple_grading_periods?
set_current_grading_period if grading_periods?
@presenter = grade_summary_presenter
# do this as the very first thing, if the current user is a
# teacher in the course and they are not trying to view another
@ -59,9 +59,10 @@ class GradebooksController < ApplicationController
add_crumb(@presenter.student_name, named_context_url(@context, :context_student_grades_url,
@presenter.student_id))
gp_id = nil
if multiple_grading_periods?
if grading_periods?
@grading_periods = active_grading_periods_json
gp_id = @current_grading_period_id unless view_all_grading_periods?
effective_due_dates = EffectiveDueDates.new(@context).to_hash
end
@exclude_total = exclude_total?(@context)
@ -89,7 +90,6 @@ class GradebooksController < ApplicationController
grading_period = @grading_periods && @grading_periods.find { |period| period[:id] == gp_id }
ags_json = light_weight_ags_json(@presenter.groups, {student: @presenter.student})
grading_scheme = @context.grading_standard.try(:data) ||
@ -100,7 +100,10 @@ class GradebooksController < ApplicationController
group_weighting_scheme: @context.group_weighting_scheme,
show_total_grade_as_points: @context.settings[:show_total_grade_as_points],
grading_scheme: grading_scheme,
grading_period_set: grading_period_group_json,
grading_period: grading_period,
grading_periods: @grading_periods,
effective_due_dates: effective_due_dates,
exclude_total: @exclude_total,
student_outcome_gradebook_enabled: @context.feature_enabled?(:student_outcome_gradebook),
student_id: @presenter.student_id)
@ -124,7 +127,7 @@ class GradebooksController < ApplicationController
assignment_groups.map do |ag|
visible_assignments = ag.visible_assignments(opts[:student] || @current_user).to_a
if multiple_grading_periods? && @current_grading_period_id && !view_all_grading_periods?
if grading_periods? && @current_grading_period_id && !view_all_grading_periods?
current_period = GradingPeriod.for(@context).find_by(id: @current_grading_period_id)
visible_assignments = current_period.assignments_for_student(visible_assignments, opts[:student])
end
@ -193,7 +196,7 @@ class GradebooksController < ApplicationController
def show
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
@last_exported_gradebook_csv = GradebookCsv.last_successful_export(course: @context, user: @current_user)
set_current_grading_period if multiple_grading_periods?
set_current_grading_period if grading_periods?
set_js_env
@course_is_concluded = @context.completed?
@post_grades_tools = post_grades_tools
@ -289,27 +292,34 @@ class GradebooksController < ApplicationController
@current_grading_period_id == 0
end
def grading_period_group
return @grading_period_group if defined? @grading_period_group
@grading_period_group = active_grading_periods.first&.grading_period_group
end
def active_grading_periods
@active_grading_periods ||= GradingPeriod.for(@context).sort_by(&:start_date)
end
def grading_period_group_json
return @grading_period_group_json if defined? @grading_period_group_json
return @grading_period_group_json = nil unless grading_period_group.present?
@grading_period_group_json = grading_period_group
.as_json
.fetch(:grading_period_group)
.merge(grading_periods: active_grading_periods_json)
end
def active_grading_periods_json
@agp_json ||= GradingPeriod.periods_json(active_grading_periods, @current_user)
end
def latest_end_date_of_admin_created_grading_periods_in_the_past
periods = active_grading_periods.select do |period|
admin_created = period.account_group?
admin_created && period.end_date.past?
end
periods.map(&:end_date).compact.sort.last
end
private :latest_end_date_of_admin_created_grading_periods_in_the_past
def set_js_env
@gradebook_is_editable = @context.grants_right?(@current_user, session, :manage_grades)
per_page = Setting.get('api_max_per_page', '50').to_i
teacher_notes = @context.custom_gradebook_columns.not_deleted.where(:teacher_notes=> true).first
teacher_notes = @context.custom_gradebook_columns.not_deleted.where(teacher_notes: true).first
ag_includes = [:assignments, :assignment_visibility]
chunk_size = if @context.assignments.published.count < Setting.get('gradebook2.assignments_threshold', '20').to_i
Setting.get('gradebook2.submissions_chunk_size', '35').to_i
@ -317,82 +327,88 @@ class GradebooksController < ApplicationController
Setting.get('gradebook2.many_submissions_chunk_size', '10').to_i
end
js_env STUDENT_CONTEXT_CARDS_ENABLED: @domain_root_account.feature_enabled?(:student_context_cards)
js_env :GRADEBOOK_OPTIONS => {
:gradezilla => @context.root_account.feature_enabled?(:gradezilla),
:chunk_size => chunk_size,
:assignment_groups_url => api_v1_course_assignment_groups_url(
js_env GRADEBOOK_OPTIONS: {
gradezilla: @context.root_account.feature_enabled?(:gradezilla),
chunk_size: chunk_size,
assignment_groups_url: api_v1_course_assignment_groups_url(
@context,
include: ag_includes,
override_assignment_dates: "false",
exclude_assignment_submission_types: ['wiki_page']
),
:sections_url => api_v1_course_sections_url(@context),
:course_url => api_v1_course_url(@context),
:effective_due_dates_url => api_v1_course_effective_due_dates_url(@context),
:enrollments_url => custom_course_enrollments_api_url(per_page: per_page),
:enrollments_with_concluded_url =>
sections_url: api_v1_course_sections_url(@context),
course_url: api_v1_course_url(@context),
effective_due_dates_url: api_v1_course_effective_due_dates_url(@context),
enrollments_url: custom_course_enrollments_api_url(per_page: per_page),
enrollments_with_concluded_url:
custom_course_enrollments_api_url(include_concluded: true, per_page: per_page),
:enrollments_with_inactive_url =>
enrollments_with_inactive_url:
custom_course_enrollments_api_url(include_inactive: true, per_page: per_page),
:enrollments_with_concluded_and_inactive_url =>
enrollments_with_concluded_and_inactive_url:
custom_course_enrollments_api_url(include_concluded: true, include_inactive: true, per_page: per_page),
:students_url => custom_course_users_api_url(per_page: per_page),
:students_with_concluded_enrollments_url =>
students_url: custom_course_users_api_url(per_page: per_page),
students_with_concluded_enrollments_url:
custom_course_users_api_url(include_concluded: true, per_page: per_page),
:students_with_inactive_enrollments_url =>
students_with_inactive_enrollments_url:
custom_course_users_api_url(include_inactive: true, per_page: per_page),
:students_with_concluded_and_inactive_enrollments_url =>
students_with_concluded_and_inactive_enrollments_url:
custom_course_users_api_url(include_concluded: true, include_inactive: true, per_page: per_page),
:submissions_url => api_v1_course_student_submissions_url(@context, :grouped => '1'),
:outcome_links_url => api_v1_course_outcome_group_links_url(@context, :outcome_style => :full),
:outcome_rollups_url => api_v1_course_outcome_rollups_url(@context, :per_page => 100),
:change_grade_url => api_v1_course_assignment_submission_url(@context, ":assignment", ":submission", :include =>[:visibility]),
:context_url => named_context_url(@context, :context_url),
:download_assignment_submissions_url => named_context_url(@context, :context_assignment_submissions_url, "{{ assignment_id }}", :zip => 1),
:re_upload_submissions_url => named_context_url(@context, :submissions_upload_context_gradebook_url, "{{ assignment_id }}"),
:context_id => @context.id.to_s,
:context_code => @context.asset_string,
:context_sis_id => @context.sis_source_id,
:group_weighting_scheme => @context.group_weighting_scheme,
:grading_standard => @context.grading_standard_enabled? && (@context.grading_standard.try(:data) || GradingStandard.default_grading_standard),
:course_is_concluded => @context.completed?,
:course_name => @context.name,
:gradebook_is_editable => @gradebook_is_editable,
:context_allows_gradebook_uploads => @context.allows_gradebook_uploads?,
:gradebook_import_url => new_course_gradebook_upload_path(@context),
:setting_update_url => api_v1_course_settings_url(@context),
:show_total_grade_as_points => @context.settings[:show_total_grade_as_points],
:publish_to_sis_enabled => @context.allows_grade_publishing_by(@current_user) && @gradebook_is_editable,
:publish_to_sis_url => context_url(@context, :context_details_url, :anchor => 'tab-grade-publishing'),
:speed_grader_enabled => @context.allows_speed_grader?,
:multiple_grading_periods_enabled => multiple_grading_periods?,
:active_grading_periods => active_grading_periods_json,
:latest_end_date_of_admin_created_grading_periods_in_the_past => latest_end_date_of_admin_created_grading_periods_in_the_past,
:current_grading_period_id => @current_grading_period_id,
:outcome_gradebook_enabled => @context.feature_enabled?(:outcome_gradebook),
:custom_columns_url => api_v1_course_custom_gradebook_columns_url(@context),
:custom_column_url => api_v1_course_custom_gradebook_column_url(@context, ":id"),
:custom_column_data_url => api_v1_course_custom_gradebook_column_data_url(@context, ":id", per_page: per_page),
:custom_column_datum_url => api_v1_course_custom_gradebook_column_datum_url(@context, ":id", ":user_id"),
:reorder_custom_columns_url => api_v1_custom_gradebook_columns_reorder_url(@context),
:teacher_notes => teacher_notes && custom_gradebook_column_json(teacher_notes, @current_user, session),
:change_gradebook_version_url => context_url(@context, :change_gradebook_version_context_gradebook_url, :version => 2),
:export_gradebook_csv_url => course_gradebook_csv_url,
:gradebook_csv_progress => @last_exported_gradebook_csv.try(:progress),
:attachment_url => @last_exported_gradebook_csv.try(:attachment).try(:download_url),
:attachment => @last_exported_gradebook_csv.try(:attachment),
:sis_app_url => Setting.get('sis_app_url', nil),
:sis_app_token => Setting.get('sis_app_token', nil),
:list_students_by_sortable_name_enabled => @context.list_students_by_sortable_name?,
:gradebook_column_size_settings => @current_user.preferences[:gradebook_column_size],
:gradebook_column_size_settings_url => change_gradebook_column_size_course_gradebook_url,
:gradebook_column_order_settings => @current_user.preferences[:gradebook_column_order].try(:[], @context.id),
:gradebook_column_order_settings_url => save_gradebook_column_order_course_gradebook_url,
:all_grading_periods_totals => @context.feature_enabled?(:all_grading_periods_totals),
:sections => sections_json(@context.active_course_sections, @current_user, session),
:settings_update_url => api_v1_course_gradebook_settings_update_url(@context),
:settings => @current_user.preferences.fetch(:gradebook_settings, {}).fetch(@context.id, {}),
:version => params.fetch(:version, nil)
submissions_url: api_v1_course_student_submissions_url(@context, grouped: '1'),
outcome_links_url: api_v1_course_outcome_group_links_url(@context, outcome_style: :full),
outcome_rollups_url: api_v1_course_outcome_rollups_url(@context, per_page: 100),
change_grade_url:
api_v1_course_assignment_submission_url(@context, ":assignment", ":submission", include: [:visibility]),
context_url: named_context_url(@context, :context_url),
download_assignment_submissions_url:
named_context_url(@context, :context_assignment_submissions_url, "{{ assignment_id }}", zip: 1),
re_upload_submissions_url:
named_context_url(@context, :submissions_upload_context_gradebook_url, "{{ assignment_id }}"),
context_id: @context.id.to_s,
context_code: @context.asset_string,
context_sis_id: @context.sis_source_id,
group_weighting_scheme: @context.group_weighting_scheme,
grading_standard: (
@context.grading_standard_enabled? &&
(@context.grading_standard.try(:data) || GradingStandard.default_grading_standard)
),
course_is_concluded: @context.completed?,
course_name: @context.name,
gradebook_is_editable: @gradebook_is_editable,
context_allows_gradebook_uploads: @context.allows_gradebook_uploads?,
gradebook_import_url: new_course_gradebook_upload_path(@context),
setting_update_url: api_v1_course_settings_url(@context),
show_total_grade_as_points: @context.settings[:show_total_grade_as_points],
publish_to_sis_enabled: @context.allows_grade_publishing_by(@current_user) && @gradebook_is_editable,
publish_to_sis_url: context_url(@context, :context_details_url, anchor: 'tab-grade-publishing'),
speed_grader_enabled: @context.allows_speed_grader?,
has_grading_periods: grading_periods?,
active_grading_periods: active_grading_periods_json,
grading_period_set: grading_period_group_json,
current_grading_period_id: @current_grading_period_id,
outcome_gradebook_enabled: @context.feature_enabled?(:outcome_gradebook),
custom_columns_url: api_v1_course_custom_gradebook_columns_url(@context),
custom_column_url: api_v1_course_custom_gradebook_column_url(@context, ":id"),
custom_column_data_url: api_v1_course_custom_gradebook_column_data_url(@context, ":id", per_page: per_page),
custom_column_datum_url: api_v1_course_custom_gradebook_column_datum_url(@context, ":id", ":user_id"),
reorder_custom_columns_url: api_v1_custom_gradebook_columns_reorder_url(@context),
teacher_notes: teacher_notes && custom_gradebook_column_json(teacher_notes, @current_user, session),
change_gradebook_version_url: context_url(@context, :change_gradebook_version_context_gradebook_url, version: 2),
export_gradebook_csv_url: course_gradebook_csv_url,
gradebook_csv_progress: @last_exported_gradebook_csv.try(:progress),
attachment_url: @last_exported_gradebook_csv.try(:attachment).try(:download_url),
attachment: @last_exported_gradebook_csv.try(:attachment),
sis_app_url: Setting.get('sis_app_url', nil),
sis_app_token: Setting.get('sis_app_token', nil),
list_students_by_sortable_name_enabled: @context.list_students_by_sortable_name?,
gradebook_column_size_settings: @current_user.preferences[:gradebook_column_size],
gradebook_column_size_settings_url: change_gradebook_column_size_course_gradebook_url,
gradebook_column_order_settings: @current_user.preferences[:gradebook_column_order].try(:[], @context.id),
gradebook_column_order_settings_url: save_gradebook_column_order_course_gradebook_url,
all_grading_periods_totals: @context.feature_enabled?(:all_grading_periods_totals),
sections: sections_json(@context.active_course_sections, @current_user, session),
settings_update_url: api_v1_course_gradebook_settings_update_url(@context),
settings: @current_user.preferences.fetch(:gradebook_settings, {}).fetch(@context.id, {}),
version: params.fetch(:version, nil)
}
end
@ -752,7 +768,7 @@ class GradebooksController < ApplicationController
return true if context.hide_final_grades
all_grading_periods_selected =
multiple_grading_periods? && view_all_grading_periods?
grading_periods? && view_all_grading_periods?
hide_all_grading_periods_totals = !context.feature_enabled?(:all_grading_periods_totals)
all_grading_periods_selected && hide_all_grading_periods_totals
end
@ -787,7 +803,7 @@ class GradebooksController < ApplicationController
options = {}
return options unless @context.present?
if @current_grading_period_id.present? && !view_all_grading_periods? && multiple_grading_periods?
if @current_grading_period_id.present? && !view_all_grading_periods? && grading_periods?
options[:grading_period_id] = @current_grading_period_id
end

View File

@ -1,9 +1,6 @@
class GradingPeriodSetsController < ApplicationController
include ::Filters::GradingPeriods
before_action :require_user
before_action :get_context
before_action :check_feature_flag
before_action :check_manage_rights, except: [:index]
before_action :check_read_rights, except: [:update, :create, :destroy]
@ -42,7 +39,11 @@ class GradingPeriodSetsController < ApplicationController
end
def update
old_term_ids = grading_period_set.enrollment_terms.pluck(:id)
grading_period_set.enrollment_terms = enrollment_terms
# we need to recompute scores for enrollment terms that were removed since the line above
# will not run callbacks for the removed enrollment terms
EnrollmentTerm.where(id: old_term_ids - enrollment_terms.map(&:id)).each(&:recompute_course_scores)
respond_to do |format|
if grading_period_set.update(set_params)
@ -74,7 +75,7 @@ class GradingPeriodSetsController < ApplicationController
end
def set_params
params.require(:grading_period_set).permit(:title)
params.require(:grading_period_set).permit(:title, :weighted)
end
def check_read_rights

View File

@ -61,11 +61,8 @@
# }
#
class GradingPeriodsController < ApplicationController
include ::Filters::GradingPeriods
before_action :require_user
before_action :get_context
before_action :check_feature_flag
# @API List grading periods
# @beta
@ -240,6 +237,7 @@ class GradingPeriodsController < ApplicationController
set_subquery = GradingPeriodGroup.active.select(:account_id).where(id: params[:set_id])
@context = Account.active.where(id: set_subquery).take
render json: {message: t('Page not found')}, status: :not_found unless @context
end
# model level validations
@ -325,11 +323,6 @@ class GradingPeriodsController < ApplicationController
def index_permissions
can_create_grading_periods = @context.is_a?(Account) &&
@context.root_account? && @context.grants_right?(@current_user, :manage)
can_toggle_grading_periods = @domain_root_account.grants_right?(@current_user, :manage) ||
@context.feature_allowed?(:multiple_grading_periods, exclude_enabled: true)
{
can_create_grading_periods: can_create_grading_periods,
can_toggle_grading_periods: can_toggle_grading_periods
}.as_json
{can_create_grading_periods: can_create_grading_periods}.as_json
end
end

View File

@ -30,7 +30,7 @@ class GradingStandardsController < ApplicationController
GRADING_STANDARDS_URL: context_url(@context, :context_grading_standards_url),
GRADING_PERIOD_SETS_URL: api_v1_account_grading_period_sets_url(@context),
ENROLLMENT_TERMS_URL: api_v1_enrollment_terms_url(@context),
MULTIPLE_GRADING_PERIODS: multiple_grading_periods?,
HAS_GRADING_PERIODS: grading_periods?,
DEFAULT_GRADING_STANDARD_DATA: GradingStandard.default_grading_standard,
CONTEXT_SETTINGS_URL: context_url(@context, :context_settings_url)
}

View File

@ -320,12 +320,12 @@ class Quizzes::QuizzesController < ApplicationController
:quiz_max_combination_count => QUIZ_MAX_COMBINATION_COUNT,
:SHOW_QUIZ_ALT_TEXT_WARNING => true,
:VALID_DATE_RANGE => CourseDateRange.new(@context),
:MULTIPLE_GRADING_PERIODS_ENABLED => @context.feature_enabled?(:multiple_grading_periods),
:HAS_GRADING_PERIODS => @context.grading_periods?,
:MAX_NAME_LENGTH_REQUIRED_FOR_ACCOUNT => max_name_length_required_for_account,
:MAX_NAME_LENGTH => max_name_length
}
if @context.feature_enabled?(:multiple_grading_periods)
if @context.grading_periods?
hash[:active_grading_periods] = GradingPeriod.json_for(@context, @current_user)
end

View File

@ -245,7 +245,7 @@ class SubmissionsApiController < ApplicationController
#
# @argument grading_period_id [Integer]
# The id of the grading period in which submissions are being requested
# (Requires the Multiple Grading Periods account feature turned on)
# (Requires grading periods to exist on the account)
#
# @argument order [String, "id"|"graded_at"]
# The order submissions will be returned in. Defaults to "id". Doesn't
@ -330,7 +330,7 @@ class SubmissionsApiController < ApplicationController
assignment_scope = assignment_scope.where(:id => requested_assignment_ids)
end
if params[:grading_period_id].present? && multiple_grading_periods?
if params[:grading_period_id].present?
assignments = GradingPeriod.active.find(params[:grading_period_id]).assignments(assignment_scope)
else
assignments = assignment_scope.to_a

View File

@ -175,7 +175,12 @@ class UsersController < ApplicationController
add_crumb(@current_user.short_name, crumb_url)
add_crumb(t('crumbs.grades', 'Grades'), grades_path)
current_active_enrollments = @user.enrollments.current.preload(:course, :enrollment_state).shard(@user).to_a
current_active_enrollments = @user.
enrollments.
current.
preload(:course, :enrollment_state, :scores).
shard(@user).
to_a
@presenter = GradesPresenter.new(current_active_enrollments)
@ -197,19 +202,11 @@ class UsersController < ApplicationController
enrollment = Enrollment.active.find(params[:enrollment_id])
return render_unauthorized_action unless enrollment.grants_right?(@current_user, session, :read_grades)
course = enrollment.course
grading_period_id = params[:grading_period_id].to_i
grading_period = GradingPeriod.for(course).find_by(id: grading_period_id)
grading_periods = {
course.id => {
periods: [grading_period],
selected_period_id: grading_period_id
}
grading_period_id = generate_grading_period_id(params[:grading_period_id])
render json: {
grade: enrollment.computed_current_score(grading_period_id: grading_period_id),
hide_final_grades: enrollment.course.hide_final_grades?
}
calculator = grade_calculator([enrollment.user_id], course, grading_periods)
totals = calculator.compute_scores.first[:current]
totals[:hide_final_grades] = course.hide_final_grades?
render json: totals
end
def oauth
@ -2119,6 +2116,12 @@ class UsersController < ApplicationController
private
def generate_grading_period_id(period_id)
# nil and '' will get converted to 0 in the .to_i call
id = period_id.to_i
id == 0 ? nil : id
end
def render_new_user_tutorial_statuses(user)
render(json: { new_user_tutorial_statuses: { collapsed: user.new_user_tutorial_statuses }})
end
@ -2137,47 +2140,30 @@ class UsersController < ApplicationController
presenter.observed_enrollments.group_by { |enrollment| enrollment[:course_id] }
grouped_observed_enrollments.each do |course_id, enrollments|
grading_period_id = generate_grading_period_id(
grading_periods[course_id].try(:selected_period_id)
)
grades[:observed_enrollments][course_id] = {}
if grading_periods[course_id].present?
user_ids = enrollments.map(&:user_id)
course = enrollments.first.course
grades[:observed_enrollments][course_id] = grades_from_grade_calculator(user_ids, course, grading_periods)
else
grades[:observed_enrollments][course_id] = grades_from_enrollments(enrollments)
end
grades[:observed_enrollments][course_id] = grades_from_enrollments(
enrollments,
grading_period_id: grading_period_id
)
end
presenter.student_enrollments.each do |enrollment_course_pair|
course = enrollment_course_pair.first
enrollment = enrollment_course_pair.second
if grading_periods[course.id].present?
computed_score = grades_from_grade_calculator([enrollment.user_id], course, grading_periods)[enrollment.user_id]
grades[:student_enrollments][course.id] = computed_score
else
computed_score = enrollment.computed_current_score
grades[:student_enrollments][course.id] = computed_score
end
presenter.student_enrollments.each do |course, enrollment|
grading_period_id = generate_grading_period_id(
grading_periods[course.id].try(:[], :selected_period_id)
)
computed_score = enrollment.computed_current_score(grading_period_id: grading_period_id)
grades[:student_enrollments][course.id] = computed_score
end
grades
end
def grades_from_grade_calculator(user_ids, course, grading_periods)
calculator = grade_calculator(user_ids, course, grading_periods)
grades = {}
calculator.compute_scores.each_with_index do |score, index|
computed_score = score[:current][:grade]
user_id = user_ids[index]
grades[user_id] = computed_score
end
grades
end
def grades_from_enrollments(enrollments)
def grades_from_enrollments(enrollments, grading_period_id: nil)
grades = {}
enrollments.each do |enrollment|
computed_score = enrollment.computed_current_score
computed_score = enrollment.computed_current_score(grading_period_id: grading_period_id)
grades[enrollment.user_id] = computed_score
end
grades
@ -2191,7 +2177,7 @@ class UsersController < ApplicationController
grading_periods = {}
courses.each do |course|
next unless course.feature_enabled?(:multiple_grading_periods)
next unless course.grading_periods?
course_periods = GradingPeriod.for(course)
grading_period_specified = grading_period_id &&
@ -2212,19 +2198,6 @@ class UsersController < ApplicationController
grading_periods
end
def grade_calculator(user_ids, course, grading_periods)
if course.feature_enabled?(:multiple_grading_periods) &&
grading_periods[course.id][:selected_period_id] != 0
grading_period = grading_periods[course.id][:periods].find do |period|
period.id == grading_periods[course.id][:selected_period_id]
end
GradeCalculator.new(user_ids, course, grading_period: grading_period)
else
GradeCalculator.new(user_ids, course)
end
end
def create_user
run_login_hooks
# Look for an incomplete registration with this pseudonym

View File

@ -26,7 +26,7 @@ define([
syncWithBackbone: React.PropTypes.func.isRequired,
sections: React.PropTypes.array.isRequired,
defaultSectionId: React.PropTypes.string.isRequired,
multipleGradingPeriodsEnabled: React.PropTypes.bool.isRequired,
hasGradingPeriods: React.PropTypes.bool.isRequired,
gradingPeriods: React.PropTypes.array.isRequired,
isOnlyVisibleToOverrides: React.PropTypes.bool.isRequired,
dueAt: function(props) {
@ -350,7 +350,7 @@ define([
let validGroups = this.valuesWithOmission({object: this.groupsForSelectedSet(), keysToOmit: this.chosenGroupIds()})
let validSections = this.valuesWithOmission({object: this.state.sections, keysToOmit: this.chosenSectionIds()})
let validNoops = this.valuesWithOmission({object: this.state.noops, keysToOmit: this.chosenNoops()})
if (this.props.multipleGradingPeriodsEnabled && !_.contains(ENV.current_user_roles, "admin")) {
if (this.props.hasGradingPeriods && !_.contains(ENV.current_user_roles, "admin")) {
({validStudents, validGroups, validSections} =
this.filterDropdownOptionsForMultipleGradingPeriods(validStudents, validGroups, validSections))
}
@ -431,7 +431,7 @@ define([
disableInputs(row) {
const rowIsNewOrUserIsAdmin = !row.persisted || _.contains(ENV.current_user_roles, "admin")
if (!this.props.multipleGradingPeriodsEnabled || rowIsNewOrUserIsAdmin) {
if (!this.props.hasGradingPeriods || rowIsNewOrUserIsAdmin) {
return false
}

View File

@ -18,109 +18,105 @@
define([
'underscore'
], function (_) {
const sum = function (collection) {
return _.reduce(collection, function (sum, value) {
return sum + value;
}, 0);
};
], (_) => {
function sum (collection) {
return _.reduce(collection, (total, value) => total + value, 0);
}
const sumBy = function (collection, attr) {
function sumBy (collection, attr) {
const values = _.map(collection, attr);
return sum(values);
};
}
const partition = function (collection, partitionFn) {
function partition (collection, partitionFn) {
const grouped = _.groupBy(collection, partitionFn);
return [grouped[true] || [], grouped[false] || []];
};
return [grouped.true || [], grouped.false || []];
}
const parseScore = function (score) {
function parseScore (score) {
const result = parseFloat(score);
return (result && isFinite(result)) ? result : 0;
};
}
function sortPairsDescending ([scoreA, submissionA], [scoreB, submissionB]) {
const scoreDiff = scoreB - scoreA;
if (scoreDiff !== 0) {
return scoreDiff;
}
// To ensure stable sorting, use the assignment id as a secondary sort.
return submissionA.assignment_id - submissionB.assignment_id;
}
// Some browser sorting functions (such as in V8) are not stable.
// This function ensures that the same submission will be dropped regardless
// of browser.
const stableSubmissionSort = function (sortFn, getAssignmentIdFn) {
return function (a, b) {
const ret = sortFn(a, b);
if (ret === 0) {
return getAssignmentIdFn(a) - getAssignmentIdFn(b);
} else {
return ret;
}
};
};
function sortPairsAscending ([scoreA, submissionA], [scoreB, submissionB]) {
const scoreDiff = scoreA - scoreB;
if (scoreDiff !== 0) {
return scoreDiff;
}
// To ensure stable sorting, use the assignment id as a secondary sort.
return submissionA.assignment_id - submissionB.assignment_id;
}
const sortDescending = function ([a, xx], [b, yy]) {
return b - a;
};
const sortAscending = function ([a, xx], [b, yy]) {
return a - b;
};
const getAssignmentIdFn = function ([score, submission]) {
return submission.submission.assignment_id;
};
function sortSubmissionsAscending (submissionA, submissionB) {
const scoreDiff = submissionA.score - submissionB.score;
if (scoreDiff !== 0) {
return scoreDiff;
}
// To ensure stable sorting, use the assignment id as a secondary sort.
return submissionA.assignment_id - submissionB.assignment_id;
}
const getSubmissionGrade = function ({ score, total }) {
function getSubmissionGrade ({ score, total }) {
return score / total;
};
}
const estimateQHigh = function (pointed, unpointed, grades) {
function estimateQHigh (pointed, unpointed, grades) {
if (unpointed.length > 0) {
const pointsPossible = sumBy(pointed, 'total');
const bestPointedScore = Math.max(pointsPossible, sumBy(pointed, 'score'));
const unpointedScore = sumBy(unpointed, 'score');
return (bestPointedScore + unpointedScore) / pointsPossible;
} else {
return grades[grades.length - 1];
}
};
const dropPointed = function (submissions, cannotDrop, keepHighest, keepLowest) {
const totals = _.map(submissions, 'total');
const maxTotal = Math.max.apply(Math, totals);
return grades[grades.length - 1];
}
const keepHelper = function (submissions, keep, bigFSort) {
keep = Math.max(1, keep);
function buildBigF (keepCount, cannotDrop, sortFn) {
return function bigF (q, submissions) {
const ratedScores = _.map(submissions, submission => (
[submission.score - (q * submission.total), submission]
));
const rankedScores = ratedScores.sort(sortFn);
const keptScores = rankedScores.slice(0, keepCount);
const qKept = sumBy(keptScores, ([score]) => score);
const keptSubmissions = _.map(keptScores, ([_score, submission]) => submission);
const qCannotDrop = sumBy(cannotDrop, submission => submission.score - (q * submission.total));
return [qKept + qCannotDrop, keptSubmissions];
}
}
if (submissions.length <= keep) {
function dropPointed (droppableSubmissionData, cannotDrop, keepHighest, keepLowest) {
const totals = _.map(droppableSubmissionData, 'total');
const maxTotal = Math.max(...totals);
function keepHelper (submissions, initialKeepCount, bigFSort) {
const keepCount = Math.max(1, initialKeepCount);
if (submissions.length <= keepCount) {
return submissions;
}
const allSubmissions = [...submissions, ...cannotDrop];
const [unpointed, pointed] = partition(allSubmissions, function (submission) {
return submission.total == 0;
});
const allSubmissionData = [...submissions, ...cannotDrop];
const [unpointed, pointed] = partition(allSubmissionData, submissionDatum => submissionDatum.total === 0);
const grades = _.map(pointed, getSubmissionGrade).sort();
let qHigh = estimateQHigh(pointed, unpointed, grades);
let qLow = grades[0];
let qMid = (qLow + qHigh) / 2;
const bigF = function (q, submissions) {
const ratedScores = _.map(submissions, function (submission) {
return [submission.score - (q * submission.total), submission];
});
const rankedScores = ratedScores.sort(bigFSort);
const keptScores = rankedScores.slice(0, keep);
const qKept = sumBy(keptScores, function ([score]) {
return score;
});
const keptSubmissions = _.map(keptScores, function ([score, submission]) {
return submission;
});
const qCantDrop = sumBy(cannotDrop, function (submission) {
return submission.score - q * submission.total;
});
return [qKept + qCantDrop, keptSubmissions];
};
const bigF = buildBigF(keepCount, cannotDrop, bigFSort);
let [x, kept] = bigF(qMid, submissions);
const threshold = 1 / (2 * keep * Math.pow(maxTotal, 2));
let [x, submissionsToKeep] = bigF(qMid, submissions);
const threshold = 1 / (2 * keepCount * (maxTotal ** 2));
while (qHigh - qLow >= threshold) {
if (x < 0) {
qHigh = qMid;
@ -132,22 +128,24 @@ define([
break;
}
[x, kept] = bigF(qMid, submissions);
[x, submissionsToKeep] = bigF(qMid, submissions);
}
return kept;
};
return submissionsToKeep;
}
const kept = keepHelper(submissions, keepHighest, stableSubmissionSort(sortDescending, getAssignmentIdFn));
return keepHelper(kept, keepLowest, stableSubmissionSort(sortAscending, getAssignmentIdFn));
};
const submissionsWithLowestDropped = keepHelper(
droppableSubmissionData, keepHighest, sortPairsDescending
);
return keepHelper(
submissionsWithLowestDropped, keepLowest, sortPairsAscending
);
}
const dropUnpointed = function (submissions, keepHighest, keepLowest) {
const sortAscending = function (a, b) { return a.score - b.score };
const getAssignmentIdFn = function ({ submission }) { return submission.assignment_id };
const sortedSubmissions = submissions.sort(stableSubmissionSort(sortAscending, getAssignmentIdFn));
function dropUnpointed (submissions, keepHighest, keepLowest) {
const sortedSubmissions = submissions.sort(sortSubmissionsAscending);
return _.chain(sortedSubmissions).last(keepHighest).first(keepLowest).value();
};
}
// I am not going to pretend that this code is understandable.
//
@ -159,130 +157,121 @@ define([
// Grades" by Daniel Kane and Jonathan Kane. Please see that paper for
// a full explanation of the math.
// (http://cseweb.ucsd.edu/~dakane/droplowest.pdf)
const dropAssignments = function (submissions, rules) {
rules = rules || {};
function dropAssignments (allSubmissionData, rules = {}) {
let dropLowest = rules.drop_lowest || 0;
let dropHighest = rules.drop_highest || 0;
const neverDropIds = rules.never_drop || [];
if (!(dropLowest || dropHighest)) {
return submissions;
return allSubmissionData;
}
let cannot_drop = [];
let cannotDrop = [];
let droppableSubmissionData = allSubmissionData;
if (neverDropIds.length > 0) {
[cannot_drop, submissions] = partition(submissions, function (submission) {
return _.contains(neverDropIds, submission.submission.assignment_id);
});
[cannotDrop, droppableSubmissionData] = partition(allSubmissionData, submission => (
_.contains(neverDropIds, submission.submission.assignment_id)
));
}
if (submissions.length === 0) {
return cannot_drop;
if (droppableSubmissionData.length === 0) {
return cannotDrop;
}
dropLowest = Math.min(dropLowest, submissions.length - 1);
dropHighest = (dropLowest + dropHighest) >= submissions.length ? 0 : dropHighest;
dropLowest = Math.min(dropLowest, droppableSubmissionData.length - 1);
dropHighest = (dropLowest + dropHighest) >= droppableSubmissionData.length ? 0 : dropHighest;
const keepHighest = submissions.length - dropLowest;
const keepHighest = droppableSubmissionData.length - dropLowest;
const keepLowest = keepHighest - dropHighest;
const hasPointed = _.some(submissions, function (submission) { return submission.total > 0 });
const hasPointed = _.some(droppableSubmissionData, submission => submission.total > 0);
let kept;
let submissionsToKeep;
if (hasPointed) {
kept = dropPointed(submissions, cannot_drop, keepHighest, keepLowest);
submissionsToKeep = dropPointed(droppableSubmissionData, cannotDrop, keepHighest, keepLowest);
} else {
kept = dropUnpointed(submissions, keepHighest, keepLowest);
submissionsToKeep = dropUnpointed(droppableSubmissionData, keepHighest, keepLowest);
}
kept = [ ...kept, ...cannot_drop];
submissionsToKeep = [...submissionsToKeep, ...cannotDrop];
_.difference(submissions, kept).forEach(function (submission) {
submission.drop = true;
});
_.difference(droppableSubmissionData, submissionsToKeep).forEach((submission) => { submission.drop = true });
return kept;
};
return submissionsToKeep;
}
const calculateGroupSum = function (group, submissions, includeUngraded) {
// remove assignments without visibility from gradeableAssignments
const hiddenAssignments = _.chain(submissions).filter('hidden').indexBy('assignment_id').value();
const gradeableAssignments = _.reject(group.assignments, function (assignment) {
return assignment.omit_from_final_grade ||
hiddenAssignments[assignment.id] ||
_.isEqual(assignment.submission_types, ['not_graded']);
});
function calculateGroupGrade (group, allSubmissions, includeUngraded) {
// Remove assignments without visibility from gradeableAssignments.
const hiddenAssignmentsById = _.chain(allSubmissions).filter('hidden').indexBy('assignment_id').value();
const gradeableAssignments = _.reject(group.assignments, assignment => (
assignment.omit_from_final_grade ||
hiddenAssignmentsById[assignment.id] ||
_.isEqual(assignment.submission_types, ['not_graded'])
));
const assignments = _.indexBy(gradeableAssignments, 'id');
// filter out submissions from other assignment groups
submissions = _.filter(submissions, function (submission) {
return assignments[submission.assignment_id];
});
// Remove submissions from other assignment groups.
let submissions = _.filter(allSubmissions, submission => assignments[submission.assignment_id]);
// fill in any missing submissions
// To calculate grades for assignments to which the student has not yet
// submitted, create a submission stub with a score of `null`.
if (includeUngraded) {
const submissionAssignmentIds = _.map(submissions, function ({ assignment_id }) {
return assignment_id.toString();
});
const missingSubmissions = _.difference(_.keys(assignments), submissionAssignmentIds);
const submissionStubs = _.map(missingSubmissions, (assignment_id) => {
return { assignment_id, score: null };
});
submissions = [ ...submissions, ...submissionStubs ];
const submissionAssignmentIds = _.map(submissions, ({ assignment_id }) => assignment_id.toString());
const missingAssignmentIds = _.difference(_.keys(assignments), submissionAssignmentIds);
const submissionStubs = _.map(missingAssignmentIds, assignmentId => (
{ assignment_id: assignmentId, score: null }
));
submissions = [...submissions, ...submissionStubs];
}
// filter out excused assignments
// Remove excused submissions.
submissions = _.reject(submissions, 'excused');
const submissionsByAssignment = _.indexBy(submissions, 'assignment_id');
const submissionData = _.map(submissions, function (submission) {
return {
const submissionData = _.map(submissions, submission => (
{
total: parseScore(assignments[submission.assignment_id].points_possible),
score: parseScore(submission.score),
submitted: submission.score != null && submission.score !== '',
pending_review: submission.workflow_state === 'pending_review',
submission
};
});
}
));
let relevantSubmissionData = submissionData;
if (!includeUngraded) {
relevantSubmissionData = _.filter(submissionData, function (submission) {
return submission.submitted && !submission.pending_review;
});
relevantSubmissionData = _.filter(submissionData, submission => (
submission.submitted && !submission.pending_review
));
}
const kept = dropAssignments(relevantSubmissionData, group.rules);
const score = sum(_.chain(kept).map('score').map(parseScore).value());
const possible = sumBy(kept, 'total');
const submissionsToKeep = dropAssignments(relevantSubmissionData, group.rules);
const score = sum(_.chain(submissionsToKeep).map('score').map(parseScore).value());
const possible = sumBy(submissionsToKeep, 'total');
return {
possible,
score,
weight: group.group_weight,
possible,
submission_count: _.filter(submissionData, 'submitted').length,
submissions: _.map(submissionData, function (submission) {
return {
drop: submission.drop,
percent: parseScore(submission.score / submission.total),
possible: submission.total,
score: parseScore(submission.score),
submission: submission.submission,
submitted: submission.submitted
};
})
submissions: _.map(submissionData, submissionDatum => (
{
drop: submissionDatum.drop,
percent: parseScore(submissionDatum.score / submissionDatum.total),
score: parseScore(submissionDatum.score),
possible: submissionDatum.total,
submission: submissionDatum.submission,
submitted: submissionDatum.submitted
}
))
};
};
}
// Each submission requires the following properties:
// * score: number
// * points_possible: non-negative integer
// * assignment_id: Canvas id
// * assignment_group_id: Canvas id
// * assignment_id: <Canvas id>
// * assignment_group_id: <Canvas id>
// * excused: boolean
//
// To represent assignments which the student has not yet submitted, set the
// score of the related submission to `null`.
// Ungraded submissions will have a score of `null`.
//
// An assignment group requires the following properties:
// * id: Canvas id
@ -296,21 +285,37 @@ define([
// * never_drop: [array of assignment ids]
//
// `assignments` is an array of objects with the following properties:
// * id: Canvas id
// * id: <Canvas id>
// * points_possible: non-negative number
// * submission_types: [array of strings]
//
// The weighting scheme is one of [`percent`, `points`]
// An AssignmentGroup Grade has the following shape:
// {
// score: number|null
// possible: number|null
// submission_count: non-negative number
// submissions: [array of Submissions]
// }
//
// When weightingScheme is `percent`, assignment group weights are used.
// Otherwise, no weighting is applied.
const calculate = function (submissions, assignmentGroup, weightingScheme) {
// Return value is an AssignmentGroup Grade Set.
// An AssignmentGroup Grade Set has the following shape:
// {
// assignmentGroupId: <Canvas id>
// assignmentGroupWeight: number
// current: <AssignmentGroup Grade *see above>
// final: <AssignmentGroup Grade *see above>
// scoreUnit: 'points'
// }
function calculate (allSubmissions, assignmentGroup) {
const submissions = _.uniq(allSubmissions, 'assignment_id');
return {
group: assignmentGroup,
current: calculateGroupSum(assignmentGroup, submissions, false),
final: calculateGroupSum(assignmentGroup, submissions, true)
assignmentGroupId: assignmentGroup.id,
assignmentGroupWeight: assignmentGroup.group_weight,
current: calculateGroupGrade(assignmentGroup, submissions, false),
final: calculateGroupGrade(assignmentGroup, submissions, true),
scoreUnit: 'points'
};
};
}
return {
calculate

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 Instructure, Inc.
* Copyright (C) 2016 - 2017 Instructure, Inc.
*
* This file is part of Canvas.
*
@ -20,51 +20,188 @@ define([
'underscore',
'compiled/util/round',
'jsx/gradebook/AssignmentGroupGradeCalculator'
], function (_, round, AssignmentGroupGradeCalculator) {
const sum = function (collection) {
return _.reduce(collection, function (sum, value) {
return sum + value;
}, 0);
};
], (_, round, AssignmentGroupGradeCalculator) => {
function sum (collection) {
return _.reduce(collection, (total, value) => (total + value), 0);
}
const sumBy = function (collection, attr) {
function sumBy (collection, attr) {
const values = _.map(collection, attr);
return sum(values);
};
}
const getGroupSumWeightedPercent = ({ score, possible, weight }) => {
return (score / possible) * weight;
};
function getWeightedPercent ({ score, possible, weight }) {
return score ? (score / possible) * weight : 0;
}
const calculateTotal = function (groupSums, includeUngraded, weightingScheme) {
groupSums = _.map(groupSums, function (groupSum) {
const sumVersion = includeUngraded ? groupSum.final : groupSum.current;
return { ...sumVersion, weight: groupSum.group.group_weight };
function combineAssignmentGroupGrades (assignmentGroupGrades, includeUngraded, options) {
const scopedAssignmentGroupGrades = _.map(assignmentGroupGrades, (assignmentGroupGrade) => {
const gradeVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current;
return { ...gradeVersion, weight: assignmentGroupGrade.assignmentGroupWeight };
});
if (weightingScheme === 'percent') {
const relevantGroupSums = _.filter(groupSums, 'possible');
let finalGrade = sum(_.map(relevantGroupSums, getGroupSumWeightedPercent));
const fullWeight = sumBy(relevantGroupSums, 'weight');
if (options.weightAssignmentGroups) {
const relevantGroupGrades = _.filter(scopedAssignmentGroupGrades, 'possible');
const fullWeight = sumBy(relevantGroupGrades, 'weight');
let finalGrade = sum(_.map(relevantGroupGrades, getWeightedPercent));
if (fullWeight === 0) {
finalGrade = null;
} else if (fullWeight < 100) {
finalGrade = finalGrade * 100 / fullWeight;
finalGrade = (finalGrade * 100) / fullWeight;
}
const submissionCount = sumBy(relevantGroupSums, 'submission_count');
const submissionCount = sumBy(relevantGroupGrades, 'submission_count');
const possible = ((submissionCount > 0) || includeUngraded) ? 100 : 0;
let score = finalGrade && round(finalGrade, 2);
score = isNaN(score) ? null : score;
return { score, possible };
} else {
return {
score: sumBy(groupSums, 'score'),
possible: sumBy(groupSums, 'possible')
}
}
};
return {
score: sumBy(scopedAssignmentGroupGrades, 'score'),
possible: sumBy(scopedAssignmentGroupGrades, 'possible')
}
}
function combineGradingPeriodGrades (gradingPeriodGradesByPeriodId, includeUngraded) {
const scopedGradingPeriodGrades = _.map(gradingPeriodGradesByPeriodId, (gradingPeriodGrade) => {
const gradeVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current;
return { ...gradeVersion, weight: gradingPeriodGrade.gradingPeriodWeight };
});
const weightedScores = _.map(scopedGradingPeriodGrades, getWeightedPercent);
const totalWeight = sumBy(scopedGradingPeriodGrades, 'weight');
const totalScore = totalWeight === 0 ? 0 : (sum(weightedScores) * 100) / Math.min(totalWeight, 100);
return {
score: round(totalScore, 2),
possible: 100
};
}
function divideGroupByGradingPeriods (assignmentGroup, effectiveDueDates) {
// When using weighted grading periods, assignment groups must not contain assignments due in different grading
// periods. This allows for calculated assignment group grades in closed grading periods to be accidentally
// changed if a related assignment is considered to be in an open grading period.
//
// To avoid this, assignment groups meeting this criteria are "divided" (duplicated) in a way where each
// instance of the assignment group includes assignments only from one grading period.
const assignmentsByGradingPeriodId = _.groupBy(assignmentGroup.assignments, assignment => (
effectiveDueDates[assignment.id].grading_period_id
));
return _.map(assignmentsByGradingPeriodId, assignments => (
{ ...assignmentGroup, assignments }
));
}
function extractPeriodBasedAssignmentGroups (assignmentGroups, effectiveDueDates) {
return _.reduce(assignmentGroups, (periodBasedGroups, assignmentGroup) => {
const assignedAssignments = _.filter(assignmentGroup.assignments, assignment => (
effectiveDueDates[assignment.id]
));
if (assignedAssignments.length > 0) {
const groupWithAssignedAssignments = { ...assignmentGroup, assignments: assignedAssignments };
return [
...periodBasedGroups,
...divideGroupByGradingPeriods(groupWithAssignedAssignments, effectiveDueDates)
];
}
return periodBasedGroups;
}, []);
}
function recombinePeriodBasedAssignmentGroupGrades (grades) {
const map = {};
_.forEach(grades, (grade) => {
const previousGrade = map[grade.assignmentGroupId];
if (previousGrade) {
map[grade.assignmentGroupId] = {
...previousGrade,
current: {
score: previousGrade.current.score + grade.current.score,
possible: previousGrade.current.possible + grade.current.possible
},
final: {
score: previousGrade.final.score + grade.final.score,
possible: previousGrade.final.possible + grade.final.possible
}
};
} else {
map[grade.assignmentGroupId] = grade;
}
});
return map;
}
function calculateWithGradingPeriods (submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options) {
const periodBasedGroups = extractPeriodBasedAssignmentGroups(assignmentGroups, effectiveDueDates);
const assignmentGroupsByGradingPeriodId = _.groupBy(periodBasedGroups, (assignmentGroup) => {
const assignmentId = assignmentGroup.assignments[0].id;
return effectiveDueDates[assignmentId].grading_period_id;
});
const gradingPeriodsById = _.indexBy(gradingPeriods, 'id');
const gradingPeriodGradesByPeriodId = {};
const periodBasedAssignmentGroupGrades = [];
_.forEach(gradingPeriods, (gradingPeriod) => {
const groupGrades = {};
(assignmentGroupsByGradingPeriodId[gradingPeriod.id] || []).forEach((assignmentGroup) => {
groupGrades[assignmentGroup.id] = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup);
periodBasedAssignmentGroupGrades.push(groupGrades[assignmentGroup.id]);
});
const groupGradesList = _.values(groupGrades);
gradingPeriodGradesByPeriodId[gradingPeriod.id] = {
gradingPeriodId: gradingPeriod.id,
gradingPeriodWeight: gradingPeriodsById[gradingPeriod.id].weight || 0,
assignmentGroups: groupGrades,
current: combineAssignmentGroupGrades(groupGradesList, false, options),
final: combineAssignmentGroupGrades(groupGradesList, true, options),
scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
};
});
if (options.weightGradingPeriods) {
return {
assignmentGroups: recombinePeriodBasedAssignmentGroupGrades(periodBasedAssignmentGroupGrades),
gradingPeriods: gradingPeriodGradesByPeriodId,
current: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, false, options),
final: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, true, options),
scoreUnit: 'percentage'
};
}
const allAssignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
));
return {
assignmentGroups: _.indexBy(allAssignmentGroupGrades, grade => grade.assignmentGroupId),
gradingPeriods: gradingPeriodGradesByPeriodId,
current: combineAssignmentGroupGrades(allAssignmentGroupGrades, false, options),
final: combineAssignmentGroupGrades(allAssignmentGroupGrades, true, options),
scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
};
}
function calculateWithoutGradingPeriods (submissions, assignmentGroups, options) {
const assignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
));
return {
assignmentGroups: _.indexBy(assignmentGroupGrades, grade => grade.assignmentGroupId),
current: combineAssignmentGroupGrades(assignmentGroupGrades, false, options),
final: combineAssignmentGroupGrades(assignmentGroupGrades, true, options),
scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
};
}
// Each submission requires the following properties:
// * score: number
@ -73,8 +210,7 @@ define([
// * assignment_group_id: Canvas id
// * excused: boolean
//
// To represent assignments which the student has not yet submitted, set the
// score of the related submission to `null`.
// Ungraded submissions will have a score of `null`.
//
// Each assignment group requires the following properties:
// * id: Canvas id
@ -96,17 +232,89 @@ define([
//
// When weightingScheme is `percent`, assignment group weights are used.
// Otherwise, no weighting is applied.
const calculate = function (submissions, assignmentGroups, weightingScheme) {
const groupSums = _.map(assignmentGroups, function (group) {
return AssignmentGroupGradeCalculator.calculate(submissions, group);
});
return {
group_sums: groupSums,
current: calculateTotal(groupSums, false, weightingScheme),
final: calculateTotal(groupSums, true, weightingScheme)
//
// Grading period set and effective due dates are optional, but must be used
// together.
//
// `gradingPeriodSet` is an object with at least the following shape:
// * gradingPeriods: [array of grading periods *see below]
// * weight: non-negative number
//
// Each grading period requires the following properties:
// * id: Canvas id
// * weight: non-negative number
//
// `effectiveDueDates` is an object with at least the following shape:
// {
// <assignment id (Canvas id)>: {
// grading_period_id: <grading period id (Canvas id)>
// }
// }
//
// `effectiveDueDates` should generally include an assignment id for most/all
// assignments in use for the course and student. The structure above is the
// "user-scoped" form of effective due dates, which includes only the
// necessary data to perform a grade calculation. Effective due date entries
// would otherwise include more information about a student's relationship
// with an assignment and related grading periods.
//
// Grades minimally have the following shape:
// {
// score: number|null
// possible: number|null
// }
//
// AssignmentGroup Grade maps have the following shape:
// {
// <assignment group id (Canvas id)>: <AssignmentGroup Grade Set *see below>
// }
//
// GradingPeriod Grade Sets have the following shape:
// {
// gradingPeriodId: <Canvas id>
// gradingPeriodWeight: number
// assignmentGroups: <AssignmentGroup Grade map>
// current: <AssignmentGroup Grade *see below>
// final: <AssignmentGroup Grade *see below>
// scoreUnit: 'points'|'percent'
// }
//
// GradingPeriod Grade maps have the following shape:
// {
// <grading period id (Canvas id)>: <GradingPeriod Grade Set *see above>
// }
//
// Each grading period will have a map for assignment group grades, keyed to
// the id of assignment groups graded within the grading period. Not every
// call to `calculate` will include grading period grades, as some courses do
// not use grading periods.
//
// An AssignmentGroup Grade Set is the returned result from the
// AssignmentGroupGradeCalculator.calculate function.
//
// Return value is a Course Grade Set.
// A Course Grade Set has the following shape:
// {
// assignmentGroups: <AssignmentGroup Grade map *see above>
// gradingPeriods: <GradingPeriod Grade map *see above>
// current: <AssignmentGroup Grade *see above>
// final: <AssignmentGroup Grade *see above>
// scoreUnit: 'points'|'percent'
// }
function calculate (submissions, assignmentGroups, weightingScheme, gradingPeriodSet, effectiveDueDates) {
const options = {
weightGradingPeriods: gradingPeriodSet && !!gradingPeriodSet.weighted,
weightAssignmentGroups: weightingScheme === 'percent'
};
};
if (gradingPeriodSet && effectiveDueDates) {
return calculateWithGradingPeriods(
submissions, assignmentGroups, gradingPeriodSet.gradingPeriods, effectiveDueDates, options
);
}
return calculateWithoutGradingPeriods(submissions, assignmentGroups, options);
}
return {
calculate

View File

@ -0,0 +1,17 @@
define([
'underscore'
], (_) => {
function scopeToUser (dueDateData, userId) {
const scopedData = {};
_.forEach(dueDateData, (dueDateDataByUserId, assignmentId) => {
if (dueDateDataByUserId[userId]) {
scopedData[assignmentId] = dueDateDataByUserId[userId];
}
});
return scopedData;
}
return {
scopeToUser
};
});

View File

@ -18,17 +18,18 @@
define([
'underscore'
], function (_) {
const scoreToGrade = function (score, gradingScheme) {
score = Math.max(score, 0);
const letter = _.find(gradingScheme, function (row, i) {
], (_) => {
function scoreToGrade (score, gradingScheme) {
const scoreWithLowerBound = Math.max(score, 0);
const letter = _.find(gradingScheme, (row, i) => {
const schemeScore = (row[1] * 100).toPrecision(4);
// The precision of the lower bound (* 100) must be limited to eliminate
// floating-point errors.
// e.g. 0.545 * 100 returns 54.50000000000001 in JavaScript.
return score >= (row[1] * 100).toPrecision(4) || i === (gradingScheme.length - 1);
return scoreWithLowerBound >= schemeScore || i === (gradingScheme.length - 1);
});
return letter[0];
};
}
return {
scoreToGrade

View File

@ -17,10 +17,10 @@ define([
return _.contains(assignment.assignment_visibility, student.id);
}
function cellMapForSubmission(assignment, student, gradingPeriodsEnabled, selectedGradingPeriodID, isAdmin) {
function cellMapForSubmission(assignment, student, hasGradingPeriods, selectedGradingPeriodID, isAdmin) {
if (!visibleToStudent(assignment, student)) {
return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NONE };
} else if (gradingPeriodsEnabled) {
} else if (hasGradingPeriods) {
return cellMappingsForMultipleGradingPeriods(assignment, student, selectedGradingPeriodID, isAdmin);
} else {
return { locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE };
@ -51,8 +51,8 @@ define([
}
class SubmissionState {
constructor({ gradingPeriodsEnabled, selectedGradingPeriodID, isAdmin }) {
this.gradingPeriodsEnabled = gradingPeriodsEnabled;
constructor({ hasGradingPeriods, selectedGradingPeriodID, isAdmin }) {
this.hasGradingPeriods = hasGradingPeriods;
this.selectedGradingPeriodID = selectedGradingPeriodID;
this.isAdmin = isAdmin;
this.submissionCellMap = {};
@ -74,7 +74,7 @@ define([
const params = [
assignment,
student,
this.gradingPeriodsEnabled,
this.hasGradingPeriods,
this.selectedGradingPeriodID,
this.isAdmin
];

View File

@ -17,10 +17,10 @@ define([
return _.contains(assignment.assignment_visibility, student.id);
}
function cellMapForSubmission(assignment, student, gradingPeriodsEnabled, selectedGradingPeriodID, isAdmin) {
function cellMapForSubmission(assignment, student, hasGradingPeriods, selectedGradingPeriodID, isAdmin) {
if (!visibleToStudent(assignment, student)) {
return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NONE };
} else if (gradingPeriodsEnabled) {
} else if (hasGradingPeriods) {
return cellMappingsForMultipleGradingPeriods(assignment, student, selectedGradingPeriodID, isAdmin);
} else {
return { locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE };
@ -51,8 +51,8 @@ define([
}
class SubmissionState {
constructor({ gradingPeriodsEnabled, selectedGradingPeriodID, isAdmin }) {
this.gradingPeriodsEnabled = gradingPeriodsEnabled;
constructor({ hasGradingPeriods, selectedGradingPeriodID, isAdmin }) {
this.hasGradingPeriods = hasGradingPeriods;
this.selectedGradingPeriodID = selectedGradingPeriodID;
this.isAdmin = isAdmin;
this.submissionCellMap = {};
@ -74,7 +74,7 @@ define([
const params = [
assignment,
student,
this.gradingPeriodsEnabled,
this.hasGradingPeriods,
this.selectedGradingPeriodID,
this.isAdmin
];

View File

@ -15,10 +15,12 @@ define([
period: Types.shape({
id: Types.string.isRequired,
title: Types.string.isRequired,
weight: Types.number,
startDate: Types.instanceOf(Date).isRequired,
endDate: Types.instanceOf(Date).isRequired,
closeDate: Types.instanceOf(Date).isRequired
}).isRequired,
weighted: Types.bool,
onEdit: Types.func.isRequired,
actionsDisabled: Types.bool,
readOnly: Types.bool.isRequired,
@ -91,13 +93,13 @@ define([
}
},
dateWithTimezone(date) {
const displayDatetime = DateHelper.formatDatetimeForDisplay(date);
if(ENV.CONTEXT_TIMEZONE === ENV.TIMEZONE) {
return displayDatetime;
} else {
return `${displayDatetime} ${tz.format(date, '%Z')}`;
renderWeight() {
if (this.props.weighted) {
return (
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-2">
<span ref="weight">{ I18n.t("Weight:") } { I18n.n(this.props.period.weight, {percentage: true}) }</span>
</div>
);
}
},
@ -105,18 +107,19 @@ define([
return (
<div className="GradingPeriodList__period">
<div className="GradingPeriodList__period__attributes grid-row">
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-3">
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-4">
<span ref="title">{this.props.period.title}</span>
</div>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-3">
<span ref="startDate">{I18n.t("Start Date:")} {this.dateWithTimezone(this.props.period.startDate)}</span>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-2">
<span ref="startDate">{I18n.t("Starts:")} {DateHelper.formatDateForDisplay(this.props.period.startDate)}</span>
</div>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-3">
<span ref="endDate">{I18n.t("End Date:")} {this.dateWithTimezone(this.props.period.endDate)}</span>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-2">
<span ref="endDate">{I18n.t("Ends:")} {DateHelper.formatDateForDisplay(this.props.period.endDate)}</span>
</div>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-3">
<span ref="closeDate">{I18n.t("Close Date:")} {this.dateWithTimezone(this.props.period.closeDate)}</span>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-2">
<span ref="closeDate">{I18n.t("Closes:")} {DateHelper.formatDateForDisplay(this.props.period.closeDate)}</span>
</div>
{this.renderWeight()}
</div>
<div className="GradingPeriodList__period__actions">
{this.renderEditButton()}

View File

@ -9,7 +9,6 @@ define([
class AccountTabContainer extends React.Component {
static propTypes = {
multipleGradingPeriodsEnabled: bool.isRequired,
readOnly: bool.isRequired,
urls: shape({
gradingPeriodSetsURL: string.isRequired,
@ -20,11 +19,10 @@ define([
}
componentDidMount () {
if (!this.props.multipleGradingPeriodsEnabled) return;
$(this.tabContainer).children('.ui-tabs-minimal').tabs();
}
renderSetsAndStandards () {
render () {
return (
<div ref={(el) => { this.tabContainer = el; }}>
<h1>{I18n.t('Grading')}</h1>
@ -52,22 +50,6 @@ define([
</div>
);
}
renderStandards () {
return (
<div ref={(el) => { this.gradingStandards = el; }}>
<h1>{I18n.t('Grading Schemes')}</h1>
<GradingStandardCollection />
</div>
);
}
render () {
if (this.props.multipleGradingPeriodsEnabled) {
return this.renderSetsAndStandards();
}
return this.renderStandards();
}
}
return AccountTabContainer;

View File

@ -8,11 +8,11 @@ define([
], (React, GradingStandardCollection, GradingPeriodCollection, $, I18n) => {
class CourseTabContainer extends React.Component {
static propTypes = {
multipleGradingPeriodsEnabled: React.PropTypes.bool.isRequired
hasGradingPeriods: React.PropTypes.bool.isRequired
}
componentDidMount () {
if (!this.props.multipleGradingPeriodsEnabled) return;
if (!this.props.hasGradingPeriods) return;
$(this.tabContainer).children('.ui-tabs-minimal').tabs();
}
@ -52,7 +52,7 @@ define([
}
render () {
if (this.props.multipleGradingPeriodsEnabled) {
if (this.props.hasGradingPeriods) {
return this.renderSetsAndStandards();
}
return this.renderStandards();

View File

@ -4,16 +4,18 @@ define([
'underscore',
'jquery',
'instructure-ui/Button',
'instructure-ui/Checkbox',
'i18n!grading_periods',
'jsx/grading/EnrollmentTermInput',
'compiled/jquery.rails_flash_notifications'
], function(React, ReactDOM, _, $, { default: Button }, I18n, EnrollmentTermInput) {
], function(React, ReactDOM, _, $, { default: Button }, { default: Checkbox }, I18n, EnrollmentTermInput) {
const { array, bool, func, shape, string } = React.PropTypes;
const buildSet = function(attr = {}) {
return {
id: attr.id,
title: attr.title || "",
weighted: attr.weighted || false,
enrollmentTermIDs: attr.enrollmentTermIDs || []
};
};
@ -30,6 +32,7 @@ define([
set: shape({
id: string,
title: string,
weighted: bool,
enrollmentTermIDs: array
}).isRequired,
enrollmentTerms: array.isRequired,
@ -53,15 +56,18 @@ define([
},
changeTitle(e) {
let set = _.clone(this.state.set);
set.title = e.target.value;
this.setState({ set: set });
const set = { ...this.state.set, title: e.target.value };
this.setState({ set });
},
changeWeighted(e) {
const set = { ...this.state.set, weighted: e.target.checked };
this.setState({ set });
},
changeEnrollmentTermIDs(termIDs) {
let set = _.clone(this.state.set);
set.enrollmentTermIDs = termIDs;
this.setState({ set: set });
const set = { ...this.state.set, enrollmentTermIDs: termIDs };
this.setState({ set });
},
triggerSave: function(e) {
@ -129,6 +135,16 @@ define([
enrollmentTerms = {this.props.enrollmentTerms}
selectedIDs = {this.state.set.enrollmentTermIDs}
setSelectedEnrollmentTermIDs = {this.changeEnrollmentTermIDs} />
<div className="ic-Input">
<Checkbox
ref={(ref) => { this.weightedCheckbox = ref }}
label={I18n.t('Weighted grading periods')}
value="weighted"
checked={this.state.set.weighted}
onChange={this.changeWeighted}
/>
</div>
</div>
</div>

View File

@ -6,63 +6,41 @@ define([
'instructure-ui/Button',
'i18n!external_tools',
'jsx/due_dates/DueDateCalendarPicker',
'jsx/shared/helpers/accessibleDateFormat'
'jsx/shared/helpers/accessibleDateFormat',
'jsx/shared/helpers/numberHelper',
'compiled/util/round'
], function(React, ReactDOM, update, _, { default: Button }, I18n,
DueDateCalendarPicker, accessibleDateFormat) {
DueDateCalendarPicker, accessibleDateFormat, numberHelper, round) {
const Types = React.PropTypes;
const buildPeriod = function(attr) {
function roundWeight (val) {
const value = numberHelper.parse(val);
return isNaN(value) ? null : round(value, 2);
};
function buildPeriod (attr) {
return {
id: attr.id,
title: attr.title,
weight: roundWeight(attr.weight),
startDate: attr.startDate,
endDate: attr.endDate,
closeDate: attr.closeDate
};
};
const hasDistinctCloseDate = ({ endDate, closeDate }) => {
return closeDate && !_.isEqual(endDate, closeDate);
};
const mergePeriod = (form, attr) => {
return update(form.state.period, {$merge: attr});
}
const changeTitle = function(e) {
let period = mergePeriod(this, {title: e.target.value});
this.setState({period: period});
};
const changeStartDate = function(date) {
let period = mergePeriod(this, {startDate: date});
this.setState({period: period});
};
const changeEndDate = function(date) {
let attr = {endDate: date};
if (!this.state.preserveCloseDate && !hasDistinctCloseDate(this.state.period)) {
attr.closeDate = date;
}
let period = mergePeriod(this, attr);
this.setState({period: period});
};
const changeCloseDate = function(date) {
let period = mergePeriod(this, {closeDate: date});
this.setState({period: period, preserveCloseDate: !!date});
};
let GradingPeriodForm = React.createClass({
propTypes: {
period: Types.shape({
id: Types.string.isRequired,
title: Types.string.isRequired,
weight: Types.number,
startDate: Types.instanceOf(Date).isRequired,
endDate: Types.instanceOf(Date).isRequired,
closeDate: Types.instanceOf(Date)
}),
weighted: Types.bool.isRequired,
disabled: Types.bool.isRequired,
onSave: Types.func.isRequired,
onCancel: Types.func.isRequired
@ -72,7 +50,7 @@ define([
let period = buildPeriod(this.props.period || {});
return {
period: period,
preserveCloseDate: hasDistinctCloseDate(period)
preserveCloseDate: this.hasDistinctCloseDate(period)
};
},
@ -93,6 +71,43 @@ define([
}
},
hasDistinctCloseDate: function ({ endDate, closeDate }) {
return closeDate && !_.isEqual(endDate, closeDate);
},
mergePeriod: function (attr) {
return update(this.state.period, {$merge: attr});
},
changeTitle: function (e) {
const period = this.mergePeriod({title: e.target.value});
this.setState({period});
},
changeWeight: function (e) {
const period = this.mergePeriod({weight: roundWeight(e.target.value)});
this.setState({period});
},
changeStartDate: function (date) {
const period = this.mergePeriod({startDate: date});
this.setState({period});
},
changeEndDate: function (date) {
let attr = {endDate: date};
if (!this.state.preserveCloseDate && !this.hasDistinctCloseDate(this.state.period)) {
attr.closeDate = date;
}
const period = this.mergePeriod(attr);
this.setState({period});
},
changeCloseDate: function (date) {
const period = this.mergePeriod({closeDate: date});
this.setState({period: period, preserveCloseDate: !!date});
},
hackTheDatepickers: function() {
// This can be replaced when we have an extensible datepicker
let $form = ReactDOM.findDOMNode(this);
@ -147,6 +162,28 @@ define([
);
},
renderWeightInput: function () {
if (!this.props.weighted) return null;
return (
<div className="ic-Form-control">
<label className="ic-Label" htmlFor="weight">
{I18n.t('Grading Period Weight')}
</label>
<div className="input-append">
<input
id="weight"
ref={(ref) => { this.weightInput = ref }}
type="text"
className="span1"
defaultValue={I18n.n(this.state.period.weight)}
onChange={this.changeWeight}
/>
<span className="add-on">%</span>
</div>
</div>
);
},
render: function() {
return (
<div className='GradingPeriodForm'>
@ -163,7 +200,7 @@ define([
className='ic-Input'
title={I18n.t('Grading Period Title')}
defaultValue={this.state.period.title}
onChange={changeTitle.bind(this)}
onChange={this.changeTitle}
type='text'
/>
</div>
@ -178,7 +215,7 @@ define([
dateValue = {this.state.period.startDate}
ref = "startDate"
dateType = "due_at"
handleUpdate = {changeStartDate.bind(this)}
handleUpdate = {this.changeStartDate}
rowKey = "start-date"
labelledBy = "start-date-label"
isFancyMidnight = {false}
@ -195,7 +232,7 @@ define([
dateValue = {this.state.period.endDate}
ref = "endDate"
dateType = "due_at"
handleUpdate = {changeEndDate.bind(this)}
handleUpdate = {this.changeEndDate}
rowKey = "end-date"
labelledBy = "end-date-label"
isFancyMidnight = {true}
@ -212,12 +249,14 @@ define([
dateValue = {this.state.period.closeDate}
ref = "closeDate"
dateType = "due_at"
handleUpdate = {changeCloseDate.bind(this)}
handleUpdate = {this.changeCloseDate}
rowKey = "close-date"
labelledBy = "close-date-label"
isFancyMidnight = {true}
/>
</div>
{this.renderWeightInput()}
</div>
</div>
</div>

View File

@ -33,11 +33,15 @@ define([
!isNaN(date.getTime());
};
const validatePeriods = function(periods) {
const validatePeriods = function(periods, weighted) {
if (_.any(periods, (period) => { return !(period.title || "").trim() })) {
return [I18n.t('All grading periods must have a title')];
}
if (weighted && _.any(periods, (period) => { return isNaN(period.weight) || period.weight < 0 })) {
return [I18n.t('All weights must be greater than or equal to 0')];
}
let validDates = _.all(periods, (period) => {
return isValidDate(period.startDate) &&
isValidDate(period.endDate) &&
@ -101,7 +105,8 @@ define([
set: shape({
id: string.isRequired,
title: string.isRequired
title: string.isRequired,
weighted: bool
}).isRequired,
urls: shape({
@ -121,6 +126,7 @@ define([
getInitialState() {
return {
title: this.props.set.title,
weighted: this.props.set.weighted || false,
gradingPeriods: sortPeriods(this.props.gradingPeriods),
newPeriod: {
period: null,
@ -199,7 +205,7 @@ define([
saveNewPeriod(period) {
let periods = this.state.gradingPeriods.concat([period]);
let validations = validatePeriods(periods);
let validations = validatePeriods(periods, this.state.weighted);
if (_.isEmpty(validations)) {
this.setNewPeriod({saving: true});
gradingPeriodsApi.batchUpdate(this.props.set.id, periods)
@ -236,7 +242,7 @@ define([
let periods = _.reject(this.state.gradingPeriods, function(_period) {
return period.id === _period.id;
}).concat([period]);
let validations = validatePeriods(periods);
let validations = validatePeriods(periods, this.state.weighted);
if (_.isEmpty(validations)) {
this.setEditPeriod({ saving: true });
gradingPeriodsApi.batchUpdate(this.props.set.id, periods)
@ -334,6 +340,7 @@ define([
className = 'GradingPeriodList__period--editing pad-box'>
<GradingPeriodForm ref = "editPeriodForm"
period = {period}
weighted = {this.state.weighted}
disabled = {this.state.editPeriod.saving}
onSave = {this.updatePeriod}
onCancel = {this.cancelEditPeriod} />
@ -344,6 +351,7 @@ define([
<GradingPeriod key={"show-grading-period-" + period.id}
ref={getShowGradingPeriodRef(period)}
period={period}
weighted={this.state.weighted}
actionsDisabled={actionsDisabled}
onEdit={this.editPeriod}
readOnly={this.props.readOnly}
@ -387,6 +395,7 @@ define([
<div className='GradingPeriodList__new-period--editing border border-rbl border-round-b pad-box'>
<GradingPeriodForm key = 'new-grading-period'
ref = 'newPeriodForm'
weighted = {this.state.weighted}
disabled = {this.state.newPeriod.saving}
onSave = {this.saveNewPeriod}
onCancel = {this.removeNewPeriodForm} />

View File

@ -3,11 +3,12 @@ define([
'underscore',
'jquery',
'instructure-ui/Button',
'instructure-ui/Checkbox',
'i18n!grading_periods',
'compiled/api/gradingPeriodSetsApi',
'jsx/grading/EnrollmentTermInput',
'compiled/jquery.rails_flash_notifications'
], function(React, _, $, { default: Button }, I18n, setsApi, EnrollmentTermInput) {
], function(React, _, $, { default: Button }, { default: Checkbox }, I18n, setsApi, EnrollmentTermInput) {
let NewGradingPeriodSetForm = React.createClass({
propTypes: {
@ -24,6 +25,7 @@ define([
return {
buttonsDisabled: false,
title: "",
weighted: false,
selectedEnrollmentTermIDs: []
};
},
@ -55,7 +57,7 @@ define([
event.preventDefault();
this.setState({ buttonsDisabled: true }, () => {
if(this.isValid()) {
let set = { title: this.state.title.trim() };
let set = { title: this.state.title.trim(), weighted: this.state.weighted };
set.enrollmentTermIDs = this.state.selectedEnrollmentTermIDs;
setsApi.create(set)
.then(this.submitSucceeded)
@ -80,6 +82,10 @@ define([
this.setState({ title: event.target.value });
},
onSetWeightedChange(event) {
this.setState({ weighted: event.target.checked });
},
render() {
return (
<div className="GradingPeriodSetForm pad-box">
@ -100,6 +106,15 @@ define([
selectedIDs = {this.state.selectedEnrollmentTermIDs}
setSelectedEnrollmentTermIDs = {this.setSelectedEnrollmentTermIDs}
/>
<div className="ic-Input">
<Checkbox
ref={(ref) => { this.weightedCheckbox = ref }}
label={I18n.t('Weighted grading periods')}
value="weighted"
checked={this.state.weighted}
onChange={this.onSetWeightedChange}
/>
</div>
</div>
</div>
<div className="grid-row">

View File

@ -40,7 +40,7 @@ define([
title: nextProps.title,
startDate: nextProps.startDate,
endDate: nextProps.endDate,
weight: nextProps.weight,
weight: nextProps.weight
});
},

View File

@ -25,8 +25,7 @@ function(React, update, GradingPeriod, $, I18n, _, ConvertCase) {
periods: null,
readOnly: false,
disabled: false,
saveDisabled: true,
canChangeGradingPeriodsSetting: false
saveDisabled: true
};
},
@ -41,7 +40,6 @@ function(React, update, GradingPeriod, $, I18n, _, ConvertCase) {
self.setState({
periods: self.deserializePeriods(periods),
readOnly: periods.grading_periods_read_only,
canChangeGradingPeriodsSetting: periods.can_toggle_grading_periods,
disabled: false,
saveDisabled: _.isEmpty(periods.grading_periods)
});
@ -200,22 +198,8 @@ function(React, update, GradingPeriod, $, I18n, _, ConvertCase) {
});
},
renderLinkToSettingsPage: function () {
if (this.state.canChangeGradingPeriodsSetting && this.state.periods && this.state.periods.length <= 1) {
return (
<span id='disable-feature-message' ref='linkToSettings'>
{I18n.t('You can disable this feature ')}
<a
href={ENV.CONTEXT_SETTINGS_URL + '#tab-features'}
aria-label={I18n.t('Feature Options')}
> {I18n.t('here.')} </a>
</span>);
}
},
renderSaveButton: function () {
if (periodsAreLoaded(this.state) && !this.state.readOnly && _.all(this.state.periods, period => period.permissions.update)) {
let buttonText = this.state.disabled ? I18n.t('Updating') : I18n.t('Save');
return (
<div className='form-actions'>
<button
@ -224,7 +208,7 @@ function(React, update, GradingPeriod, $, I18n, _, ConvertCase) {
disabled={this.state.disabled || this.state.saveDisabled}
onClick={this.batchUpdatePeriods}
>
{buttonText}
{this.state.disabled ? I18n.t('Updating') : I18n.t('Save')}
</button>
</div>
);
@ -257,9 +241,6 @@ function(React, update, GradingPeriod, $, I18n, _, ConvertCase) {
render: function () {
return (
<div>
<div id='messages'>
{this.renderLinkToSettingsPage()}
</div>
<div id='grading_periods' className='content-box'>
{this.renderGradingPeriods()}
</div>

View File

@ -1044,10 +1044,15 @@ class Course < ActiveRecord::Base
end
end
def recompute_student_scores(student_ids = nil, grading_period_id: nil)
def recompute_student_scores(student_ids = nil, grading_period_id: nil, update_all_grading_period_scores: true)
student_ids ||= self.student_ids
Rails.logger.info "GRADES: recomputing scores in course=#{global_id} students=#{student_ids.inspect}"
GradeCalculator.recompute_final_score(student_ids, self.id, grading_period_id: grading_period_id)
Enrollment.recompute_final_score(
student_ids,
self.id,
grading_period_id: grading_period_id,
update_all_grading_period_scores: update_all_grading_period_scores
)
end
handle_asynchronously_if_production :recompute_student_scores,
:singleton => proc { |c| "recompute_student_scores:#{ c.global_id }" }
@ -1666,7 +1671,6 @@ class Course < ActiveRecord::Base
update_all(:grade_publishing_status => 'error', :grade_publishing_message => "Timed out.")
end
def gradebook_to_csv_in_background(filename, user, options = {})
progress = progresses.build(tag: 'gradebook_to_csv')
progress.save!
@ -3092,6 +3096,18 @@ class Course < ActiveRecord::Base
effective_due_dates.any_in_closed_grading_period?
end
# Does this course have grading periods?
# checks for both legacy and account-level grading period groups
def grading_periods?
return @has_grading_periods unless @has_grading_periods.nil?
@has_grading_periods = shard.activate do
GradingPeriodGroup.active.
where("id = ? or course_id = ?", enrollment_term.grading_period_group_id, id).
exists?
end
end
def quiz_lti_tool
query = { tool_id: 'Quizzes 2' }
context_external_tools.active.find_by(query) ||

View File

@ -35,6 +35,7 @@ class EnrollmentTerm < ActiveRecord::Base
before_validation :verify_unique_sis_source_id
after_save :update_courses_later_if_necessary
after_save :recompute_course_scores, if: :grading_period_group_id_has_changed?
include StickySisFields
are_sis_sticky :name, :start_at, :end_at
@ -64,6 +65,12 @@ class EnrollmentTerm < ActiveRecord::Base
self.courses.touch_all
end
def recompute_course_scores(update_all_grading_period_scores: true)
courses.active.each do |course|
course.recompute_student_scores(update_all_grading_period_scores: update_all_grading_period_scores)
end
end
def update_courses_and_states_later(enrollment_type=nil)
return if new_record?
@ -186,4 +193,12 @@ class EnrollmentTerm < ActiveRecord::Base
scope :not_started, -> { where('enrollment_terms.start_at IS NOT NULL AND enrollment_terms.start_at > ?', Time.now.utc) }
scope :not_default, -> { where.not(name: EnrollmentTerm::DEFAULT_TERM_NAME)}
scope :by_name, -> { order(best_unicode_collation_key('name')) }
private
def grading_period_group_id_has_changed?
# migration 20111111214313_add_trust_link_for_default_account
# will throw an error without this check
respond_to?(:grading_period_group_id_changed?) && grading_period_group_id_changed?
end
end

View File

@ -53,11 +53,12 @@ class FeatureFlag < ActiveRecord::Base
def clear_cache
if self.context
self.class.connection.after_transaction_commit { MultiCache.delete(self.context.feature_flag_cache_key(feature)) }
self.context.touch if Feature.definitions[feature].touch_context
self.context.touch if Feature.definitions[feature].try(:touch_context)
end
end
private
private
def valid_state
errors.add(:state, "is not valid in context") unless %w(off on).include?(state) || context.is_a?(Account) && state == 'allowed'
end

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2015-2016 Instructure, Inc.
# Copyright (C) 2015 - 2016 Instructure, Inc.
#
# This file is part of Canvas.
#
@ -23,6 +23,7 @@ class GradingPeriod < ActiveRecord::Base
has_many :scores, -> { active }
validates :title, :start_date, :end_date, :close_date, :grading_period_group_id, presence: true
validates :weight, numericality: true, allow_nil: true
validate :start_date_is_before_end_date
validate :close_date_is_on_or_after_end_date
validate :not_overlapping, unless: :skip_not_overlapping_validator?
@ -30,7 +31,7 @@ class GradingPeriod < ActiveRecord::Base
before_validation :adjust_close_date_for_course_period
before_validation :ensure_close_date
after_save :recompute_scores, if: :dates_changed?
after_save :recompute_scores, if: :dates_or_weight_changed?
after_destroy :destroy_grading_period_set, if: :last_remaining_legacy_period?
after_destroy :destroy_scores
@ -151,7 +152,7 @@ class GradingPeriod < ActiveRecord::Base
def as_json_with_user_permissions(user)
as_json(
only: [:id, :title, :start_date, :end_date, :close_date],
only: [:id, :title, :start_date, :end_date, :close_date, :weight],
permissions: { user: user },
methods: [:is_last, :is_closed],
).fetch(:grading_period)
@ -230,13 +231,31 @@ class GradingPeriod < ActiveRecord::Base
courses = [grading_period_group.course]
else
term_ids = grading_period_group.enrollment_terms.pluck(:id)
courses = Course.where(enrollment_term_id: term_ids)
courses = Course.active.where(enrollment_term_id: term_ids)
end
courses.each do |course|
course.recompute_student_scores(
# different assignments could fall in this period if time
# boundaries changed so we need to recalculate scores.
# otherwise, weight must have changed, in which case we
# do not need to recompute the grading period scores (we
# only need to recompute the overall course score)
grading_period_id: time_boundaries_changed? ? id : nil,
update_all_grading_period_scores: false
)
end
# Course#recompute_student_scores is asynchronous
courses.each { |course| course.recompute_student_scores(grading_period_id: self.id) }
end
def dates_changed?
def weight_actually_changed?
grading_period_group.weighted && weight_changed?
end
def time_boundaries_changed?
start_date_changed? || end_date_changed?
end
def dates_or_weight_changed?
time_boundaries_changed? || weight_actually_changed?
end
end

View File

@ -26,31 +26,28 @@ class GradingPeriodGroup < ActiveRecord::Base
validate :associated_with_course_or_root_account, if: :active?
after_save :recompute_course_scores, if: :weighted_changed?
after_destroy :dissociate_enrollment_terms
set_policy do
given do |user|
multiple_grading_periods_enabled? &&
(course || root_account).grants_right?(user, :read)
end
can :read
given do |user|
root_account &&
multiple_grading_periods_enabled? &&
root_account.associated_user?(user)
end
can :read
given do |user|
multiple_grading_periods_enabled? &&
(course || root_account).grants_right?(user, :manage)
end
can :update and can :delete
given do |user|
root_account &&
multiple_grading_periods_enabled? &&
root_account.grants_right?(user, :manage)
end
can :create
@ -62,12 +59,14 @@ class GradingPeriodGroup < ActiveRecord::Base
root_account.grading_period_groups.active
end
def multiple_grading_periods_enabled?
multiple_grading_periods_on_course? || multiple_grading_periods_on_account?
end
private
def recompute_course_scores
return course.recompute_student_scores(update_all_grading_period_scores: false) if course_id.present?
enrollment_terms.each { |term| term.recompute_course_scores(update_all_grading_period_scores: false) }
end
def associated_with_course_or_root_account
if course_id.blank? && account_id.blank?
errors.add(:course_id, t("cannot be nil when account_id is nil"))
@ -84,17 +83,6 @@ class GradingPeriodGroup < ActiveRecord::Base
end
end
def multiple_grading_periods_on_account?
root_account.present? && (
root_account.feature_enabled?(:multiple_grading_periods) ||
root_account.feature_allowed?(:multiple_grading_periods)
)
end
def multiple_grading_periods_on_course?
course.present? && course.feature_enabled?(:multiple_grading_periods)
end
def dissociate_enrollment_terms
enrollment_terms.update_all(grading_period_group_id: nil)
end

View File

@ -715,6 +715,10 @@ class Group < ActiveRecord::Base
context.feature_enabled?(feature)
end
def grading_periods?
!!context.try(:grading_periods?)
end
def serialize_permissions(permissions_hash, user, session)
permissions_hash.merge(
create_discussion_topic: DiscussionTopic.context_allows_user_to_create?(self, user, session),

View File

@ -4,6 +4,7 @@ class GradingPeriodSetSerializer < Canvas::APISerializer
attributes :id,
:title,
:weighted,
:account_id,
:course_id,
:grading_periods,

View File

@ -54,7 +54,7 @@
<div id="print-grades-container" class="grid-row middle-xs between-xs">
<div class="col-xs-6" >
<% if multiple_grading_periods? %>
<% if grading_periods? %>
<%= render partial: "shared/grading_periods_selector", locals: {selected_period_id: @current_grading_period_id, grading_periods: @grading_periods, enrollment_id: @presenter.student_enrollment.id, all_grading_periods_option: true} %>
<% end %>
</div>

View File

@ -91,7 +91,7 @@
<div class="assignment-gradebook-container hidden">
<div id="gradebook-toolbar" class="toolbar">
<div class="gradebook_dropdowns">
<% if multiple_grading_periods? %>
<% if grading_periods? %>
<span class="multiple-grading-periods-selector-placeholder"></span>
<% end %>
</div>

View File

@ -70,7 +70,7 @@
<span data-component="GradebookMenu" data-variant="DefaultGradebook"></span>
<span data-component="ViewOptionsMenu"></span>
<span data-component="ActionMenu"></span>
<% if multiple_grading_periods? %>
<% if grading_periods? %>
<span class="multiple-grading-periods-selector-placeholder"></span>
<% end %>
</div>

View File

@ -11,7 +11,7 @@
<div class="header-bar">
<div class="header-bar-left ic-Form-control assignment-search">
<div class="ic-Multi-input">
{{#if ENV.MULTIPLE_GRADING_PERIODS_ENABLED}}
{{#if ENV.HAS_GRADING_PERIODS}}
<select class="ic-Input" id="grading_period_selector" aria-label="{{#t}}Select a Grading Period{{/t}}">
<option value="all">{{#t}}All Grading Periods{{/t}}</option>
{{#each ENV.active_grading_periods}}

View File

@ -0,0 +1,11 @@
class AddWeightedToGradingPeriodGroups < ActiveRecord::Migration[4.2]
tag :predeploy
def self.up
add_column :grading_period_groups, :weighted, :boolean
end
def self.down
remove_column :grading_period_groups, :weighted
end
end

View File

@ -0,0 +1,16 @@
class ClearAnyMultipleGradingPeriodsFeatureFlags < ActiveRecord::Migration[4.2]
tag :postdeploy
def self.up
DataFixup::ClearAnyMultipleGradingPeriodsFeatureFlags.send_later_if_production_enqueue_args(
:run,
priority: Delayed::LOWER_PRIORITY,
max_attempts: 1,
n_strand: "DataFixup::ClearAnyMultipleGradingPeriodsFeatureFlags:#{Shard.current.database_server.id}"
)
end
def self.down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -45,7 +45,8 @@ module Api::V1
@hash['course_format'] = @course.course_format if @course.course_format.present?
@hash['restrict_enrollments_to_course_dates'] = !!@course.restrict_enrollments_to_course_dates
if @includes.include?(:current_grading_period_scores)
@hash['multiple_grading_periods_enabled'] = @course.feature_enabled?(:multiple_grading_periods)
@hash['has_grading_periods'] = @course.grading_periods?
@hash['multiple_grading_periods_enabled'] = @hash['has_grading_periods'] # for backwards compatibility
end
clear_unneeded_fields(@hash)
end
@ -134,14 +135,14 @@ module Api::V1
end
def grading_period_scores(student_enrollments)
mgp_enabled = @course.feature_enabled?(:multiple_grading_periods)
totals_for_all_grading_periods_option = mgp_enabled &&
has_grading_periods = @course.grading_periods?
totals_for_all_grading_periods_option = has_grading_periods &&
@course.feature_enabled?(:all_grading_periods_totals)
current_period = mgp_enabled && GradingPeriod.current_period_for(@course)
if mgp_enabled && current_period
current_period = has_grading_periods && GradingPeriod.current_period_for(@course)
if has_grading_periods && current_period
calculated_grading_period_scores(student_enrollments, current_period, totals_for_all_grading_periods_option)
else
nil_grading_period_scores(student_enrollments, mgp_enabled, totals_for_all_grading_periods_option)
nil_grading_period_scores(student_enrollments, has_grading_periods, totals_for_all_grading_periods_option)
end
end
@ -157,7 +158,8 @@ module Api::V1
scores = {}
student_enrollments.each_with_index do |enrollment, index|
scores[enrollment.id] = current_period_scores[index].merge({
multiple_grading_periods_enabled: true,
has_grading_periods: true,
multiple_grading_periods_enabled: true, # for backwards compatibility
totals_for_all_grading_periods_option: totals_for_all_grading_periods_option,
current_grading_period_title: current_period.title,
current_grading_period_id: current_period.id
@ -167,11 +169,12 @@ module Api::V1
end
def nil_grading_period_scores(student_enrollments, mgp_enabled, totals_for_all_grading_periods_option)
def nil_grading_period_scores(student_enrollments, has_grading_periods, totals_for_all_grading_periods_option)
scores = {}
student_enrollments.each do |enrollment|
scores[enrollment.id] = {
multiple_grading_periods_enabled: mgp_enabled,
has_grading_periods: has_grading_periods,
multiple_grading_periods_enabled: has_grading_periods, # for backwards compatibility
totals_for_all_grading_periods_option: totals_for_all_grading_periods_option,
current_grading_period_title: nil,
current_grading_period_id: nil,

View File

@ -279,8 +279,7 @@ module Api::V1::User
return opts[:grading_period] if opts[:grading_period]
return nil unless opts[:current_grading_period_scores]
mgp_enabled = course.feature_enabled?(:multiple_grading_periods)
mgp_enabled ? GradingPeriod.current_period_for(course) : nil
GradingPeriod.current_period_for(course)
end
def grade_permissions?(user, enrollment)

View File

@ -0,0 +1,7 @@
module DataFixup
module ClearAnyMultipleGradingPeriodsFeatureFlags
def self.run
FeatureFlag.where(feature: 'multiple_grading_periods').destroy_all
end
end
end

View File

@ -50,7 +50,7 @@ class EffectiveDueDates
def any_in_closed_grading_period?
return @any_in_closed_grading_period unless @any_in_closed_grading_period.nil?
@any_in_closed_grading_period = grading_periods_enabled? &&
@any_in_closed_grading_period = @context.grading_periods? &&
to_hash.any? do |_, assignment_due_dates|
any_student_in_closed_grading_period?(assignment_due_dates)
end
@ -62,8 +62,8 @@ class EffectiveDueDates
assignment_id = assignment_id.id if assignment_id.is_a?(Assignment)
return false if assignment_id.nil?
# false if MGP isn't even enabled
return false unless grading_periods_enabled?
# false if there aren't even grading periods set up
return false unless @context.grading_periods?
# if we've already checked all assignments and it was false,
# no need to check this one specifically
return false if @any_in_closed_grading_period == false
@ -99,11 +99,6 @@ class EffectiveDueDates
true
end
def grading_periods_enabled?
return @grading_periods_enabled unless @grading_periods_enabled.nil?
@grading_periods_enabled = @context.feature_enabled?(:multiple_grading_periods)
end
def any_student_in_closed_grading_period?(assignment_due_dates)
return false unless assignment_due_dates
assignment_due_dates.any? { |_, student| student[:in_closed_grading_period] }
@ -114,11 +109,11 @@ class EffectiveDueDates
end
# This beauty of a method brings together assignment overrides,
# due dates, multiple grading periods, course/group enrollments,
# etc to calculate each student's effective due date and whether
# or not that due date is in a closed grading period. If a
# student is not included in this hash, that student cannot see
# this assignment. The format of the returned hash is:
# due dates, grading periods, course/group enrollments, etc
# to calculate each student's effective due date and whether or
# not that due date is in a closed grading period. If a student
# is not included in this hash, that student cannot see this
# assignment. The format of the returned hash is:
# {
# assignment_id => {
# student_id => {

View File

@ -243,17 +243,6 @@ END
applies_to: 'RootAccount',
state: 'hidden'
},
'multiple_grading_periods' =>
{
display_name: -> { I18n.t('features.multiple_grading_periods', 'Multiple Grading Periods') },
description: -> { I18n.t('enable_multiple_grading_periods', <<-END) },
Multiple Grading Periods allows teachers and admins to create grading periods with set
cutoff dates. Assignments can be filtered by these grading periods in the gradebook.
END
applies_to: 'Course',
state: 'allowed',
root_opt_in: true
},
'course_catalog' =>
{
display_name: -> { I18n.t("Public Course Index") },
@ -390,7 +379,7 @@ END
'all_grading_periods_totals' =>
{
display_name: -> { I18n.t('Display Totals for "All Grading Periods"') },
description: -> { I18n.t('Display total grades when the "All Grading Periods" dropdown option is selected (Multiple Grading Periods must be enabled).') },
description: -> { I18n.t('Display total grades when the "All Grading Periods" dropdown option is selected (grading periods must exist).') },
applies_to: 'Course',
state: 'allowed',
root_opt_in: true

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2011 - 2014 Instructure, Inc.
# Copyright (C) 2011 - 2017 Instructure, Inc.
#
# This file is part of Canvas.
#
@ -65,33 +65,18 @@ class GradeCalculator
def compute_scores
@submissions = @course.submissions.
except(:order, :select).
for_user(@user_ids).
where(assignment_id: @assignments).
select("submissions.id, user_id, assignment_id, score, excused, submissions.workflow_state")
submissions_by_user = @submissions.group_by(&:user_id)
result = []
except(:order, :select).
for_user(@user_ids).
where(assignment_id: @assignments).
select("submissions.id, user_id, assignment_id, score, excused, submissions.workflow_state")
scores_and_group_sums = []
@user_ids.each_slice(100) do |batched_ids|
load_assignment_visibilities_for_users(batched_ids)
batched_ids.each do |user_id|
user_submissions = submissions_by_user[user_id] || []
user_submissions.select!{|s| assignment_ids_visible_to_user(user_id).include?(s.assignment_id)}
current, current_groups = calculate_current_score(user_id, user_submissions)
final, final_groups = calculate_final_score(user_id, user_submissions)
scores = {
current: current,
current_groups: current_groups,
final: final,
final_groups: final_groups
}
result << scores
end
scores_and_group_sums_batch = compute_scores_and_group_sums_for_batch(batched_ids)
scores_and_group_sums.concat(scores_and_group_sums_batch)
clear_assignment_visibilities_cache
end
result
scores_and_group_sums
end
def compute_and_save_scores
@ -103,8 +88,114 @@ class GradeCalculator
private
def compute_scores_and_group_sums_for_batch(user_ids)
user_ids.map do |user_id|
group_sums = compute_group_sums_for_user(user_id)
scores = compute_scores_for_user(user_id, group_sums)
update_changes_hash_for_user(user_id, scores)
{
current: scores[:current],
current_groups: group_sums[:current].index_by { |group| group[:id] },
final: scores[:final],
final_groups: group_sums[:final].index_by { |group| group[:id] }
}
end
end
def compute_group_sums_for_user(user_id)
user_submissions = submissions_by_user.fetch(user_id, []).select do |submission|
assignment_ids_visible_to_user(user_id).include?(submission.assignment_id)
end
{
current: create_group_sums(user_submissions, user_id, ignore_ungraded: true),
final: create_group_sums(user_submissions, user_id, ignore_ungraded: false)
}
end
def compute_scores_for_user(user_id, group_sums)
if compute_course_scores_from_weighted_grading_periods?
scores = calculate_total_from_weighted_grading_periods(user_id)
else
scores = {
current: calculate_total_from_group_scores(group_sums[:current]),
final: calculate_total_from_group_scores(group_sums[:final])
}
end
Rails.logger.info "GRADES: calculated: #{scores.inspect}"
scores
end
def update_changes_hash_for_user(user_id, scores)
@current_updates[user_id] = scores[:current][:grade]
@final_updates[user_id] = scores[:final][:grade]
end
def calculate_total_from_weighted_grading_periods(user_id)
enrollment = enrollments_by_user[user_id].first
grading_period_ids = grading_periods_for_course.map(&:id)
# using Enumberable#select because the scores are preloaded
grading_period_scores = enrollment.scores.select do |score|
grading_period_ids.include?(score.grading_period_id)
end
scores = apply_grading_period_weights_to_scores(grading_period_scores)
scale_and_round_scores(scores, grading_period_scores)
end
def apply_grading_period_weights_to_scores(grading_period_scores)
grading_period_scores.each_with_object(
{ current: { full_weight: 0.0, grade: 0.0 }, final: { full_weight: 0.0, grade: 0.0 } }
) do |score, scores|
weight = grading_period_weights[score.grading_period_id] || 0.0
scores[:final][:full_weight] += weight
scores[:current][:full_weight] += weight if score.current_score
scores[:current][:grade] += (score.current_score || 0.0) * (weight / 100.0)
scores[:final][:grade] += (score.final_score || 0.0) * (weight / 100.0)
end
end
def scale_and_round_scores(scores, grading_period_scores)
[:current, :final].each_with_object({ current: {}, final: {} }) do |score_type, adjusted_scores|
score = scores[score_type][:grade]
full_weight = scores[score_type][:full_weight]
score = scale_score_up(score, full_weight) if full_weight < 100
if score == 0.0 && score_type == :current && grading_period_scores.none?(&:current_score)
score = nil
end
adjusted_scores[score_type][:grade] = score ? score.round(2) : score
end
end
def scale_score_up(score, weight)
return 0.0 if weight.zero?
(score * 100.0) / weight
end
def compute_course_scores_from_weighted_grading_periods?
return @compute_from_weighted_periods if @compute_from_weighted_periods.present?
if @grading_period || grading_periods_for_course.empty?
@compute_from_weighted_periods = false
else
@compute_from_weighted_periods = grading_periods_for_course.first.grading_period_group.weighted?
end
end
def grading_periods_for_course
@periods ||= GradingPeriod.for(@course)
end
def grading_period_weights
@grading_period_weights ||= grading_periods_for_course.each_with_object({}) do |period, weights|
weights[period.id] = period.weight
end
end
def submissions_by_user
@submissions_by_user ||= @submissions.group_by(&:user_id)
end
def calculate_grading_period_scores
GradingPeriod.for(@course).each do |grading_period|
grading_periods_for_course.each do |grading_period|
# update this grading period score, and do not
# update any other scores (grading period or course)
# after this one
@ -132,7 +223,7 @@ class GradeCalculator
def enrollments
@enrollments ||= Enrollment.shard(@course).active.
where(user_id: @user_ids, course_id: @course.id).
select(:id, :user_id)
select(:id, :user_id).preload(:scores)
end
def joined_enrollment_ids
@ -225,27 +316,6 @@ class GradeCalculator
end
end
# The score ignoring unsubmitted assignments
def calculate_current_score(user_id, submissions)
calculate_score(submissions, user_id, true)
end
# The final score for the class, so unsubmitted assignments count as zeros
def calculate_final_score(user_id, submissions)
calculate_score(submissions, user_id, false)
end
def calculate_score(submissions, user_id, ignore_ungraded)
group_sums = create_group_sums(submissions, user_id, ignore_ungraded)
info = calculate_total_from_group_scores(group_sums)
Rails.logger.info "GRADES: calculated: #{info.inspect}"
updates_hash = ignore_ungraded ? @current_updates : @final_updates
updates_hash[user_id] = info[:grade]
[info, group_sums.index_by { |s| s[:id] }]
end
# returns information about assignments groups in the form:
# [
# {
@ -256,7 +326,7 @@ class GradeCalculator
# :weight => 50},
# ...]
# each group
def create_group_sums(submissions, user_id, ignore_ungraded=true)
def create_group_sums(submissions, user_id, ignore_ungraded: true)
visible_assignments = @assignments
visible_assignments = visible_assignments.select{|a| assignment_ids_visible_to_user(user_id).include?(a.id)}

View File

@ -17,7 +17,6 @@
#
class GradebookExporter
include GradebookTransformer
include GradebookSettingsHelpers
def initialize(course, user, options = {})
@ -29,7 +28,7 @@ class GradebookExporter
def to_csv
enrollment_scope = @course.apply_enrollment_visibility(gradebook_enrollment_scope, @user, nil,
include: gradebook_includes)
student_enrollments = enrollments_for_csv(enrollment_scope, @options)
student_enrollments = enrollments_for_csv(enrollment_scope)
student_section_names = {}
student_enrollments.each do |enrollment|
@ -53,7 +52,7 @@ class GradebookExporter
submissions = {}
calc.submissions.each { |s| submissions[[s.user_id, s.assignment_id]] = s }
assignments = select_in_grading_period calc.assignments, @course, grading_period
assignments = select_in_grading_period calc.assignments, grading_period
assignments = assignments.sort_by do |a|
[a.assignment_group_id, a.position || 0, a.due_at || CanvasSort::Last, a.title]
@ -168,7 +167,8 @@ class GradebookExporter
end
private
def enrollments_for_csv(scope, options={})
def enrollments_for_csv(scope)
# user: used for name in csv output
# course_section: used for display_name in csv output
# user > pseudonyms: used for sis_user_id/unique_id if options[:include_sis_id]
@ -208,7 +208,7 @@ class GradebookExporter
end
def show_totals?
return true if !@course.feature_enabled?(:multiple_grading_periods)
return true unless @course.grading_periods?
return true if @options[:grading_period_id].try(:to_i) != 0
@course.feature_enabled?(:all_grading_periods_totals)
end
@ -223,4 +223,12 @@ class GradebookExporter
name = "=\"#{name}\"" if name =~ STARTS_WITH_EQUAL
name
end
def select_in_grading_period(assignments, grading_period)
if grading_period
grading_period.assignments(assignments)
else
assignments
end
end
end

View File

@ -19,8 +19,6 @@
require 'csv'
class GradebookImporter
include GradebookTransformer
ASSIGNMENT_PRELOADED_FIELDS = %i/
id title points_possible grading_type updated_at context_id context_type group_category_id
created_at due_at only_visible_to_overrides
@ -321,7 +319,7 @@ class GradebookImporter
end
def prevent_new_assignment_creation?(periods, is_admin)
return false unless context.feature_enabled?(:multiple_grading_periods)
return false unless context.grading_periods?
return false if is_admin
GradingPeriod.date_in_closed_grading_period?(
@ -502,7 +500,7 @@ class GradebookImporter
def gradeable?(submission:, periods:, is_admin:)
user_can_grade_submission = submission.grants_right?(@user, :grade)
return user_can_grade_submission unless @context.feature_enabled?(:multiple_grading_periods)
return user_can_grade_submission unless @context.grading_periods?
user_can_grade_submission &&
(is_admin || !GradingPeriod.date_in_closed_grading_period?(

View File

@ -1,28 +0,0 @@
#
# Copyright (C) 2015 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module GradebookTransformer
private
def select_in_grading_period(assignments, course, grading_period)
if grading_period && course.feature_enabled?(:multiple_grading_periods)
grading_period.assignments(assignments)
else
assignments
end
end
end

View File

@ -2,7 +2,7 @@ module SubmittablesGradingPeriodProtection
def grading_periods_allow_submittable_create?(submittable, submittable_params, flash_message: false)
apply_grading_params(submittable, submittable_params)
return true unless submittable.graded?
return true unless context_grading_periods_enabled? && !current_user_is_context_admin?
return true unless grading_periods? && !current_user_is_context_admin?
return true if submittable_params[:only_visible_to_overrides]
submittable.due_at = submittable_params[:due_at]
@ -19,7 +19,7 @@ module SubmittablesGradingPeriodProtection
submittable_params[:only_visible_to_overrides] if submittable_params.key?(:only_visible_to_overrides)
submittable.due_at = submittable_params[:due_at] if submittable_params.key?(:due_at)
return true unless submittable.only_visible_to_overrides_changed? || due_at_changed?(submittable)
return true unless context_grading_periods_enabled? && !current_user_is_context_admin?
return true unless grading_periods? && !current_user_is_context_admin?
in_closed_grading_period = date_in_closed_grading_period?(submittable.due_at_was)
@ -42,7 +42,7 @@ module SubmittablesGradingPeriodProtection
end
def grading_periods_allow_assignment_overrides_batch_create?(submittable, overrides, flash_message: false)
return true unless context_grading_periods_enabled? && !current_user_is_context_admin?
return true unless grading_periods? && !current_user_is_context_admin?
return true unless overrides.any? {|override| date_in_closed_grading_period?(override[:due_at])}
apply_error(submittable, :due_at, ERROR_MESSAGES[:set_override_due_at_in_closed], flash_message)
@ -50,7 +50,7 @@ module SubmittablesGradingPeriodProtection
end
def grading_periods_allow_assignment_overrides_batch_update?(submittable, prepared_batch, flash_message: false)
return true unless context_grading_periods_enabled? && !current_user_is_context_admin?
return true unless grading_periods? && !current_user_is_context_admin?
can_create_overrides?(submittable, prepared_batch[:overrides_to_create], flash_message: flash_message) &&
can_update_overrides?(submittable, prepared_batch[:overrides_to_update], flash_message: flash_message) &&
can_delete_overrides?(submittable, prepared_batch[:overrides_to_delete], flash_message: flash_message)

View File

@ -22,7 +22,9 @@ define([
'jquery' /* $ */,
'underscore',
'jsx/gradebook/CourseGradeCalculator',
'jsx/gradebook/EffectiveDueDates',
'jsx/gradebook/GradingSchemeHelper',
'compiled/api/gradingPeriodSetsApi',
'compiled/util/round',
'str/htmlEscape',
'jquery.ajaxJSON' /* ajaxJSON */,
@ -31,11 +33,58 @@ define([
'jquery.instructure_misc_plugins' /* showIf */,
'jquery.templateData' /* fillTemplateData, getTemplateData */,
'compiled/jquery/mediaCommentThumbnail', /* mediaCommentThumbnail */
'media_comments' /* mediaComment */
], function (INST, I18n, $, _, CourseGradeCalculator, GradingSchemeHelper, round, htmlEscape) {
'media_comments' /* mediaComment, mediaCommentThumbnail */
], function (
INST, I18n, $, _, CourseGradeCalculator, EffectiveDueDates, GradingSchemeHelper, gradingPeriodSetsApi, round,
htmlEscape
) {
/* eslint-disable vars-on-top */
/* eslint-disable newline-per-chained-call */
var GradeSummary = {
getGradingPeriodIdFromUrl (url) {
var matches = url.match(/grading_period_id=(\d*)/);
if (matches && matches[1] !== '0') {
return matches[1];
}
return null;
}
};
function getGradingPeriodSet () {
if (ENV.grading_period_set) {
return gradingPeriodSetsApi.deserializeSet(ENV.grading_period_set);
}
return null;
}
function calculateGrades () {
var grades;
if (ENV.effective_due_dates && ENV.grading_period_set) {
grades = CourseGradeCalculator.calculate(
ENV.submissions,
ENV.assignment_groups,
ENV.group_weighting_scheme,
getGradingPeriodSet(),
EffectiveDueDates.scopeToUser(ENV.effective_due_dates, ENV.student_id)
);
} else {
grades = CourseGradeCalculator.calculate(
ENV.submissions,
ENV.assignment_groups,
ENV.group_weighting_scheme
);
}
var selectedGradingPeriodId = GradeSummary.getGradingPeriodIdFromUrl(location.href);
if (selectedGradingPeriodId) {
return grades.gradingPeriods[selectedGradingPeriodId];
}
return grades;
}
var whatIfAssignments = [];
function addWhatIfAssignment (assignmentId) {
@ -55,14 +104,6 @@ define([
});
}
function calculateGrades () {
return CourseGradeCalculator.calculate(
ENV.submissions,
listAssignmentGroupsForGradeCalculation(),
ENV.group_weighting_scheme
);
}
function canBeConvertedToGrade (score, possible) {
return possible > 0 && !isNaN(score);
}
@ -86,15 +127,20 @@ define([
function calculateTotals (calculatedGrades, currentOrFinal, groupWeightingScheme) {
var showTotalGradeAsPoints = ENV.show_total_grade_as_points;
for (var i = 0; i < calculatedGrades.group_sums.length; i++) {
var groupSum = calculatedGrades.group_sums[i];
var $groupRow = $('#submission_group-' + groupSum.group.id);
var groupGradeInfo = groupSum[currentOrFinal];
for (var i = 0; i < ENV.assignment_groups.length; i++) {
var assignmentGroupId = ENV.assignment_groups[i].id;
var grade = calculatedGrades.assignmentGroups[assignmentGroupId];
var $groupRow = $('#submission_group-' + assignmentGroupId);
if (grade) {
grade = grade[currentOrFinal];
} else {
grade = { score: 0, possible: 0 };
}
$groupRow.find('.grade').text(
calculateGrade(groupGradeInfo.score, groupGradeInfo.possible)
calculateGrade(grade.score, grade.possible)
);
$groupRow.find('.score_teaser').text(
I18n.n(groupGradeInfo.score, {precision: round.DEFAULT}) + ' / ' + I18n.n(groupGradeInfo.possible, {precision: round.DEFAULT})
I18n.n(grade.score, {precision: round.DEFAULT}) + ' / ' + I18n.n(grade.possible, {precision: round.DEFAULT})
);
}
@ -152,11 +198,13 @@ define([
// mark dropped assignments
$('.student_assignment').find('.points_possible').attr('aria-label', '');
_.chain(calculatedGrades.group_sums).map(function (groupSum) {
return groupSum[currentOrFinal].submissions;
}).flatten().each(function (s) {
$('#submission_' + s.submission.assignment_id).toggleClass('dropped', !!s.drop);
_.forEach(calculatedGrades.assignmentGroups, function (grades) {
_.forEach(grades[currentOrFinal].submissions, function (submission) {
$('#submission_' + submission.submission.assignment_id).toggleClass('dropped', !!submission.drop);
});
});
$('.dropped').attr('aria-label', droppedMessage);
$('.dropped').attr('title', droppedMessage);
@ -495,10 +543,11 @@ define([
});
}
return {
_.extend(GradeSummary, {
setup: setup,
addWhatIfAssignment: addWhatIfAssignment,
removeWhatIfAssignment: removeWhatIfAssignment,
getGradingPeriodSet: getGradingPeriodSet,
listAssignmentGroupsForGradeCalculation: listAssignmentGroupsForGradeCalculation,
canBeConvertedToGrade: canBeConvertedToGrade,
calculateGrade: calculateGrade,
@ -506,5 +555,7 @@ define([
calculateTotals: calculateTotals,
calculatePercentGrade: calculatePercentGrade,
formatPercentGrade: formatPercentGrade
}
});
return GradeSummary;
});

View File

@ -63,8 +63,7 @@ module AssignmentGroupsApiSpecHelper
)
end
def setup_multiple_grading_periods
@course.account.enable_feature!(:multiple_grading_periods)
def setup_grading_periods
setup_groups
@group1_assignment_today = @course.assignments.create!(assignment_group: @group1, due_at: Time.zone.now)
@group1_assignment_future = @course.assignments.create!(assignment_group: @group1, due_at: 3.months.from_now)
@ -297,9 +296,9 @@ describe AssignmentGroupsController, type: :request do
end
end
context "multiple grading periods" do
context "grading periods" do
before :once do
setup_multiple_grading_periods
setup_grading_periods
end
describe "#index" do
@ -613,7 +612,7 @@ describe AssignmentGroupsApiController, type: :request do
end
it 'should only return assignments in the given grading period with MGP on' do
setup_multiple_grading_periods
setup_grading_periods
json = api_call(:get, "/api/v1/courses/#{@course.id}/assignment_groups/#{@group1.id}?include[]=assignments&grading_period_id=#{@gp_future.id}",
controller: 'assignment_groups_api',
@ -627,10 +626,10 @@ describe AssignmentGroupsApiController, type: :request do
expect(json['assignments'].length).to eq 1
end
it 'should not return an error when Multiple Grading Periods is turned on and no grading_period_id is passed in' do
setup_multiple_grading_periods
it 'should not return an error when there are grading periods and no grading_period_id is passed in' do
setup_grading_periods
json = api_call(:get, "/api/v1/courses/#{@course.id}/assignment_groups/#{@group1.id}?include[]=assignments",
api_call(:get, "/api/v1/courses/#{@course.id}/assignment_groups/#{@group1.id}?include[]=assignments",
controller: 'assignment_groups_api',
action: 'show',
format: 'json',
@ -898,7 +897,6 @@ describe AssignmentGroupsApiController, type: :request do
end
before :once do
@course.root_account.enable_feature!(:multiple_grading_periods)
@grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
@grading_period_group.enrollment_terms << @course.enrollment_term
Factories::GradingPeriodHelper.new.create_for_group(@grading_period_group, {
@ -948,12 +946,6 @@ describe AssignmentGroupsApiController, type: :request do
expect(@assignment_group.reload.rules).to eq(rules_encoded)
end
it "succeeds when multiple grading periods is disabled" do
@course.root_account.disable_feature!(:multiple_grading_periods)
call_update.call({ group_weight: 75 }, 200)
expect(@assignment_group.reload.group_weight).to eq(75)
end
it "ignores deleted assignments" do
@assignment.destroy
call_update.call({ group_weight: 75 }, 200)

View File

@ -1623,7 +1623,7 @@ describe AssignmentsApiController, type: :request do
end
end
context "with multiple grading periods enabled" do
context "with grading periods" do
def call_create(params, expected_status)
api_call_as_user(
@current_user,
@ -1645,7 +1645,6 @@ describe AssignmentsApiController, type: :request do
end
before :once do
@course.root_account.enable_feature!(:multiple_grading_periods)
grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = grading_period_group
@ -2508,7 +2507,7 @@ describe AssignmentsApiController, type: :request do
end
end
context "with multiple grading periods enabled" do
context "with grading periods" do
def create_assignment(attr)
@course.assignments.create!({ name: "Example Assignment", submission_types: "points" }.merge(attr))
end
@ -2541,7 +2540,6 @@ describe AssignmentsApiController, type: :request do
end
before :once do
@course.root_account.enable_feature!(:multiple_grading_periods)
grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = grading_period_group

View File

@ -945,7 +945,6 @@ describe CoursesController, type: :request do
context "when an assignment is due in a closed grading period" do
before(:once) do
@course.root_account.enable_feature!(:multiple_grading_periods)
@course.update_attributes(group_weighting_scheme: "equal")
@grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
@ -1102,10 +1101,6 @@ describe CoursesController, type: :request do
})
end
before :each do
@course.root_account.enable_feature!(:multiple_grading_periods)
end
it "cannot change apply_assignment_group_weights with a term change" do
@term.grading_period_group = @grading_period_group
@term.save!
@ -1146,14 +1141,6 @@ describe CoursesController, type: :request do
expect(@course.group_weighting_scheme).to eql("equal")
end
it "succeeds when multiple grading periods is disabled" do
@course.root_account.disable_feature!(:multiple_grading_periods)
raw_api_call(:put, @path, @params, @new_values)
expect(response.code).to eql '200'
@course.reload
expect(@course.group_weighting_scheme).to eql("percent")
end
it "succeeds when apply_assignment_group_weights is not changed" do
@new_values['course']['apply_assignment_group_weights'] = false
raw_api_call(:put, @path, @params, @new_values)
@ -1636,6 +1623,7 @@ describe CoursesController, type: :request do
context "include current grading period scores" do
let(:grading_period_keys) do
[ 'multiple_grading_periods_enabled',
'has_grading_periods',
'totals_for_all_grading_periods_option',
'current_period_computed_current_score',
'current_period_computed_final_score',
@ -1673,33 +1661,29 @@ describe CoursesController, type: :request do
@course2.save
json_response = courses_api_index_call(includes: ['total_scores', 'current_grading_period_scores'])
enrollment_json = enrollment(json_response)
expect(enrollment_json).to_not include(*grading_period_keys)
expect(enrollment_json).not_to include(*grading_period_keys)
end
it "returns true for 'multiple_grading_periods_enabled' on the enrollment " \
"JSON if the course has Multiple Grading Periods enabled" do
it "returns true for 'has_grading_periods' on the enrollment " \
"JSON if the course has grading periods" do
json_response = courses_api_index_call(includes: ['total_scores', 'current_grading_period_scores'])
enrollment_json = enrollment(json_response)
expect(enrollment_json['multiple_grading_periods_enabled']).to eq(true)
expect(enrollment_json['has_grading_periods']).to be true
expect(enrollment_json['multiple_grading_periods_enabled']).to be true
end
it "returns false for 'multiple_grading_periods_enabled' if the course has Multiple Grading Periods disabled" do
@course2.root_account.disable_feature!(:multiple_grading_periods)
json_response = courses_api_index_call(includes: ['total_scores', 'current_grading_period_scores'])
enrollment_json = enrollment(json_response)
expect(enrollment_json['multiple_grading_periods_enabled']).to eq(false)
end
it "returns a 'multiple_grading_periods_enabled' key at the course-level " \
it "returns a 'has_grading_periods' key at the course-level " \
"on the JSON response if 'current_grading_period_scores' are requested" do
course_json_response = courses_api_index_call(includes: ['total_scores', 'current_grading_period_scores']).first
expect(course_json_response).to have_key 'has_grading_periods'
expect(course_json_response).to have_key 'multiple_grading_periods_enabled'
end
it "does not return a 'multiple_grading_periods_enabled' key at the course-level " \
it "does not return a 'has_grading_periods' key at the course-level " \
"on the JSON response if 'current_grading_period_scores' are not requested" do
course_json_response = courses_api_index_call.first
expect(course_json_response).to_not have_key 'multiple_grading_periods_enabled'
expect(course_json_response).not_to have_key 'has_grading_periods'
expect(course_json_response).not_to have_key 'multiple_grading_periods_enabled'
end
context "computed scores" do
@ -3487,7 +3471,6 @@ describe CoursesController, type: :request do
let_once(:account) { Account.default }
let_once(:test_course) { account.courses.create! }
let_once(:grading_period) do
account.enable_feature!(:multiple_grading_periods)
group = account.grading_period_groups.create!(title: "Score Test Group")
group.enrollment_terms << test_course.enrollment_term
Factories::GradingPeriodHelper.new.create_presets_for_group(group, :current)

View File

@ -774,79 +774,73 @@ describe EnrollmentsApiController, type: :request do
)
end
context "multiple grading periods feature flag enabled" do
before :once do
@course.root_account.enable_feature!(:multiple_grading_periods)
describe "user endpoint" do
let!(:enroll_student_in_the_course) do
student_in_course({course: @course, user: @user})
end
describe "user endpoint" do
let!(:enroll_student_in_the_course) do
student_in_course({course: @course, user: @user})
it "works for users" do
@user_params[:grading_period_id] = @first_grading_period.id
raw_api_call(:get, @user_path, @user_params)
expect(response).to be_ok
end
it "returns an error if the user is not in the grading period" do
course = Course.create!
grading_period_group = group_helper.legacy_create_for_course(course)
grading_period = grading_period_group.grading_periods.create!(
title: "unconnected to the user's course",
start_date: 2.months.ago,
end_date: 2.months.from_now(now)
)
@user_params[:grading_period_id] = grading_period.id
raw_api_call(:get, @user_path, @user_params)
expect(response).not_to be_ok
end
describe "grade summary" do
let!(:grade_assignments) do
first = @course.assignments.create! due_at: 1.month.ago
last = @course.assignments.create! due_at: 1.month.from_now
no_due_at = @course.assignments.create!
Timecop.freeze(@first_grading_period.end_date - 1.day) do
first.grade_student @user, grade: 7, grader: @teacher
end
last.grade_student @user, grade: 10, grader: @teacher
no_due_at.grade_student @user, grade: 1, grader: @teacher
end
it "works for users" do
@user_params[:grading_period_id] = @first_grading_period.id
raw_api_call(:get, @user_path, @user_params)
expect(response).to be_ok
end
describe "provides a grade summary" do
it "returns an error if the user is not in the grading period" do
course = Course.create!
grading_period_group = group_helper.legacy_create_for_course(course)
grading_period = grading_period_group.grading_periods.create!(
title: "unconnected to the user's course",
start_date: 2.months.ago,
end_date: 2.months.from_now(now)
)
it "for assignments due during the first grading period." do
@user_params[:grading_period_id] = @first_grading_period.id
@user_params[:grading_period_id] = grading_period.id
raw_api_call(:get, @user_path, @user_params)
expect(response).to_not be_ok
end
describe "grade summary" do
let!(:grade_assignments) do
first = @course.assignments.create! due_at: 1.month.ago
last = @course.assignments.create! due_at: 1.month.from_now
no_due_at = @course.assignments.create!
Timecop.freeze(@first_grading_period.end_date - 1.day) do
first.grade_student @user, grade: 7, grader: @teacher
end
last.grade_student @user, grade: 10, grader: @teacher
no_due_at.grade_student @user, grade: 1, grader: @teacher
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
# ten times assignment's grade of 7
expect(final_score).to eq 70
end
describe "provides a grade summary" do
it "for assignments due during the last grading period." do
@user_params[:grading_period_id] = @last_grading_period.id
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
it "for assignments due during the first grading period." do
@user_params[:grading_period_id] = @first_grading_period.id
# ((10 + 1) / 1) * 10 => 110
# ((last + no_due_at) / number_of_grading_periods) * 10
expect(final_score).to eq 110
end
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
# ten times assignment's grade of 7
expect(final_score).to eq 70
end
it "for all assignments when no grading period is specified." do
@user_params[:grading_period_id] = nil
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
it "for assignments due during the last grading period." do
@user_params[:grading_period_id] = @last_grading_period.id
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
# ((10 + 1) / 1) * 10 => 110
# ((last + no_due_at) / number_of_grading_periods) * 10
expect(final_score).to eq 110
end
it "for all assignments when no grading period is specified." do
@user_params[:grading_period_id] = nil
raw_api_call(:get, @user_path, @user_params)
final_score = JSON.parse(response.body).first["grades"]["final_score"]
# ((7 + 10 + 1) / 2) * 10 => 60
# ((first + last + no_due_at) / number_of_grading_periods) * 10
expect(final_score).to eq 90
end
# ((7 + 10 + 1) / 2) * 10 => 60
# ((first + last + no_due_at) / number_of_grading_periods) * 10
expect(final_score).to eq 90
end
end
end
@ -878,15 +872,6 @@ describe EnrollmentsApiController, type: :request do
expect(student_grade.(json)).to eq 0
end
end
context "multiple grading periods feature flag disabled" do
it "should return an error message if the multiple grading periods flag is disabled" do
@user_params[:grading_period_id] = @first_grading_period.id
json = api_call(:get, @user_path, @user_params, {}, {}, { expected_status: 403 })
expect(json['message']).to eq 'Multiple Grading Periods feature is disabled. Cannot filter by grading_period_id with this feature disabled'
end
end
end
context "an account admin" do

View File

@ -24,9 +24,7 @@ describe GradingPeriodsController, type: :request do
context "A grading period is associated with a course." do
before :once do
course_with_teacher active_all: true
Account.default.set_feature_flag! :multiple_grading_periods, 'on'
grading_period_group =
@course.grading_period_groups.create!(title: 'A Group')
grading_period_group = @course.grading_period_groups.create!(title: 'A Group')
@grading_period = grading_period_group.grading_periods.create! do |period|
period.title = 'A Period'
period.start_date = 1.month.from_now(now)
@ -35,117 +33,95 @@ describe GradingPeriodsController, type: :request do
end
end
context "multiple grading periods feature flag turned on" do
describe 'GET show' do
def get_show(raw = false)
helper = method(raw ? :raw_api_call : :api_call)
helper.call(
:get,
"/api/v1/courses/#{@course.id}/grading_periods/#{@grading_period.id}",
{
controller: 'grading_periods',
action: 'show',
format: 'json',
course_id: @course.id,
id: @grading_period.id,
},
{}
)
end
it "retrieves the grading period specified" do
json = get_show
period = json['grading_periods'].first
expect(period['id']).to eq(@grading_period.id.to_s)
expect(period['weight']).to eq(@grading_period.weight)
expect(period['title']).to eq(@grading_period.title)
expect(period['permissions']).to include(
"read" => true,
"create" => false,
"delete" => true,
"update" => true
)
end
it "doesn't return deleted grading periods" do
@grading_period.destroy
get_show(true)
expect(response.status).to eq 404
end
describe 'GET show' do
def get_show(raw = false)
helper = method(raw ? :raw_api_call : :api_call)
helper.call(
:get,
"/api/v1/courses/#{@course.id}/grading_periods/#{@grading_period.id}",
{
controller: 'grading_periods',
action: 'show',
format: 'json',
course_id: @course.id,
id: @grading_period.id,
},
{}
)
end
describe 'PUT update' do
def put_update(params, raw=false)
helper = method(raw ? :raw_api_call : :api_call)
helper.call(
:put,
"/api/v1/courses/#{@course.id}/grading_periods/#{@grading_period.id}",
{
controller: 'grading_periods',
action: 'update',
format: 'json',
course_id: @course.id,
id: @grading_period.id
},
{ grading_periods: [params] }
)
end
it "updates a grading period successfully" do
new_weight = @grading_period.weight + 11.11
put_update(weight: new_weight)
expect(@grading_period.reload.weight).to eql(new_weight)
end
it "doesn't update deleted grading periods" do
@grading_period.destroy
put_update({weight: 80}, true)
expect(response.status).to eq 404
end
it "retrieves the grading period specified" do
json = get_show
period = json['grading_periods'].first
expect(period['id']).to eq(@grading_period.id.to_s)
expect(period['weight']).to eq(@grading_period.weight)
expect(period['title']).to eq(@grading_period.title)
expect(period['permissions']).to include(
"read" => true,
"create" => false,
"delete" => true,
"update" => true
)
end
describe 'DELETE destroy' do
def delete_destroy
raw_api_call(
:delete,
"/api/v1/courses/#{@course.id}/grading_periods/#{@grading_period.id}",
{
controller: 'grading_periods',
action: 'destroy',
format: 'json',
course_id: @course.id,
id: @grading_period.id.to_s
},
)
end
it "deletes a grading period successfully" do
delete_destroy
expect(response.code).to eq '204'
expect(@grading_period.reload).to be_deleted
end
it "doesn't return deleted grading periods" do
@grading_period.destroy
get_show(true)
expect(response.status).to eq 404
end
end
context "multiple grading periods feature flag turned off" do
before :once do
@course.root_account.disable_feature! :multiple_grading_periods
end
describe 'PUT update' do
def put_update(params, raw=false)
helper = method(raw ? :raw_api_call : :api_call)
it "index should return 404" do
json = api_call(
:get,
"/api/v1/courses/#{@course.id}/grading_periods",
helper.call(
:put,
"/api/v1/courses/#{@course.id}/grading_periods/#{@grading_period.id}",
{
controller: 'grading_periods',
action: 'index',
action: 'update',
format: 'json',
course_id: @course.id
}, {}, {}, expected_status: 404
course_id: @course.id,
id: @grading_period.id
},
{ grading_periods: [params] }
)
expect(json["message"]).to eq('Page not found')
end
it "updates a grading period successfully" do
new_weight = @grading_period.weight + 11.11
put_update(weight: new_weight)
expect(@grading_period.reload.weight).to eql(new_weight)
end
it "doesn't update deleted grading periods" do
@grading_period.destroy
put_update({weight: 80}, true)
expect(response.status).to eq 404
end
end
describe 'DELETE destroy' do
def delete_destroy
raw_api_call(
:delete,
"/api/v1/courses/#{@course.id}/grading_periods/#{@grading_period.id}",
{
controller: 'grading_periods',
action: 'destroy',
format: 'json',
course_id: @course.id,
id: @grading_period.id.to_s
},
)
end
it "deletes a grading period successfully" do
delete_destroy
expect(response.code).to eq '204'
expect(@grading_period.reload).to be_deleted
end
end
end

View File

@ -422,7 +422,7 @@ describe Quizzes::QuizzesApiController, type: :request do
end
end
context "with multiple grading periods enabled" do
context "with grading periods" do
def call_create(params, expected_status)
api_call_as_user(
@current_user,
@ -441,7 +441,6 @@ describe Quizzes::QuizzesApiController, type: :request do
before :once do
teacher_in_course(active_all: true)
@course.root_account.enable_feature!(:multiple_grading_periods)
grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = grading_period_group
@ -738,7 +737,7 @@ describe Quizzes::QuizzesApiController, type: :request do
end
end
context "with multiple grading periods enabled" do
context "with grading periods" do
def create_quiz(attr)
@course.quizzes.create!({ title: "Example Quiz", quiz_type: "assignment" }.merge(attr))
end
@ -772,7 +771,6 @@ describe Quizzes::QuizzesApiController, type: :request do
before :once do
teacher_in_course(active_all: true)
@course.root_account.enable_feature!(:multiple_grading_periods)
grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = grading_period_group

View File

@ -1795,7 +1795,7 @@ describe 'Submissions API', type: :request do
expect(json.detect { |u| u['user_id'] == student2.id }['submissions'].size).to eq 0
end
context "Multiple Grading Periods" do
context "with grading periods" do
before :once do
@student1 = user_factory(active_all: true)
@student2 = user_with_pseudonym(:active_all => true)
@ -1805,7 +1805,6 @@ describe 'Submissions API', type: :request do
@course.enroll_student(@student1).accept!
@course.enroll_student(@student2).accept!
@course.account.enable_feature!(:multiple_grading_periods)
gpg = Factories::GradingPeriodGroupHelper.new.legacy_create_for_course(@course)
@gp1 = gpg.grading_periods.create!(
title: 'first',

View File

@ -5,68 +5,84 @@ define [
], (axios, fakeENV, api) ->
deserializedSets = [
{
id: "1",
title: "Fall 2015",
id: '1',
title: 'Fall 2015',
weighted: false,
gradingPeriods: [
{
id: "1",
title: "Q1",
startDate: new Date("2015-09-01T12:00:00Z"),
endDate: new Date("2015-10-31T12:00:00Z"),
closeDate: new Date("2015-11-07T12:00:00Z")
id: '1',
title: 'Q1',
startDate: new Date('2015-09-01T12:00:00Z'),
endDate: new Date('2015-10-31T12:00:00Z'),
closeDate: new Date('2015-11-07T12:00:00Z'),
isClosed: true,
isLast: false,
weight: 43.5
},{
id: "2",
title: "Q2",
startDate: new Date("2015-11-01T12:00:00Z"),
endDate: new Date("2015-12-31T12:00:00Z"),
closeDate: new Date("2016-01-07T12:00:00Z")
id: '2',
title: 'Q2',
startDate: new Date('2015-11-01T12:00:00Z'),
endDate: new Date('2015-12-31T12:00:00Z'),
closeDate: new Date('2016-01-07T12:00:00Z'),
isClosed: false,
isLast: true,
weight: null
}
],
permissions: { read: true, create: true, update: true, delete: true },
createdAt: new Date("2015-12-29T12:00:00Z")
createdAt: new Date('2015-12-29T12:00:00Z')
},{
id: "2",
title: "Spring 2016",
id: '2',
title: 'Spring 2016',
weighted: true,
gradingPeriods: [],
permissions: { read: true, create: true, update: true, delete: true },
createdAt: new Date("2015-11-29T12:00:00Z")
createdAt: new Date('2015-11-29T12:00:00Z')
}
]
serializedSets = {
grading_period_sets: [
{
id: "1",
title: "Fall 2015",
id: '1',
title: 'Fall 2015',
weighted: false,
grading_periods: [
{
id: "1",
title: "Q1",
start_date: new Date("2015-09-01T12:00:00Z"),
end_date: new Date("2015-10-31T12:00:00Z"),
close_date: new Date("2015-11-07T12:00:00Z")
id: '1',
title: 'Q1',
start_date: new Date('2015-09-01T12:00:00Z'),
end_date: new Date('2015-10-31T12:00:00Z'),
close_date: new Date('2015-11-07T12:00:00Z'),
is_closed: true,
is_last: false,
weight: 43.5
},{
id: "2",
title: "Q2",
start_date: new Date("2015-11-01T12:00:00Z"),
end_date: new Date("2015-12-31T12:00:00Z"),
close_date: new Date("2016-01-07T12:00:00Z")
id: '2',
title: 'Q2',
start_date: new Date('2015-11-01T12:00:00Z'),
end_date: new Date('2015-12-31T12:00:00Z'),
close_date: new Date('2016-01-07T12:00:00Z'),
is_closed: false,
is_last: true,
weight: null
}
],
permissions: { read: true, create: true, update: true, delete: true },
created_at: "2015-12-29T12:00:00Z"
created_at: '2015-12-29T12:00:00Z'
},
{
id: "2",
title: "Spring 2016",
id: '2',
title: 'Spring 2016',
weighted: true,
grading_periods: [],
permissions: { read: true, create: true, update: true, delete: true },
created_at: "2015-11-29T12:00:00Z"
created_at: '2015-11-29T12:00:00Z'
}
]
}
QUnit.module "list",
QUnit.module 'gradingPeriodSetsApi.list',
setup: ->
@server = sinon.fakeServer.create()
@fakeHeaders =
@ -77,183 +93,159 @@ define [
fakeENV.teardown()
@server.restore()
test "calls the resolved endpoint", ->
test 'calls the resolved endpoint', ->
@stub($, 'ajaxJSON').returns(new Promise(->))
api.list()
ok $.ajaxJSON.calledWith('api/grading_period_sets')
asyncTest "deserializes returned grading period sets", ->
@server.respondWith "GET", /grading_period_sets/, [200, {"Content-Type":"application/json", "Link": @fakeHeaders}, JSON.stringify serializedSets]
api.list()
.then (sets) =>
deepEqual sets, deserializedSets
start()
@server.respond()
test 'deserializes returned grading period sets', ->
@server.respondWith(
'GET',
/grading_period_sets/,
[200, {'Content-Type':'application/json', 'Link': @fakeHeaders}, JSON.stringify serializedSets]
)
@server.autoRespond = true
promise = api.list()
.then (sets) =>
deepEqual sets, deserializedSets
asyncTest "creates a title from the creation date when the set has no title", ->
test 'creates a title from the creation date when the set has no title', ->
untitledSets =
grading_period_sets: [
id: "1"
id: '1'
title: null
grading_periods: []
permissions: { read: true, create: true, update: true, delete: true }
created_at: "2015-11-29T12:00:00Z"
created_at: '2015-11-29T12:00:00Z'
]
jsonString = JSON.stringify(untitledSets)
@server.respondWith(
"GET",
'GET',
/grading_period_sets/,
[200, { "Content-Type":"application/json", "Link": @fakeHeaders }, jsonString]
[200, { 'Content-Type':'application/json', 'Link': @fakeHeaders }, jsonString]
)
@server.autoRespond = true
api.list()
.then (sets) =>
equal sets[0].title, "Set created Nov 29, 2015"
start()
@server.respond()
asyncTest "uses the endDate as the closeDate when a period has no closeDate", ->
setsWithoutPeriodCloseDate =
grading_period_sets: [
id: "1"
title: "Fall 2015"
grading_periods: [{
id: "1",
title: "Q1",
start_date: new Date("2015-09-01T12:00:00Z"),
end_date: new Date("2015-10-31T12:00:00Z"),
close_date: null
}]
permissions: { read: true, create: true, update: true, delete: true }
created_at: "2015-11-29T12:00:00Z"
]
jsonString = JSON.stringify(setsWithoutPeriodCloseDate)
@server.respondWith(
"GET",
/grading_period_sets/,
[200, { "Content-Type":"application/json", "Link": @fakeHeaders }, jsonString]
)
api.list()
.then (sets) =>
deepEqual sets[0].gradingPeriods[0].closeDate, new Date("2015-10-31T12:00:00Z")
start()
@server.respond()
.then (sets) =>
equal sets[0].title, 'Set created Nov 29, 2015'
deserializedSetCreating = {
title: "Fall 2015",
enrollmentTermIDs: [ "1", "2" ]
title: 'Fall 2015',
weighted: null,
enrollmentTermIDs: ['1', '2']
}
deserializedSetCreated = {
id: "1",
title: "Fall 2015",
id: '1',
title: 'Fall 2015',
weighted: false,
gradingPeriods: [],
enrollmentTermIDs: [ "1", "2" ],
enrollmentTermIDs: ['1', '2'],
permissions: { read: true, create: true, update: true, delete: true },
createdAt: new Date("2015-12-31T12:00:00Z")
createdAt: new Date('2015-12-31T12:00:00Z')
}
serializedSetCreating = {
grading_period_set: { title: "Fall 2015" },
enrollment_term_ids: [ "1", "2" ]
grading_period_set: { title: 'Fall 2015', weighted: null },
enrollment_term_ids: ['1', '2']
}
serializedSetCreated = {
grading_period_set: {
id: "1",
title: "Fall 2015",
enrollment_term_ids: [ "1", "2" ],
id: '1',
title: 'Fall 2015',
weighted: false,
enrollment_term_ids: ['1', '2'],
grading_periods: [],
permissions: { read: true, create: true, update: true, delete: true },
created_at: "2015-12-31T12:00:00Z"
created_at: '2015-12-31T12:00:00Z'
}
}
QUnit.module "create",
QUnit.module 'gradingPeriodSetsApi.create',
setup: ->
fakeENV.setup()
ENV.GRADING_PERIOD_SETS_URL = 'api/grading_period_sets'
teardown: ->
fakeENV.teardown()
test "calls the resolved endpoint with the serialized grading period set", ->
apiSpy = @stub(axios, "post").returns(new Promise(->))
test 'calls the resolved endpoint with the serialized grading period set', ->
apiSpy = @stub(axios, 'post').returns(new Promise(->))
api.create(deserializedSetCreating)
ok axios.post.calledWith('api/grading_period_sets', serializedSetCreating)
asyncTest "deserializes returned grading period sets", ->
test 'deserializes returned grading period sets', ->
successPromise = new Promise (resolve) => resolve({ data: serializedSetCreated })
@stub(axios, "post").returns(successPromise)
@stub(axios, 'post').returns(successPromise)
api.create(deserializedSetCreating)
.then (set) =>
deepEqual set, deserializedSetCreated
start()
.then (set) =>
deepEqual set, deserializedSetCreated
asyncTest "rejects the promise upon errors", ->
failurePromise = new Promise (_, reject) => reject("FAIL")
@stub(axios, "post").returns(failurePromise)
test 'rejects the promise upon errors', ->
@stub(axios, 'post').returns(Promise.reject('FAIL'))
api.create(deserializedSetCreating).catch (error) =>
equal error, "FAIL"
start()
equal error, 'FAIL'
deserializedSetUpdating = {
id: "1",
title: "Fall 2015",
enrollmentTermIDs: [ "1", "2" ],
id: '1',
title: 'Fall 2015',
weighted: true,
enrollmentTermIDs: ['1', '2'],
permissions: { read: true, create: true, update: true, delete: true }
}
serializedSetUpdating = {
grading_period_set: { title: "Fall 2015" },
enrollment_term_ids: [ "1", "2" ]
grading_period_set: { title: 'Fall 2015', weighted: true },
enrollment_term_ids: ['1', '2']
}
serializedSetUpdated = {
grading_period_set: {
id: "1",
title: "Fall 2015",
enrollment_term_ids: [ "1", "2" ],
id: '1',
title: 'Fall 2015',
weighted: true,
enrollment_term_ids: ['1', '2'],
grading_periods: [
{
id: "1",
title: "Q1",
start_date: new Date("2015-09-01T12:00:00Z"),
end_date: new Date("2015-10-31T12:00:00Z"),
close_date: new Date("2015-11-07T12:00:00Z")
id: '1',
title: 'Q1',
start_date: new Date('2015-09-01T12:00:00Z'),
end_date: new Date('2015-10-31T12:00:00Z'),
close_date: new Date('2015-11-07T12:00:00Z'),
weight: 40
},{
id: "2",
title: "Q2",
start_date: new Date("2015-11-01T12:00:00Z"),
end_date: new Date("2015-12-31T12:00:00Z"),
close_date: null
id: '2',
title: 'Q2',
start_date: new Date('2015-11-01T12:00:00Z'),
end_date: new Date('2015-12-31T12:00:00Z'),
close_date: null,
weight: 60
}
],
permissions: { read: true, create: true, update: true, delete: true }
}
}
QUnit.module "update",
QUnit.module 'gradingPeriodSetsApi.update',
setup: ->
fakeENV.setup()
ENV.GRADING_PERIOD_SET_UPDATE_URL = 'api/grading_period_sets/%7B%7B%20id%20%7D%7D'
teardown: ->
fakeENV.teardown()
test "calls the resolved endpoint with the serialized grading period set", ->
apiSpy = @stub(axios, "patch").returns(new Promise(->))
test 'calls the resolved endpoint with the serialized grading period set', ->
apiSpy = @stub(axios, 'patch').returns(new Promise(->))
api.update(deserializedSetUpdating)
ok axios.patch.calledWith('api/grading_period_sets/1', serializedSetUpdating)
asyncTest "returns the given grading period set", ->
successPromise = new Promise (resolve) => resolve({ data: serializedSetUpdated })
@stub(axios, "patch").returns(successPromise)
test 'returns the given grading period set', ->
@stub(axios, 'patch').returns(Promise.resolve({ data: serializedSetUpdated }))
api.update(deserializedSetUpdating)
.then (set) =>
deepEqual set, deserializedSetUpdating
start()
asyncTest "rejects the promise upon errors", ->
failurePromise = new Promise (_, reject) => reject("FAIL")
@stub(axios, "patch").returns(failurePromise)
api.update(deserializedSetUpdating).catch (error) =>
equal error, "FAIL"
start()
test 'rejects the promise upon errors', ->
@stub(axios, 'patch').returns(Promise.reject('FAIL'))
api.update(deserializedSetUpdating)
.catch (error) =>
equal error, 'FAIL'

View File

@ -6,38 +6,42 @@ define [
], (_, axios, fakeENV, api) ->
deserializedPeriods = [
{
id: "1",
title: "Q1",
startDate: new Date("2015-09-01T12:00:00Z"),
endDate: new Date("2015-10-31T12:00:00Z"),
closeDate: new Date("2015-11-07T12:00:00Z"),
id: '1',
title: 'Q1',
startDate: new Date('2015-09-01T12:00:00Z'),
endDate: new Date('2015-10-31T12:00:00Z'),
closeDate: new Date('2015-11-07T12:00:00Z'),
isClosed: true,
isLast: false
isLast: false,
weight: 40
},{
id: "2",
title: "Q2",
startDate: new Date("2015-11-01T12:00:00Z"),
endDate: new Date("2015-12-31T12:00:00Z"),
closeDate: new Date("2016-01-07T12:00:00Z"),
id: '2',
title: 'Q2',
startDate: new Date('2015-11-01T12:00:00Z'),
endDate: new Date('2015-12-31T12:00:00Z'),
closeDate: new Date('2016-01-07T12:00:00Z'),
isClosed: true,
isLast: true
isLast: true,
weight: 60
}
]
serializedPeriods = {
grading_periods: [
{
id: "1",
title: "Q1",
start_date: new Date("2015-09-01T12:00:00Z"),
end_date: new Date("2015-10-31T12:00:00Z"),
close_date: new Date("2015-11-07T12:00:00Z")
id: '1',
title: 'Q1',
start_date: new Date('2015-09-01T12:00:00Z'),
end_date: new Date('2015-10-31T12:00:00Z'),
close_date: new Date('2015-11-07T12:00:00Z'),
weight: 40
},{
id: "2",
title: "Q2",
start_date: new Date("2015-11-01T12:00:00Z"),
end_date: new Date("2015-12-31T12:00:00Z"),
close_date: new Date("2016-01-07T12:00:00Z")
id: '2',
title: 'Q2',
start_date: new Date('2015-11-01T12:00:00Z'),
end_date: new Date('2015-12-31T12:00:00Z'),
close_date: new Date('2016-01-07T12:00:00Z'),
weight: 60
}
]
}
@ -45,79 +49,59 @@ define [
periodsData = {
grading_periods: [
{
id: "1",
title: "Q1",
start_date: "2015-09-01T12:00:00Z",
end_date: "2015-10-31T12:00:00Z",
close_date: "2015-11-07T12:00:00Z",
id: '1',
title: 'Q1',
start_date: '2015-09-01T12:00:00Z',
end_date: '2015-10-31T12:00:00Z',
close_date: '2015-11-07T12:00:00Z',
is_closed: true,
is_last: false
is_last: false,
weight: 40
},{
id: "2",
title: "Q2",
start_date: "2015-11-01T12:00:00Z",
end_date: "2015-12-31T12:00:00Z",
close_date: "2016-01-07T12:00:00Z",
id: '2',
title: 'Q2',
start_date: '2015-11-01T12:00:00Z',
end_date: '2015-12-31T12:00:00Z',
close_date: '2016-01-07T12:00:00Z',
is_closed: true,
is_last: true
is_last: true,
weight: 60
}
]
}
QUnit.module "batchUpdate",
QUnit.module 'batchUpdate',
setup: ->
fakeENV.setup()
ENV.GRADING_PERIODS_UPDATE_URL = 'api/{{ set_id }}/batch_update'
teardown: ->
fakeENV.teardown()
test "calls the resolved endpoint with serialized grading periods", ->
apiSpy = @stub(axios, "patch").returns(new Promise(->))
test 'calls the resolved endpoint with serialized grading periods', ->
apiSpy = @stub(axios, 'patch').returns(new Promise(->))
api.batchUpdate(123, deserializedPeriods)
ok axios.patch.calledWith('api/123/batch_update', serializedPeriods)
asyncTest "deserializes returned grading periods", ->
successPromise = new Promise (resolve) => resolve({ data: periodsData })
@stub(axios, "patch").returns(successPromise)
test 'deserializes returned grading periods', ->
@stub(axios, 'patch').returns(Promise.resolve({ data: periodsData }))
api.batchUpdate(123, deserializedPeriods)
.then (periods) =>
deepEqual periods, deserializedPeriods
start()
.then (periods) =>
deepEqual periods, deserializedPeriods
asyncTest "uses the endDate as the closeDate when a period has no closeDate", ->
periodsWithoutCloseDate = {
grading_periods: [
{
id: "1",
title: "Q1",
start_date: new Date("2015-09-01T12:00:00Z"),
end_date: new Date("2015-10-31T12:00:00Z"),
close_date: null
}
]
}
successPromise = new Promise (resolve) => resolve({ data: periodsWithoutCloseDate })
@stub(axios, "patch").returns(successPromise)
test 'rejects the promise upon errors', ->
@stub(axios, 'patch').returns(Promise.reject('FAIL'))
api.batchUpdate(123, deserializedPeriods)
.then (periods) =>
deepEqual periods[0].closeDate, new Date("2015-10-31T12:00:00Z")
start()
.catch (error) =>
equal error, 'FAIL'
asyncTest "rejects the promise upon errors", ->
failurePromise = new Promise (_, reject) => reject("FAIL")
@stub(axios, "patch").returns(failurePromise)
api.batchUpdate(123, deserializedPeriods).catch (error) =>
equal error, "FAIL"
start()
QUnit.module 'deserializePeriods'
QUnit.module "deserializePeriods"
test "returns an empty array if passed undefined", ->
test 'returns an empty array if passed undefined', ->
propEqual api.deserializePeriods(undefined), []
test "returns an empty array if passed null", ->
test 'returns an empty array if passed null', ->
propEqual api.deserializePeriods(null), []
test "deserializes periods", ->
test 'deserializes periods', ->
result = api.deserializePeriods(periodsData.grading_periods)
propEqual result, deserializedPeriods

View File

@ -164,7 +164,7 @@ define [
setup: ->
fakeENV.setup({
GRADEBOOK_OPTIONS: {
multiple_grading_periods_enabled: true
has_grading_periods: true
}
})
@disableUnavailableMenuActions = GradebookHeaderMenu.prototype.disableUnavailableMenuActions
@ -224,7 +224,7 @@ define [
setup: ->
fakeENV.setup({
GRADEBOOK_OPTIONS: {
multiple_grading_periods_enabled: true
has_grading_periods: true
},
current_user_roles: ['admin']
})
@ -274,7 +274,7 @@ define [
setup: ->
fakeENV.setup({
GRADEBOOK_OPTIONS: {
multiple_grading_periods_enabled: true
has_grading_periods: true
},
current_user_roles: ['admin']
})

View File

@ -1,11 +1,165 @@
#
# Copyright (C) 2014 - 2017 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'spec/jsx/gradebook/GradeCalculatorSpecHelper'
'compiled/gradebook/Gradebook'
'jsx/gradebook/DataLoader'
'underscore'
'timezone'
'compiled/SubmissionDetailsDialog'
'compiled/util/natcompare'
], (Gradebook, DataLoader, _, tz, SubmissionDetailsDialog, natcompare) ->
'compiled/SubmissionDetailsDialog'
'jsx/gradebook/CourseGradeCalculator'
], (
GradeCalculatorSpecHelper, Gradebook, DataLoader, _, tz, natcompare, SubmissionDetailsDialog, CourseGradeCalculator
) ->
exampleGradebookOptions =
settings:
show_concluded_enrollments: 'true'
show_inactive_enrollments: 'true'
sections: []
createExampleGrades = GradeCalculatorSpecHelper.createCourseGradesWithGradingPeriods
QUnit.module 'Gradebook'
test 'normalizes the grading period set from the env', ->
options = _.extend {}, exampleGradebookOptions,
grading_period_set:
id: '1501'
grading_periods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
gradingPeriodSet = new Gradebook(options).gradingPeriodSet
deepEqual(gradingPeriodSet.id, '1501')
equal(gradingPeriodSet.gradingPeriods.length, 2)
deepEqual(_.map(gradingPeriodSet.gradingPeriods, 'id'), ['701', '702'])
test 'sets grading period set to null when not defined in the env', ->
gradingPeriodSet = new Gradebook(exampleGradebookOptions).gradingPeriodSet
deepEqual(gradingPeriodSet, null)
QUnit.module 'Gradebook#calculateStudentGrade',
setupThis:(options = {}) ->
assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }]
submissions = [{ assignment_id: 201, score: 10 }]
defaults = {
gradingPeriodToShow: '0'
isAllGradingPeriods: Gradebook.prototype.isAllGradingPeriods
assignmentGroups: [{ id: 301, group_weight: 60, rules: {}, assignments }]
options: { group_weighting_scheme: 'points' }
gradingPeriods: [{ id: 701, weight: 50 }, { id: 702, weight: 50 }]
gradingPeriodSet:
id: '1501'
gradingPeriods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
effectiveDueDates: { 201: { 101: { grading_period_id: '701' } } }
submissionsForStudent: () ->
submissions
addDroppedClass: () ->
}
_.defaults options, defaults
setup: ->
@calculate = Gradebook.prototype.calculateStudentGrade
test 'calculates grades using properties from the gradebook', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroups)
equal(args[2], self.options.group_weighting_scheme)
equal(args[3], self.gradingPeriodSet)
test 'scopes effective due dates to the user', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
dueDates = CourseGradeCalculator.calculate.getCall(0).args[4]
deepEqual(dueDates, 201: { grading_period_id: '701' })
test 'calculates grades without grading period data when grading period set is null', ->
self = @setupThis(gradingPeriodSet: null)
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroups)
equal(args[2], self.options.group_weighting_scheme)
equal(typeof args[3], 'undefined')
equal(typeof args[4], 'undefined')
test 'calculates grades without grading period data when effective due dates are not defined', ->
self = @setupThis(effectiveDueDates: null)
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroups)
equal(args[2], self.options.group_weighting_scheme)
equal(typeof args[3], 'undefined')
equal(typeof args[4], 'undefined')
test 'stores the current grade on the student when not including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(include_ungraded_assignments: false)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.current)
test 'stores the final grade on the student when including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(include_ungraded_assignments: true)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.final)
test 'stores the current grade from the selected grading period when not including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(gradingPeriodToShow: 701, include_ungraded_assignments: false)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.gradingPeriods[701].current)
test 'stores the final grade from the selected grading period when including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(gradingPeriodToShow: 701, include_ungraded_assignments: true)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.gradingPeriods[701].final)
test 'does not calculate when the student is not loaded', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: false, initialized: true)
notOk(CourseGradeCalculator.calculate.called)
test 'does not calculate when the student is not initialized', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: false)
notOk(CourseGradeCalculator.calculate.called)
QUnit.module "Gradebook#localeSort"
test "delegates to natcompare.strings", ->
@ -18,9 +172,9 @@ define [
Gradebook.prototype.localeSort(0, false)
ok natCompareSpy.calledWith('', '')
QUnit.module "Gradebook#gradeSort"
QUnit.module 'Gradebook#gradeSort'
test "gradeSort - total_grade", ->
test 'gradeSort - total_grade', ->
gradeSort = (showTotalGradeAsPoints, a, b, field, asc) ->
asc = true unless asc?
@ -32,27 +186,27 @@ define [
, {total_grade: {score: 10, possible: 20}}
, {total_grade: {score: 5, possible: 10}}
, 'total_grade') == 0
, "total_grade sorts by percent (normally)"
, 'total_grade sorts by percent (normally)'
ok gradeSort(true
, {total_grade: {score: 10, possible: 20}}
, {total_grade: {score: 5, possible: 10}}
, 'total_grade') > 0
, "total_grade sorts by score when if show_total_grade_as_points"
, 'total_grade sorts by score when if show_total_grade_as_points'
ok gradeSort(true
, {assignment_group_1: {score: 10, possible: 20}}
, {assignment_group_1: {score: 5, possible: 10}}
, 'assignment_group_1') == 0
, "assignment groups are always sorted by percent"
, 'assignment groups are always sorted by percent'
ok gradeSort(false
, {assignment1: {score: 5, possible: 10}}
, {assignment1: {score: 10, possible: 20}}
, 'assignment1') < 0
, "other fields are sorted by score"
, 'other fields are sorted by score'
QUnit.module "Gradebook#hideAggregateColumns",
QUnit.module 'Gradebook#hideAggregateColumns',
gradebookStubs: ->
indexedOverrides: Gradebook.prototype.indexedOverrides
indexedGradingPeriods: _.indexBy(@gradingPeriods, 'id')
@ -60,7 +214,7 @@ define [
setupThis: (options) ->
customOptions = options || {}
defaults =
gradingPeriodsEnabled: true
hasGradingPeriods: true
getGradingPeriodToShow: -> '1'
options:
all_grading_periods_totals: false
@ -71,13 +225,13 @@ define [
@hideAggregateColumns = Gradebook.prototype.hideAggregateColumns
teardown: ->
test 'returns false if multiple grading periods is disabled', ->
self = @setupThis(gradingPeriodsEnabled: false, isAllGradingPeriods: -> false)
test 'returns false if there are no grading periods', ->
self = @setupThis(hasGradingPeriods: false, isAllGradingPeriods: -> false)
notOk @hideAggregateColumns.call(self)
test 'returns false if multiple grading periods is disabled, even if isAllGradingPeriods is true', ->
test 'returns false if there are no grading periods, even if isAllGradingPeriods is true', ->
self = @setupThis
gradingPeriodsEnabled: false
hasGradingPeriods: false
getGradingPeriodToShow: -> '0'
isAllGradingPeriods: -> true
@ -114,9 +268,9 @@ define [
@getStoredSortOrder = Gradebook.prototype.getStoredSortOrder
@defaultSortType = 'assignment_group'
@allAssignmentColumns = [
{ object: { assignment_group: { position: 1 }, position: 1, name: "first" } },
{ object: { assignment_group: { position: 1 }, position: 2, name: "second" } },
{ object: { assignment_group: { position: 1 }, position: 3, name: "third" } }
{ object: { assignment_group: { position: 1 }, position: 1, name: 'first' } },
{ object: { assignment_group: { position: 1 }, position: 2, name: 'second' } },
{ object: { assignment_group: { position: 1 }, position: 3, name: 'third' } }
]
@aggregateColumns = []
@parentColumns = []
@ -147,21 +301,21 @@ define [
setup: ->
@excludedFields = Gradebook.prototype.fieldsToExcludeFromAssignments
test "includes 'description' in the response", ->
test 'includes "description" in the response', ->
ok _.contains(@excludedFields, 'description')
test "includes 'needs_grading_count' in the response", ->
test 'includes "needs_grading_count" in the response', ->
ok _.contains(@excludedFields, 'needs_grading_count')
QUnit.module "Gradebook#submissionsForStudent",
QUnit.module 'Gradebook#submissionsForStudent',
setupThis: (options = {}) ->
effectiveDueDates = {
1: { 1: { grading_period_id: "1" } },
2: { 1: { grading_period_id: "2" } }
1: { 1: { grading_period_id: '1' } },
2: { 1: { grading_period_id: '2' } }
}
defaults = {
gradingPeriodsEnabled: false,
hasGradingPeriods: false,
gradingPeriodToShow: null,
isAllGradingPeriods: -> false,
effectiveDueDates
@ -170,32 +324,32 @@ define [
setup: ->
@student =
id: "1"
assignment_1: { assignment_id: "1", user_id: "1", name: "yolo" }
assignment_2: { assignment_id: "2", user_id: "1", name: "froyo" }
id: '1'
assignment_1: { assignment_id: '1', user_id: '1', name: 'yolo' }
assignment_2: { assignment_id: '2', user_id: '1', name: 'froyo' }
@submissionsForStudent = Gradebook.prototype.submissionsForStudent
test "returns all submissions for the student (multiple grading periods disabled)", ->
test 'returns all submissions for the student when there are no grading periods', ->
self = @setupThis()
submissions = @submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["1", "2"]
propEqual _.pluck(submissions, 'assignment_id'), ['1', '2']
test "returns all submissions if 'All Grading Periods' is selected", ->
test 'returns all submissions if "All Grading Periods" is selected', ->
self = @setupThis(
gradingPeriodsEnabled: true,
gradingPeriodToShow: "0",
hasGradingPeriods: true,
gradingPeriodToShow: '0',
isAllGradingPeriods: -> true
)
submissions = @submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["1", "2"]
propEqual _.pluck(submissions, 'assignment_id'), ['1', '2']
test "only returns submissions due for the student in the selected grading period", ->
test 'only returns submissions due for the student in the selected grading period', ->
self = @setupThis(
gradingPeriodsEnabled: true,
gradingPeriodToShow: "2"
hasGradingPeriods: true,
gradingPeriodToShow: '2'
)
submissions = @submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["2"]
propEqual _.pluck(submissions, 'assignment_id'), ['2']
QUnit.module 'Gradebook#studentsUrl',
setupThis:(options) ->
@ -224,9 +378,61 @@ define [
self = @setupThis(showConcludedEnrollments: true, showInactiveEnrollments: true)
equal @studentsUrl.call(self), 'students_with_concluded_and_inactive_enrollments_url'
QUnit.module 'Gradebook#weightedGroups',
setup: ->
@weightedGroups = Gradebook.prototype.weightedGroups
test 'returns true when group_weighting_scheme is "percent"', ->
equal @weightedGroups.call(options: { group_weighting_scheme: 'percent' }), true
test 'returns false when group_weighting_scheme is not "percent"', ->
equal @weightedGroups.call(options: { group_weighting_scheme: 'points' }), false
equal @weightedGroups.call(options: { group_weighting_scheme: null }), false
QUnit.module 'Gradebook#weightedGrades',
setupThis:(group_weighting_scheme, gradingPeriodSet) ->
{ options: { group_weighting_scheme }, gradingPeriodSet }
setup: ->
@weightedGrades = Gradebook.prototype.weightedGrades
test 'returns true when group_weighting_scheme is "percent"', ->
self = @setupThis('percent', { weighted: false })
equal @weightedGrades.call(self), true
test 'returns true when the gradingPeriodSet is weighted', ->
self = @setupThis('points', { weighted: true })
equal @weightedGrades.call(self), true
test 'returns false when group_weighting_scheme is not "percent" and gradingPeriodSet is not weighted', ->
self = @setupThis('points', { weighted: false })
equal @weightedGrades.call(self), false
test 'returns false when group_weighting_scheme is not "percent" and gradingPeriodSet is not defined', ->
self = @setupThis('points', null)
equal @weightedGrades.call(self), false
QUnit.module 'Gradebook#displayPointTotals',
setupThis:(show_total_grade_as_points, weightedGrades) ->
options: { show_total_grade_as_points }
weightedGrades: () -> weightedGrades
setup: ->
@displayPointTotals = Gradebook.prototype.displayPointTotals
test 'returns true when grades are not weighted and show_total_grade_as_points is true', ->
self = @setupThis(true, false)
equal @displayPointTotals.call(self), true
test 'returns false when grades are weighted', ->
self = @setupThis(true, true)
equal @displayPointTotals.call(self), false
test 'returns false when show_total_grade_as_points is false', ->
self = @setupThis(false, false)
equal @displayPointTotals.call(self), false
QUnit.module 'Gradebook#showNotesColumn',
setup: ->
@loadNotes = @stub(DataLoader, "getDataForColumn")
@loadNotes = @stub(DataLoader, 'getDataForColumn')
setupShowNotesColumn: (opts) ->
defaultOptions =
@ -276,7 +482,7 @@ define [
}
teardown: ->
@fixtureParent.innerHTML = ""
@fixtureParent.innerHTML = ''
@fixture = undefined
test 'when not editable, returns false if the active cell node has the "cannot_edit" class', ->

View File

@ -12,8 +12,7 @@ define [
defaults =
current_user_roles: [ "teacher" ]
GRADEBOOK_OPTIONS:
multiple_grading_periods_enabled: true
latest_end_date_of_admin_created_grading_periods_in_the_past: 'Thu Jul 30 2015 00:00:00 GMT-0700 (PDT)'
has_grading_periods: true
@previousWindowENV = window.ENV
_.extend(window.ENV, defaults)
@ -54,8 +53,7 @@ define [
defaults =
current_user_roles: [ "teacher" ]
GRADEBOOK_OPTIONS:
multiple_grading_periods_enabled: true
latest_end_date_of_admin_created_grading_periods_in_the_past: 'Thu Jul 30 2015 00:00:00 GMT-0700 (PDT)'
has_grading_periods: true
@previousWindowENV = window.ENV
_.extend(window.ENV, defaults)
@ -86,8 +84,7 @@ define [
defaults =
current_user_roles: [ "teacher" ]
GRADEBOOK_OPTIONS:
multiple_grading_periods_enabled: true
latest_end_date_of_admin_created_grading_periods_in_the_past: '2013-10-01T10:00:00Z'
has_grading_periods: true
@previousWindowENV = window.ENV
_.extend(window.ENV, defaults)

View File

@ -164,7 +164,7 @@ define [
setup: ->
fakeENV.setup({
GRADEBOOK_OPTIONS: {
multiple_grading_periods_enabled: true
has_grading_periods: true
}
})
@disableUnavailableMenuActions = GradebookHeaderMenu.prototype.disableUnavailableMenuActions
@ -224,7 +224,7 @@ define [
setup: ->
fakeENV.setup({
GRADEBOOK_OPTIONS: {
multiple_grading_periods_enabled: true
has_grading_periods: true
},
current_user_roles: ['admin']
})
@ -274,7 +274,7 @@ define [
setup: ->
fakeENV.setup({
GRADEBOOK_OPTIONS: {
multiple_grading_periods_enabled: true
has_grading_periods: true
},
current_user_roles: ['admin']
})

View File

@ -1,11 +1,165 @@
#
# Copyright (C) 2016 - 2017 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'spec/jsx/gradebook/GradeCalculatorSpecHelper'
'compiled/gradezilla/Gradebook'
'jsx/gradezilla/DataLoader'
'underscore'
'timezone'
'compiled/SubmissionDetailsDialog'
'compiled/util/natcompare'
], (Gradebook, DataLoader, _, tz, SubmissionDetailsDialog, natcompare) ->
'compiled/SubmissionDetailsDialog'
'jsx/gradebook/CourseGradeCalculator'
], (
GradeCalculatorSpecHelper, Gradebook, DataLoader, _, tz, natcompare, SubmissionDetailsDialog, CourseGradeCalculator
) ->
exampleGradebookOptions =
settings:
show_concluded_enrollments: 'true'
show_inactive_enrollments: 'true'
sections: []
createExampleGrades = GradeCalculatorSpecHelper.createCourseGradesWithGradingPeriods
QUnit.module 'Gradebook'
test 'normalizes the grading period set from the env', ->
options = _.extend {}, exampleGradebookOptions,
grading_period_set:
id: '1501'
grading_periods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
gradingPeriodSet = new Gradebook(options).gradingPeriodSet
deepEqual(gradingPeriodSet.id, '1501')
equal(gradingPeriodSet.gradingPeriods.length, 2)
deepEqual(_.map(gradingPeriodSet.gradingPeriods, 'id'), ['701', '702'])
test 'sets grading period set to null when not defined in the env', ->
gradingPeriodSet = new Gradebook(exampleGradebookOptions).gradingPeriodSet
deepEqual(gradingPeriodSet, null)
QUnit.module 'Gradebook#calculateStudentGrade',
setupThis:(options = {}) ->
assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }]
submissions = [{ assignment_id: 201, score: 10 }]
defaults = {
gradingPeriodToShow: '0'
isAllGradingPeriods: Gradebook.prototype.isAllGradingPeriods
assignmentGroups: [{ id: 301, group_weight: 60, rules: {}, assignments }]
options: { group_weighting_scheme: 'points' }
gradingPeriods: [{ id: 701, weight: 50 }, { id: 702, weight: 50 }]
gradingPeriodSet:
id: '1501'
gradingPeriods: [{ id: '701', weight: 50 }, { id: '702', weight: 50 }]
weighted: true
effectiveDueDates: { 201: { 101: { grading_period_id: '701' } } }
submissionsForStudent: () ->
submissions
addDroppedClass: () ->
}
_.defaults options, defaults
setup: ->
@calculate = Gradebook.prototype.calculateStudentGrade
test 'calculates grades using properties from the gradebook', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroups)
equal(args[2], self.options.group_weighting_scheme)
equal(args[3], self.gradingPeriodSet)
test 'scopes effective due dates to the user', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
dueDates = CourseGradeCalculator.calculate.getCall(0).args[4]
deepEqual(dueDates, 201: { grading_period_id: '701' })
test 'calculates grades without grading period data when grading period set is null', ->
self = @setupThis(gradingPeriodSet: null)
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroups)
equal(args[2], self.options.group_weighting_scheme)
equal(typeof args[3], 'undefined')
equal(typeof args[4], 'undefined')
test 'calculates grades without grading period data when effective due dates are not defined', ->
self = @setupThis(effectiveDueDates: null)
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: true)
args = CourseGradeCalculator.calculate.getCall(0).args
equal(args[0], self.submissionsForStudent())
equal(args[1], self.assignmentGroups)
equal(args[2], self.options.group_weighting_scheme)
equal(typeof args[3], 'undefined')
equal(typeof args[4], 'undefined')
test 'stores the current grade on the student when not including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(include_ungraded_assignments: false)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.current)
test 'stores the final grade on the student when including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(include_ungraded_assignments: true)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.final)
test 'stores the current grade from the selected grading period when not including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(gradingPeriodToShow: 701, include_ungraded_assignments: false)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.gradingPeriods[701].current)
test 'stores the final grade from the selected grading period when including ungraded assignments', ->
exampleGrades = createExampleGrades()
self = @setupThis(gradingPeriodToShow: 701, include_ungraded_assignments: true)
@stub(CourseGradeCalculator, 'calculate').returns(exampleGrades)
student = { id: '101', loaded: true, initialized: true }
@calculate.call(self, student)
equal(student.total_grade, exampleGrades.gradingPeriods[701].final)
test 'does not calculate when the student is not loaded', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: false, initialized: true)
notOk(CourseGradeCalculator.calculate.called)
test 'does not calculate when the student is not initialized', ->
self = @setupThis()
@stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades())
@calculate.call(self, id: '101', loaded: true, initialized: false)
notOk(CourseGradeCalculator.calculate.called)
QUnit.module "Gradebook#localeSort"
test "delegates to natcompare.strings", ->
@ -18,9 +172,9 @@ define [
Gradebook.prototype.localeSort(0, false)
ok natCompareSpy.calledWith('', '')
QUnit.module "Gradebook#gradeSort"
QUnit.module 'Gradebook#gradeSort'
test "gradeSort - total_grade", ->
test 'gradeSort - total_grade', ->
gradeSort = (showTotalGradeAsPoints, a, b, field, asc) ->
asc = true unless asc?
@ -32,27 +186,27 @@ define [
, {total_grade: {score: 10, possible: 20}}
, {total_grade: {score: 5, possible: 10}}
, 'total_grade') == 0
, "total_grade sorts by percent (normally)"
, 'total_grade sorts by percent (normally)'
ok gradeSort(true
, {total_grade: {score: 10, possible: 20}}
, {total_grade: {score: 5, possible: 10}}
, 'total_grade') > 0
, "total_grade sorts by score when if show_total_grade_as_points"
, 'total_grade sorts by score when if show_total_grade_as_points'
ok gradeSort(true
, {assignment_group_1: {score: 10, possible: 20}}
, {assignment_group_1: {score: 5, possible: 10}}
, 'assignment_group_1') == 0
, "assignment groups are always sorted by percent"
, 'assignment groups are always sorted by percent'
ok gradeSort(false
, {assignment1: {score: 5, possible: 10}}
, {assignment1: {score: 10, possible: 20}}
, 'assignment1') < 0
, "other fields are sorted by score"
, 'other fields are sorted by score'
QUnit.module "Gradebook#hideAggregateColumns",
QUnit.module 'Gradebook#hideAggregateColumns',
gradebookStubs: ->
indexedOverrides: Gradebook.prototype.indexedOverrides
indexedGradingPeriods: _.indexBy(@gradingPeriods, 'id')
@ -60,7 +214,7 @@ define [
setupThis: (options) ->
customOptions = options || {}
defaults =
gradingPeriodsEnabled: true
hasGradingPeriods: true
getGradingPeriodToShow: -> '1'
options:
all_grading_periods_totals: false
@ -71,13 +225,13 @@ define [
@hideAggregateColumns = Gradebook.prototype.hideAggregateColumns
teardown: ->
test 'returns false if multiple grading periods is disabled', ->
self = @setupThis(gradingPeriodsEnabled: false, isAllGradingPeriods: -> false)
test 'returns false if there are no grading periods', ->
self = @setupThis(hasGradingPeriods: false, isAllGradingPeriods: -> false)
notOk @hideAggregateColumns.call(self)
test 'returns false if multiple grading periods is disabled, even if isAllGradingPeriods is true', ->
test 'returns false if there are no grading periods, even if isAllGradingPeriods is true', ->
self = @setupThis
gradingPeriodsEnabled: false
hasGradingPeriods: false
getGradingPeriodToShow: -> '0'
isAllGradingPeriods: -> true
@ -114,9 +268,9 @@ define [
@getStoredSortOrder = Gradebook.prototype.getStoredSortOrder
@defaultSortType = 'assignment_group'
@allAssignmentColumns = [
{ object: { assignment_group: { position: 1 }, position: 1, name: "first" } },
{ object: { assignment_group: { position: 1 }, position: 2, name: "second" } },
{ object: { assignment_group: { position: 1 }, position: 3, name: "third" } }
{ object: { assignment_group: { position: 1 }, position: 1, name: 'first' } },
{ object: { assignment_group: { position: 1 }, position: 2, name: 'second' } },
{ object: { assignment_group: { position: 1 }, position: 3, name: 'third' } }
]
@aggregateColumns = []
@parentColumns = []
@ -147,21 +301,21 @@ define [
setup: ->
@excludedFields = Gradebook.prototype.fieldsToExcludeFromAssignments
test "includes 'description' in the response", ->
test 'includes "description" in the response', ->
ok _.contains(@excludedFields, 'description')
test "includes 'needs_grading_count' in the response", ->
test 'includes "needs_grading_count" in the response', ->
ok _.contains(@excludedFields, 'needs_grading_count')
QUnit.module "Gradebook#submissionsForStudent",
QUnit.module 'Gradebook#submissionsForStudent',
setupThis: (options = {}) ->
effectiveDueDates = {
1: { 1: { grading_period_id: "1" } },
2: { 1: { grading_period_id: "2" } }
1: { 1: { grading_period_id: '1' } },
2: { 1: { grading_period_id: '2' } }
}
defaults = {
gradingPeriodsEnabled: false,
hasGradingPeriods: false,
gradingPeriodToShow: null,
isAllGradingPeriods: -> false,
effectiveDueDates
@ -170,32 +324,32 @@ define [
setup: ->
@student =
id: "1"
assignment_1: { assignment_id: "1", user_id: "1", name: "yolo" }
assignment_2: { assignment_id: "2", user_id: "1", name: "froyo" }
id: '1'
assignment_1: { assignment_id: '1', user_id: '1', name: 'yolo' }
assignment_2: { assignment_id: '2', user_id: '1', name: 'froyo' }
@submissionsForStudent = Gradebook.prototype.submissionsForStudent
test "returns all submissions for the student (multiple grading periods disabled)", ->
test 'returns all submissions for the student when there are no grading periods', ->
self = @setupThis()
submissions = @submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["1", "2"]
propEqual _.pluck(submissions, 'assignment_id'), ['1', '2']
test "returns all submissions if 'All Grading Periods' is selected", ->
test 'returns all submissions if "All Grading Periods" is selected', ->
self = @setupThis(
gradingPeriodsEnabled: true,
gradingPeriodToShow: "0",
hasGradingPeriods: true,
gradingPeriodToShow: '0',
isAllGradingPeriods: -> true
)
submissions = @submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["1", "2"]
propEqual _.pluck(submissions, 'assignment_id'), ['1', '2']
test "only returns submissions due for the student in the selected grading period", ->
test 'only returns submissions due for the student in the selected grading period', ->
self = @setupThis(
gradingPeriodsEnabled: true,
gradingPeriodToShow: "2"
hasGradingPeriods: true,
gradingPeriodToShow: '2'
)
submissions = @submissionsForStudent.call(self, @student)
propEqual _.pluck(submissions, "assignment_id"), ["2"]
propEqual _.pluck(submissions, 'assignment_id'), ['2']
QUnit.module 'Gradebook#studentsUrl',
setupThis:(options) ->
@ -224,9 +378,61 @@ define [
self = @setupThis(showConcludedEnrollments: true, showInactiveEnrollments: true)
equal @studentsUrl.call(self), 'students_with_concluded_and_inactive_enrollments_url'
QUnit.module 'Gradebook#weightedGroups',
setup: ->
@weightedGroups = Gradebook.prototype.weightedGroups
test 'returns true when group_weighting_scheme is "percent"', ->
equal @weightedGroups.call(options: { group_weighting_scheme: 'percent' }), true
test 'returns false when group_weighting_scheme is not "percent"', ->
equal @weightedGroups.call(options: { group_weighting_scheme: 'points' }), false
equal @weightedGroups.call(options: { group_weighting_scheme: null }), false
QUnit.module 'Gradebook#weightedGrades',
setupThis:(group_weighting_scheme, gradingPeriodSet) ->
{ options: { group_weighting_scheme }, gradingPeriodSet }
setup: ->
@weightedGrades = Gradebook.prototype.weightedGrades
test 'returns true when group_weighting_scheme is "percent"', ->
self = @setupThis('percent', { weighted: false })
equal @weightedGrades.call(self), true
test 'returns true when the gradingPeriodSet is weighted', ->
self = @setupThis('points', { weighted: true })
equal @weightedGrades.call(self), true
test 'returns false when group_weighting_scheme is not "percent" and gradingPeriodSet is not weighted', ->
self = @setupThis('points', { weighted: false })
equal @weightedGrades.call(self), false
test 'returns false when group_weighting_scheme is not "percent" and gradingPeriodSet is not defined', ->
self = @setupThis('points', null)
equal @weightedGrades.call(self), false
QUnit.module 'Gradebook#displayPointTotals',
setupThis:(show_total_grade_as_points, weightedGrades) ->
options: { show_total_grade_as_points }
weightedGrades: () -> weightedGrades
setup: ->
@displayPointTotals = Gradebook.prototype.displayPointTotals
test 'returns true when grades are not weighted and show_total_grade_as_points is true', ->
self = @setupThis(true, false)
equal @displayPointTotals.call(self), true
test 'returns false when grades are weighted', ->
self = @setupThis(true, true)
equal @displayPointTotals.call(self), false
test 'returns false when show_total_grade_as_points is false', ->
self = @setupThis(false, false)
equal @displayPointTotals.call(self), false
QUnit.module 'Gradebook#showNotesColumn',
setup: ->
@loadNotes = @stub(DataLoader, "getDataForColumn")
@loadNotes = @stub(DataLoader, 'getDataForColumn')
setupShowNotesColumn: (opts) ->
defaultOptions =
@ -276,7 +482,7 @@ define [
}
teardown: ->
@fixtureParent.innerHTML = ""
@fixtureParent.innerHTML = ''
@fixture = undefined
test 'when not editable, returns false if the active cell node has the "cannot_edit" class', ->

View File

@ -12,8 +12,7 @@ define [
defaults =
current_user_roles: [ "teacher" ]
GRADEBOOK_OPTIONS:
multiple_grading_periods_enabled: true
latest_end_date_of_admin_created_grading_periods_in_the_past: 'Thu Jul 30 2015 00:00:00 GMT-0700 (PDT)'
has_grading_periods: true
@previousWindowENV = window.ENV
_.extend(window.ENV, defaults)
@ -54,8 +53,7 @@ define [
defaults =
current_user_roles: [ "teacher" ]
GRADEBOOK_OPTIONS:
multiple_grading_periods_enabled: true
latest_end_date_of_admin_created_grading_periods_in_the_past: 'Thu Jul 30 2015 00:00:00 GMT-0700 (PDT)'
has_grading_periods: true
@previousWindowENV = window.ENV
_.extend(window.ENV, defaults)
@ -86,8 +84,7 @@ define [
defaults =
current_user_roles: [ "teacher" ]
GRADEBOOK_OPTIONS:
multiple_grading_periods_enabled: true
latest_end_date_of_admin_created_grading_periods_in_the_past: '2013-10-01T10:00:00Z'
has_grading_periods: true
@previousWindowENV = window.ENV
_.extend(window.ENV, defaults)

View File

@ -32,7 +32,7 @@ define [
groups: {1:{id: "1", name: "Reading Group One"}, 2: {id: "2", name: "Reading Group Two"}}
overrideModel: AssignmentOverride
syncWithBackbone: ->
multipleGradingPeriodsEnabled: false
hasGradingPeriods: false
gradingPeriods: []
isOnlyVisibleToOverrides: false
dueAt: null
@ -135,7 +135,7 @@ define [
attributes = _.keys(@dueDates.getAllOverrides()[0].attributes)
ok _.contains(attributes, "persisted")
QUnit.module 'DueDates with Multiple Grading Periods enabled',
QUnit.module 'DueDates with grading periods',
setup: ->
fakeENV.setup()
@server = sinon.fakeServer.create()
@ -247,7 +247,7 @@ define [
sections: sections
groups: {1:{id: "1", name: "Reading Group One"}, 2: {id: "2", name: "Reading Group Two"}}
syncWithBackbone: ->
multipleGradingPeriodsEnabled: true
hasGradingPeriods: true
gradingPeriods: gradingPeriods
isOnlyVisibleToOverrides: true
dueAt: null
@ -305,7 +305,7 @@ define [
students: {"1":{id: "1", name: "Scipio Africanus"}, "3":{id: 3, name: "Publius Publicoa"}}
overrideModel: AssignmentOverride
syncWithBackbone: ->
multipleGradingPeriodsEnabled: false
hasGradingPeriods: false
gradingPeriods: []
isOnlyVisibleToOverrides: false
dueAt: null

View File

@ -41,8 +41,7 @@ define [
}
]
"grading_periods_read_only": false,
"can_create_grading_periods": true,
"can_toggle_grading_periods": true
"can_create_grading_periods": true
@formattedIndexData =
"grading_periods":[
@ -66,8 +65,7 @@ define [
}
]
"grading_periods_read_only": false,
"can_create_grading_periods": true,
"can_toggle_grading_periods": true
"can_create_grading_periods": true
@createdPeriodData = "grading_periods":[
{
@ -260,51 +258,6 @@ define [
test 'renderSaveButton renders a button if the user is not at the course grading periods page', ->
ok @gradingPeriodCollection.renderSaveButton()
QUnit.module 'GradingPeriodCollection with one grading period',
setup: ->
@server = sinon.fakeServer.create()
fakeENV.setup()
ENV.current_user_roles = ["admin"]
ENV.GRADING_PERIODS_URL = "/api/v1/accounts/1/grading_periods"
@indexData =
"grading_periods":[
{
"id":"1", "start_date":"2015-03-01T06:00:00Z", "end_date":"2015-05-31T05:00:00Z",
"weight":null, "title":"Spring", "permissions": { "update":true, "delete":true }
}
]
"grading_periods_read_only": false,
"can_create_grading_periods": true,
"can_toggle_grading_periods": true
@formattedIndexData =
"grading_periods":[
{
"id":"1", "startDate": new Date("2015-03-01T06:00:00Z"), "endDate": new Date("2015-05-31T05:00:00Z"),
"weight":null, "title":"Spring", "permissions": { "update":true, "delete":true }
}
]
"grading_periods_read_only": false,
"can_create_grading_periods": true,
"can_toggle_grading_periods": true
@server.respondWith "GET", ENV.GRADING_PERIODS_URL, [200, {"Content-Type":"application/json"}, JSON.stringify @indexData]
GradingPeriodCollectionElement = React.createElement(GradingPeriodCollection)
@gradingPeriodCollection = TestUtils.renderIntoDocument(GradingPeriodCollectionElement)
@server.respond()
teardown: ->
ReactDOM.unmountComponentAtNode(@gradingPeriodCollection.getDOMNode().parentNode)
fakeENV.teardown()
@server.restore()
test 'shows a link to the settings page if the user can toggle the multiple grading periods feature', ->
ok @gradingPeriodCollection.refs.linkToSettings
test 'does not show a link to the settings page if the user cannot toggle the multiple grading periods feature', ->
@gradingPeriodCollection.setState(canChangeGradingPeriodsSetting: false)
notOk @gradingPeriodCollection.refs.linkToSettings
QUnit.module 'GradingPeriodCollection with read-only grading periods',
setup: ->
@server = sinon.fakeServer.create()
@ -319,19 +272,7 @@ define [
}
]
"grading_periods_read_only": true,
"can_create_grading_periods": true,
"can_toggle_grading_periods": true
@formattedIndexData =
"grading_periods":[
{
"id":"1", "startDate": new Date("2015-03-01T06:00:00Z"), "endDate": new Date("2015-05-31T05:00:00Z"),
"weight":null, "title":"Spring", "permissions": { "update":true, "delete":true }
}
]
"grading_periods_read_only": true,
"can_create_grading_periods": true,
"can_toggle_grading_periods": true
"can_create_grading_periods": true
@server.respondWith "GET", ENV.GRADING_PERIODS_URL, [200, {"Content-Type":"application/json"}, JSON.stringify @indexData]
GradingPeriodCollectionElement = React.createElement(GradingPeriodCollection)

View File

@ -304,7 +304,7 @@ define [
equal errors["name"][0]["message"], "Name is required!"
test "requires due_at to be in an open grading period if it is being changed and the user is a teacher", ->
ENV.MULTIPLE_GRADING_PERIODS_ENABLED = true
ENV.HAS_GRADING_PERIODS = true
ENV.active_grading_periods = [{
id: "1"
start_date: "2103-07-01T06:00:00Z"

View File

@ -82,8 +82,6 @@ describe AssignmentGroupsController do
context 'given an assignment group with and without integration data' do
before(:once) do
root_account.allow_feature!(:multiple_grading_periods)
root_account.enable_feature!(:multiple_grading_periods)
account_admin_user(account: root_account)
end
@ -127,8 +125,6 @@ describe AssignmentGroupsController do
context 'given a root account with a grading period and a sub account with a grading period' do
before(:once) do
root_account.allow_feature!(:multiple_grading_periods)
root_account.enable_feature!(:multiple_grading_periods)
account_admin_user(account: root_account)
end
@ -241,18 +237,6 @@ describe AssignmentGroupsController do
expect(response).to be_success
end
end
context 'multiple grading periods feature enabled' do
before do
@course.root_account.enable_feature!(:multiple_grading_periods)
end
it 'does not throw an error when grading_period_id is passed in as empty string' do
user_session(@teacher)
get 'index', :course_id => @course.id, :include => ['assignments', 'assignment_visibility'], :grading_period_id => '', :format => :json
expect(response).to be_success
end
end
end
describe 'passing include_param submission', type: :request do
@ -365,9 +349,8 @@ describe AssignmentGroupsController do
expect(@group1.assignments.count).to eq(3)
end
context 'with multiple grading periods enabled' do
context 'with grading periods' do
before :once do
@course.root_account.enable_feature!(:multiple_grading_periods)
group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = group

View File

@ -424,7 +424,7 @@ describe AssignmentsController do
end
describe "GET 'edit'" do
include_context "multiple grading periods within controller" do
include_context "grading periods within controller" do
let(:course) { @course }
let(:teacher) { @teacher }
let(:request_params) { [:edit, course_id: course, id: @assignment] }

View File

@ -616,14 +616,15 @@ describe DiscussionTopicsController do
course_topic
end
include_context "multiple grading periods within controller" do
include_context "grading periods within controller" do
let(:course) { @course }
let(:teacher) { @teacher }
let(:request_params) { [:edit, course_id: course, id: @topic] }
end
it "should not explode with mgp and group context" do
@course.root_account.enable_feature!(:multiple_grading_periods)
group1 = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
group1.enrollment_terms << @course.enrollment_term
user_session(@teacher)
group = group_model(:context => @course)
group_topic = group.discussion_topics.create!(:title => "title")

View File

@ -315,9 +315,16 @@ describe GradebooksController do
end
end
context "Multiple Grading Periods" do
context "with grading periods" do
let(:group_helper) { Factories::GradingPeriodGroupHelper.new }
let(:period_helper) { Factories::GradingPeriodHelper.new }
before :once do
@course.root_account.enable_feature!(:multiple_grading_periods)
@grading_period_group = group_helper.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = @grading_period_group
term.save!
@grading_periods = period_helper.create_presets_for_group(@grading_period_group, :past, :current, :future)
end
it "does not display totals if 'All Grading Periods' is selected" do
@ -332,6 +339,37 @@ describe GradebooksController do
get 'grade_summary', :course_id => @course.id, :id => @student.id, grading_period_id: 1
expect(assigns[:exclude_total]).to eq false
end
it "includes the grading period group (as 'set') in the ENV" do
user_session(@teacher)
get :grade_summary, { course_id: @course.id, id: @student.id }
grading_period_set = assigns[:js_env][:grading_period_set]
expect(grading_period_set[:id]).to eq @grading_period_group.id
end
it "includes grading periods within the group" do
user_session(@teacher)
get :grade_summary, { course_id: @course.id, id: @student.id }
grading_period_set = assigns[:js_env][:grading_period_set]
expect(grading_period_set[:grading_periods].count).to eq 3
period = grading_period_set[:grading_periods][0]
expect(period).to have_key(:is_closed)
expect(period).to have_key(:is_last)
end
it "includes necessary keys with each grading period" do
user_session(@teacher)
get :grade_summary, { course_id: @course.id, id: @student.id }
periods = assigns[:js_env][:grading_period_set][:grading_periods]
periods.each do |period|
expect(period).to have_key(:id)
expect(period).to have_key(:start_date)
expect(period).to have_key(:end_date)
expect(period).to have_key(:close_date)
expect(period).to have_key(:is_closed)
expect(period).to have_key(:is_last)
end
end
end
context "with assignment due date overrides" do
@ -558,6 +596,49 @@ describe GradebooksController do
expect(assigns[:js_env][:STUDENT_CONTEXT_CARDS_ENABLED]).to eq true
end
end
context "with grading periods" do
let(:group_helper) { Factories::GradingPeriodGroupHelper.new }
let(:period_helper) { Factories::GradingPeriodHelper.new }
before :once do
@grading_period_group = group_helper.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = @grading_period_group
term.save!
@grading_periods = period_helper.create_presets_for_group(@grading_period_group, :past, :current, :future)
end
before { user_session(@teacher) }
it "includes the grading period group (as 'set') in the ENV" do
get :show, { course_id: @course.id }
grading_period_set = assigns[:js_env][:GRADEBOOK_OPTIONS][:grading_period_set]
expect(grading_period_set[:id]).to eq @grading_period_group.id
end
it "includes grading periods within the group" do
get :show, { course_id: @course.id }
grading_period_set = assigns[:js_env][:GRADEBOOK_OPTIONS][:grading_period_set]
expect(grading_period_set[:grading_periods].count).to eq 3
period = grading_period_set[:grading_periods][0]
expect(period).to have_key(:is_closed)
expect(period).to have_key(:is_last)
end
it "includes necessary keys with each grading period" do
get :show, { course_id: @course.id }
periods = assigns[:js_env][:GRADEBOOK_OPTIONS][:grading_period_set][:grading_periods]
periods.each do |period|
expect(period).to have_key(:id)
expect(period).to have_key(:start_date)
expect(period).to have_key(:end_date)
expect(period).to have_key(:close_date)
expect(period).to have_key(:is_closed)
expect(period).to have_key(:is_last)
end
end
end
end
describe "GET 'change_gradebook_version'" do

View File

@ -9,8 +9,6 @@ RSpec.describe GradingPeriodSetsController, type: :controller do
let(:valid_session) { {} }
before do
root_account.allow_feature!(:multiple_grading_periods)
root_account.enable_feature!(:multiple_grading_periods)
request.accept = 'application/json'
@root_user = root_account.users.create! do |user|
user.accept_terms
@ -44,7 +42,7 @@ RSpec.describe GradingPeriodSetsController, type: :controller do
post :create, {
account_id: root_account.to_param,
enrollment_term_ids: [enrollment_term.to_param],
grading_period_set: group_helper.valid_attributes
grading_period_set: group_helper.valid_attributes(weighted: true)
}, valid_session
end
@ -58,6 +56,7 @@ RSpec.describe GradingPeriodSetsController, type: :controller do
set_json = json_parse.fetch('grading_period_set')
expect(response.status).to eql Rack::Utils.status_code(:created)
expect(set_json["title"]).to eql group_helper.valid_attributes[:title]
expect(set_json["weighted"]).to be true
end
end
@ -87,7 +86,7 @@ RSpec.describe GradingPeriodSetsController, type: :controller do
end
describe "PATCH #update" do
let(:new_attributes) { { title: 'An updated title!' } }
let(:new_attributes) { { title: 'An updated title!', weighted: false } }
let(:grading_period_set) { group_helper.create_for_account(root_account) }
context "with valid params" do
@ -104,12 +103,26 @@ RSpec.describe GradingPeriodSetsController, type: :controller do
patch_update
grading_period_set.reload
expect(grading_period_set.title).to eql new_attributes.fetch(:title)
expect(grading_period_set.weighted).to eql new_attributes.fetch(:weighted)
end
it "returns no content" do
patch_update
expect(response.status).to eql Rack::Utils.status_code(:no_content)
end
it 'recomputes grades when an enrollment term is removed from the set' do
term = root_account.enrollment_terms.create!
root_account.courses.create!(enrollment_term: term)
grading_period_set.enrollment_terms << term
Enrollment.expects(:recompute_final_score).once
patch :update, {
account_id: root_account.to_param,
id: grading_period_set.to_param,
enrollment_term_ids: [],
grading_period_set: new_attributes
}, valid_session
end
end
it "defaults enrollment_term_ids to empty array" do

View File

@ -61,7 +61,6 @@ describe GradingPeriodsController do
before do
account_admin_user(account: root_account)
user_session(@admin)
root_account.enable_feature!(:multiple_grading_periods)
request.accept = 'application/json'
end
@ -76,65 +75,16 @@ describe GradingPeriodsController do
describe 'with root account admins' do
it 'disallows creating grading periods' do
root_account.enable_feature!(:multiple_grading_periods)
get :index, { course_id: course.id }
expect(json_parse['can_create_grading_periods']).to eql(false)
end
it 'allows toggling grading periods when multiple grading periods are enabled' do
root_account.enable_feature!(:multiple_grading_periods)
get :index, { course_id: course.id }
expect(json_parse['can_toggle_grading_periods']).to eql(true)
end
it "returns 'not found' when multiple grading periods are allowed" do
root_account.allow_feature!(:multiple_grading_periods)
get :index, { course_id: course.id }
expect(response).to be_not_found
end
it "returns 'not found' when multiple grading periods are disabled" do
root_account.disable_feature!(:multiple_grading_periods)
get :index, { course_id: course.id }
expect(response).to be_not_found
expect(json_parse['can_create_grading_periods']).to be false
end
end
describe 'with sub account admins' do
it 'disallows creating grading periods' do
root_account.enable_feature!(:multiple_grading_periods)
login_sub_account
get :index, { course_id: course.id }
expect(json_parse['can_create_grading_periods']).to eql(false)
end
it 'disallows toggling grading periods when multiple grading periods are root account enabled' do
root_account.enable_feature!(:multiple_grading_periods)
login_sub_account
get :index, { course_id: course.id }
expect(json_parse['can_toggle_grading_periods']).to eql(false)
end
it "returns 'not found' when multiple grading periods are root account disabled" do
root_account.disable_feature!(:multiple_grading_periods)
login_sub_account
get :index, { course_id: course.id }
expect(response).to be_not_found
end
it "returns 'not found' when multiple grading periods are allowed" do
root_account.allow_feature!(:multiple_grading_periods)
login_sub_account
get :index, { course_id: course.id }
expect(response).to be_not_found
end
it "returns 'not found' when multiple grading periods are sub account disabled" do
root_account.allow_feature!(:multiple_grading_periods)
sub_account.disable_feature!(:multiple_grading_periods)
login_sub_account
get :index, { course_id: course.id }
expect(response).to be_not_found
expect(json_parse['can_create_grading_periods']).to be false
end
end

View File

@ -186,7 +186,7 @@ describe Quizzes::QuizzesController do
describe "GET 'edit'" do
before(:once) { course_quiz }
include_context "multiple grading periods within controller" do
include_context "grading periods within controller" do
let(:course) { @course }
let(:teacher) { @teacher }
let(:request_params) { [:edit, course_id: course, id: @quiz] }
@ -1066,7 +1066,7 @@ describe Quizzes::QuizzesController do
expect(@student.recent_stream_items.map {|item| item.data['notification_id']}).not_to include notification.id
end
context "with multiple grading periods enabled" do
context "with grading periods" do
def call_create(params)
post('create', course_id: @course.id, quiz: {
title: "Example Quiz", quiz_type: "assignment"
@ -1077,7 +1077,6 @@ describe Quizzes::QuizzesController do
before :once do
teacher_in_course(active_all: true)
@course.root_account.enable_feature!(:multiple_grading_periods)
grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = grading_period_group
@ -1420,7 +1419,7 @@ describe Quizzes::QuizzesController do
end
end
context "with multiple grading periods enabled" do
context "with grading periods" do
def create_quiz(attr)
@course.quizzes.create!({ title: "Example Quiz", quiz_type: "assignment" }.merge(attr))
end
@ -1443,7 +1442,6 @@ describe Quizzes::QuizzesController do
before :once do
teacher_in_course(active_all: true)
@course.root_account.enable_feature!(:multiple_grading_periods)
grading_period_group = Factories::GradingPeriodGroupHelper.new.create_for_account(@course.root_account)
term = @course.enrollment_term
term.grading_period_group = grading_period_group

View File

@ -607,7 +607,6 @@ describe UsersController do
describe "GET 'grades_for_student'" do
let(:test_course) do
test_course = course_factory(active_all: true)
test_course.root_account.enable_feature!(:multiple_grading_periods)
test_course
end
let(:student) { user_factory(active_all: true) }
@ -632,13 +631,13 @@ describe UsersController do
end
context "as a student" do
it "returns the grade and the total for the student, filtered by the grading period" do
it "returns the grade for the student, filtered by the grading period" do
user_session(student)
get('grades_for_student', grading_period_id: grading_period.id,
enrollment_id: student_enrollment.id)
expect(response).to be_ok
expected_response = {'grade' => 40, 'total' => 4, 'possible' => 10, 'hide_final_grades' => false}
expected_response = {'grade' => 40.0, 'hide_final_grades' => false}
expect(json_parse(response.body)).to eq expected_response
grading_period.end_date = 4.months.from_now
@ -648,7 +647,7 @@ describe UsersController do
enrollment_id: student_enrollment.id)
expect(response).to be_ok
expected_response = {'grade' => 94.55, 'total' => 104, 'possible' => 110, 'hide_final_grades' => false}
expected_response = {'grade' => 94.55, 'hide_final_grades' => false}
expect(json_parse(response.body)).to eq expected_response
end
@ -660,7 +659,7 @@ describe UsersController do
enrollment_id: student_enrollment.id)
expect(response).to be_ok
expected_response = {'grade' => 94.55, 'total' => 104, 'possible' => 110, 'hide_final_grades' => false}
expected_response = {'grade' => 94.55, 'hide_final_grades' => false}
expect(json_parse(response.body)).to eq expected_response
end
@ -686,7 +685,7 @@ describe UsersController do
grading_period_id: grading_period.id)
expect(response).to be_ok
expected_response = {'grade' => 40, 'total' => 4, 'possible' => 10, 'hide_final_grades' => false}
expected_response = {'grade' => 40.0, 'hide_final_grades' => false}
expect(json_parse(response.body)).to eq expected_response
grading_period.end_date = 4.months.from_now
@ -696,7 +695,7 @@ describe UsersController do
enrollment_id: student_enrollment.id)
expect(response).to be_ok
expected_response = {'grade' => 94.55, 'total' => 104, 'possible' => 110, 'hide_final_grades' => false}
expected_response = {'grade' => 94.55, 'hide_final_grades' => false}
expect(json_parse(response.body)).to eq expected_response
end
@ -709,7 +708,7 @@ describe UsersController do
enrollment_id: student_enrollment.id)
expect(response).to be_ok
expected_response = {'grade' => 94.55, 'total' => 104, 'possible' => 110, 'hide_final_grades' => false}
expected_response = {'grade' => 94.55, 'hide_final_grades' => false}
expect(json_parse(response.body)).to eq expected_response
end
@ -746,29 +745,7 @@ describe UsersController do
observer
end
context "with Multiple Grading periods disabled" do
it "returns grades of observees" do
user_session(observer)
get 'grades'
grades = assigns[:grades][:observed_enrollments][test_course.id]
expect(grades.length).to eq 2
expect(grades.key?(student1.id)).to eq true
expect(grades.key?(student2.id)).to eq true
end
it "returns an empty hash for grading periods" do
user_session(observer)
get 'grades'
grading_periods = assigns[:grading_periods]
expect(grading_periods).to be_empty
end
end
context "with Multiple Grading Periods enabled" do
before(:once) { course_factory.root_account.enable_feature!(:multiple_grading_periods) }
context "with grading periods" do
it "returns the grading periods" do
user_session(observer)
get 'grades'
@ -817,30 +794,8 @@ describe UsersController do
course_with_user('StudentEnrollment', course: another_test_course, user: student, active_all: true)
student
end
context "with Multiple Grading periods disabled" do
it "returns grades" do
user_session(test_student)
get 'grades'
grades = assigns[:grades][:student_enrollments]
expect(grades.length).to eq 2
expect(grades.key?(test_course.id)).to eq true
expect(grades.key?(another_test_course.id)).to eq true
end
it "returns an empty hash for grading periods" do
user_session(test_student)
get 'grades'
grading_periods = assigns[:grading_periods]
expect(grading_periods).to be_empty
end
end
context "with Multiple Grading Periods enabled" do
before(:once) { course_factory.root_account.enable_feature!(:multiple_grading_periods) }
context "with grading periods" do
it "returns the grading periods" do
user_session(test_student)
get 'grades'
@ -859,6 +814,18 @@ describe UsersController do
expect(selected_period_id).to eq grading_period.global_id
end
it "returns the grade for the current grading period, if one exists " \
"and no grading period is passed in" do
assignment = test_course.assignments.create!(
due_at: 3.days.from_now(grading_period.end_date),
points_possible: 10
)
assignment.grade_student(test_student, grader: test_course.teachers.first, grade: 10)
user_session(test_student)
get :grades
expect(assigns[:grades][:student_enrollments][test_course.id]).to be_nil
end
it "returns 0 (signifying 'All Grading Periods') if no current " \
"grading period exists and no grading period parameter is passed in" do
grading_period.start_date = 1.month.from_now
@ -870,6 +837,19 @@ describe UsersController do
expect(selected_period_id).to eq 0
end
it "returns the grade for 'All Grading Periods' if no current " \
"grading period exists and no grading period is passed in" do
grading_period.update!(start_date: 1.month.from_now)
assignment = test_course.assignments.create!(
due_at: 3.days.from_now(grading_period.end_date),
points_possible: 10
)
assignment.grade_student(test_student, grader: test_course.teachers.first, grade: 10)
user_session(test_student)
get :grades
expect(assigns[:grades][:student_enrollments][test_course.id]).to eq(100.0)
end
it "returns the grading_period_id passed in, if one is provided along with a course_id" do
user_session(test_student)
get 'grades', course_id: test_course.id, grading_period_id: 2939
@ -885,7 +865,6 @@ describe UsersController do
course_with_user('StudentEnrollment', course: test_course, user: student1, active_all: true)
@shard1.activate do
account = Account.create!
account.enable_feature!(:multiple_grading_periods)
@course2 = course_factory(active_all: true, account: account)
course_with_user('StudentEnrollment', course: @course2, user: student1, active_all: true)
grading_period_group2 = group_helper.legacy_create_for_course(@course2)

View File

@ -16,12 +16,11 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# this factory creates an Account with the multiple_grading_periods feature flag enabled.
# this factory creates an Account with n grading periods.
# it also creates two grading periods for the account
# the grading_periods both have a weight of 1
module Factories
def grading_periods(options = {})
Account.default.set_feature_flag! :multiple_grading_periods, 'on'
course = options[:context] || @course || course_factory()
count = options[:count] || 2
@ -38,10 +37,7 @@ module Factories
end
def create_grading_periods_for(course, opts={})
opts = { mgp_flag_enabled: true }.merge(opts)
course.root_account = Account.default if !course.root_account
course.root_account.enable_feature!(:multiple_grading_periods) if opts[:mgp_flag_enabled]
course.root_account = Account.default unless course.root_account
gp_group = Factories::GradingPeriodGroupHelper.new.legacy_create_for_course(course)
class_name = course.class.name.demodulize
timeframes = opts[:grading_periods] || [:current]

Some files were not shown because too many files have changed in this diff Show More