From 8f862215c62ef29920a4ec986e0b03e0ce661714 Mon Sep 17 00:00:00 2001 From: Jeremy Neander Date: Tue, 13 Dec 2016 14:01:55 -0600 Subject: [PATCH 1/8] calculate grades with weighted grading periods Grading periods can have weights, which must be incorporated into grade calculations on the front end. Courses without weighted grading periods must calculate grades exactly as they did before these changes were applied. All courses with weighted grading periods will calculate grades according to the rules written in the specs as well as the test plan below. closes CNVS-33572 test plan: - Grading period weights currently can only be set via Rails console. - acceptance criteria must be met for: * Default Gradebook * Screenreader Gradebook * Student Grade Summary Page A. select or create 1. an account with grading periods enabled 2. a course with assignment group weighting enabled 3. two grading periods ('GP1' and 'GP2') 4. a teacher for the course 5. a student enrolled in the course 6. an assignment group 'AG1' a. with a weight of 60 b. with an assignment 'A1' i. due in GP1 ii. worth 10 points c. with an assignment 'A2' i. due in GP1 ii. worth 10 points 7. an assignment group 'AG2' a. with a weight of 20 b. with an assignment 'A3' i. due in GP2 ii. worth 20 points 8. an assignment group 'AG3' a. with a weight of 20 b. with an assignment 'A4' i. due in GP2 ii. worth 40 points B. set scores and verify grades 1. score A1 with 10 points 2. score A2 with 5 points 3. score A3 with 12 points 4. score A4 with 16 points 5. confirm course grade of 62.5 % C. verify alternate calculations * NOTE: restore state above before each example 1. without assignment group weighting a. disable assignment group weighting b. confirm course grade of 60.83 % 2. with sub-100 grading period weight a. set each grading period weight to 5 b. confirm course grade of 62.5 % 3. with over-100 grading period weight a. set each grading period weight to 100 b. confirm course grade of 125 % 4. with unequal grading period weights a. set GP1 weight to 25 b. set GP2 weight to 75 c. confirm course grade of 56.25 % 5. with an empty grading period a. delete AG2 and AG3 b. confirm course grade of 37.5 % 6. with an empty assignment group a. delete assignments in AG2 b. confirm course grade of 57.5 % 7. with a null grading period weight a. set GP1 weight to null b. confirm course grade of 50 % 8. with null grading period weights a. set GP1 weight to null b. set GP2 weight to null c. confirm course grade of 65 % D. smoke test for grades without grading periods Change-Id: I83af44fe574fa65b83a23c69b104537dcdae54ca Reviewed-on: https://gerrit.instructure.com/98289 Tested-by: Jenkins Reviewed-by: Keith T. Garner Reviewed-by: Spencer Olson QA-Review: Anju Reddy Product-Review: Keith T. Garner --- .../api/gradingPeriodsApi.coffee | 2 + .../screenreader_gradebook_controller.coffee | 39 +- .../screenreader_gradebook.spec.coffee | 49 +- .../tests/start_app.coffee | 5 - app/coffeescripts/gradebook2/Gradebook.coffee | 27 +- app/controllers/gradebooks_controller.rb | 4 +- .../AssignmentGroupGradeCalculator.jsx | 304 ++++++------ app/jsx/gradebook/CourseGradeCalculator.jsx | 237 +++++++-- app/jsx/gradebook/EffectiveDueDates.jsx | 17 + app/jsx/gradebook/GradingSchemeHelper.jsx | 13 +- app/models/grading_period.rb | 4 +- public/javascripts/grade_summary.js | 33 +- .../api/gradingPeriodsApiSpec.coffee | 21 +- .../gradebook2/GradebookSpec.coffee | 61 ++- .../controllers/gradebooks_controller_spec.rb | 7 + spec/javascripts/jsx/grade_summary.spec.jsx | 61 ++- .../AssignmentGroupGradeCalculatorSpec.jsx | 465 +++++++++--------- .../gradebook/CourseGradeCalculatorSpec.jsx | 435 +++++++++++++--- .../jsx/gradebook/EffectiveDueDatesSpec.jsx | 70 +++ .../jsx/gradebook/GradingSchemeHelperSpec.jsx | 6 +- spec/models/grading_period_spec.rb | 7 +- 21 files changed, 1325 insertions(+), 542 deletions(-) create mode 100644 app/jsx/gradebook/EffectiveDueDates.jsx create mode 100644 spec/javascripts/jsx/gradebook/EffectiveDueDatesSpec.jsx diff --git a/app/coffeescripts/api/gradingPeriodsApi.coffee b/app/coffeescripts/api/gradingPeriodsApi.coffee index 0a43b52f8a3..2cbaa6a8170 100644 --- a/app/coffeescripts/api/gradingPeriodsApi.coffee +++ b/app/coffeescripts/api/gradingPeriodsApi.coffee @@ -15,6 +15,7 @@ define [ start_date: period.startDate end_date: period.endDate close_date: period.closeDate + weight: period.weight } grading_periods: serialized @@ -30,6 +31,7 @@ define [ closeDate: new Date(period.close_date || period.end_date) isLast: period.is_last isClosed: period.is_closed + weight: period.weight } batchUpdate: (setId, periods) -> diff --git a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee index 7b3e8f9d316..3d76e24192e 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee @@ -11,6 +11,7 @@ define [ 'compiled/AssignmentDetailsDialog' 'compiled/AssignmentMuter' 'jsx/gradebook/CourseGradeCalculator' + 'jsx/gradebook/EffectiveDueDates' 'compiled/gradebook2/OutcomeGradebookGrid' '../../shared/components/ic_submission_download_dialog_component' 'str/htmlEscape' @@ -19,10 +20,9 @@ define [ 'compiled/api/gradingPeriodsApi' 'jquery.instructure_date_and_time' ], ( - ajax, round, userSettings, fetchAllPages, parseLinkHeader, I18n, Ember, _, tz, - AssignmentDetailsDialog, AssignmentMuter, CourseGradeCalculator, outcomeGrid, - ic_submission_download_dialog, htmlEscape, CalculationMethodContent, SubmissionStateMap, - GradingPeriodsAPI + ajax, round, userSettings, fetchAllPages, parseLinkHeader, I18n, Ember, _, tz, AssignmentDetailsDialog, + AssignmentMuter, CourseGradeCalculator, EffectiveDueDates, outcomeGrid, ic_submission_download_dialog, + htmlEscape, CalculationMethodContent, SubmissionStateMap, GradingPeriodsAPI ) -> {get, set, setProperties} = Ember @@ -98,6 +98,12 @@ define [ mgpEnabled: get(window, 'ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled') + gradingPeriodData: + (-> + periods = get(window, 'ENV.GRADEBOOK_OPTIONS.active_grading_periods') + GradingPeriodsAPI.deserializePeriods(periods) + )() + gradingPeriods: (-> periods = get(window, 'ENV.GRADEBOOK_OPTIONS.active_grading_periods') @@ -290,8 +296,14 @@ define [ id != userId set(assignment, 'assignment_visibility', filteredVisibilities) - calculate: (submissionsArray) -> - CourseGradeCalculator.calculate submissionsArray, @assignmentGroupsHash(), @get('weightingScheme') + calculate: (student) -> + mgpEnabled = @get('mgpEnabled') + submissions = @submissionsForStudent(student) + assignmentGroups = @assignmentGroupsHash() + weightingScheme = @get('weightingScheme') + gradingPeriods = @gradingPeriodData if mgpEnabled + effectiveDueDates = EffectiveDueDates.scopeToUser(@get('effectiveDueDates.content'), student.id) if mgpEnabled + CourseGradeCalculator.calculate(submissions, assignmentGroups, weightingScheme, gradingPeriods, effectiveDueDates) submissionsForStudent: (student) -> allSubmissions = (value for key, value of student when key.match /^assignment_(?!group)/) @@ -306,22 +318,25 @@ define [ calculateStudentGrade: (student) -> if student.isLoaded finalOrCurrent = if @get('includeUngradedAssignments') then 'final' else 'current' - result = @calculate(@submissionsForStudent(student)) - for group in result.group_sums + grades = @calculate(student) + for group in grades.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 = grades[finalOrCurrent] - percent = round (result.score / result.possible * 100), 2 + percent = round (grades.score / grades.possible * 100), 2 percent = 0 if isNaN(percent) setProperties student, - total_grade: result + total_grade: grades total_percent: percent 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" diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee index 13121bd71c4..10bc3df64d4 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee @@ -7,8 +7,9 @@ define [ '../shared_ajax_fixtures' '../../controllers/screenreader_gradebook_controller' 'compiled/userSettings' + 'jsx/gradebook/CourseGradeCalculator' 'vendor/jquery.ba-tinypubsub' -], ($, _, ajax, startApp, Ember, fixtures, SRGBController, userSettings) -> +], ($, _, ajax, startApp, Ember, fixtures, SRGBController, userSettings, CourseGradeCalculator) -> workAroundRaceCondition = -> ajax.request() @@ -436,6 +437,52 @@ define [ workAroundRaceCondition().then => equal @srgb.get('students.firstObject.total_percent'), 0 + 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 } } + props = _.defaults options, + mgpEnabled: true + gradingPeriodData: [{ id: 701, weight: 50 }, { id: 702, weight: 50 }] + weightingScheme: 'points' + '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.gradingPeriodData) + + 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 grading periods are disabled", -> + self = @setupThis(mgpEnabled: false) + @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(args[3], undefined) + equal(args[4], undefined) module 'screenreader_gradebook_controller: notes computed props', setup: -> diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/start_app.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/start_app.coffee index c9e837003fb..1e47ad8dc37 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/start_app.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/start_app.coffee @@ -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() diff --git a/app/coffeescripts/gradebook2/Gradebook.coffee b/app/coffeescripts/gradebook2/Gradebook.coffee index 992dabbd29b..efe8d550a81 100644 --- a/app/coffeescripts/gradebook2/Gradebook.coffee +++ b/app/coffeescripts/gradebook2/Gradebook.coffee @@ -17,6 +17,7 @@ define [ 'i18n!gradebook2' 'compiled/gradebook2/GradebookTranslations' 'jsx/gradebook/CourseGradeCalculator' + 'jsx/gradebook/EffectiveDueDates' 'jsx/gradebook/GradingSchemeHelper' 'compiled/userSettings' 'spin.js' @@ -58,11 +59,11 @@ define [ ], ( $, _, Backbone, tz, DataLoader, React, ReactDOM, LongTextEditor, KeyboardNavDialog, KeyboardNavTemplate, Slick, TotalColumnHeaderView, round, InputFilterView, I18n, GRADEBOOK_TRANSLATIONS, CourseGradeCalculator, - GradingSchemeHelper, UserSettings, Spinner, SubmissionDetailsDialog, AssignmentGroupWeightsDialog, - GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell, GradebookHeaderMenu, NumberCompare, htmlEscape, - PostGradesStore, PostGradesApp, SubmissionStateMap, ColumnHeaderTemplate, GroupTotalCellTemplate, - RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, assignmentHelper, - GradingPeriodsAPI + EffectiveDueDates, GradingSchemeHelper, UserSettings, Spinner, SubmissionDetailsDialog, + AssignmentGroupWeightsDialog, GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell, + GradebookHeaderMenu, NumberCompare, htmlEscape, PostGradesStore, PostGradesApp, SubmissionStateMap, + ColumnHeaderTemplate, GroupTotalCellTemplate, RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView, + GradebookKeyboardNav, assignmentHelper, GradingPeriodsAPI ) -> class Gradebook @@ -714,17 +715,23 @@ define [ calculateStudentGrade: (student) => if student.loaded and student.initialized - finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current' - result = CourseGradeCalculator.calculate( + gradingPeriods = @gradingPeriods if @gradingPeriodsEnabled + effectiveDueDates = EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if @gradingPeriodsEnabled + + grades = CourseGradeCalculator.calculate( @submissionsForStudent(student), @assignmentGroups, - @options.group_weighting_scheme + @options.group_weighting_scheme, + gradingPeriods, + effectiveDueDates ) - for group in result.group_sums + + finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current' + for group in grades.group_sums student["assignment_group_#{group.group.id}"] = group[finalOrCurrent] for submissionData in group[finalOrCurrent].submissions submissionData.submission.drop = submissionData.drop - student["total_grade"] = result[finalOrCurrent] + student["total_grade"] = grades[finalOrCurrent] @addDroppedClass(student) diff --git a/app/controllers/gradebooks_controller.rb b/app/controllers/gradebooks_controller.rb index cc9feac7773..b0dfbf61c5e 100644 --- a/app/controllers/gradebooks_controller.rb +++ b/app/controllers/gradebooks_controller.rb @@ -62,6 +62,7 @@ class GradebooksController < ApplicationController if multiple_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) || @@ -101,6 +101,8 @@ class GradebooksController < ApplicationController show_total_grade_as_points: @context.settings[:show_total_grade_as_points], grading_scheme: grading_scheme, 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) diff --git a/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx b/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx index 77f5cf16fee..e02094c3f59 100644 --- a/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx +++ b/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx @@ -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,120 +157,113 @@ 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, submission_count: _.filter(submissionData, 'submitted').length, - submissions: _.map(submissionData, function (submission) { - return { + submissions: _.map(submissionData, submission => ( + { drop: submission.drop, percent: parseScore(submission.score / submission.total), possible: submission.total, score: parseScore(submission.score), submission: submission.submission, submitted: submission.submitted - }; - }) + } + )) }; - }; + } // Each submission requires the following properties: // * score: number @@ -281,8 +272,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`. // // An assignment group requires the following properties: // * id: Canvas id @@ -300,17 +290,29 @@ define([ // * points_possible: non-negative number // * submission_types: [array of strings] // - // The weighting scheme is one of [`percent`, `points`] + // AssignmentGroup Grade information has the following shape: + // { + // score: number|null + // possible: number|null + // weight: non-negative 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 has the following shape: + // { + // group: + // current: + // final: + // } + function calculate (allSubmissions, assignmentGroup) { + const submissions = _.uniq(allSubmissions, 'assignment_id'); return { group: assignmentGroup, - current: calculateGroupSum(assignmentGroup, submissions, false), - final: calculateGroupSum(assignmentGroup, submissions, true) + current: calculateGroupGrade(assignmentGroup, submissions, false), + final: calculateGroupGrade(assignmentGroup, submissions, true) }; - }; + } return { calculate diff --git a/app/jsx/gradebook/CourseGradeCalculator.jsx b/app/jsx/gradebook/CourseGradeCalculator.jsx index 4e80084f160..c03d99059be 100644 --- a/app/jsx/gradebook/CourseGradeCalculator.jsx +++ b/app/jsx/gradebook/CourseGradeCalculator.jsx @@ -20,51 +20,158 @@ 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 }) => { + function getWeightedPercent ({ score, possible, weight }) { return (score / possible) * weight; - }; + } - 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 sumVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current; + return { ...sumVersion, weight: assignmentGroupGrade.group.group_weight }; }); - 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 gradesVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current; + return { ...gradesVersion, weight: gradingPeriodGrade.weight }; + }); + + const weightedScores = _.map(scopedGradingPeriodGrades, (gradingPeriodGrade) => { + if (gradingPeriodGrade.score) { + return getWeightedPercent(gradingPeriodGrade); + } + return 0; + }); + + const totalWeight = sumBy(scopedGradingPeriodGrades, 'weight'); + const totalScore = (sum(weightedScores) * 100) / Math.min(totalWeight, 100); + + return { + score: round(totalScore, 2), + possible: 100 + }; + } + + function divideGroupByGradingPeriods (assignmentGroup, effectiveDueDates) { + const assignmentsByGradingPeriodId = _.groupBy(assignmentGroup.assignments, assignment => ( + effectiveDueDates[assignment.id].grading_period_id + )); + return _.map(assignmentsByGradingPeriodId, assignments => ( + { ...assignmentGroup, assignments } + )); + } + + function extractUsableAssignmentGroups (assignmentGroups, effectiveDueDates) { + return _.reduce(assignmentGroups, (usableGroups, assignmentGroup) => { + const assignedAssignments = _.filter(assignmentGroup.assignments, assignment => ( + effectiveDueDates[assignment.id] + )); + if (assignedAssignments.length > 0) { + const groupWithAssignedAssignments = { ...assignmentGroup, assignments: assignedAssignments }; + return [ + ...usableGroups, + ...divideGroupByGradingPeriods(groupWithAssignedAssignments, effectiveDueDates) + ]; + } + return usableGroups; + }, []); + } + + function calculateWithGradingPeriods ( + submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options + ) { + const usableGroups = extractUsableAssignmentGroups(assignmentGroups, effectiveDueDates); + + const assignmentGroupsByGradingPeriodId = _.groupBy(usableGroups, (assignmentGroup) => { + const assignmentId = assignmentGroup.assignments[0].id; + return effectiveDueDates[assignmentId].grading_period_id; + }); + + const gradingPeriodsById = _.indexBy(gradingPeriods, 'id'); + const gradingPeriodGradesByPeriodId = {}; + const allAssignmentGroupGrades = []; + + _.forEach(gradingPeriods, (gradingPeriod) => { + const groupGrades = {}; + + (assignmentGroupsByGradingPeriodId[gradingPeriod.id] || []).forEach((assignmentGroup) => { + groupGrades[assignmentGroup.id] = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); + allAssignmentGroupGrades.push(groupGrades[assignmentGroup.id]); + }); + + const groupGradesList = _.values(groupGrades); + + gradingPeriodGradesByPeriodId[gradingPeriod.id] = { + weight: gradingPeriodsById[gradingPeriod.id].weight, + current: combineAssignmentGroupGrades(groupGradesList, false, options), + final: combineAssignmentGroupGrades(groupGradesList, true, options), + assignmentGroups: groupGrades + }; + }); + + if (options.weightGradingPeriods) { + return { + gradingPeriods: gradingPeriodGradesByPeriodId, + group_sums: allAssignmentGroupGrades, + current: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, false, options), + final: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, true, options) + }; + } + + return { + gradingPeriods: gradingPeriodGradesByPeriodId, + group_sums: allAssignmentGroupGrades, + current: combineAssignmentGroupGrades(allAssignmentGroupGrades, false, options), + final: combineAssignmentGroupGrades(allAssignmentGroupGrades, true, options) + }; + } + + function calculateWithoutGradingPeriods (submissions, assignmentGroups, options) { + const assignmentGroupGrades = _.map(assignmentGroups, group => ( + AssignmentGroupGradeCalculator.calculate(submissions, group) + )); + + return { + group_sums: assignmentGroupGrades, + current: combineAssignmentGroupGrades(assignmentGroupGrades, false, options), + final: combineAssignmentGroupGrades(assignmentGroupGrades, true, options) + }; + } // Each submission requires the following properties: // * score: number @@ -73,8 +180,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 +202,72 @@ 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 periods and effective due dates are optional, but must be used + // together. + // + // Each grading period requires the following properties: + // * id: Canvas id + // * weight: non-negative number + // + // `effectiveDueDates` is an object with at least the following shape: + // { + // : { + // grading_period_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. + // + // GradingPeriod Grade information has the following shape: + // { + // : { + // assignmentGroups: { + // : + // } + // } + // } + // + // Course Grade information has the following shape: + // { + // score: number|null + // possible: number|null + // } + // + // 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. + // + // AssignmentGroup Grade information is the returned result from the + // AssignmentGroupGradeCalculator.calculate function. + // + // Return value has the following shape: + // { + // gradingPeriods: + // group_sums: [array of AssignmentGroup Grade information *see above] + // current: + // final: + // } + function calculate (submissions, assignmentGroups, weightingScheme, gradingPeriods, effectiveDueDates) { + const options = { + weightGradingPeriods: _.some(gradingPeriods, 'weight'), + weightAssignmentGroups: weightingScheme === 'percent' }; - }; + + if (gradingPeriods && effectiveDueDates) { + return calculateWithGradingPeriods( + submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options + ); + } + + return calculateWithoutGradingPeriods(submissions, assignmentGroups, options); + } return { calculate diff --git a/app/jsx/gradebook/EffectiveDueDates.jsx b/app/jsx/gradebook/EffectiveDueDates.jsx new file mode 100644 index 00000000000..3e696a01bfb --- /dev/null +++ b/app/jsx/gradebook/EffectiveDueDates.jsx @@ -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 + }; +}); diff --git a/app/jsx/gradebook/GradingSchemeHelper.jsx b/app/jsx/gradebook/GradingSchemeHelper.jsx index 4dbc3a03463..2f0f59f70a0 100644 --- a/app/jsx/gradebook/GradingSchemeHelper.jsx +++ b/app/jsx/gradebook/GradingSchemeHelper.jsx @@ -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 diff --git a/app/models/grading_period.rb b/app/models/grading_period.rb index 4582fdf287f..ac383884b93 100644 --- a/app/models/grading_period.rb +++ b/app/models/grading_period.rb @@ -1,5 +1,5 @@ # -# Copyright (C) 2015-2016 Instructure, Inc. +# Copyright (C) 2015 - 2016 Instructure, Inc. # # This file is part of Canvas. # @@ -149,7 +149,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) diff --git a/public/javascripts/grade_summary.js b/public/javascripts/grade_summary.js index 69ceee77c41..6a4c5e5ebab 100644 --- a/public/javascripts/grade_summary.js +++ b/public/javascripts/grade_summary.js @@ -1,5 +1,5 @@ /** - * Copyright (C) 2011 Instructure, Inc. + * Copyright (C) 2011 - 2016 Instructure, Inc. * * This file is part of Canvas. * @@ -22,6 +22,7 @@ define([ 'jquery' /* $ */, 'underscore', 'jsx/gradebook/CourseGradeCalculator', + 'jsx/gradebook/EffectiveDueDates', 'jsx/gradebook/GradingSchemeHelper', 'compiled/util/round', 'str/htmlEscape', @@ -31,7 +32,24 @@ define([ 'jquery.instructure_misc_plugins' /* showIf */, 'jquery.templateData' /* fillTemplateData, getTemplateData */, 'media_comments' /* mediaComment, mediaCommentThumbnail */ -], function(INST, I18n, $, _, CourseGradeCalculator, GradingSchemeHelper, round, htmlEscape) { +], function (INST, I18n, $, _, CourseGradeCalculator, EffectiveDueDates, GradingSchemeHelper, round, htmlEscape) { + function calculateGrades () { + if (ENV.effective_due_dates) { + return CourseGradeCalculator.calculate( + ENV.submissions, + ENV.assignment_groups, + ENV.group_weighting_scheme, + ENV.grading_periods, + EffectiveDueDates.scopeToUser(ENV.effective_due_dates, ENV.student_id) + ); + } + + return CourseGradeCalculator.calculate( + ENV.submissions, + ENV.assignment_groups, + ENV.group_weighting_scheme + ); + } function updateStudentGrades() { var ignoreUngradedSubmissions = $("#only_consider_graded_assignments").attr('checked'); @@ -39,11 +57,7 @@ define([ var groupWeightingScheme = ENV.group_weighting_scheme; var includeTotal = !ENV.exclude_total; - var calculatedGrades = CourseGradeCalculator.calculate( - ENV.submissions, - ENV.assignment_groups, - groupWeightingScheme - ); + var calculatedGrades = calculateGrades(); $('.dropped').attr('aria-label', ""); $('.dropped').attr('title', ""); @@ -113,7 +127,7 @@ define([ $.screenReaderFlashMessageExclusive(msg); } - if(ENV.grading_scheme) { + if (ENV.grading_scheme) { $(".final_letter_grade .grade").text(GradingSchemeHelper.scoreToGrade(scoreAsPercent, ENV.grading_scheme)); } @@ -420,6 +434,7 @@ define([ return { setup: setup, - calculateTotals: calculateTotals + calculateTotals: calculateTotals, + calculateGrades: calculateGrades } }); diff --git a/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee b/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee index f9427ac6a90..30f6830e122 100644 --- a/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee +++ b/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee @@ -12,7 +12,8 @@ define [ 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", @@ -20,7 +21,8 @@ define [ endDate: new Date("2015-12-31T12:00:00Z"), closeDate: new Date("2016-01-07T12:00:00Z"), isClosed: true, - isLast: true + isLast: true, + weight: 60 } ] @@ -31,13 +33,15 @@ define [ 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") + 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") + close_date: new Date("2016-01-07T12:00:00Z"), + weight: 60 } ] } @@ -51,7 +55,8 @@ define [ 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", @@ -59,7 +64,8 @@ define [ 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 } ] } @@ -92,7 +98,8 @@ define [ title: "Q1", start_date: new Date("2015-09-01T12:00:00Z"), end_date: new Date("2015-10-31T12:00:00Z"), - close_date: null + close_date: null, + weight: 40 } ] } diff --git a/spec/coffeescripts/gradebook2/GradebookSpec.coffee b/spec/coffeescripts/gradebook2/GradebookSpec.coffee index 92a9a1ff441..5618608b5a2 100644 --- a/spec/coffeescripts/gradebook2/GradebookSpec.coffee +++ b/spec/coffeescripts/gradebook2/GradebookSpec.coffee @@ -4,7 +4,66 @@ define [ 'underscore' 'timezone' 'compiled/SubmissionDetailsDialog' -], (Gradebook, DataLoader, _, tz, SubmissionDetailsDialog) -> + 'jsx/gradebook/CourseGradeCalculator' +], (Gradebook, DataLoader, _, tz, SubmissionDetailsDialog, CourseGradeCalculator) -> + module "Gradebook2#calculateStudentGrade", + setupThis:(options = {}) -> + assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }] + submissions = [{ assignment_id: 201, score: 10 }] + defaults = { + gradingPeriodsEnabled: true + assignmentGroups: [{ id: 301, group_weight: 60, rules: {}, assignments }] + options: { group_weighting_scheme: 'points' } + gradingPeriods: [{ id: 701, weight: 50 }, { id: 702, weight: 50 }] + 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(group_sums: []) + @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.gradingPeriods) + + test "scopes effective due dates to the user", -> + self = @setupThis() + @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + @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 grading periods are disabled", -> + self = @setupThis(gradingPeriodsEnabled: false) + @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + @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], undefined) + equal(args[4], undefined) + + test "does not calculate when the student is not loaded", -> + self = @setupThis(gradingPeriodsEnabled: false) + @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + @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(gradingPeriodsEnabled: false) + @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + @calculate.call(self, id: '101', loaded: true, initialized: false) + notOk(CourseGradeCalculator.calculate.called) module "Gradebook2#gradeSort" diff --git a/spec/controllers/gradebooks_controller_spec.rb b/spec/controllers/gradebooks_controller_spec.rb index 18880898314..8260e71253e 100644 --- a/spec/controllers/gradebooks_controller_spec.rb +++ b/spec/controllers/gradebooks_controller_spec.rb @@ -323,6 +323,13 @@ 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 "assigns values for grade calculator to ENV" do + user_session(@teacher) + get 'grade_summary', :course_id => @course.id, :id => @student.id + expect(assigns[:js_env][:grading_periods]).not_to be_nil + expect(assigns[:js_env][:effective_due_dates]).not_to be_nil + end end context "with assignment due date overrides" do diff --git a/spec/javascripts/jsx/grade_summary.spec.jsx b/spec/javascripts/jsx/grade_summary.spec.jsx index b8a1d02eb99..5e544160cef 100644 --- a/spec/javascripts/jsx/grade_summary.spec.jsx +++ b/spec/javascripts/jsx/grade_summary.spec.jsx @@ -1,9 +1,11 @@ define([ + 'jquery', 'helpers/fakeENV', + 'jsx/gradebook/CourseGradeCalculator', 'grade_summary' -], (fakeENV, grade_summary) => { - module('grade_summary#calculateTotals', { - setup() { +], ($, fakeENV, CourseGradeCalculator, grade_summary) => { + module('grade_summary.calculateTotals', { + setup () { fakeENV.setup(); this.screenReaderFlashMessageExclusive = this.stub($, 'screenReaderFlashMessageExclusive'); @@ -111,21 +113,68 @@ define([ }; }, - teardown() { + teardown () { fakeENV.teardown(); } }); - test('generates a screenreader-only alert when grades have been changed', function() { + test('generates a screenreader-only alert when grades have been changed', function () { grade_summary.calculateTotals(this.calculatedGrades, this.currentOrFinal, this.groupWeightingScheme); ok(this.screenReaderFlashMessageExclusive.calledOnce); }); - test('does not generate a screenreader-only alert when grades are unchanged', function() { + test('does not generate a screenreader-only alert when grades are unchanged', function () { $('#fixtures').html(''); grade_summary.calculateTotals(this.calculatedGrades, this.currentOrFinal, this.groupWeightingScheme); notOk(this.screenReaderFlashMessageExclusive.called); }); + + module('grade_summary.calculateGrades', { + setup () { + fakeENV.setup(); + ENV.submissions = [{ assignment_id: 201, score: 10 }]; + const assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }]; + ENV.assignment_groups = [{ id: 301, group_weight: 60, rules: {}, assignments }]; + ENV.group_weighting_scheme = 'points'; + ENV.grading_periods = [{ id: 701, weight: 50 }, { id: 702, weight: 50 }]; + ENV.effective_due_dates = { 201: { 101: { grading_period_id: '701' } } }; + ENV.student_id = '101'; + }, + + teardown () { + fakeENV.teardown(); + } + }); + + test('calculates grades using data in the env', function () { + this.stub(CourseGradeCalculator, 'calculate').returns('expected'); + const grades = grade_summary.calculateGrades(); + equal(grades, 'expected'); + const args = CourseGradeCalculator.calculate.getCall(0).args; + equal(args[0], ENV.submissions); + equal(args[1], ENV.assignment_groups); + equal(args[2], ENV.group_weighting_scheme); + equal(args[3], ENV.grading_periods); + }); + + test('scopes effective due dates to the user', function () { + this.stub(CourseGradeCalculator, 'calculate'); + grade_summary.calculateGrades(); + const dueDates = CourseGradeCalculator.calculate.getCall(0).args[4]; + deepEqual(dueDates, { 201: { grading_period_id: '701' } }); + }); + + test('calculates grades without grading period data when effective due dates are not defined', function () { + delete ENV.effective_due_dates; + this.stub(CourseGradeCalculator, 'calculate'); + grade_summary.calculateGrades(); + const args = CourseGradeCalculator.calculate.getCall(0).args; + equal(args[0], ENV.submissions); + equal(args[1], ENV.assignment_groups); + equal(args[2], ENV.group_weighting_scheme); + equal(args[3], undefined); + equal(args[4], undefined); + }); }); diff --git a/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx b/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx index d489dd874e3..5d935df8edc 100644 --- a/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx +++ b/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx @@ -19,230 +19,243 @@ define([ 'underscore', 'jsx/gradebook/AssignmentGroupGradeCalculator' -], function (_, AssignmentGroupGradeCalculator) { +], (_, AssignmentGroupGradeCalculator) => { + let submissions; + let assignments; + let assignmentGroup; + module('AssignmentGroupGradeCalculator.calculate with no submissions and no assignments', { setup () { - this.submissions = []; - this.assignmentGroup = { id: 301, rules: {}, assignments: [] }; + submissions = []; + assignmentGroup = { id: 301, rules: {}, assignments: [] }; } }); - test('returns a current and final score of 0', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('returns a current and final score of 0', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 0); equal(grades.final.score, 0); }); - test('includes 0 points possible', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('includes 0 points possible', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 0); equal(grades.final.possible, 0); }); module('AssignmentGroupGradeCalculator.calculate with no submissions and some assignments', { setup () { - this.submissions = []; - const assignments = [ + submissions = []; + assignments = [ { id: 201, points_possible: 100, omit_from_final_grade: false }, { id: 202, points_possible: 91, omit_from_final_grade: false }, { id: 203, points_possible: 55, omit_from_final_grade: false }, { id: 204, points_possible: 38, omit_from_final_grade: false }, { id: 205, points_possible: 1000, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: {}, assignments }; + assignmentGroup = { id: 301, rules: {}, assignments }; } }); - test('returns a current and final score of 0', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('returns a current and final score of 0', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 0); equal(grades.final.score, 0); }); - test('include the sum of points possible', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('include the sum of points possible', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 0); equal(grades.final.possible, 1284); }); module('AssignmentGroupGradeCalculator.calculate with some assignments and submissions', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 100 }, { assignment_id: 202, score: 42 }, { assignment_id: 203, score: 14 }, { assignment_id: 204, score: 3 }, { assignment_id: 205, score: null } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 100, omit_from_final_grade: false }, { id: 202, points_possible: 91, omit_from_final_grade: false }, { id: 203, points_possible: 55, omit_from_final_grade: false }, { id: 204, points_possible: 38, omit_from_final_grade: false }, { id: 205, points_possible: 1000, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: {}, assignments }; + assignmentGroup = { id: 301, rules: {}, assignments }; } }); - test('adds all scores for current and final grades', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('adds all scores for current and final grades', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 159); equal(grades.final.score, 159); }); - test('excludes assignment points on ungraded submissions for the current grade', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes assignment points on ungraded submissions for the current grade', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 284); equal(grades.final.possible, 1284); }); - test('ignores hidden submissions', function () { - this.submissions[1].hidden = true; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('ignores hidden submissions', () => { + submissions[1].hidden = true; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 117); equal(grades.final.score, 117); }); - test('excludes assignment points on hidden submissions', function () { - this.submissions[1].hidden = true; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes assignment points on hidden submissions', () => { + submissions[1].hidden = true; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 193); equal(grades.final.possible, 1193); }); - test('ignores excused submissions', function () { - this.submissions[1].excused = true; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('ignores excused submissions', () => { + submissions[1].excused = true; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 117); equal(grades.final.score, 117); }); - test('excludes assignment points on excused submissions', function () { - this.submissions[1].excused = true; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes assignment points on excused submissions', () => { + submissions[1].excused = true; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 193); equal(grades.final.possible, 1193); }); - test('excludes submissions "pending review" from the current grade', function () { - this.submissions[1].workflow_state = 'pending_review'; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes submissions "pending review" from the current grade', () => { + submissions[1].workflow_state = 'pending_review'; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 117); equal(grades.current.possible, 193); }); - test('includes submissions "pending review" in the final grade', function () { - this.submissions[1].workflow_state = 'pending_review'; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('includes submissions "pending review" in the final grade', () => { + submissions[1].workflow_state = 'pending_review'; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 159); equal(grades.final.possible, 1284); }); - test('excludes assignments "omitted from final grade" from the current grade', function () { - this.assignmentGroup.assignments[2].omit_from_final_grade = true; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes assignments "omitted from final grade" from the current grade', () => { + assignmentGroup.assignments[2].omit_from_final_grade = true; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 145); equal(grades.current.possible, 229); }); - test('excludes assignments "omitted from final grade" from the final grade', function () { - this.assignmentGroup.assignments[2].omit_from_final_grade = true; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes assignments "omitted from final grade" from the final grade', () => { + assignmentGroup.assignments[2].omit_from_final_grade = true; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 145); equal(grades.final.possible, 1229); }); - test('excludes ungraded assignments "omitted from final grade" from the final grade', function () { - this.assignmentGroup.assignments[4].omit_from_final_grade = true; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes ungraded assignments "omitted from final grade" from the final grade', () => { + assignmentGroup.assignments[4].omit_from_final_grade = true; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 159); equal(grades.final.possible, 284); }); + test('eliminates multiple submissions for the same assignment', () => { + submissions.push({ ...submissions[0] }); + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); + equal(grades.current.score, 159); + equal(grades.final.score, 159); + equal(grades.current.possible, 284); + equal(grades.final.possible, 1284); + }); + module('AssignmentGroupGradeCalculator.calculate with assignments having no points possible', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 10 }, { assignment_id: 202, score: 10 } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 0, omit_from_final_grade: false }, { id: 202, points_possible: 10, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: {}, assignments }; + assignmentGroup = { id: 301, rules: {}, assignments }; } }); - test('includes scores for submissions on unpointed assignments', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('includes scores for submissions on unpointed assignments', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 20); equal(grades.final.score, 20); }); module('AssignmentGroupGradeCalculator.calculate "drop_lowest" rule (set to 1)', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 31 }, { assignment_id: 202, score: 17 }, { assignment_id: 203, score: 6 } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 40, omit_from_final_grade: false }, { id: 202, points_possible: 24, omit_from_final_grade: false }, { id: 203, points_possible: 10, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: { drop_lowest: 1 }, assignments }; + assignmentGroup = { id: 301, rules: { drop_lowest: 1 }, assignments }; } }); - test('drops one submission to maximize overall percentage grade', function () { + test('drops one submission to maximize overall percentage grade', () => { // drop 31/40, keep 17/24, keep 6/10 = 23/34 = 67.6% // keep 31/40, drop 17/24, keep 6/10 = 37/50 = 74.0% // keep 31/40, keep 17/24, drop 6/10 = 48/64 = 75.0% - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 48); ok(grades.current.submissions[2].drop); equal(grades.final.score, 48); ok(grades.final.submissions[2].drop); }); - test('drops pointed assignments over unpointed assignments', function () { - this.assignmentGroup.assignments[0].points_possible = 0; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops pointed assignments over unpointed assignments', () => { + assignmentGroup.assignments[0].points_possible = 0; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 37); ok(grades.current.submissions[1].drop); equal(grades.final.score, 37); ok(grades.final.submissions[1].drop); }); - test('excludes points possible from the assignment for the dropped submission', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes points possible from the assignment for the dropped submission', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 64); equal(grades.final.possible, 64); }); - test('ignores ungraded submissions for the current grade', function () { - this.submissions[2].score = null; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('ignores ungraded submissions for the current grade', () => { + submissions[2].score = null; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 31); equal(grades.final.score, 48); }); - test('excludes points possible for assignments with ungraded submissions for the current grade', function () { - this.submissions[2].score = null; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes points possible for assignments with ungraded submissions for the current grade', () => { + submissions[2].score = null; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 40); equal(grades.final.possible, 64); }); - test('accounts for impact on overall grade rather than score alone', function () { - this.submissions[2].score = 7; + test('accounts for impact on overall grade rather than score alone', () => { + submissions[2].score = 7; // drop 31/40, keep 17/24, keep 7/10 = 24/34 = 70.6% // keep 31/40, drop 17/24, keep 7/10 = 38/50 = 76.0% // keep 31/40, keep 17/24, drop 7/10 = 48/64 = 75.0% - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 38); equal(grades.current.possible, 50); ok(grades.current.submissions[1].drop); @@ -251,9 +264,9 @@ define([ ok(grades.final.submissions[1].drop); }); - test('does not drop submissions or assignments when drop_lowest is 0', function () { - this.assignmentGroup.rules.drop_lowest = 0; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('does not drop submissions or assignments when drop_lowest is 0', () => { + assignmentGroup.rules.drop_lowest = 0; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 54, 'current score includes all submission scores'); equal(grades.current.possible, 74, 'current possible includes all assignments'); equal(grades.final.score, 54, 'final score includes all submission scores'); @@ -262,33 +275,33 @@ define([ module('AssignmentGroupGradeCalculator.calculate "drop_lowest" rule', { setup () { - this._qunitTimeout = QUnit.config.testTimeout; + this.qunitTimeout = QUnit.config.testTimeout; QUnit.config.testTimeout = 100; - this.submissions = [ + submissions = [ { assignment_id: 201, score: 100 }, { assignment_id: 202, score: 42 }, { assignment_id: 203, score: 14 }, { assignment_id: 204, score: 3 }, { assignment_id: 205, score: null } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 100, omit_from_final_grade: false }, { id: 202, points_possible: 91, omit_from_final_grade: false }, { id: 203, points_possible: 55, omit_from_final_grade: false }, { id: 204, points_possible: 38, omit_from_final_grade: false }, { id: 205, points_possible: 1000, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: { drop_lowest: 2 }, assignments }; + assignmentGroup = { id: 301, rules: { drop_lowest: 2 }, assignments }; }, teardown () { - QUnit.config.testTimeout = this._qunitTimeout; - this._qunitTimeout = null; + QUnit.config.testTimeout = this.qunitTimeout; + this.qunitTimeout = null; } }); - test('drops multiple submissions to maximize overall percentage grade', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops multiple submissions to maximize overall percentage grade', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); // drop 100/100, drop 42/91, keep 14/55, keep 3/38, ignore -/1000 = 17/93 = 18.3% // drop 100/100, keep 42/91, drop 14/55, keep 3/38, ignore -/1000 = 45/129 = 34.9% @@ -317,9 +330,9 @@ define([ ok(grades.final.submissions[4].drop); }); - test('drops all but one score when drop_lowest is equal to the number of submissions', function () { - this.assignmentGroup.rules = { drop_lowest: 4 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops all but one score when drop_lowest is equal to the number of submissions', () => { + assignmentGroup.rules = { drop_lowest: 4 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 100); equal(grades.current.possible, 100); ok(grades.current.submissions[1].drop); @@ -339,19 +352,19 @@ define([ // This test is here because the reductive algorithm used for grading can // potentially enter into an infinite loop. While this setup data is indeed // ridiculous, its presence guarantees that the algorithm will always finish. - test('works in ridiculous circumstances', function () { - this.submissions[0].score = null; - this.submissions[1].score = 3; - this.submissions[2].score = null; - this.submissions[3].score = null; - this.submissions[4].score = null; - this.assignmentGroup.assignments[0].points_possible = 20; - this.assignmentGroup.assignments[1].points_possible = 10; - this.assignmentGroup.assignments[2].points_possible = 10; - this.assignmentGroup.assignments[3].points_possible = 100000000000000007629769841091887003294964970946560; - this.assignmentGroup.assignments[4].points_possible = null; - this.assignmentGroup.rules = { drop_lowest: 2 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('works in ridiculous circumstances', () => { + submissions[0].score = null; + submissions[1].score = 3; + submissions[2].score = null; + submissions[3].score = null; + submissions[4].score = null; + assignmentGroup.assignments[0].points_possible = 20; + assignmentGroup.assignments[1].points_possible = 10; + assignmentGroup.assignments[2].points_possible = 10; + assignmentGroup.assignments[3].points_possible = 100000000000000007629769841091887003294964970946560; + assignmentGroup.assignments[4].points_possible = null; + assignmentGroup.rules = { drop_lowest: 2 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 3); equal(grades.current.possible, 10); equal(grades.final.score, 3); @@ -360,58 +373,58 @@ define([ module('AssignmentGroupGradeCalculator.calculate "drop_highest" rule (set to 1)', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 31 }, { assignment_id: 202, score: 17 }, { assignment_id: 203, score: 6 } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 40, omit_from_final_grade: false }, { id: 202, points_possible: 24, omit_from_final_grade: false }, { id: 203, points_possible: 10, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: { drop_highest: 1 }, assignments }; + assignmentGroup = { id: 301, rules: { drop_highest: 1 }, assignments }; } }); - test('drops one submission to minimize overall percentage grade', function () { + test('drops one submission to minimize overall percentage grade', () => { // drop 31/40, keep 17/24, keep 6/10 = 23/34 = 67.6% // keep 31/40, drop 17/24, keep 6/10 = 37/50 = 74.0% // keep 31/40, keep 17/24, drop 6/10 = 48/64 = 75.0% - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 23); ok(grades.current.submissions[0].drop); equal(grades.final.score, 23); ok(grades.final.submissions[0].drop); }); - test('excludes points possible from the assignment for the dropped submission', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes points possible from the assignment for the dropped submission', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 34); equal(grades.final.possible, 34); }); - test('ignores ungraded submissions for the current grade', function () { - this.submissions[0].score = null; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('ignores ungraded submissions for the current grade', () => { + submissions[0].score = null; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 6); equal(grades.final.score, 6); }); - test('excludes points possible for assignments with ungraded submissions for the current grade', function () { - this.submissions[0].score = null; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('excludes points possible for assignments with ungraded submissions for the current grade', () => { + submissions[0].score = null; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 10); equal(grades.final.possible, 50); }); - test('accounts for impact on overall grade rather than score alone', function () { - this.submissions[2].score = 10; + test('accounts for impact on overall grade rather than score alone', () => { + submissions[2].score = 10; // drop 31/40, keep 17/24, keep 10/10 = 27/34 = 79.4% // keep 31/40, drop 17/24, keep 10/10 = 41/50 = 82.0% // keep 31/40, keep 17/24, drop 10/10 = 48/64 = 75.0% - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 48); equal(grades.current.possible, 64); ok(grades.current.submissions[2].drop); @@ -420,9 +433,9 @@ define([ ok(grades.final.submissions[2].drop); }); - test('does not drop submissions or assignments when drop_highest is 0', function () { - this.assignmentGroup.rules.drop_highest = 0; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('does not drop submissions or assignments when drop_highest is 0', () => { + assignmentGroup.rules.drop_highest = 0; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 54, 'current score includes all submission scores'); equal(grades.current.possible, 74, 'current possible includes all assignments'); equal(grades.final.score, 54, 'final score includes all submission scores'); @@ -431,26 +444,26 @@ define([ module('AssignmentGroupGradeCalculator.calculate "drop_highest" rule', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 100 }, { assignment_id: 202, score: 42 }, { assignment_id: 203, score: 14 }, { assignment_id: 204, score: 30 }, { assignment_id: 205, score: null } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 100, omit_from_final_grade: false }, { id: 202, points_possible: 91, omit_from_final_grade: false }, { id: 203, points_possible: 55, omit_from_final_grade: false }, { id: 204, points_possible: 38, omit_from_final_grade: false }, { id: 205, points_possible: 1000, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: { drop_highest: 2 }, assignments }; + assignmentGroup = { id: 301, rules: { drop_highest: 2 }, assignments }; } }); - test('drops multiple submissions to minimize overall percentage grade', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops multiple submissions to minimize overall percentage grade', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); // drop 100/100, drop 42/91, keep 14/55, keep 30/38, ignore -/1000 = 34/93 = 36.6% // drop 100/100, keep 42/91, drop 14/55, keep 30/38, ignore -/1000 = 72/129 = 55.8% @@ -480,11 +493,11 @@ define([ }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. - test('does not drop any scores when drop_highest is equal to the number of submissions', function () { - this.assignmentGroup.rules = { drop_highest: 4 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. + test('does not drop any scores when drop_highest is equal to the number of submissions', () => { + assignmentGroup.rules = { drop_highest: 4 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 186); equal(grades.current.possible, 284); notOk(grades.current.submissions[0].drop); @@ -502,11 +515,11 @@ define([ }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. - test('does not drop any scores when drop_highest is greater than the number of submissions', function () { - this.assignmentGroup.rules = { drop_highest: 5 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. + test('does not drop any scores when drop_highest is greater than the number of submissions', () => { + assignmentGroup.rules = { drop_highest: 5 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 186); equal(grades.current.possible, 284); notOk(grades.current.submissions[0].drop); @@ -525,26 +538,26 @@ define([ module('AssignmentGroupGradeCalculator.calculate with "drop_lowest" and "drop_highest" rules', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 100 }, { assignment_id: 202, score: 42 }, { assignment_id: 203, score: 14 }, { assignment_id: 204, score: 3 }, { assignment_id: 205, score: null } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 100, omit_from_final_grade: false }, { id: 202, points_possible: 91, omit_from_final_grade: false }, { id: 203, points_possible: 55, omit_from_final_grade: false }, { id: 204, points_possible: 38, omit_from_final_grade: false }, { id: 205, points_possible: 1000, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: { drop_lowest: 1, drop_highest: 1 }, assignments }; + assignmentGroup = { id: 301, rules: { drop_lowest: 1, drop_highest: 1 }, assignments }; } }); - test('drops the most and least favorable scores', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops the most and least favorable scores', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 56); equal(grades.current.possible, 146); ok(grades.current.submissions[0].drop); @@ -556,11 +569,11 @@ define([ }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. - test('does not drop higher scores when combined drop rules match the number of submissions', function () { - this.assignmentGroup.rules = { drop_lowest: 2, drop_highest: 2 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. + test('does not drop higher scores when combined drop rules match the number of submissions', () => { + assignmentGroup.rules = { drop_lowest: 2, drop_highest: 2 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 103); equal(grades.current.possible, 138); ok(grades.current.submissions[1].drop); @@ -578,11 +591,11 @@ define([ }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. - test('does not drop higher scores when combined drop rules exceed the number of submissions', function () { - this.assignmentGroup.rules = { drop_lowest: 2, drop_highest: 3 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. + test('does not drop higher scores when combined drop rules exceed the number of submissions', () => { + assignmentGroup.rules = { drop_lowest: 2, drop_highest: 3 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 103); equal(grades.current.possible, 138); ok(grades.current.submissions[1].drop); @@ -601,105 +614,105 @@ define([ module('AssignmentGroupGradeCalculator.calculate with equivalent submissions and assignments', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 9 }, { assignment_id: 202, score: 9 }, { assignment_id: 203, score: 9 }, { assignment_id: 204, score: 9 } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 10, omit_from_final_grade: false }, { id: 202, points_possible: 10, omit_from_final_grade: false }, { id: 203, points_possible: 10, omit_from_final_grade: false }, { id: 204, points_possible: 10, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: {}, assignments }; + assignmentGroup = { id: 301, rules: {}, assignments }; } }); - test('drops the same low-score submission regardless of submission order', function () { - this.assignmentGroup.rules = { drop_lowest: 1 }; - let grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops the same low-score submission regardless of submission order', () => { + assignmentGroup.rules = { drop_lowest: 1 }; + let grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission1 = _.find(grades.current.submissions, 'drop'); - this.submissions.reverse(); - grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + submissions.reverse(); + grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission2 = _.find(grades.current.submissions, 'drop'); equal(droppedSubmission1.assignment_id, droppedSubmission2.assignment_id); }); - test('drops the same high-score submission regardless of submission order', function () { - this.assignmentGroup.rules = { drop_highest: 1 }; - let grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops the same high-score submission regardless of submission order', () => { + assignmentGroup.rules = { drop_highest: 1 }; + let grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission1 = _.find(grades.current.submissions, 'drop'); - this.submissions.reverse(); - grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + submissions.reverse(); + grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission2 = _.find(grades.current.submissions, 'drop'); equal(droppedSubmission1.assignment_id, droppedSubmission2.assignment_id); }); - test('drops the same low-score submission for unpointed assignments', function () { - this.assignmentGroup.rules = { drop_lowest: 1 }; - this.assignmentGroup.assignments[0].points_possible = 0; - this.assignmentGroup.assignments[1].points_possible = 0; - this.assignmentGroup.assignments[2].points_possible = 0; - this.assignmentGroup.assignments[3].points_possible = 0; - let grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops the same low-score submission for unpointed assignments', () => { + assignmentGroup.rules = { drop_lowest: 1 }; + assignmentGroup.assignments[0].points_possible = 0; + assignmentGroup.assignments[1].points_possible = 0; + assignmentGroup.assignments[2].points_possible = 0; + assignmentGroup.assignments[3].points_possible = 0; + let grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission1 = _.find(grades.current.submissions, 'drop'); - this.submissions.reverse(); - grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + submissions.reverse(); + grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission2 = _.find(grades.current.submissions, 'drop'); equal(droppedSubmission1.assignment_id, droppedSubmission2.assignment_id); }); - test('drops the same high-score submission for unpointed assignments', function () { - this.assignmentGroup.rules = { drop_highest: 1 }; - this.assignmentGroup.assignments[0].points_possible = 0; - this.assignmentGroup.assignments[1].points_possible = 0; - this.assignmentGroup.assignments[2].points_possible = 0; - this.assignmentGroup.assignments[3].points_possible = 0; - let grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops the same high-score submission for unpointed assignments', () => { + assignmentGroup.rules = { drop_highest: 1 }; + assignmentGroup.assignments[0].points_possible = 0; + assignmentGroup.assignments[1].points_possible = 0; + assignmentGroup.assignments[2].points_possible = 0; + assignmentGroup.assignments[3].points_possible = 0; + let grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission1 = _.find(grades.current.submissions, 'drop'); - this.submissions.reverse(); - grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + submissions.reverse(); + grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission2 = _.find(grades.current.submissions, 'drop'); equal(droppedSubmission1.assignment_id, droppedSubmission2.assignment_id); }); module('AssignmentGroupGradeCalculator.calculate with only unpointed assignments', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 10 }, { assignment_id: 202, score: 5 }, { assignment_id: 203, score: 20 }, { assignment_id: 204, score: 0 } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 0, omit_from_final_grade: false }, { id: 202, points_possible: 0, omit_from_final_grade: false }, { id: 203, points_possible: 0, omit_from_final_grade: false }, { id: 204, points_possible: 0, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: {}, assignments }; + assignmentGroup = { id: 301, rules: {}, assignments }; } }); - test('drops the submission with the lowest score when drop_lowest is 1', function () { - this.assignmentGroup.rules = { drop_lowest: 1 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops the submission with the lowest score when drop_lowest is 1', () => { + assignmentGroup.rules = { drop_lowest: 1 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 35, 'all scores above the 0 are included'); ok(grades.current.submissions[3].drop); }); - test('drops the submission with the highest score when drop_highest is 1', function () { - this.assignmentGroup.rules = { drop_highest: 1 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops the submission with the highest score when drop_highest is 1', () => { + assignmentGroup.rules = { drop_highest: 1 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 15, 'all scores below the 20 are included'); ok(grades.current.submissions[2].drop); }); - test('drops submissions that match the given rules', function () { - this.assignmentGroup.rules = { drop_highest: 1, drop_lowest: 2 }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('drops submissions that match the given rules', () => { + assignmentGroup.rules = { drop_highest: 1, drop_lowest: 2 }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 10, 'only the score of 10 is included'); ok(grades.current.submissions[1].drop); ok(grades.current.submissions[2].drop); @@ -708,28 +721,28 @@ define([ module('AssignmentGroupGradeCalculator.calculate with only ungraded submissions', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: null }, { assignment_id: 202, score: null }, { assignment_id: 203, score: null } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 5, omit_from_final_grade: false }, { id: 202, points_possible: 10, omit_from_final_grade: false }, { id: 203, points_possible: 20, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: {}, assignments }; + assignmentGroup = { id: 301, rules: {}, assignments }; } }); - test('sets current score as 0', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('sets current score as 0', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 0, 'current score is 0 points when all submissions are excluded'); equal(grades.current.possible, 0, 'current possible is 0 when all assignment points are excluded'); }); - test('sets final score as 0', function () { - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('sets final score as 0', () => { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 0, 'final score is 0 points when all submissions are excluded'); equal(grades.final.possible, 35, 'final possible is sum of all assignment points'); }); @@ -741,25 +754,25 @@ define([ // keep 31/40, keep 19/24, keep 12/16, drop 6/10 = 62/80 = 77.5% setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 31 }, { assignment_id: 202, score: 19 }, { assignment_id: 203, score: 12 }, { assignment_id: 204, score: 6 } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 40, omit_from_final_grade: false }, { id: 202, points_possible: 24, omit_from_final_grade: false }, { id: 203, points_possible: 16, omit_from_final_grade: false }, { id: 204, points_possible: 10, omit_from_final_grade: false } ]; - this.assignmentGroup = { id: 301, rules: { never_drop: [204] }, assignments }; + assignmentGroup = { id: 301, rules: { never_drop: [204] }, assignments }; } }); - test('prevents submissions from being dropped for low scores', function () { - this.assignmentGroup.rules.drop_lowest = 1; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('prevents submissions from being dropped for low scores', () => { + assignmentGroup.rules.drop_lowest = 1; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 56); equal(grades.current.possible, 74); ok(grades.current.submissions[2].drop); @@ -774,9 +787,9 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('prevents submissions from being dropped for high scores', function () { - this.assignmentGroup.rules = { drop_highest: 1, never_drop: [201] }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('prevents submissions from being dropped for high scores', () => { + assignmentGroup.rules = { drop_highest: 1, never_drop: [201] }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 49); equal(grades.current.possible, 66); ok(grades.current.submissions[1].drop); @@ -791,9 +804,9 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('considers multiple assignments', function () { - this.assignmentGroup.rules = { drop_lowest: 1, never_drop: [203, 204] }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('considers multiple assignments', () => { + assignmentGroup.rules = { drop_lowest: 1, never_drop: [203, 204] }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 49); equal(grades.current.possible, 66); ok(grades.current.submissions[1].drop); @@ -809,11 +822,11 @@ define([ }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. - test('does not drop any scores when drop_lowest is equal to the number of droppable submissions', function () { - this.assignmentGroup.rules = { drop_lowest: 1, never_drop: [202, 203, 204] }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. + test('does not drop any scores when drop_lowest is equal to the number of droppable submissions', () => { + assignmentGroup.rules = { drop_lowest: 1, never_drop: [202, 203, 204] }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 68); equal(grades.current.possible, 90); notOk(grades.current.submissions[0].drop); @@ -829,11 +842,11 @@ define([ }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. - test('does not drop any scores when drop_highest is equal to the number of droppable submissions', function () { - this.assignmentGroup.rules = { drop_highest: 1, never_drop: [202, 203, 204] }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. + test('does not drop any scores when drop_highest is equal to the number of droppable submissions', () => { + assignmentGroup.rules = { drop_highest: 1, never_drop: [202, 203, 204] }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 68); equal(grades.current.possible, 90); notOk(grades.current.submissions[0].drop); @@ -848,9 +861,9 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('does not drop any low score submissions when all assignments are listed as "never drop"', function () { - this.assignmentGroup.rules = { drop_lowest: 1, never_drop: [201, 202, 203, 204] }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('does not drop any low score submissions when all assignments are listed as "never drop"', () => { + assignmentGroup.rules = { drop_lowest: 1, never_drop: [201, 202, 203, 204] }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); notOk(grades.current.submissions[0].drop); notOk(grades.current.submissions[1].drop); notOk(grades.current.submissions[2].drop); @@ -861,9 +874,9 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('does not drop any high score submissions when all assignments are listed as "never drop"', function () { - this.assignmentGroup.rules = { drop_highest: 1, never_drop: [201, 202, 203, 204] }; - const grades = AssignmentGroupGradeCalculator.calculate(this.submissions, this.assignmentGroup); + test('does not drop any high score submissions when all assignments are listed as "never drop"', () => { + assignmentGroup.rules = { drop_highest: 1, never_drop: [201, 202, 203, 204] }; + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); notOk(grades.current.submissions[0].drop); notOk(grades.current.submissions[1].drop); notOk(grades.current.submissions[2].drop); diff --git a/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx b/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx index 5b89654428d..8f3cc1b27ad 100644 --- a/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx +++ b/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx @@ -19,263 +19,572 @@ define([ 'underscore', 'jsx/gradebook/CourseGradeCalculator' -], function (_, CourseGradeCalculator) { +], (_, CourseGradeCalculator) => { + let submissions; + let assignments; + let assignmentGroups; + let gradingPeriods; + let effectiveDueDates; + + function calculateWithoutGradingPeriods (weightingScheme) { + return CourseGradeCalculator.calculate( + submissions, assignmentGroups, weightingScheme + ); + } + + function calculateWithGradingPeriods (weightingScheme) { + return CourseGradeCalculator.calculate( + submissions, assignmentGroups, weightingScheme, gradingPeriods, effectiveDueDates + ); + } + module('CourseGradeCalculator.calculate with no submissions and no assignments', { setup () { - this.submissions = []; - this.assignmentGroups = [ + submissions = []; + assignmentGroups = [ { id: 301, rules: {}, group_weight: 100, assignments: [] } ]; } }); test('returns a current and final score of 0 when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 0, 'current score is 0 when there are no submissions'); equal(grades.final.score, 0, 'final score is 0 when there are no submissions'); }); test('includes 0 points possible when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.possible, 0, 'current possible is sum of all assignment points'); equal(grades.final.possible, 0, 'final possible is sum of all assignment points'); }); test('returns a current and final score of null when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, null, 'current score cannot be calculated when there is no data'); equal(grades.final.score, null, 'final score cannot be calculated when there is no data'); }); test('sets possible to 0 for current grade when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.possible, 0, 'current possible is 0 when there are no assignments'); equal(grades.final.possible, 100, 'percent possible is 100'); }); module('CourseGradeCalculator.calculate with no submissions and some assignments', { setup () { - this.submissions = []; - this.assignments = [ + submissions = []; + assignments = [ { id: 201, points_possible: 10, omit_from_final_grade: false }, { id: 202, points_possible: 5, omit_from_final_grade: false }, { id: 203, points_possible: 20, omit_from_final_grade: false }, { id: 204, points_possible: 0, omit_from_final_grade: false } ]; - this.assignmentGroups = [ - { id: 301, rules: {}, group_weight: 50, assignments: this.assignments.slice(0, 2) }, - { id: 302, rules: {}, group_weight: 50, assignments: this.assignments.slice(2, 4) } + assignmentGroups = [ + { id: 301, rules: {}, group_weight: 50, assignments: assignments.slice(0, 2) }, + { id: 302, rules: {}, group_weight: 50, assignments: assignments.slice(2, 4) } ]; } }); test('sets scores to 0 when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 0, 'current score is 0 when there are no submissions'); equal(grades.final.score, 0, 'final score is 0 when there are no submissions'); }); test('sets all possible to 0 when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.possible, 0, 'current possible is sum of all assignment points'); equal(grades.final.possible, 35, 'final possible is sum of all assignment points'); }); test('sets current score to null when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, null, 'current score cannot be calculated when all assignments are excluded'); equal(grades.final.score, 0, 'final score is 0 when there are no submissions'); }); test('sets current possible to 0 when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.possible, 0, 'current possible is 0 when no assignments have submissions'); equal(grades.final.possible, 100, 'percent possible is 100 when submissions are counted'); }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. test('sets scores to null when assignment groups have no weight', function () { - this.assignmentGroups[0].group_weight = null; - this.assignmentGroups[1].group_weight = null; - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + assignmentGroups[0].group_weight = null; + assignmentGroups[1].group_weight = null; + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, null, 'assignment groups must have a defined group weight'); equal(grades.final.score, null, 'assignment groups must have a defined group weight'); }); module('CourseGradeCalculator.calculate with some assignments and submissions', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 100 }, { assignment_id: 202, score: 42 }, { assignment_id: 203, score: 14 }, { assignment_id: 204, score: 3 }, { assignment_id: 205, score: null } ]; - this.assignments = [ + assignments = [ { id: 201, points_possible: 100, omit_from_final_grade: false }, { id: 202, points_possible: 91, omit_from_final_grade: false }, { id: 203, points_possible: 55, omit_from_final_grade: false }, { id: 204, points_possible: 38, omit_from_final_grade: false }, { id: 205, points_possible: 1000, omit_from_final_grade: false } ]; - this.assignmentGroups = [ - { id: 301, rules: {}, group_weight: 50, assignments: this.assignments.slice(0, 2) }, - { id: 302, rules: {}, group_weight: 50, assignments: this.assignments.slice(2, 5) } + assignmentGroups = [ + { id: 301, rules: {}, group_weight: 50, assignments: assignments.slice(0, 2) }, + { id: 302, rules: {}, group_weight: 50, assignments: assignments.slice(2, 5) } ]; } }); test('adds all scores for current and final grades when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 159, 'current score is sum of all graded submission scores'); equal(grades.final.score, 159, 'final score is sum of all graded submission scores'); }); test('excludes ungraded assignments for the current grade when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.possible, 284, 'current possible excludes points for ungraded assignments'); equal(grades.final.possible, 1284, 'final possible includes points for ungraded assignments'); }); test('sets current and final scores as percentages when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, 46.31, 'current score is weighted using points from graded assignments'); equal(grades.final.score, 37.95, 'final score is weighted using points from all assignments'); }); test('excludes ungraded assignments for the current grade when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.possible, 100, 'current possible is 100 percent'); equal(grades.final.possible, 100, 'final possible is 100 percent'); }); - test('supports group weights which do not add up to exactly 100 percent', function () { - this.assignmentGroups[0].group_weight = 5; - this.assignmentGroups[1].group_weight = 5; - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + test('up-scales group weights which do not add up to exactly 100 percent', function () { + assignmentGroups[0].group_weight = 5; + assignmentGroups[1].group_weight = 5; + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, 46.31, 'current score is weighted using points from graded assignments'); equal(grades.final.score, 37.95, 'final score is weighted using points from all assignments'); }); - test('adjusts each assignment group score according to its group weight', function () { - this.assignmentGroups[0].group_weight = 75; - this.assignmentGroups[1].group_weight = 25; - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + test('does not down-scale group weights which add up to over 100 percent', function () { + assignmentGroups[0].group_weight = 100; + assignmentGroups[1].group_weight = 100; + const grades = calculateWithoutGradingPeriods('percent'); + equal(grades.current.score, 92.63, 'current score is effectively double the weight'); + equal(grades.final.score, 75.9, 'final score is effectively double the weight'); + equal(grades.current.possible, 100, 'current possible remains 100 percent'); + equal(grades.final.possible, 100, 'final possible remains 100 percent'); + }); + + test('weights each assignment group score according to its group weight', function () { + assignmentGroups[0].group_weight = 75; + assignmentGroups[1].group_weight = 25; + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, 60.33, 'current score is weighted using points from graded assignments'); equal(grades.final.score, 56.15, 'final score is weighted using points from all assignments'); }); test('rounds percent scores to two decimal places', function () { - this.assignmentGroups[0].group_weight = 33.33; - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + assignmentGroups[0].group_weight = 33.33; + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, 40.7, 'current score is weighted using points from graded assignments'); equal(grades.final.score, 30.67, 'final score is weighted using points from all assignments'); }); // This behavior was explicitly written into the grade calculator. While - // possibly a bug, this test is here to ensure this behavior is protected - // until a decision is made to correct it. + // possibly unintended, this test is here to ensure this behavior is protected + // until a decision is made to change it. test('sets scores to null when assignment groups have no weight', function () { - this.assignmentGroups[0].group_weight = null; - this.assignmentGroups[1].group_weight = null; - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + assignmentGroups[0].group_weight = null; + assignmentGroups[1].group_weight = null; + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, null, 'assignment groups must have a defined group weight'); equal(grades.final.score, null, 'assignment groups must have a defined group weight'); }); module('CourseGradeCalculator.calculate with zero-point assignments', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: 10 }, { assignment_id: 202, score: 5 }, { assignment_id: 203, score: 20 }, { assignment_id: 204, score: 0 } ]; - this.assignments = [ + assignments = [ { id: 201, points_possible: 0, omit_from_final_grade: false }, { id: 202, points_possible: 0, omit_from_final_grade: false }, { id: 203, points_possible: 0, omit_from_final_grade: false }, { id: 204, points_possible: 0, omit_from_final_grade: false } ]; - this.assignmentGroups = [ - { id: 301, rules: {}, group_weight: 50, assignments: this.assignments.slice(0, 2) }, - { id: 302, rules: {}, group_weight: 50, assignments: this.assignments.slice(2, 4) } + assignmentGroups = [ + { id: 301, rules: {}, group_weight: 50, assignments: assignments.slice(0, 2) }, + { id: 302, rules: {}, group_weight: 50, assignments: assignments.slice(2, 4) } ]; } }); test('adds all scores for current and final grades when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 35, 'current score is sum of all submission scores'); equal(grades.final.score, 35, 'final score is sum of all submission scores'); }); test('sets all possible to 0 when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.possible, 0, 'current possible is sum of all assignment points'); equal(grades.final.possible, 0, 'final possible is sum of all assignment points'); }); test('sets scores to null when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, null, 'current score cannot be calculated without points possible'); equal(grades.final.score, null, 'final score cannot be calculated without points possible'); }); test('sets current possible to 0 when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.possible, 0, 'current possible is 0 when all assignment points total 0'); equal(grades.final.possible, 100, 'percent possible is 100 when submissions are counted'); }); module('CourseGradeCalculator.calculate with only ungraded submissions', { setup () { - this.submissions = [ + submissions = [ { assignment_id: 201, score: null }, { assignment_id: 202, score: null }, { assignment_id: 203, score: null } ]; - const assignments = [ + assignments = [ { id: 201, points_possible: 5, omit_from_final_grade: false }, { id: 202, points_possible: 10, omit_from_final_grade: false }, { id: 203, points_possible: 20, omit_from_final_grade: false } ]; - this.assignmentGroups = [ + assignmentGroups = [ { id: 301, group_weight: 100, rules: {}, assignments } ]; } }); test('sets current score to 0 when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 0, 'current score is 0 points when all submissions are excluded'); equal(grades.current.possible, 0, 'current possible is 0 when all assignment points are excluded'); }); test('sets final score to 0 when weighting scheme is points', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'points'); + const grades = calculateWithoutGradingPeriods('points'); equal(grades.final.score, 0, 'final score is 0 points when all submissions are excluded'); equal(grades.final.possible, 35, 'final possible is sum of all assignment points'); }); test('sets current score to null when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, null, 'current score cannot be calculated when all submissions are excluded'); equal(grades.current.possible, 0, 'current possible is 0 when all assignment points are excluded'); }); test('sets final score to null when weighting scheme is percent', function () { - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.final.score, 0, 'final score cannot be calculated when all submissions are excluded'); equal(grades.final.possible, 100, 'final possible is 100 percent'); }); test('sets scores to 0 when weighting scheme is percent and group weight is not defined', function () { - this.assignmentGroups[0].group_weight = null; - const grades = CourseGradeCalculator.calculate(this.submissions, this.assignmentGroups, 'percent'); + assignmentGroups[0].group_weight = null; + const grades = calculateWithoutGradingPeriods('percent'); equal(grades.current.score, null, 'current score cannot be calculated without group weight'); equal(grades.final.score, null, 'final score cannot be calculated without group weight'); }); + + module('CourseGradeCalculator.calculate with weighted grading periods', { + setup () { + submissions = [ + { assignment_id: 201, score: 10 }, + { assignment_id: 202, score: 5 }, + { assignment_id: 203, score: 12 }, + { assignment_id: 204, score: 16 } + ]; + assignments = [ + { id: 201, points_possible: 10, omit_from_final_grade: false }, + { id: 202, points_possible: 10, omit_from_final_grade: false }, + { id: 203, points_possible: 20, omit_from_final_grade: false }, + { id: 204, points_possible: 40, omit_from_final_grade: false } + ]; + assignmentGroups = [ + { id: 301, group_weight: 60, rules: {}, assignments: assignments.slice(0, 2) }, + { id: 302, group_weight: 20, rules: {}, assignments: assignments.slice(2, 3) }, + { id: 303, group_weight: 20, rules: {}, assignments: assignments.slice(3, 4) } + ]; + gradingPeriods = [ + { id: 701, weight: 50 }, + { id: 702, weight: 50 } + ]; + effectiveDueDates = { + 201: { grading_period_id: '701' }, + 202: { grading_period_id: '701' }, + 203: { grading_period_id: '702' }, + 204: { grading_period_id: '702' } + }; + } + }); + + test('includes grading period weights in gradingPeriods', function () { + const grades = calculateWithGradingPeriods('percent'); + ok(grades.gradingPeriods); + equal(grades.gradingPeriods[701].weight, 50); + equal(grades.gradingPeriods[702].weight, 50); + }); + + test('includes assignment groups point scores in grading period grades', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].assignmentGroups[301].current.score, 15); + equal(grades.gradingPeriods[701].assignmentGroups[301].final.score, 15); + equal(grades.gradingPeriods[702].assignmentGroups[302].current.score, 12); + equal(grades.gradingPeriods[702].assignmentGroups[302].final.score, 12); + equal(grades.gradingPeriods[702].assignmentGroups[303].current.score, 16); + equal(grades.gradingPeriods[702].assignmentGroups[303].final.score, 16); + }); + + test('calculates current and final percent grades within grading periods', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].current.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].final.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].current.possible, 100, 'current possible is 100 percent'); + equal(grades.gradingPeriods[701].final.possible, 100, 'final possible is 100 percent'); + equal(grades.gradingPeriods[702].current.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].final.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].current.possible, 100, 'current possible is 100 percent'); + equal(grades.gradingPeriods[702].final.possible, 100, 'final possible is 100 percent'); + }); + + test('does not weight assignment groups within grading periods when weighting scheme is not percent', function () { + const grades = calculateWithGradingPeriods('points'); + equal(grades.gradingPeriods[701].current.score, 15, 'current score is sum of scores in grading period 701'); + equal(grades.gradingPeriods[701].final.score, 15, 'final score is sum of scores in grading period 701'); + equal(grades.gradingPeriods[701].current.possible, 20, 'current possible is sum of points in grading period 701'); + equal(grades.gradingPeriods[701].final.possible, 20, 'final possible is sum of points in grading period 701'); + equal(grades.gradingPeriods[702].current.score, 28, 'current score is sum of scores in grading period 702'); + equal(grades.gradingPeriods[702].final.score, 28, 'final score is sum of scores in grading period 702'); + equal(grades.gradingPeriods[702].current.possible, 60, 'current possible is sum of points in grading period 702'); + equal(grades.gradingPeriods[702].final.possible, 60, 'final possible is sum of points in grading period 702'); + }); + + test('weights percent grades of assignment groups for the course grade', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 62.5, 'each assignment group is half the grade'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 62.5, 'each assignment group is half the grade'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('does not weight assignment groups for course grade when weighting scheme is not percent', function () { + const grades = calculateWithGradingPeriods('points'); + equal(grades.current.score, 60.83, 'assignment group scores are totaled per grading period as points'); + equal(grades.final.score, 60.83, 'assignment group scores are totaled per grading period as points'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('up-scales grading period weights which do not add up to exactly 100 percent', function () { + gradingPeriods[0].weight = 5; + gradingPeriods[1].weight = 5; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 62.5); + equal(grades.current.possible, 100); + equal(grades.final.score, 62.5); + equal(grades.final.possible, 100); + }); + + test('does not down-scale grading period weights which add up to over 100 percent', function () { + gradingPeriods[0].weight = 100; + gradingPeriods[1].weight = 100; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 125, 'current score is effectively double the weight'); + equal(grades.current.possible, 100, 'current possible remains 100 percent'); + equal(grades.final.score, 125, 'final score is effectively double the weight'); + equal(grades.final.possible, 100, 'final possible remains 100 percent'); + }); + + test('weights grading periods with unequal grading period weights', function () { + gradingPeriods[0].weight = 25; + gradingPeriods[1].weight = 75; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 56.25); + equal(grades.final.score, 56.25); + }); + + test('uses full weight for grading periods with no assignments groups', function () { + assignmentGroups = [assignmentGroups[0]]; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 37.5, 'the grading period with a score is weighted as half of the overall score'); + equal(grades.current.possible, 100); + equal(grades.final.score, 37.5, 'the grading period with a score is weighted as half of the overall score'); + equal(grades.final.possible, 100); + }); + + // Empty assignment groups are not associated with any grading period. + test('ignores empty assignments groups', function () { + assignmentGroups[1].assignments = []; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 57.5); + equal(grades.final.score, 57.5); + }); + + test('evaluates null grading period weights as 0 when some grading periods have weight', function () { + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = 50; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 50, 'grading period 702 score of 50 effectively has 100 percent weight'); + equal(grades.current.possible, 100, 'current possible remains 100 percent'); + equal(grades.final.score, 50, 'grading period 702 score of 50 effectively has 100 percent weight'); + equal(grades.final.possible, 100, 'final possible remains 100 percent'); + }); + + test('sums assignment group scores as points when no grading periods have weight', function () { + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = null; + const grades = calculateWithGradingPeriods('points'); + equal(grades.current.score, 43, 'assignment group scores are totaled per grading period as points'); + equal(grades.current.possible, 80, 'current possible is sum of all assignment points'); + equal(grades.final.score, 43, 'assignment group scores are totaled per grading period as points'); + equal(grades.final.possible, 80, 'final possible is sum of all assignment points'); + }); + + test('combines weighted assignment group scores as percent when no grading periods have weight', function () { + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 65, 'all assignment groups are weighted together'); + equal(grades.current.possible, 100, 'current possible is 100 percent with weighted groups'); + equal(grades.final.score, 65, 'all assignment groups are weighted together'); + equal(grades.final.possible, 100, 'final possible is 100 percent with weighted groups'); + }); + + test('combines weighted assignment group scores as percent in grading periods without weight', function () { + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].current.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].current.possible, 100, 'current possible is 100 percent'); + equal(grades.gradingPeriods[701].final.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].final.possible, 100, 'final possible is 100 percent'); + equal(grades.gradingPeriods[702].current.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].current.possible, 100, 'current possible is 100 percent'); + equal(grades.gradingPeriods[702].final.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].final.possible, 100, 'final possible is 100 percent'); + }); + + // This is a use case that is STRONGLY discouraged to users, but is still not + // prevented. Assignment group rules must never be applied to multiple grading + // periods in combination. Doing so would impact grades in closed grading + // periods, which must never occur. + module('CourseGradeCalculator.calculate with assignment groups across multiple grading periods', { + setup () { + submissions = [ + { assignment_id: 201, score: 10 }, + { assignment_id: 202, score: 5 }, + { assignment_id: 203, score: 3 } + ]; + assignments = [ + { id: 201, points_possible: 10, omit_from_final_grade: false }, + { id: 202, points_possible: 10, omit_from_final_grade: false }, + { id: 203, points_possible: 10, omit_from_final_grade: false } + ]; + assignmentGroups = [ + { id: 301, group_weight: 50, rules: {}, assignments: assignments.slice(0, 2) }, + { id: 302, group_weight: 50, rules: {}, assignments: assignments.slice(2, 3) } + ]; + gradingPeriods = [ + { id: 701, weight: 50 }, + { id: 702, weight: 50 } + ]; + effectiveDueDates = { + 201: { grading_period_id: '701' }, // in first assignment group and first grading period + 202: { grading_period_id: '702' }, // in first assignment group and second grading period + 203: { grading_period_id: '702' } + }; + } + }); + + test('divides assignment groups across related grading periods', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].assignmentGroups[301].current.score, 10); + equal(grades.gradingPeriods[701].assignmentGroups[301].final.score, 10); + equal(grades.gradingPeriods[702].assignmentGroups[301].current.score, 5); + equal(grades.gradingPeriods[702].assignmentGroups[301].final.score, 5); + equal(grades.gradingPeriods[702].assignmentGroups[302].current.score, 3); + equal(grades.gradingPeriods[702].assignmentGroups[302].final.score, 3); + }); + + test('accounts for divided assignment groups in grading period scores', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].current.score, 100, 'grading period 701 scores include only assignment 201'); + equal(grades.gradingPeriods[701].final.score, 100, 'grading period 701 scores include only assignment 201'); + equal(grades.gradingPeriods[702].current.score, 40, 'grading period 702 scores include assignments 202 & 203'); + equal(grades.gradingPeriods[702].final.score, 40, 'grading period 702 scores include assignments 202 & 203'); + }); + + test('weights assignments groups with equal grading period weights', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 70, 'each grading period accounts for half of the current score'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 70, 'each grading period accounts for half of the final score'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('weights assignments groups with unequal grading period weights', function () { + gradingPeriods[0].weight = 25; + gradingPeriods[1].weight = 75; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 55, 'lower-scoring grading periods with higher weight decrease the current score'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 55, 'lower-scoring grading periods with higher weight decrease the final score'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('excludes assignment groups containing only assignments not assigned to the given student', function () { + delete effectiveDueDates[203]; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 75, 'assignment 203 is not assigned to the student'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 75, 'assignment 203 is not assigned to the student'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('excludes assignments not assigned to the given student', function () { + delete effectiveDueDates[202]; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 65, 'assignment 202 is not assigned to the student'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 65, 'assignment 202 is not assigned to the student'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + // When assignment groups cross multiple grading periods, the rules and + // weights are consequently duplicated and apply to the assignments in each + // grading period. This means weights can inadvertently exceed 100 in total. + test('duplicates weights of duplicated assignment groups when no grading periods have weight', function () { + // grading period 701 + assignment group 301: 10/10 * weight of 50 (50/100%) + // grading period 702 + assignment group 301: 5/10 * weight of 50 (25/100%) + // grading period 702 + assignment group 302: 3/10 * weight of 50 (15/100%) + // total: 50% + 25% + 15% = 90% + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 90, 'all assignment groups are weighted together'); + equal(grades.current.possible, 100, 'current possible is 100 percent with weighted groups'); + equal(grades.final.score, 90, 'all assignment groups are weighted together'); + equal(grades.final.possible, 100, 'final possible is 100 percent with weighted groups'); + }); }); diff --git a/spec/javascripts/jsx/gradebook/EffectiveDueDatesSpec.jsx b/spec/javascripts/jsx/gradebook/EffectiveDueDatesSpec.jsx new file mode 100644 index 00000000000..e8808d12abf --- /dev/null +++ b/spec/javascripts/jsx/gradebook/EffectiveDueDatesSpec.jsx @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 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 . + */ + +define([ + 'underscore', + 'jsx/gradebook/EffectiveDueDates' +], (_, EffectiveDueDates) => { + const exampleDueDatesData = { + 201: { + 101: { + due_at: '2015-05-04T12:00:00Z', + grading_period_id: '701', + in_closed_grading_period: true + }, + 102: { + due_at: '2015-05-05T12:00:00Z', + grading_period_id: '701', + in_closed_grading_period: true + } + }, + 202: { + 101: { + due_at: '2015-06-04T12:00:00Z', + grading_period_id: '702', + in_closed_grading_period: false + } + } + }; + + module('EffectiveDueDates.scopeToUser'); + + test('returns a map with effective due dates keyed to assignment ids', () => { + const scopedDueDates = EffectiveDueDates.scopeToUser(exampleDueDatesData, '101'); + deepEqual(_.keys(scopedDueDates).sort(), ['201', '202']); + deepEqual(_.keys(scopedDueDates[201]).sort(), ['due_at', 'grading_period_id', 'in_closed_grading_period']); + }); + + test('includes all effective due dates for the given user', () => { + const scopedDueDates = EffectiveDueDates.scopeToUser(exampleDueDatesData, '101'); + equal(scopedDueDates[201].due_at, '2015-05-04T12:00:00Z'); + equal(scopedDueDates[201].grading_period_id, '701'); + equal(scopedDueDates[201].in_closed_grading_period, true); + equal(scopedDueDates[202].due_at, '2015-06-04T12:00:00Z'); + equal(scopedDueDates[202].grading_period_id, '702'); + equal(scopedDueDates[202].in_closed_grading_period, false); + }); + + test('excludes assignments not assigned to the given user', () => { + const scopedDueDates = EffectiveDueDates.scopeToUser(exampleDueDatesData, '102'); + deepEqual(_.keys(scopedDueDates), ['201']); + equal(scopedDueDates[201].due_at, '2015-05-05T12:00:00Z'); + equal(scopedDueDates[201].grading_period_id, '701'); + equal(scopedDueDates[201].in_closed_grading_period, true); + }); +}); diff --git a/spec/javascripts/jsx/gradebook/GradingSchemeHelperSpec.jsx b/spec/javascripts/jsx/gradebook/GradingSchemeHelperSpec.jsx index d452d6034d3..51547266a75 100644 --- a/spec/javascripts/jsx/gradebook/GradingSchemeHelperSpec.jsx +++ b/spec/javascripts/jsx/gradebook/GradingSchemeHelperSpec.jsx @@ -19,15 +19,15 @@ define([ 'underscore', 'jsx/gradebook/GradingSchemeHelper' -], function (_, GradingSchemeHelper) { +], (_, GradingSchemeHelper) => { module('GradingSchemeHelper.scoreToGrade'); - test('returns the lowest grade to below-scale scores', function () { + test('returns the lowest grade to below-scale scores', () => { const gradingScheme = [['A', 0.90], ['B', 0.80], ['C', 0.70], ['D', 0.60], ['E', 0.50]]; equal(GradingSchemeHelper.scoreToGrade(40, gradingScheme), 'E'); }); - test('accounts for floating-point rounding errors', function () { + test('accounts for floating-point rounding errors', () => { // Keep this spec close to identical to the ruby GradeCalculator specs to ensure they both do the same thing. const gradingScheme = [ ['A', 0.90], ['B+', 0.886], ['B', 0.80], ['C', 0.695], ['D', 0.555], ['E', 0.545], ['M', 0.00] diff --git a/spec/models/grading_period_spec.rb b/spec/models/grading_period_spec.rb index e62212508e6..761bef11d95 100644 --- a/spec/models/grading_period_spec.rb +++ b/spec/models/grading_period_spec.rb @@ -1,5 +1,5 @@ # -# Copyright (C) 2014-2016 Instructure, Inc. +# Copyright (C) 2014 - 2016 Instructure, Inc. # # This file is part of Canvas. # @@ -200,6 +200,11 @@ describe GradingPeriod do json = grading_period.as_json_with_user_permissions(User.new) expect(json).to have_key("close_date") end + + it "includes the weight in the returned object" do + json = grading_period.as_json_with_user_permissions(User.new) + expect(json).to have_key("weight") + end end describe "close_date" do From daa985e58408266c4f17819a29a796230efe9a21 Mon Sep 17 00:00:00 2001 From: Neil Gupta Date: Tue, 3 Jan 2017 13:25:35 -0600 Subject: [PATCH 2/8] Add new weighted column to grading_period_groups Fixes CNVS-34094 Test plan: * run rake db:migrate * As an admin, create and edit a grading period set This is just adding a new column to the backend, nothing should change in the UI with this commit. Change-Id: I458516c73e509de63ce50a8f37bf21880cd01d91 Reviewed-on: https://gerrit.instructure.com/98833 Tested-by: Jenkins Reviewed-by: Jeremy Neander Reviewed-by: Derek Bender QA-Review: Anju Reddy Product-Review: Keith T. Garner --- app/controllers/grading_period_sets_controller.rb | 2 +- app/models/grading_period_group.rb | 2 +- app/serializers/grading_period_set_serializer.rb | 1 + ...103170627_add_weighted_to_grading_period_groups.rb | 11 +++++++++++ .../grading_period_sets_controller_spec.rb | 6 ++++-- 5 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20170103170627_add_weighted_to_grading_period_groups.rb diff --git a/app/controllers/grading_period_sets_controller.rb b/app/controllers/grading_period_sets_controller.rb index 05644e0c928..ea8f0685f7f 100644 --- a/app/controllers/grading_period_sets_controller.rb +++ b/app/controllers/grading_period_sets_controller.rb @@ -74,7 +74,7 @@ class GradingPeriodSetsController < ApplicationController end def set_params - strong_params.require(:grading_period_set).permit(:title) + strong_params.require(:grading_period_set).permit(:title, :weighted) end def check_read_rights diff --git a/app/models/grading_period_group.rb b/app/models/grading_period_group.rb index af11630570d..1d46b6dc70a 100644 --- a/app/models/grading_period_group.rb +++ b/app/models/grading_period_group.rb @@ -19,7 +19,7 @@ class GradingPeriodGroup < ActiveRecord::Base include Canvas::SoftDeletable - attr_accessible :title + attr_accessible :title, :weighted belongs_to :root_account, inverse_of: :grading_period_groups, foreign_key: :account_id, class_name: "Account" belongs_to :course has_many :grading_periods, dependent: :destroy diff --git a/app/serializers/grading_period_set_serializer.rb b/app/serializers/grading_period_set_serializer.rb index f8b78f84c63..caa56d201ba 100644 --- a/app/serializers/grading_period_set_serializer.rb +++ b/app/serializers/grading_period_set_serializer.rb @@ -4,6 +4,7 @@ class GradingPeriodSetSerializer < Canvas::APISerializer attributes :id, :title, + :weighted, :account_id, :course_id, :grading_periods, diff --git a/db/migrate/20170103170627_add_weighted_to_grading_period_groups.rb b/db/migrate/20170103170627_add_weighted_to_grading_period_groups.rb new file mode 100644 index 00000000000..b7db3ef7fb4 --- /dev/null +++ b/db/migrate/20170103170627_add_weighted_to_grading_period_groups.rb @@ -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 diff --git a/spec/controllers/grading_period_sets_controller_spec.rb b/spec/controllers/grading_period_sets_controller_spec.rb index 3251f641156..eb8017786a9 100644 --- a/spec/controllers/grading_period_sets_controller_spec.rb +++ b/spec/controllers/grading_period_sets_controller_spec.rb @@ -44,7 +44,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 +58,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 +88,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,6 +105,7 @@ 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 From e8f70811b530fbea705b5fb551e8961d8a9948b7 Mon Sep 17 00:00:00 2001 From: Neil Gupta Date: Thu, 5 Jan 2017 18:41:43 -0600 Subject: [PATCH 3/8] Add weighted grading periods checkbox for grading period sets Fixes CNVS-33571 This adds a new "weighted grading periods" checkbox to Grading Period Sets that should toggle and persist. Test plan: * As an admin, create a Grading Period Set for your account * Check "Weighted grading periods" * Save * Edit the new grading period, the checkbox should be checked * Uncheck it and save changes * Edit again, the checkbox should be unchecked Change-Id: I5112275f19e799ded1d172a8d055bfc1ade3d92f Reviewed-on: https://gerrit.instructure.com/98934 Tested-by: Jenkins Reviewed-by: Jeremy Neander Reviewed-by: Spencer Olson QA-Review: Anju Reddy Product-Review: Christi Wruck --- .../api/gradingPeriodSetsApi.coffee | 3 +- app/jsx/grading/EditGradingPeriodSetForm.jsx | 30 ++++++++++++++----- app/jsx/grading/NewGradingPeriodSetForm.jsx | 19 ++++++++++-- .../api/gradingPeriodSetsApiSpec.coffee | 13 ++++++-- .../grading/EditGradingPeriodSetFormSpec.jsx | 13 ++++++-- .../grading/NewGradingPeriodSetFormSpec.jsx | 8 +++++ 6 files changed, 71 insertions(+), 15 deletions(-) diff --git a/app/coffeescripts/api/gradingPeriodSetsApi.coffee b/app/coffeescripts/api/gradingPeriodSetsApi.coffee index d2a41e563db..3264713e822 100644 --- a/app/coffeescripts/api/gradingPeriodSetsApi.coffee +++ b/app/coffeescripts/api/gradingPeriodSetsApi.coffee @@ -17,7 +17,7 @@ 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) => @@ -36,6 +36,7 @@ define [ { id: set.id.toString() title: gradingPeriodSetTitle(set) + weighted: set.weighted || false gradingPeriods: deserializePeriods(set.grading_periods) permissions: set.permissions createdAt: new Date(set.created_at) diff --git a/app/jsx/grading/EditGradingPeriodSetForm.jsx b/app/jsx/grading/EditGradingPeriodSetForm.jsx index fa38e2a7d09..572b2e77104 100644 --- a/app/jsx/grading/EditGradingPeriodSetForm.jsx +++ b/app/jsx/grading/EditGradingPeriodSetForm.jsx @@ -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} /> + +
+ { this.weightedCheckbox = ref }} + label={I18n.t('Weighted grading periods')} + value="weighted" + checked={this.state.set.weighted} + onChange={this.changeWeighted} + /> +
diff --git a/app/jsx/grading/NewGradingPeriodSetForm.jsx b/app/jsx/grading/NewGradingPeriodSetForm.jsx index 629ab7d6253..5ea2ad66727 100644 --- a/app/jsx/grading/NewGradingPeriodSetForm.jsx +++ b/app/jsx/grading/NewGradingPeriodSetForm.jsx @@ -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 (
@@ -100,6 +106,15 @@ define([ selectedIDs = {this.state.selectedEnrollmentTermIDs} setSelectedEnrollmentTermIDs = {this.setSelectedEnrollmentTermIDs} /> +
+ { this.weightedCheckbox = ref }} + label={I18n.t('Weighted grading periods')} + value="weighted" + checked={this.state.weighted} + onChange={this.onSetWeightedChange} + /> +
diff --git a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee index 63373d8a626..dd050b3c1de 100644 --- a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee +++ b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee @@ -7,6 +7,7 @@ define [ { id: "1", title: "Fall 2015", + weighted: false, gradingPeriods: [ { id: "1", @@ -27,6 +28,7 @@ define [ },{ 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") @@ -38,6 +40,7 @@ define [ { id: "1", title: "Fall 2015", + weighted: false, grading_periods: [ { id: "1", @@ -59,6 +62,7 @@ define [ { 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" @@ -140,12 +144,14 @@ define [ deserializedSetCreating = { title: "Fall 2015", + weighted: null, enrollmentTermIDs: [ "1", "2" ] } deserializedSetCreated = { id: "1", title: "Fall 2015", + weighted: false, gradingPeriods: [], enrollmentTermIDs: [ "1", "2" ], permissions: { read: true, create: true, update: true, delete: true }, @@ -153,7 +159,7 @@ define [ } serializedSetCreating = { - grading_period_set: { title: "Fall 2015" }, + grading_period_set: { title: "Fall 2015", weighted: null }, enrollment_term_ids: [ "1", "2" ] } @@ -161,6 +167,7 @@ define [ grading_period_set: { id: "1", title: "Fall 2015", + weighted: false, enrollment_term_ids: [ "1", "2" ], grading_periods: [], permissions: { read: true, create: true, update: true, delete: true }, @@ -198,12 +205,13 @@ define [ deserializedSetUpdating = { 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" }, + grading_period_set: { title: "Fall 2015", weighted: true }, enrollment_term_ids: [ "1", "2" ] } @@ -211,6 +219,7 @@ define [ grading_period_set: { id: "1", title: "Fall 2015", + weighted: true, enrollment_term_ids: [ "1", "2" ], grading_periods: [ { diff --git a/spec/javascripts/jsx/grading/EditGradingPeriodSetFormSpec.jsx b/spec/javascripts/jsx/grading/EditGradingPeriodSetFormSpec.jsx index 36f000c1553..f5582e17b40 100644 --- a/spec/javascripts/jsx/grading/EditGradingPeriodSetFormSpec.jsx +++ b/spec/javascripts/jsx/grading/EditGradingPeriodSetFormSpec.jsx @@ -2,10 +2,9 @@ define([ 'react', 'react-dom', 'react-addons-test-utils', - 'jquery', 'underscore', 'jsx/grading/EditGradingPeriodSetForm' -], (React, ReactDOM, {Simulate}, $, _, GradingPeriodSetForm) => { +], (React, ReactDOM, {Simulate}, _, GradingPeriodSetForm) => { const wrapper = document.getElementById('fixtures'); const assertDisabled = function(component) { @@ -18,7 +17,7 @@ define([ notEqual($el.getAttribute('aria-disabled'), 'true'); }; - const exampleSet = { id: "1", title: "Fall 2015" }; + const exampleSet = { id: "1", title: "Fall 2015", weighted: true }; module('EditGradingPeriodSetForm', { renderComponent(opts={}) { @@ -61,9 +60,17 @@ define([ test('uses attributes from the given set', function() { let form = this.renderComponent(); equal(ReactDOM.findDOMNode(form.refs.title).value, "Fall 2015"); + equal(form.weightedCheckbox.checked, true); equal(form.state.set.id, "1"); }); + test('updates weighted state when checkbox is clicked', function() { + const form = this.renderComponent(); + equal(form.state.set.weighted, true); + form.weightedCheckbox.handleChange({ target: { checked: false } }); + equal(form.state.set.weighted, false); + }); + test('uses associated enrollment terms to update set state', function() { let form = this.renderComponent(); deepEqual(form.state.set.enrollmentTermIDs, ["1", "3"]); diff --git a/spec/javascripts/jsx/grading/NewGradingPeriodSetFormSpec.jsx b/spec/javascripts/jsx/grading/NewGradingPeriodSetFormSpec.jsx index a566295a1f1..13edbaeade0 100644 --- a/spec/javascripts/jsx/grading/NewGradingPeriodSetFormSpec.jsx +++ b/spec/javascripts/jsx/grading/NewGradingPeriodSetFormSpec.jsx @@ -23,6 +23,7 @@ define([ grading_period_set: { id: "81", title: "Example Set!", + weighted: false, grading_periods: [], permissions: { read: true, update: true, delete: true, create: true }, created_at: "2013-06-03T02:57:42Z" @@ -88,6 +89,13 @@ define([ assertDisabled(form.refs.cancelButton); }); + test('updates weighted state when checkbox is clicked', function() { + const form = this.renderComponent(); + equal(form.state.weighted, false); + form.weightedCheckbox.handleChange({ target: { checked: true } }); + equal(form.state.weighted, true); + }); + asyncTest('re-enables the cancel button when the ajax call fails', function() { this.stubCreateFailure(); let form = this.renderComponent(); From 10e0f2a432646362d2336add272c864ca0ff68b4 Mon Sep 17 00:00:00 2001 From: Neil Gupta Date: Fri, 6 Jan 2017 13:37:26 -0600 Subject: [PATCH 4/8] Add UI for weighted grading periods Fixes CNVS-28204 Test plan: * As an account admin, create a grading period set * Enable weighted grading periods on that grading period set * Create a grading period and enter a weight like 50 * Refresh the page * Your entered weight should be persisted * Edit the grading period to change the weight to 25 * Refresh the page * Your new weight should be persisted * Edit the grading period to change the weight to -10 * You should see an error that says weights must be >= 0 * Edit the grading period set to turn off weighted grades * All the weights should disappear from the UI (both in list view and when editing the grading period) Change-Id: Ied29baccf3a7142436b75f9e8038e5face944e32 Reviewed-on: https://gerrit.instructure.com/99005 Tested-by: Jenkins Reviewed-by: Shahbaz Javeed Reviewed-by: Derek Bender QA-Review: Anju Reddy Product-Review: Christi Wruck --- .../api/gradingPeriodSetsApi.coffee | 1 + app/jsx/grading/AccountGradingPeriod.jsx | 31 ++--- app/jsx/grading/GradingPeriodForm.jsx | 119 ++++++++++++------ app/jsx/grading/GradingPeriodSet.jsx | 17 ++- app/jsx/grading/gradingPeriod.jsx | 2 +- app/models/grading_period.rb | 28 ++++- .../api/gradingPeriodSetsApiSpec.coffee | 22 ++-- .../jsx/grading/AccountGradingPeriodSpec.jsx | 22 ++-- .../jsx/grading/GradingPeriodFormSpec.jsx | 11 +- .../jsx/grading/GradingPeriodSetSpec.jsx | 37 ++++++ spec/models/grading_period_spec.rb | 37 ++++++ 11 files changed, 247 insertions(+), 80 deletions(-) diff --git a/app/coffeescripts/api/gradingPeriodSetsApi.coffee b/app/coffeescripts/api/gradingPeriodSetsApi.coffee index 3264713e822..7a941dcb71f 100644 --- a/app/coffeescripts/api/gradingPeriodSetsApi.coffee +++ b/app/coffeescripts/api/gradingPeriodSetsApi.coffee @@ -25,6 +25,7 @@ define [ { id: period.id.toString() title: period.title + weight: period.weight startDate: new Date(period.start_date) endDate: new Date(period.end_date) # TODO: After the close_date data fixup has run, this can become: diff --git a/app/jsx/grading/AccountGradingPeriod.jsx b/app/jsx/grading/AccountGradingPeriod.jsx index a6795401092..14b38f84ce0 100644 --- a/app/jsx/grading/AccountGradingPeriod.jsx +++ b/app/jsx/grading/AccountGradingPeriod.jsx @@ -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 ( +
+ { I18n.t("Weight:") } { I18n.n(this.props.period.weight, {percentage: true}) } +
+ ); } }, @@ -105,18 +107,19 @@ define([ return (
-
+
{this.props.period.title}
-
- {I18n.t("Start Date:")} {this.dateWithTimezone(this.props.period.startDate)} +
+ {I18n.t("Starts:")} {DateHelper.formatDateForDisplay(this.props.period.startDate)}
-
- {I18n.t("End Date:")} {this.dateWithTimezone(this.props.period.endDate)} +
+ {I18n.t("Ends:")} {DateHelper.formatDateForDisplay(this.props.period.endDate)}
-
- {I18n.t("Close Date:")} {this.dateWithTimezone(this.props.period.closeDate)} +
+ {I18n.t("Closes:")} {DateHelper.formatDateForDisplay(this.props.period.closeDate)}
+ {this.renderWeight()}
{this.renderEditButton()} diff --git a/app/jsx/grading/GradingPeriodForm.jsx b/app/jsx/grading/GradingPeriodForm.jsx index 27510064675..36bcc4bf547 100644 --- a/app/jsx/grading/GradingPeriodForm.jsx +++ b/app/jsx/grading/GradingPeriodForm.jsx @@ -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 ( +
+ +
+ { this.weightInput = ref }} + type="text" + className="span1" + defaultValue={I18n.n(this.state.period.weight)} + onChange={this.changeWeight} + /> + % +
+
+ ); + }, + render: function() { return (
@@ -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' />
@@ -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} />
+ + {this.renderWeightInput()}
diff --git a/app/jsx/grading/GradingPeriodSet.jsx b/app/jsx/grading/GradingPeriodSet.jsx index 42d5a997371..49e395392e0 100644 --- a/app/jsx/grading/GradingPeriodSet.jsx +++ b/app/jsx/grading/GradingPeriodSet.jsx @@ -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'> @@ -344,6 +351,7 @@ define([ diff --git a/app/jsx/grading/gradingPeriod.jsx b/app/jsx/grading/gradingPeriod.jsx index 830fdaf6c0a..9aa046cbd65 100644 --- a/app/jsx/grading/gradingPeriod.jsx +++ b/app/jsx/grading/gradingPeriod.jsx @@ -40,7 +40,7 @@ define([ title: nextProps.title, startDate: nextProps.startDate, endDate: nextProps.endDate, - weight: nextProps.weight, + weight: nextProps.weight }); }, diff --git a/app/models/grading_period.rb b/app/models/grading_period.rb index 302eb703aa7..fd284bf2447 100644 --- a/app/models/grading_period.rb +++ b/app/models/grading_period.rb @@ -25,6 +25,7 @@ class GradingPeriod < ActiveRecord::Base has_many :scores, -> { active }, dependent: :destroy 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? @@ -32,7 +33,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? scope :current, -> do period_table = GradingPeriod.arel_table @@ -218,11 +219,30 @@ class GradingPeriod < ActiveRecord::Base term_ids = grading_period_group.enrollment_terms.pluck(:id) courses = Course.where(enrollment_term_id: term_ids) end - # Course#recompute_student_scores is asynchronous - courses.each { |course| course.recompute_student_scores(grading_period_id: self.id) } + + if time_boundaries_changed? + # different assignments could fall in this period now, + # we need to recalculate its scores + calculator_opts = { grading_period_id: id } + elsif weight_actually_changed? + # only weight changed, we just need to recompute the overall course score + calculator_opts = { update_all_grading_period_scores: false } + end + + courses.each do |course| + GradeCalculator.send_later_if_production(:recompute_final_score, course.student_ids, course.id, calculator_opts) + end 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 diff --git a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee index dd050b3c1de..ed568f7b805 100644 --- a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee +++ b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee @@ -14,13 +14,15 @@ define [ 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") + closeDate: new Date("2015-11-07T12:00:00Z"), + 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") + closeDate: new Date("2016-01-07T12:00:00Z"), + weight: null } ], permissions: { read: true, create: true, update: true, delete: true }, @@ -47,13 +49,15 @@ define [ 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") + close_date: new Date("2015-11-07T12:00:00Z"), + 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") + close_date: new Date("2016-01-07T12:00:00Z"), + weight: null } ], permissions: { read: true, create: true, update: true, delete: true }, @@ -120,12 +124,14 @@ define [ grading_period_sets: [ 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: null + close_date: null, + weight: null }] permissions: { read: true, create: true, update: true, delete: true } created_at: "2015-11-29T12:00:00Z" @@ -227,13 +233,15 @@ define [ 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") + 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 + close_date: null, + weight: 60 } ], permissions: { read: true, create: true, update: true, delete: true } diff --git a/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx b/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx index 2f3d31d71df..d4bf7695182 100644 --- a/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx +++ b/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx @@ -18,10 +18,12 @@ define([ period: { id: "1", title: "We did it! We did it! We did it! #dora #boots", + weight: 30, startDate: new Date("2015-01-01T20:11:00+00:00"), endDate: new Date("2015-03-01T00:00:00+00:00"), closeDate: new Date("2015-03-08T00:00:00+00:00") }, + weighted: true, readOnly: false, onEdit: () => {}, readOnly: false, @@ -70,28 +72,30 @@ define([ test("displays the start date in a friendly format", function() { let period = this.renderComponent(); const startDate = ReactDOM.findDOMNode(period.refs.startDate).textContent; - equal(startDate, "Start Date: Jan 1, 2015 at 8:11pm"); + equal(startDate, "Starts: Jan 1, 2015"); }); test("displays the end date in a friendly format", function() { let period = this.renderComponent(); const endDate = ReactDOM.findDOMNode(period.refs.endDate).textContent; - equal(endDate, "End Date: Mar 1, 2015 at 12am"); + equal(endDate, "Ends: Mar 1, 2015"); }); test("displays the close date in a friendly format", function() { let period = this.renderComponent(); const closeDate = ReactDOM.findDOMNode(period.refs.closeDate).textContent; - equal(closeDate, "Close Date: Mar 8, 2015 at 12am"); + equal(closeDate, "Closes: Mar 8, 2015"); }); - test("when the local timezone diffes from the server timezone dates include the timezone", function() { - tz.preload('America/Chicago', chicago); - fakeENV.setup({TIMEZONE: 'America/Denver', CONTEXT_TIMEZONE: 'America/Chicago'}); + test("displays the weight in a friendly format", function() { let period = this.renderComponent(); - const closeDate = period.refs.closeDate.textContent; - equal(closeDate, "Close Date: Mar 7, 2015 at 6pm UTC"); - fakeENV.teardown(); + const weight = ReactDOM.findDOMNode(period.refs.weight).textContent; + equal(weight, "Weight: 30%"); + }); + + test("does not display the weight if weighted grading periods are turned off", function() { + let period = this.renderComponent({weighted: false}); + equal(period.refs.weight, null); }); test("calls the 'onEdit' callback when the edit button is clicked", function() { diff --git a/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx b/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx index 1843593248e..e2009f8ec84 100644 --- a/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx +++ b/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx @@ -14,6 +14,7 @@ define([ const examplePeriod = { id: '1', title: 'Q1', + weight: 30, startDate: new Date("2015-11-01T12:00:00Z"), endDate: new Date("2015-12-31T12:00:00Z"), closeDate: new Date("2016-01-07T12:00:00Z") @@ -23,6 +24,7 @@ define([ renderComponent: function(opts={}) { const defaults = { period: examplePeriod, + weighted: true, disabled: false, onSave: () => {}, onCancel: () => {} @@ -42,6 +44,12 @@ define([ equal(form.refs.startDate.refs.dateInput.value, 'Nov 1, 2015 at 12pm'); equal(form.refs.endDate.refs.dateInput.value, 'Dec 31, 2015 at 12pm'); equal(form.refs.closeDate.refs.dateInput.value, 'Jan 7, 2016 at 12pm'); + equal(form.weightInput.value, '30'); + }); + + test("does not render weighted input if weighted is false", function() { + const form = this.renderComponent({weighted: false}); + equal(form.weightInput, null); }); test('renders with the save button enabled', function() { @@ -145,7 +153,8 @@ define([ title: 'Q1', startDate: new Date("2015-11-01T12:00:00Z"), endDate: new Date("2015-12-31T12:00:00Z"), - closeDate: new Date("2016-01-07T12:00:00Z") + closeDate: new Date("2016-01-07T12:00:00Z"), + weight: 30 }); }); diff --git a/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx b/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx index b41483e45a3..7e98246b23e 100644 --- a/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx +++ b/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx @@ -38,18 +38,21 @@ define([ { id: "1", title: "We did it! We did it! We did it! #dora #boots", + weight: 33, startDate: new Date("2015-01-01T20:11:00+00:00"), endDate: new Date("2015-03-01T00:00:00+00:00"), closeDate: new Date("2015-03-01T00:00:00+00:00") },{ id: "3", title: "Como estas?", + weight: 25.75, startDate: new Date("2014-11-01T20:11:00+00:00"), endDate: new Date("2014-11-11T00:00:00+00:00"), closeDate: new Date("2014-11-11T00:00:00+00:00") },{ id: "2", title: "Swiper no swiping!", + weight: 0, startDate: new Date("2015-04-01T20:11:00+00:00"), endDate: new Date("2015-05-01T00:00:00+00:00"), closeDate: new Date("2015-05-01T00:00:00+00:00") @@ -59,6 +62,7 @@ define([ const examplePeriod = { id: "4", title: "Example Period", + weight: 25, startDate: new Date("2015-03-02T20:11:00+00:00"), endDate: new Date("2015-03-03T00:00:00+00:00"), closeDate: new Date("2015-03-03T00:00:00+00:00") @@ -68,6 +72,7 @@ define([ set: { id: "1", title: "Example Set", + weighted: true }, terms: [], onEdit: function(){}, @@ -407,6 +412,22 @@ define([ ok(set.refs.editPeriodForm, "form is still visible"); }); + test('does not save a grading period with a negative weight', function() { + let period = { + id: "1", + title: "Some valid title", + weight: -50, + startDate: new Date("2015-03-02T20:11:00+00:00"), + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") + }; + let update = this.stubUpdate(); + let set = this.renderComponent(); + this.callOnSave(set, period); + notOk(gradingPeriodsApi.batchUpdate.called, "does not call update"); + ok(set.refs.editPeriodForm, "form is still visible"); + }); + test('does not save a grading period without a valid startDate', function() { let period = { title: "Period without Start Date", @@ -721,6 +742,22 @@ define([ ok(set.refs.newPeriodForm, "form is still visible"); }); + test('does not save a grading period with a negative weight', function() { + let period = { + id: "1", + title: "Some valid title", + weight: -50, + startDate: new Date("2015-03-02T20:11:00+00:00"), + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") + }; + let update = this.stubUpdate(); + let set = this.renderComponent(); + this.callOnSave(set, period); + notOk(gradingPeriodsApi.batchUpdate.called, "does not call update"); + ok(set.refs.newPeriodForm, "form is still visible"); + }); + test('does not save a grading period without a valid startDate', function() { let period = { title: "Period without Start Date", diff --git a/spec/models/grading_period_spec.rb b/spec/models/grading_period_spec.rb index 35da318754d..4321aa6d7b3 100644 --- a/spec/models/grading_period_spec.rb +++ b/spec/models/grading_period_spec.rb @@ -62,6 +62,8 @@ describe GradingPeriod do expect(grading_period).not_to be_valid end + it { is_expected.to validate_numericality_of(:weight) } + describe ".in_closed_grading_period?" do let(:in_closed_grading_period) { closed_period.start_date + 1.day } let(:in_not_closed_grading_period) { not_closed_period.start_date + 1.day } @@ -729,5 +731,40 @@ describe GradingPeriod do Score.where(grading_period_id: grading_period).first.current_score }.from(nil).to(80.0) end + + it 'updates course score when the grading period weight is changed' do + grading_period.save! + grading_period_group.update!(weighted: true) + expect{ grading_period.update!(weight: 50) }.to change{ + Score.where(grading_period_id: nil).first.updated_at + } + end + + it 'does not update grading period score when the grading period weight is changed' do + grading_period.save! + grading_period_group.update!(weighted: true) + expect{ grading_period.update!(weight: 20) }.not_to change{ + Score.where(grading_period_id: grading_period).first.updated_at + } + end + + it 'does not update course score when weight is changed but weighted grading periods are disabled' do + grading_period.save! + grading_period_group.update!(weighted: false) + expect{ grading_period.update!(weight: 50) }.not_to change{ + Score.where(grading_period_id: nil).first.updated_at + } + expect{ grading_period.update!(weight: 20) }.not_to change{ + Score.where(grading_period_id: grading_period).first.updated_at + } + end + + it 'does not update grading period score when weight is changed but weighted grading periods are disabled' do + grading_period.save! + grading_period_group.update!(weighted: false) + expect{ grading_period.update!(weight: 20) }.not_to change{ + Score.where(grading_period_id: grading_period).first.updated_at + } + end end end From 74d948ff3b268d0a2db632cb4f8c195295184ef4 Mon Sep 17 00:00:00 2001 From: Spencer Olson Date: Tue, 3 Jan 2017 13:59:14 -0600 Subject: [PATCH 5/8] back-end grade calculator supports grading period weighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes CNVS-34105 test plan: - Setup - 1. At the account-level, create a new grading period set that uses weighted grading periods. Attach the default enrollment term to this set. 2. Create a grading period (GP1) that runs from January 1, 2017 to June 30, 2017. Give it a weight of 75%. 3. Create a grading period (GP2) that runs from July 1, 2017 to December 31, 2017. Give it a weight of 25%. 4. Create a course that belongs to the default enrollment term from step 1. Enroll one student in the course. Make sure that student is enrolled in at least one other course. 5. In the course settings, enable Multiple Grading Periods and enable Display ‘All Grading Period’ Totals. 6. In the course, create an assignment (Assignment in GP1) that is worth 10 points and due for everyone on March 15, 2017. 7. In the course, create an assignment (Assignment in GP2) that is worth 10 points and due for everyone on September 15, 2017. 8. Go to the gradebook and give the student 5/10 on Assignment in GP1 and 10/10 on Assignment in GP2. Ignore the totals that show up in the gradebook. - Grade Verification - 1. Sign in as the student and go to the /grades page. 2. Verify the grade for GP1 is 50%. 3. Verify the grade for GP2 is 100%. 4. Verify the grade for ‘All Grading Periods’ is 62.5%. 5. Sign in as the account admin. Change the weight for GP1 to 20% and change the weight for GP2 to 40%. 6. Sign in as the student and go to the /grades page. 7. Verify the grade for GP1 is 50%. 8. Verify the grade for GP2 is 100%. 9. Verify the grade for ‘All Grading Periods’ is 83.33%. 10. Sign in as the account admin. Change the weight for GP1 to 100% and change the weight for GP2 to 100%. 11. Sign in as the student and go to the /grades page. 12. Verify the grade for GP1 is 50%. 13. Verify the grade for GP2 is 100%. 14. Verify the grade for ‘All Grading Periods’ is 150%. 15. Sign in as the account admin. Change the weight for GP1 to 100% and change the weight for GP2 to 0%. 16. Sign in as the student and go to the /grades page. 17. Verify the grade for GP1 is 50%. 18. Verify the grade for GP2 is 100%. 19. Verify the grade for ‘All Grading Periods’ is 50%. 20. Sign in as the account admin. Change the weight for GP1 to 0% and change the weight for GP2 to 0%. 21. Sign in as the student and go to the /grades page. 22. Verify the grade for GP1 is 50%. 23. Verify the grade for GP2 is 100%. 24. Verify the grade for ‘All Grading Periods’ is 0%. 25. Sign in as the account admin. Uncheck the box on the grading period set for ‘weighting’. 26. Sign in as the student and go to the /grades page. 27. Verify the grade for GP1 is 50%. 28. Verify the grade for GP2 is 100%. 29. Verify the grade for ‘All Grading Periods’ is 75%. Change-Id: Iac23e92986f371b13de2d5e0524d6bed8a277bc4 Reviewed-on: https://gerrit.instructure.com/99040 Reviewed-by: Derek Bender Tested-by: Jenkins Reviewed-by: Jeremy Neander QA-Review: KC Naegle Product-Review: Keith T. Garner --- lib/grade_calculator.rb | 166 ++++++++--- .../assignments_controller_spec.rb | 1 + spec/lib/grade_calculator_spec.rb | 281 ++++++++++++++++-- spec/models/grading_period_spec.rb | 8 +- 4 files changed, 383 insertions(+), 73 deletions(-) diff --git a/lib/grade_calculator.rb b/lib/grade_calculator.rb index 4e3af25e907..98035b30625 100644 --- a/lib/grade_calculator.rb +++ b/lib/grade_calculator.rb @@ -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 || !@course.feature_enabled?(:multiple_grading_periods) || 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)} diff --git a/spec/controllers/assignments_controller_spec.rb b/spec/controllers/assignments_controller_spec.rb index 11615f92969..981815ed815 100644 --- a/spec/controllers/assignments_controller_spec.rb +++ b/spec/controllers/assignments_controller_spec.rb @@ -460,6 +460,7 @@ describe AssignmentsController do it "to wiki page" do Course.any_instance.stubs(:feature_enabled?).with(:conditional_release).returns(true) + Course.any_instance.stubs(:feature_enabled?).with(:multiple_grading_periods).returns(false) wiki_page_assignment_model course: @course get 'edit', :course_id => @course.id, :id => @page.assignment.id expect(response).to redirect_to controller.edit_course_wiki_page_path(@course, @page) diff --git a/spec/lib/grade_calculator_spec.rb b/spec/lib/grade_calculator_spec.rb index 13e1c5ead90..cbbeba4bc6d 100644 --- a/spec/lib/grade_calculator_spec.rb +++ b/spec/lib/grade_calculator_spec.rb @@ -479,39 +479,42 @@ describe GradeCalculator do describe '#compute_and_save_scores' do before(:once) do @first_period, @second_period = grading_periods(count: 2) - first_assignment = @course.assignments.create!( + @first_assignment = @course.assignments.create!( due_at: 1.day.from_now(@first_period.start_date), points_possible: 100 ) - second_assignment = @course.assignments.create!( + @second_assignment = @course.assignments.create!( due_at: 1.day.from_now(@second_period.start_date), points_possible: 100 ) - first_assignment.grade_student(@student, grade: 25, grader: @teacher) - second_assignment.grade_student(@student, grade: 75, grader: @teacher) - # update_column to avoid callbacks on submission that would trigger score updates - Submission.where(user: @student, assignment: first_assignment).first.update_column(:score, 100.0) - Submission.where(user: @student, assignment: second_assignment).first.update_column(:score, 95.0) + + @first_assignment.grade_student(@student, grade: 25, grader: @teacher) + @second_assignment.grade_student(@student, grade: 75, grader: @teacher) + # update_column to avoid callbacks on submission that would trigger the grade calculator + submission_for_first_assignment.update_column(:score, 99.6) + submission_for_second_assignment.update_column(:score, 95.0) end - let(:scores) { @student.enrollments.first.scores } - let(:overall_course_score) { scores.where(grading_period_id: nil).first } + let(:scores) { @student.enrollments.first.scores.index_by(&:grading_period_id) } + let(:overall_course_score) { scores[nil] } + let(:submission_for_first_assignment) { Submission.find_by(user: @student, assignment: @first_assignment) } + let(:submission_for_second_assignment) { Submission.find_by(user: @student, assignment: @second_assignment) } it 'updates the overall course score' do GradeCalculator.new(@student.id, @course).compute_and_save_scores - expect(overall_course_score.current_score).to eq(97.5) + expect(overall_course_score.current_score).to eq(97.3) end it 'updates all grading period scores' do GradeCalculator.new(@student.id, @course).compute_and_save_scores - grading_period_scores = scores.where.not(grading_period_id: nil).order(:current_score).pluck(:current_score) - expect(grading_period_scores).to match_array([100.0, 95.0]) + expect(scores[@first_period.id].current_score).to eq(99.6) + expect(scores[@second_period.id].current_score).to eq(95.0) end it 'does not update grading period scores if update_all_grading_period_scores is false' do GradeCalculator.new(@student.id, @course, update_all_grading_period_scores: false).compute_and_save_scores - grading_period_scores = scores.where.not(grading_period_id: nil).order(:current_score).pluck(:current_score) - expect(grading_period_scores).to match_array([25.0, 75.0]) + expect(scores[@first_period.id].current_score).to eq(25.0) + expect(scores[@second_period.id].current_score).to eq(75.0) end it 'restores and updates previously deleted scores' do @@ -523,19 +526,17 @@ describe GradeCalculator do context 'grading period is provided' do it 'updates the grading period score' do GradeCalculator.new(@student.id, @course, grading_period: @first_period).compute_and_save_scores - score = scores.where(grading_period_id: @first_period).first - expect(score.current_score).to eq(100.0) + expect(scores[@first_period.id].current_score).to eq(99.6) end it 'updates the overall course score' do GradeCalculator.new(@student.id, @course, grading_period: @first_period).compute_and_save_scores - expect(overall_course_score.current_score).to eq(97.5) + expect(overall_course_score.current_score).to eq(97.3) end it 'does not update scores for other grading periods' do GradeCalculator.new(@student.id, @course, grading_period: @first_period).compute_and_save_scores - score = scores.where(grading_period_id: @second_period).first - expect(score.current_score).to eq(75.0) + expect(scores[@second_period.id].current_score).to eq(75.0) end it 'does not update the overall course score if update_course_score is false' do @@ -544,14 +545,252 @@ describe GradeCalculator do ).compute_and_save_scores expect(overall_course_score.current_score).to eq(50.0) end - + it 'does not restore previously deleted score if grading period is deleted too' do - score = scores.where(grading_period_id: @first_period).first + score = scores[@first_period.id] @first_period.destroy GradeCalculator.new(@student.id, @course, grading_period: @first_period).compute_and_save_scores expect(score.reload).to be_deleted end end + + context 'weighted grading periods' do + before(:once) do + group = @first_period.grading_period_group + group.update!(weighted: true) + @ungraded_assignment = @course.assignments.create!( + due_at: 1.day.from_now(@second_period.start_date), + points_possible: 100 + ) + end + + it 'calculates the course score from weighted grading period scores' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 75.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + # (99.6 * 0.25) + (95.0 * 0.75) = 96.15 + expect(overall_course_score.current_score).to eq(96.15) + # (99.6 * 0.25) + (47.5 * 0.75) = 60.525 rounds to 60.53 + expect(overall_course_score.final_score).to eq(60.53) + end + + it 'up-scales grading period weights which add up to less than 100 percent' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 50.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + # (99.6 * 0.25) + (95.0 * 0.50) = 72.4 + # 72.4 / (0.25 + 0.50) = 96.5333 rounded to 96.53 + expect(overall_course_score.current_score).to eq(96.53) + # (99.6 * 0.25) + (47.5 * 0.50) = 48.65 + # 48.65 / (0.25 + 0.50) = 64.8666 rounded to 64.87 + expect(overall_course_score.final_score).to eq(64.87) + end + + it 'does not down-scale grading period weights which add up to greater than 100 percent' do + @first_period.update!(weight: 100.0) + @second_period.update!(weight: 50.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + # (99.6 * 1.0) + (95.0 * 0.5) = 147.1 + expect(overall_course_score.current_score).to eq(147.1) + # (99.6 * 1.0) + (47.5 * 0.5) = 123.35 + expect(overall_course_score.final_score).to eq(123.35) + end + + it 'sets current course score to zero when all grading period weights are zero' do + @first_period.update!(weight: 0) + @second_period.update!(weight: 0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to eq(0.0) + end + + it 'sets final course score to zero when all grading period weights are zero' do + @first_period.update!(weight: 0) + @second_period.update!(weight: 0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'sets current course score to zero when all grading period weights are nil' do + @first_period.update!(weight: nil) + @second_period.update!(weight: nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to eq(0.0) + end + + it 'sets current course score to zero when all grading period weights are nil or zero' do + @first_period.update!(weight: 0.0) + @second_period.update!(weight: nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to eq(0.0) + end + + it 'sets final course score to zero when all grading period weights are nil' do + @first_period.update!(weight: nil) + @second_period.update!(weight: nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'sets final course score to zero when all grading period weights are nil or zero' do + @first_period.update!(weight: 0.0) + @second_period.update!(weight: nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'treats grading periods with nil weights as zero when some grading period ' \ + 'weights are nil and computing current score' do + @first_period.update!(weight: nil) + @second_period.update!(weight: 50.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to eq(95.0) + end + + it 'treats grading periods with nil weights as zero when some grading period ' \ + 'weights are nil and computing final score' do + @first_period.update!(weight: nil) + @second_period.update!(weight: 50.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(47.50) + end + + it 'sets current course score to nil when all grading period current scores are nil' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 75.0) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to be_nil + end + + it 'sets final course score to zero when all grading period final scores are nil' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 75.0) + # update_all to avoid callbacks on assignment that would trigger the grade calculator + @course.assignments.update_all(omit_from_final_grade: true) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'does not consider grading periods with nil current score when computing course current score' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 75.0) + # update_column to avoid callbacks on submission that would trigger the grade calculator + submission_for_first_assignment.update_column(:score, nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + # (0.0 * 0.0) + (95.0 * 0.75) = 71.25 + # 71.25 / (0.0 + 0.75) = 95.0 + expect(overall_course_score.current_score).to eq(95.0) + end + + it 'considers grading periods with nil final score as having zero score when computing course final score' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 75.0) + # update_column to avoid callbacks on assignment that would trigger the grade calculator + @first_assignment.update_column(:omit_from_final_grade, true) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + # (0.0 * 0.25) + (47.5 * 0.75) = 35.625 rounded to 35.63 + expect(overall_course_score.final_score).to eq(35.63) + end + + it 'sets course current score to zero when all grading period current scores are zero' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 75.0) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: 0.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to eq(0.0) + end + + it 'sets course final score to zero when all grading period final scores are zero' do + @first_period.update!(weight: 25.0) + @second_period.update!(weight: 75.0) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: 0.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'sets course current score to nil when all grading period current scores are nil ' \ + 'and all grading period weights are nil' do + @first_period.update!(weight: nil) + @second_period.update!(weight: nil) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to be_nil + end + + it 'sets course final score to zero when all grading period final scores are nil and all ' \ + 'grading period weights are nil' do + @first_period.update!(weight: nil) + @second_period.update!(weight: nil) + # update_all to avoid callbacks on assignment that would trigger the grade calculator + @course.assignments.update_all(omit_from_final_grade: true) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'sets course current score to zero when all grading period current scores are zero ' \ + 'and all grading period weights are zero' do + @first_period.update!(weight: 0.0) + @second_period.update!(weight: 0.0) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: 0.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to eq(0.0) + end + + it 'sets course final score to zero when all grading period final scores are zero and ' \ + 'all grading period weights are zero' do + @first_period.update!(weight: 0.0) + @second_period.update!(weight: 0.0) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: 0.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'sets course current score to nil when all grading period current scores are nil and ' \ + 'all grading period weights are zero' do + @first_period.update!(weight: 0.0) + @second_period.update!(weight: 0.0) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: nil) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to be_nil + end + + it 'sets course final score to zero when all grading period final scores are nil and all ' \ + 'grading period weights are zero' do + @first_period.update!(weight: 0.0) + @second_period.update!(weight: 0.0) + # update_all to avoid callbacks on assignment that would trigger the grade calculator + @course.assignments.update_all(omit_from_final_grade: true) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + + it 'sets course current score to zero when all grading period current scores are zero and ' \ + 'all grading period weights are nil' do + @first_period.update!(weight: nil) + @second_period.update!(weight: nil) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: 0.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.current_score).to eq(0.0) + end + + it 'sets course final score to zero when all grading period final scores are zero and all ' \ + 'grading period weights are nil' do + @first_period.update!(weight: nil) + @second_period.update!(weight: nil) + # update_all to avoid callbacks on submission that would trigger the grade calculator + @student.submissions.update_all(score: 0.0) + GradeCalculator.new(@student.id, @course).compute_and_save_scores + expect(overall_course_score.final_score).to eq(0.0) + end + end end it "should return grades in the order they are requested" do diff --git a/spec/models/grading_period_spec.rb b/spec/models/grading_period_spec.rb index 4321aa6d7b3..d3df97cdd81 100644 --- a/spec/models/grading_period_spec.rb +++ b/spec/models/grading_period_spec.rb @@ -160,7 +160,7 @@ describe GradingPeriod do context "where end_date equals Time.now" do it "does not include period" do - Timecop.freeze(Time.zone.now.change(usec: 0)) do + Timecop.freeze(now.change(usec: 0)) do period.update!(end_date: now) is_expected.to be_present end @@ -169,7 +169,7 @@ describe GradingPeriod do context "where end_date equals Time.now + 1" do it "does not include period" do - Timecop.freeze(Time.zone.now.change(usec: 0) + 1) do + Timecop.freeze(now.change(usec: 0) + 1) do period.update!(end_date: now) is_expected.to be_empty end @@ -178,7 +178,7 @@ describe GradingPeriod do context "where start_date equals Time.now" do it "does not include period" do - Timecop.freeze(Time.zone.now.change(usec: 0)) do + Timecop.freeze(now.change(usec: 0)) do period.update!(start_date: now) is_expected.to be_empty end @@ -187,7 +187,7 @@ describe GradingPeriod do context "where start_date equals Time.now + 1" do it "does not include period" do - Timecop.freeze(Time.zone.now.change(usec: 0) + 1) do + Timecop.freeze(now.change(usec: 0) + 1) do period.update!(start_date: now) is_expected.to be_present end From f03c69dd72946639c081ceaa9848faaf26a7cf4a Mon Sep 17 00:00:00 2001 From: Spencer Olson Date: Wed, 4 Jan 2017 15:41:45 -0600 Subject: [PATCH 6/8] load grading period scores instead of computing on-the-fly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grading period scores on the /grades page are now loaded instead of computed on-the-fly. closes CNVS-34128 test plan: This test plan mirrors that of 74d948ff to ensure the /grades page continues to display correct grading period grades (even though they are now loaded instead of computed on-the-fly) - Setup - 1. At the account-level, create a new grading period set that uses weighted grading periods. Attach the default enrollment term to this set. 2. Create a grading period (GP1) that runs from January 1, 2017 to June 30, 2017. Give it a weight of 75%. 3. Create a grading period (GP2) that runs from July 1, 2017 to December 31, 2017. Give it a weight of 25%. 4. Create a course that belongs to the default enrollment term from step 1. Enroll one student in the course. Make sure that student is enrolled in at least one other course. 5. In the course settings, enable Multiple Grading Periods and enable Display ‘All Grading Period’ Totals. 6. In the course, create an assignment (Assignment in GP1) that is worth 10 points and due for everyone on March 15, 2017. 7. In the course, create an assignment (Assignment in GP2) that is worth 10 points and due for everyone on September 15, 2017. 8. Go to the gradebook and give the student 5/10 on Assignment in GP1 and 10/10 on Assignment in GP2. Ignore the totals that show up in the gradebook. - Grade Verification - 1. Sign in as the student and go to the /grades page. 2. Verify the grade for GP1 is 50%. 3. Verify the grade for GP2 is 100%. 4. Verify the grade for ‘All Grading Periods’ is 62.5%. 5. Sign in as the account admin. Change the weight for GP1 to 20% and change the weight for GP2 to 40%. 6. Sign in as the student and go to the /grades page. 7. Verify the grade for GP1 is 50%. 8. Verify the grade for GP2 is 100%. 9. Verify the grade for ‘All Grading Periods’ is 83.33%. 10. Sign in as the account admin. Change the weight for GP1 to 100% and change the weight for GP2 to 100%. 11. Sign in as the student and go to the /grades page. 12. Verify the grade for GP1 is 50%. 13. Verify the grade for GP2 is 100%. 14. Verify the grade for ‘All Grading Periods’ is 150%. 15. Sign in as the account admin. Change the weight for GP1 to 100% and change the weight for GP2 to 0%. 16. Sign in as the student and go to the /grades page. 17. Verify the grade for GP1 is 50%. 18. Verify the grade for GP2 is 100%. 19. Verify the grade for ‘All Grading Periods’ is 50%. 20. Sign in as the account admin. Change the weight for GP1 to 0% and change the weight for GP2 to 0%. 21. Sign in as the student and go to the /grades page. 22. Verify the grade for GP1 is 50%. 23. Verify the grade for GP2 is 100%. 24. Verify the grade for ‘All Grading Periods’ is 0%. 25. Sign in as the account admin. Uncheck the box on the grading period set for ‘weighting’. 26. Sign in as the student and go to the /grades page. 27. Verify the grade for GP1 is 50%. 28. Verify the grade for GP2 is 100%. 29. Verify the grade for ‘All Grading Periods’ is 75%. Change-Id: Ia0f436f2df10484eab881bd8d11c0cad14784217 Reviewed-on: https://gerrit.instructure.com/98808 Tested-by: Jenkins Reviewed-by: Jeremy Neander Reviewed-by: Keith T. Garner QA-Review: KC Naegle Product-Review: Keith T. Garner --- .../grading_period_sets_controller.rb | 4 + app/controllers/users_controller.rb | 89 +++++++------------ app/models/course.rb | 9 +- app/models/enrollment_term.rb | 15 ++++ app/models/grading_period.rb | 21 +++-- app/models/grading_period_group.rb | 7 ++ .../grading_period_sets_controller_spec.rb | 13 +++ spec/controllers/users_controller_spec.rb | 39 ++++++-- spec/models/enrollment_term_spec.rb | 19 ++++ spec/models/grading_period_group_spec.rb | 19 ++++ 10 files changed, 157 insertions(+), 78 deletions(-) diff --git a/app/controllers/grading_period_sets_controller.rb b/app/controllers/grading_period_sets_controller.rb index 2174dab5272..7f8d1f11b6c 100644 --- a/app/controllers/grading_period_sets_controller.rb +++ b/app/controllers/grading_period_sets_controller.rb @@ -42,7 +42,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) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a690f2851e0..2de6b36bccd 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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 @@ -2080,6 +2077,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 authenticate_observee Pseudonym.authenticate(params[:observee] || {}, [@domain_root_account.id] + @domain_root_account.trusted_account_ids) @@ -2094,47 +2097,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 @@ -2169,19 +2155,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 diff --git a/app/models/course.rb b/app/models/course.rb index 177a1e74d8f..df2a5428500 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -1034,10 +1034,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}" - Enrollment.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 }" } diff --git a/app/models/enrollment_term.rb b/app/models/enrollment_term.rb index d659d47509e..7a5edfa04d2 100644 --- a/app/models/enrollment_term.rb +++ b/app/models/enrollment_term.rb @@ -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 diff --git a/app/models/grading_period.rb b/app/models/grading_period.rb index e5b9944e431..8c0b83d86e3 100644 --- a/app/models/grading_period.rb +++ b/app/models/grading_period.rb @@ -215,20 +215,19 @@ 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) - end - - if time_boundaries_changed? - # different assignments could fall in this period now, - # we need to recalculate its scores - calculator_opts = { grading_period_id: id } - elsif weight_actually_changed? - # only weight changed, we just need to recompute the overall course score - calculator_opts = { update_all_grading_period_scores: false } + courses = Course.active.where(enrollment_term_id: term_ids) end courses.each do |course| - GradeCalculator.send_later_if_production(:recompute_final_score, course.student_ids, course.id, calculator_opts) + 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 end diff --git a/app/models/grading_period_group.rb b/app/models/grading_period_group.rb index d55cfe4b876..6d65e7a8c35 100644 --- a/app/models/grading_period_group.rb +++ b/app/models/grading_period_group.rb @@ -26,6 +26,7 @@ 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 @@ -68,6 +69,12 @@ class GradingPeriodGroup < ActiveRecord::Base 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")) diff --git a/spec/controllers/grading_period_sets_controller_spec.rb b/spec/controllers/grading_period_sets_controller_spec.rb index eb8017786a9..da11ba18111 100644 --- a/spec/controllers/grading_period_sets_controller_spec.rb +++ b/spec/controllers/grading_period_sets_controller_spec.rb @@ -112,6 +112,19 @@ RSpec.describe GradingPeriodSetsController, type: :controller 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 diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index bfd6ca22510..4e7a2ebd40c 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -632,13 +632,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 +648,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 +660,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 +686,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 +696,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 +709,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 @@ -859,6 +859,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 +882,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 diff --git a/spec/models/enrollment_term_spec.rb b/spec/models/enrollment_term_spec.rb index 520eecc9500..fa75c4675be 100644 --- a/spec/models/enrollment_term_spec.rb +++ b/spec/models/enrollment_term_spec.rb @@ -51,6 +51,25 @@ describe EnrollmentTerm do end end + describe 'computation of course scores' do + before(:once) do + @root_account = Account.create! + @term = @root_account.enrollment_terms.create! + @root_account.courses.create!(enrollment_term: @term) + end + + it 'recomputes course scores if the grading period set is changed' do + grading_period_set = @root_account.grading_period_groups.create! + Enrollment.expects(:recompute_final_score).once + @term.update!(grading_period_group_id: grading_period_set) + end + + it 'does not recompute course scores if the grading period set is not changed' do + Enrollment.expects(:recompute_final_score).never + @term.update!(name: 'The Best Term') + end + end + it "should handle the translated Default Term names correctly" do begin account_model diff --git a/spec/models/grading_period_group_spec.rb b/spec/models/grading_period_group_spec.rb index 7aa66a69aa3..1d291355606 100644 --- a/spec/models/grading_period_group_spec.rb +++ b/spec/models/grading_period_group_spec.rb @@ -449,4 +449,23 @@ describe GradingPeriodGroup do end end end + + describe 'computation of course scores' do + before(:once) do + @grading_period_set = account.grading_period_groups.create!(valid_attributes) + term = account.enrollment_terms.create! + @grading_period_set.enrollment_terms << term + account.courses.create!(enrollment_term: term) + end + + it 'recomputes course scores when the weighted attribute is changed' do + Enrollment.expects(:recompute_final_score).once + @grading_period_set.update!(weighted: true) + end + + it 'does not recompute course scores when the weighted attribute is not changed' do + Enrollment.expects(:recompute_final_score).never + @grading_period_set.update!(title: 'The Best Set') + end + end end From 42573d3eaccb59ee415a44f92de7a665ad4ec0ce Mon Sep 17 00:00:00 2001 From: Jeremy Neander Date: Mon, 16 Jan 2017 11:06:31 -0600 Subject: [PATCH 7/8] update front end calculator for weighted grading periods Grade calculator now uses the grading period set for toggling weighted grading periods. It now excludes or includes assignments when grading periods are weighted or unweighted, respectively. fixes CNVS-34120 closes CNVS-34277 test plan: * Notes: * the "Multiple Grading Periods" flag must be on * this flag is being removed, which means the logic for this use case depends exclusively on the use of a grading period set * "grading periods enabled" means the related enrollment term has a grading period set * "grading periods disabled" means the related enrollment term has no grading period set A. select or create 1. an account with grading periods enabled 2. an enrollment term for the course 3. a grading period set for the enrollment term * with grading period weighting disabled * with two grading periods ('GP1' and 'GP2') 4. a course with assignment group weighting enabled 5. a teacher for the course 6. a student enrolled in the course 7. an assignment group 'AG1' a. with a weight of 60 b. with an assignment 'A1' i. due in GP1 ii. worth 10 points c. with an assignment 'A2' i. due in GP1 ii. worth 10 points 8. an assignment group 'AG2' a. with a weight of 20 b. with an assignment 'A3' i. due in GP2 ii. worth 20 points 9. an assignment group 'AG3' a. with a weight of 20 b. with an assignment 'A4' i. due in GP2 ii. worth 40 points B. set scores 1. score A1 with 5 points 2. score A2 with 10 points 3. score A3 with 12 points 4. score A4 with 16 points C. verify calculations * NOTES: * restore initial state (A) before each example * acceptance criteria must be met for: * Default Gradebook * Gradezilla Gradebook * Screenreader Gradebook * Student Grade Summary Page 1. without any weighting a. disable assignment group weighting b. grading period weighting should be disabled c. confirm total grade of 53.75 % d. select 'GP1' e. confirm total grade of 75 % f. select 'GP2' g. confirm total grade of 46.67 % 2. with only assignment group weighting a. assignment group weighting should be enabled b. grading period weighting should be disabled c. confirm total grade of 65 % d. select 'GP1' e. confirm total grade of 75 % f. select 'GP2' g. confirm total grade of 50 % 3. with only grading period weighting a. disable assignment group weighting b. enable grading period weighting c. set 'GP1' weight to 30% d. set 'GP2' weight to 70% e. confirm total grade of 55.17 % f. select 'GP1' g. confirm total grade of 75 % h. select 'GP2' i. confirm total grade of 46.67 % 4. with all weighting a. assignment group weighting should be enabled b. enable grading period weighting c. set 'GP1' weight to 30% d. set 'GP2' weight to 70% e. confirm total grade of 57.5 % f. select 'GP1' g. confirm total grade of 75 % h. select 'GP2' i. confirm total grade of 50 % 5. with no grading periods or group weighting a. remove the enrollment term from the grading period set b. disabled assignment group weighting c. confirm total grade of 53.75 % 6. with no grading periods and group weighting a. remove the enrollment term from the grading period set b. assignment group weighting should be enabled c. confirm total grade of 65 % 7. with an assignment outside weighted grading periods a. assignment group weighting should be enabled b. enable grading period weighting c. set 'GP1' weight to 30% d. set 'GP2' weight to 70% e. set 'A2' due date before 'GP1' e. confirm total grade of 50 % 8. with an assignment outside unweighted grading periods a. assignment group weighting should be enabled b. grading period weighting should be disabled c. set 'A2' due date before 'GP1' d. confirm total grade of 65 % D. verify Total Grade cell (points -/-) tooltips * NOTES: * restore initial state (A) before each example * acceptance criteria must be met for: * Default Gradebook * Gradezilla Gradebook 1. without any weighting a. disable assignment group weighting b. grading period weighting should be disabled c. verify the student's total grade has a points tooltip 2. with only assignment group weighting a. assignment group weighting should be enabled b. grading period weighting should be disabled c. verify the student's total grade has no points tooltip 3. with only grading period weighting a. disable assignment group weighting b. enable grading period weighting c. verify the student's total grade has no points tooltip 4. with all weighting a. assignment group weighting should be enabled b. enable grading period weighting c. verify the student's total grade has no points tooltip Change-Id: Idffd70bb2bfba052685235b032f6454bdd8c72fe Reviewed-on: https://gerrit.instructure.com/99792 Tested-by: Jenkins Reviewed-by: Spencer Olson Reviewed-by: Neil Gupta QA-Review: KC Naegle Product-Review: Keith T. Garner --- .../api/gradingPeriodSetsApi.coffee | 50 +- .../api/gradingPeriodsApi.coffee | 4 +- .../components/final_grade_component.coffee | 6 +- .../screenreader_gradebook_controller.coffee | 61 ++- .../templates/student_information/index.hbs | 4 +- .../tests/components/grading_cell.spec.coffee | 1 - .../screenreader_gradebook.spec.coffee | 264 +++++++--- app/coffeescripts/gradebook/Gradebook.coffee | 57 ++- app/coffeescripts/gradezilla/Gradebook.coffee | 75 ++- app/controllers/gradebooks_controller.rb | 164 ++++--- .../AssignmentGroupGradeCalculator.jsx | 43 +- app/jsx/gradebook/CourseGradeCalculator.jsx | 159 ++++--- public/javascripts/grade_summary.js | 82 +++- .../api/gradingPeriodSetsApiSpec.coffee | 243 +++++----- .../api/gradingPeriodsApiSpec.coffee | 113 ++--- .../gradebook/GradebookSpec.coffee | 238 +++++++-- .../SubmissionDetailsDialogSpec.coffee | 3 - .../gradezilla/GradebookSpec.coffee | 262 ++++++++-- .../SubmissionDetailsDialogSpec.coffee | 3 - .../controllers/gradebooks_controller_spec.rb | 84 +++- spec/javascripts/jsx/grade_summary.spec.jsx | 209 ++++---- .../AssignmentGroupGradeCalculatorSpec.jsx | 123 ++--- .../gradebook/CourseGradeCalculatorSpec.jsx | 450 ++++++++++++++++-- .../gradebook/GradeCalculatorSpecHelper.jsx | 60 +++ 24 files changed, 1903 insertions(+), 855 deletions(-) create mode 100644 spec/javascripts/jsx/gradebook/GradeCalculatorSpecHelper.jsx diff --git a/app/coffeescripts/api/gradingPeriodSetsApi.coffee b/app/coffeescripts/api/gradingPeriodSetsApi.coffee index 7a941dcb71f..9eb98eb9424 100644 --- a/app/coffeescripts/api/gradingPeriodSetsApi.coffee +++ b/app/coffeescripts/api/gradingPeriodSetsApi.coffee @@ -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 @@ -20,25 +21,12 @@ define [ 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 - weight: period.weight - 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) weighted: set.weighted || false - gradingPeriods: deserializePeriods(set.grading_periods) + gradingPeriods: gradingPeriodsApi.deserializePeriods(set.grading_periods) permissions: set.permissions createdAt: new Date(set.created_at) } @@ -48,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) @@ -59,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 diff --git a/app/coffeescripts/api/gradingPeriodsApi.coffee b/app/coffeescripts/api/gradingPeriodsApi.coffee index 2cbaa6a8170..664286ddf85 100644 --- a/app/coffeescripts/api/gradingPeriodsApi.coffee +++ b/app/coffeescripts/api/gradingPeriodsApi.coffee @@ -26,9 +26,7 @@ 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 diff --git a/app/coffeescripts/ember/screenreader_gradebook/components/final_grade_component.coffee b/app/coffeescripts/ember/screenreader_gradebook/components/final_grade_component.coffee index 6fb6bfd7ad5..52861766966 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/components/final_grade_component.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/components/final_grade_component.coffee @@ -17,7 +17,7 @@ define [ pointRatio: ( -> "#{@get('student.total_grade.score')} / #{@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") diff --git a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee index b39d74659a6..09e907263e9 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee @@ -18,11 +18,12 @@ define [ 'compiled/models/grade_summary/CalculationMethodContent' 'jsx/gradebook/SubmissionStateMap' 'compiled/api/gradingPeriodsApi' + 'compiled/api/gradingPeriodSetsApi' 'jquery.instructure_date_and_time' ], ( ajax, round, userSettings, fetchAllPages, parseLinkHeader, I18n, Ember, _, tz, AssignmentDetailsDialog, AssignmentMuter, CourseGradeCalculator, EffectiveDueDates, outcomeGrid, ic_submission_download_dialog, - htmlEscape, CalculationMethodContent, SubmissionStateMap, GradingPeriodsAPI + htmlEscape, CalculationMethodContent, SubmissionStateMap, GradingPeriodsApi, GradingPeriodSetsApi ) -> {get, set, setProperties} = Ember @@ -98,21 +99,22 @@ define [ mgpEnabled: get(window, 'ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled') - gradingPeriodData: - (-> - periods = get(window, 'ENV.GRADEBOOK_OPTIONS.active_grading_periods') - GradingPeriodsAPI.deserializePeriods(periods) - )() - 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)) )() + getGradingPeriodSet: -> + grading_period_set = get(window, 'ENV.GRADEBOOK_OPTIONS.grading_period_set') + if grading_period_set + GradingPeriodSetsApi.deserializeSet(grading_period_set) + else + null + 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') @@ -297,13 +299,21 @@ define [ set(assignment, 'assignment_visibility', filteredVisibilities) calculate: (student) -> - mgpEnabled = @get('mgpEnabled') submissions = @submissionsForStudent(student) assignmentGroups = @assignmentGroupsHash() weightingScheme = @get('weightingScheme') - gradingPeriods = @gradingPeriodData if mgpEnabled - effectiveDueDates = EffectiveDueDates.scopeToUser(@get('effectiveDueDates.content'), student.id) if mgpEnabled - CourseGradeCalculator.calculate(submissions, assignmentGroups, weightingScheme, gradingPeriods, effectiveDueDates) + gradingPeriodSet = @getGradingPeriodSet() + effectiveDueDates = @get('effectiveDueDates.content') + + usingGradingPeriods = gradingPeriodSet and effectiveDueDates + + CourseGradeCalculator.calculate( + submissions, + assignmentGroups, + weightingScheme, + gradingPeriodSet if usingGradingPeriods, + EffectiveDueDates.scopeToUser(effectiveDueDates, student.id) if usingGradingPeriods + ) submissionsForStudent: (student) -> allSubmissions = (value for key, value of student when key.match /^assignment_(?!group)/) @@ -317,11 +327,17 @@ define [ calculateStudentGrade: (student) -> if student.isLoaded - finalOrCurrent = if @get('includeUngradedAssignments') then 'final' else 'current' grades = @calculate(student) - for group in grades.group_sums - set(student, "assignment_group_#{group.group.id}", group[finalOrCurrent]) - for submissionData in group[finalOrCurrent].submissions + + 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] @@ -447,16 +463,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( @@ -465,7 +482,7 @@ define [ url: ENV.GRADEBOOK_OPTIONS.setting_update_url data: show_total_grade_as_points: @get("displayPointTotals")) - ).observes('showTotalAsPoints', 'groupsAreWeighted') + ).observes('showTotalAsPoints', 'gradesAreWeighted') studentColumnData: {} diff --git a/app/coffeescripts/ember/screenreader_gradebook/templates/student_information/index.hbs b/app/coffeescripts/ember/screenreader_gradebook/templates/student_information/index.hbs index 9d478418522..89658f09086 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/templates/student_information/index.hbs +++ b/app/coffeescripts/ember/screenreader_gradebook/templates/student_information/index.hbs @@ -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}}
-
\ No newline at end of file + diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee index 08e19e9c6eb..c44034e2e42 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee @@ -18,7 +18,6 @@ define [ @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.current_user_roles = [] setType = (type) => diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee index 10bc3df64d4..4507f769ac8 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee @@ -5,11 +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, CourseGradeCalculator) -> +], ( + $, _, ajax, startApp, Ember, fixtures, GradeCalculatorSpecHelper, SRGBController, userSettings, + CourseGradeCalculator +) -> workAroundRaceCondition = -> ajax.request() @@ -20,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') @@ -52,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 @@ -83,19 +93,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) @@ -154,18 +164,75 @@ define [ equal @srgb.get('assignments.firstObject.name'), 'Can You Eat Just One?' equal @srgb.get('assignments.lastObject.name'), 'Drink Water' - module "#submissionsForStudent", + 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 + + 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) + + 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, + 'selectedGradingPeriod.id': null, effectiveDueDates } self = _.defaults options, defaults @@ -174,40 +241,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 (multiple grading periods disabled)', -> 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", + '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" + 'selectedGradingPeriod.id': '2' ) submissions = @srgb.submissionsForStudent.call(self, @student) - propEqual _.pluck(submissions, "assignment_id"), ["2"] + propEqual _.pluck(submissions, 'assignment_id'), ['2'] module 'screenreader_gradebook_controller: with selected student', setup: -> setup.call this + @stub(@srgb, 'calculateStudentGrade') @completeSetup = => workAroundRaceCondition().then => Ember.run => @@ -333,8 +401,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' @@ -352,24 +420,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 @@ -380,24 +431,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 @@ -442,10 +476,13 @@ define [ 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, - mgpEnabled: true - gradingPeriodData: [{ id: 701, weight: 50 }, { id: 702, weight: 50 }] weightingScheme: 'points' + getGradingPeriodSet: () -> gradingPeriodSet 'effectiveDueDates.content': { 201: { 101: { grading_period_id: '701' } } } _.extend {}, props, get: (attr) -> props[attr] @@ -455,7 +492,7 @@ define [ setup: -> @calculate = SRGBController.prototype.calculate - test "calculates grades using properties from the gradebook", -> + test 'calculates grades using properties from the gradebook', -> self = @setupThis() @stub(CourseGradeCalculator, 'calculate').returns('expected') grades = @calculate.call(self, id: '101', loaded: true) @@ -464,25 +501,95 @@ define [ equal(args[0], self.submissionsForStudent()) equal(args[1], self.assignmentGroupsHash()) equal(args[2], self.get('weightingScheme')) - equal(args[3], self.gradingPeriodData) + equal(args[3], self.getGradingPeriodSet()) - test "scopes effective due dates to the user", -> + 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 grading periods are disabled", -> - self = @setupThis(mgpEnabled: false) + 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(args[3], undefined) - equal(args[4], undefined) + 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') + + 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) module 'screenreader_gradebook_controller: notes computed props', setup: -> @@ -545,20 +652,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 => @@ -578,13 +685,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 diff --git a/app/coffeescripts/gradebook/Gradebook.coffee b/app/coffeescripts/gradebook/Gradebook.coffee index 207f488dae2..a7fb6106bb9 100644 --- a/app/coffeescripts/gradebook/Gradebook.coffee +++ b/app/coffeescripts/gradebook/Gradebook.coffee @@ -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 . +# + define [ 'jquery' 'underscore' @@ -41,6 +58,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' @@ -64,9 +82,9 @@ define [ AssignmentGroupWeightsDialog, GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell, GradebookHeaderMenu, NumberCompare, htmlEscape, PostGradesStore, PostGradesApp, SubmissionStateMap, ColumnHeaderTemplate, GroupTotalCellTemplate, RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView, - GradebookKeyboardNav, assignmentHelper, GradingPeriodsAPI + GradebookKeyboardNav, assignmentHelper, GradingPeriodsApi, GradingPeriodSetsApi ) -> - + # This class both creates the slickgrid instance, and acts as the data source for that instance. class Gradebook columnWidths = assignment: @@ -103,7 +121,11 @@ define [ @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) + @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 @@ -716,7 +738,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) -> @@ -738,21 +760,24 @@ define [ calculateStudentGrade: (student) => if student.loaded and student.initialized - gradingPeriods = @gradingPeriods if @gradingPeriodsEnabled - effectiveDueDates = EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if @gradingPeriodsEnabled + usingGradingPeriods = @gradingPeriodSet and @effectiveDueDates grades = CourseGradeCalculator.calculate( @submissionsForStudent(student), @assignmentGroups, @options.group_weighting_scheme, - gradingPeriods, - effectiveDueDates + @gradingPeriodSet if usingGradingPeriods, + EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if usingGradingPeriods ) + if @gradingPeriodToShow && !@isAllGradingPeriods(@gradingPeriodToShow) + grades = grades.gradingPeriods[@gradingPeriodToShow] + finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current' - for group in grades.group_sums - student["assignment_group_#{group.group.id}"] = group[finalOrCurrent] - for submissionData in group[finalOrCurrent].submissions + + 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"] = grades[finalOrCurrent] @@ -1220,11 +1245,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 diff --git a/app/coffeescripts/gradezilla/Gradebook.coffee b/app/coffeescripts/gradezilla/Gradebook.coffee index 612dd83d4ae..98b282be64a 100644 --- a/app/coffeescripts/gradezilla/Gradebook.coffee +++ b/app/coffeescripts/gradezilla/Gradebook.coffee @@ -1,4 +1,21 @@ -# This class both creates the slickgrid instance, and acts as the data source for that instance. +# +# 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 . +# + define [ 'jquery' 'underscore' @@ -12,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' @@ -60,14 +79,15 @@ define [ '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, htmlEscape, - AssignmentColumnHeader, AssignmentGroupColumnHeader, StudentColumnHeader, TotalGradeColumnHeader, - PostGradesStore, PostGradesApp, SubmissionStateMap, GroupTotalCellTemplate, RowStudentNameTemplate, - SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, assignmentHelper + GradingPeriodsApi, GradingPeriodSetsApi, round, InputFilterView, i18nObj, I18n, GRADEBOOK_TRANSLATIONS, + CourseGradeCalculator, EffectiveDueDates, GradingSchemeHelper, UserSettings, Spinner, SubmissionDetailsDialog, + AssignmentGroupWeightsDialog, GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell, + GradebookHeaderMenu, NumberCompare, htmlEscape, AssignmentColumnHeader, AssignmentGroupColumnHeader, + StudentColumnHeader, TotalGradeColumnHeader, PostGradesStore, PostGradesApp, SubmissionStateMap, + GroupTotalCellTemplate, RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, + assignmentHelper ) -> - + # This class both creates the slickgrid instance, and acts as the data source for that instance. class Gradebook columnWidths = assignment: @@ -104,7 +124,11 @@ define [ @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) + @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 @@ -694,7 +718,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) -> @@ -716,17 +740,26 @@ define [ calculateStudentGrade: (student) => if student.loaded and student.initialized - finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current' - result = CourseGradeCalculator.calculate( + usingGradingPeriods = @gradingPeriodSet and @effectiveDueDates + + grades = CourseGradeCalculator.calculate( @submissionsForStudent(student), @assignmentGroups, - @options.group_weighting_scheme + @options.group_weighting_scheme, + @gradingPeriodSet if usingGradingPeriods, + EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if usingGradingPeriods ) - 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) @@ -1220,11 +1253,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 diff --git a/app/controllers/gradebooks_controller.rb b/app/controllers/gradebooks_controller.rb index 2fea5ad482a..44205a73171 100644 --- a/app/controllers/gradebooks_controller.rb +++ b/app/controllers/gradebooks_controller.rb @@ -100,6 +100,7 @@ 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, @@ -280,106 +281,119 @@ 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 - else - Setting.get('gradebook2.many_submissions_chunk_size', '10').to_i - end + Setting.get('gradebook2.submissions_chunk_size', '35').to_i + else + 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 => { - :chunk_size => chunk_size, - :assignment_groups_url => api_v1_course_assignment_groups_url( + js_env GRADEBOOK_OPTIONS: { + 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, - :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, {}), + 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, + 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, + 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, {}), } end diff --git a/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx b/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx index e02094c3f59..19efd3b5ee7 100644 --- a/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx +++ b/app/jsx/gradebook/AssignmentGroupGradeCalculator.jsx @@ -248,18 +248,17 @@ define([ const possible = sumBy(submissionsToKeep, 'total'); return { - possible, score, - weight: group.group_weight, + possible, submission_count: _.filter(submissionData, 'submitted').length, - submissions: _.map(submissionData, submission => ( + submissions: _.map(submissionData, submissionDatum => ( { - drop: submission.drop, - percent: parseScore(submission.score / submission.total), - possible: submission.total, - score: parseScore(submission.score), - submission: submission.submission, - submitted: submission.submitted + drop: submissionDatum.drop, + percent: parseScore(submissionDatum.score / submissionDatum.total), + score: parseScore(submissionDatum.score), + possible: submissionDatum.total, + submission: submissionDatum.submission, + submitted: submissionDatum.submitted } )) }; @@ -268,8 +267,8 @@ define([ // Each submission requires the following properties: // * score: number // * points_possible: non-negative integer - // * assignment_id: Canvas id - // * assignment_group_id: Canvas id + // * assignment_id: + // * assignment_group_id: // * excused: boolean // // Ungraded submissions will have a score of `null`. @@ -286,31 +285,35 @@ define([ // * never_drop: [array of assignment ids] // // `assignments` is an array of objects with the following properties: - // * id: Canvas id + // * id: // * points_possible: non-negative number // * submission_types: [array of strings] // - // AssignmentGroup Grade information has the following shape: + // An AssignmentGroup Grade has the following shape: // { // score: number|null // possible: number|null - // weight: non-negative number|null // submission_count: non-negative number // submissions: [array of Submissions] // } // - // Return value has the following shape: + // Return value is an AssignmentGroup Grade Set. + // An AssignmentGroup Grade Set has the following shape: // { - // group: - // current: - // final: + // assignmentGroupId: + // assignmentGroupWeight: number + // current: + // final: + // scoreUnit: 'points' // } function calculate (allSubmissions, assignmentGroup) { const submissions = _.uniq(allSubmissions, 'assignment_id'); return { - group: assignmentGroup, + assignmentGroupId: assignmentGroup.id, + assignmentGroupWeight: assignmentGroup.group_weight, current: calculateGroupGrade(assignmentGroup, submissions, false), - final: calculateGroupGrade(assignmentGroup, submissions, true) + final: calculateGroupGrade(assignmentGroup, submissions, true), + scoreUnit: 'points' }; } diff --git a/app/jsx/gradebook/CourseGradeCalculator.jsx b/app/jsx/gradebook/CourseGradeCalculator.jsx index c03d99059be..c0c36a25b36 100644 --- a/app/jsx/gradebook/CourseGradeCalculator.jsx +++ b/app/jsx/gradebook/CourseGradeCalculator.jsx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Instructure, Inc. + * Copyright (C) 2016 - 2017 Instructure, Inc. * * This file is part of Canvas. * @@ -31,13 +31,13 @@ define([ } function getWeightedPercent ({ score, possible, weight }) { - return (score / possible) * weight; + return score ? (score / possible) * weight : 0; } function combineAssignmentGroupGrades (assignmentGroupGrades, includeUngraded, options) { const scopedAssignmentGroupGrades = _.map(assignmentGroupGrades, (assignmentGroupGrade) => { - const sumVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current; - return { ...sumVersion, weight: assignmentGroupGrade.group.group_weight }; + const gradeVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current; + return { ...gradeVersion, weight: assignmentGroupGrade.assignmentGroupWeight }; }); if (options.weightAssignmentGroups) { @@ -67,19 +67,13 @@ define([ function combineGradingPeriodGrades (gradingPeriodGradesByPeriodId, includeUngraded) { const scopedGradingPeriodGrades = _.map(gradingPeriodGradesByPeriodId, (gradingPeriodGrade) => { - const gradesVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current; - return { ...gradesVersion, weight: gradingPeriodGrade.weight }; - }); - - const weightedScores = _.map(scopedGradingPeriodGrades, (gradingPeriodGrade) => { - if (gradingPeriodGrade.score) { - return getWeightedPercent(gradingPeriodGrade); - } - return 0; + const gradeVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current; + return { ...gradeVersion, weight: gradingPeriodGrade.gradingPeriodWeight }; }); + const weightedScores = _.map(scopedGradingPeriodGrades, getWeightedPercent); const totalWeight = sumBy(scopedGradingPeriodGrades, 'weight'); - const totalScore = (sum(weightedScores) * 100) / Math.min(totalWeight, 100); + const totalScore = totalWeight === 0 ? 0 : (sum(weightedScores) * 100) / Math.min(totalWeight, 100); return { score: round(totalScore, 2), @@ -88,6 +82,12 @@ define([ } 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 )); @@ -96,80 +96,110 @@ define([ )); } - function extractUsableAssignmentGroups (assignmentGroups, effectiveDueDates) { - return _.reduce(assignmentGroups, (usableGroups, assignmentGroup) => { + 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 [ - ...usableGroups, + ...periodBasedGroups, ...divideGroupByGradingPeriods(groupWithAssignedAssignments, effectiveDueDates) ]; } - return usableGroups; + return periodBasedGroups; }, []); } - function calculateWithGradingPeriods ( - submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options - ) { - const usableGroups = extractUsableAssignmentGroups(assignmentGroups, effectiveDueDates); + 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; + } - const assignmentGroupsByGradingPeriodId = _.groupBy(usableGroups, (assignmentGroup) => { + 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 allAssignmentGroupGrades = []; + const periodBasedAssignmentGroupGrades = []; _.forEach(gradingPeriods, (gradingPeriod) => { const groupGrades = {}; (assignmentGroupsByGradingPeriodId[gradingPeriod.id] || []).forEach((assignmentGroup) => { groupGrades[assignmentGroup.id] = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); - allAssignmentGroupGrades.push(groupGrades[assignmentGroup.id]); + periodBasedAssignmentGroupGrades.push(groupGrades[assignmentGroup.id]); }); const groupGradesList = _.values(groupGrades); gradingPeriodGradesByPeriodId[gradingPeriod.id] = { - weight: gradingPeriodsById[gradingPeriod.id].weight, + gradingPeriodId: gradingPeriod.id, + gradingPeriodWeight: gradingPeriodsById[gradingPeriod.id].weight || 0, + assignmentGroups: groupGrades, current: combineAssignmentGroupGrades(groupGradesList, false, options), final: combineAssignmentGroupGrades(groupGradesList, true, options), - assignmentGroups: groupGrades + scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points' }; }); if (options.weightGradingPeriods) { return { + assignmentGroups: recombinePeriodBasedAssignmentGroupGrades(periodBasedAssignmentGroupGrades), gradingPeriods: gradingPeriodGradesByPeriodId, - group_sums: allAssignmentGroupGrades, current: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, false, options), - final: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, true, 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, - group_sums: allAssignmentGroupGrades, current: combineAssignmentGroupGrades(allAssignmentGroupGrades, false, options), - final: combineAssignmentGroupGrades(allAssignmentGroupGrades, true, options) + final: combineAssignmentGroupGrades(allAssignmentGroupGrades, true, options), + scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points' }; } function calculateWithoutGradingPeriods (submissions, assignmentGroups, options) { - const assignmentGroupGrades = _.map(assignmentGroups, group => ( - AssignmentGroupGradeCalculator.calculate(submissions, group) + const assignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => ( + AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup) )); return { - group_sums: assignmentGroupGrades, + assignmentGroups: _.indexBy(assignmentGroupGrades, grade => grade.assignmentGroupId), current: combineAssignmentGroupGrades(assignmentGroupGrades, false, options), - final: combineAssignmentGroupGrades(assignmentGroupGrades, true, options) + final: combineAssignmentGroupGrades(assignmentGroupGrades, true, options), + scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points' }; } @@ -203,9 +233,13 @@ define([ // When weightingScheme is `percent`, assignment group weights are used. // Otherwise, no weighting is applied. // - // Grading periods and effective due dates are optional, but must be used + // 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 @@ -224,45 +258,58 @@ define([ // would otherwise include more information about a student's relationship // with an assignment and related grading periods. // - // GradingPeriod Grade information has the following shape: - // { - // : { - // assignmentGroups: { - // : - // } - // } - // } - // - // Course Grade information has the following shape: + // Grades minimally have the following shape: // { // score: number|null // possible: number|null // } // + // AssignmentGroup Grade maps have the following shape: + // { + // : + // } + // + // GradingPeriod Grade Sets have the following shape: + // { + // gradingPeriodId: + // gradingPeriodWeight: number + // assignmentGroups: + // current: + // final: + // scoreUnit: 'points'|'percent' + // } + // + // GradingPeriod Grade maps have the following shape: + // { + // : + // } + // // 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. // - // AssignmentGroup Grade information is the returned result from the + // An AssignmentGroup Grade Set is the returned result from the // AssignmentGroupGradeCalculator.calculate function. // - // Return value has the following shape: + // Return value is a Course Grade Set. + // A Course Grade Set has the following shape: // { - // gradingPeriods: - // group_sums: [array of AssignmentGroup Grade information *see above] - // current: - // final: + // assignmentGroups: + // gradingPeriods: + // current: + // final: + // scoreUnit: 'points'|'percent' // } - function calculate (submissions, assignmentGroups, weightingScheme, gradingPeriods, effectiveDueDates) { + function calculate (submissions, assignmentGroups, weightingScheme, gradingPeriodSet, effectiveDueDates) { const options = { - weightGradingPeriods: _.some(gradingPeriods, 'weight'), + weightGradingPeriods: gradingPeriodSet && !!gradingPeriodSet.weighted, weightAssignmentGroups: weightingScheme === 'percent' }; - if (gradingPeriods && effectiveDueDates) { + if (gradingPeriodSet && effectiveDueDates) { return calculateWithGradingPeriods( - submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options + submissions, assignmentGroups, gradingPeriodSet.gradingPeriods, effectiveDueDates, options ); } diff --git a/public/javascripts/grade_summary.js b/public/javascripts/grade_summary.js index 7cda8c5ad21..f25cf72e964 100644 --- a/public/javascripts/grade_summary.js +++ b/public/javascripts/grade_summary.js @@ -24,6 +24,7 @@ define([ 'jsx/gradebook/CourseGradeCalculator', 'jsx/gradebook/EffectiveDueDates', 'jsx/gradebook/GradingSchemeHelper', + 'compiled/api/gradingPeriodSetsApi', 'compiled/util/round', 'str/htmlEscape', 'jquery.ajaxJSON' /* ajaxJSON */, @@ -32,26 +33,55 @@ define([ 'jquery.instructure_misc_plugins' /* showIf */, 'jquery.templateData' /* fillTemplateData, getTemplateData */, 'media_comments' /* mediaComment, mediaCommentThumbnail */ -], function (INST, I18n, $, _, CourseGradeCalculator, EffectiveDueDates, GradingSchemeHelper, round, htmlEscape) { +], 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 () { - if (ENV.effective_due_dates) { - return CourseGradeCalculator.calculate( + var grades; + + if (ENV.effective_due_dates && ENV.grading_period_set) { + grades = CourseGradeCalculator.calculate( ENV.submissions, ENV.assignment_groups, ENV.group_weighting_scheme, - ENV.grading_periods, + getGradingPeriodSet(), EffectiveDueDates.scopeToUser(ENV.effective_due_dates, ENV.student_id) ); + } else { + grades = CourseGradeCalculator.calculate( + ENV.submissions, + ENV.assignment_groups, + ENV.group_weighting_scheme + ); } - return 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 = []; @@ -83,15 +113,20 @@ define([ return round((score / possible) * 100, round.DEFAULT); }; - 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( - round(groupGradeInfo.score, round.DEFAULT) + ' / ' + round(groupGradeInfo.possible, round.DEFAULT) + round(grade.score, round.DEFAULT) + ' / ' + round(grade.possible, round.DEFAULT) ); } @@ -141,11 +176,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); @@ -468,12 +505,15 @@ define([ }); } - return { + _.extend(GradeSummary, { setup: setup, addWhatIfAssignment: addWhatIfAssignment, removeWhatIfAssignment: removeWhatIfAssignment, + getGradingPeriodSet: getGradingPeriodSet, listAssignmentGroupsForGradeCalculation: listAssignmentGroupsForGradeCalculation, calculateGrades: calculateGrades, calculateTotals: calculateTotals - } + }); + + return GradeSummary; }); diff --git a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee index ed568f7b805..ec73a28bf3d 100644 --- a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee +++ b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee @@ -5,76 +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' } ] } - module "list", + module 'gradingPeriodSetsApi.list', setup: -> @server = sinon.fakeServer.create() @fakeHeaders = @@ -85,161 +93,131 @@ 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" - 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: null, - weight: 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", + title: 'Fall 2015', weighted: null, - enrollmentTermIDs: [ "1", "2" ] + 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", weighted: null }, - 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", + id: '1', + title: 'Fall 2015', weighted: false, - enrollment_term_ids: [ "1", "2" ], + 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' } } - module "create", + 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", + id: '1', + title: 'Fall 2015', weighted: true, - enrollmentTermIDs: [ "1", "2" ], + enrollmentTermIDs: ['1', '2'], permissions: { read: true, create: true, update: true, delete: true } } serializedSetUpdating = { - grading_period_set: { title: "Fall 2015", weighted: true }, - 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", + id: '1', + title: 'Fall 2015', weighted: true, - enrollment_term_ids: [ "1", "2" ], + 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"), + 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 } @@ -248,29 +226,26 @@ define [ } } - module "update", + 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' diff --git a/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee b/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee index 30f6830e122..7f02c8d40f1 100644 --- a/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee +++ b/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee @@ -6,20 +6,20 @@ 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, 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, weight: 60 @@ -29,18 +29,18 @@ define [ 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 } ] @@ -49,20 +49,20 @@ 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, 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, weight: 60 @@ -70,61 +70,38 @@ define [ ] } - module "batchUpdate", + 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, - weight: 40 - } - ] - } - 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() + module 'deserializePeriods' - 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 diff --git a/spec/coffeescripts/gradebook/GradebookSpec.coffee b/spec/coffeescripts/gradebook/GradebookSpec.coffee index b3c63b94944..33cdd464dc0 100644 --- a/spec/coffeescripts/gradebook/GradebookSpec.coffee +++ b/spec/coffeescripts/gradebook/GradebookSpec.coffee @@ -1,20 +1,69 @@ +# +# 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 . +# + define [ + 'spec/jsx/gradebook/GradeCalculatorSpecHelper' 'compiled/gradebook/Gradebook' 'jsx/gradebook/DataLoader' 'underscore' 'timezone' 'compiled/SubmissionDetailsDialog' 'jsx/gradebook/CourseGradeCalculator' -], (Gradebook, DataLoader, _, tz, SubmissionDetailsDialog, CourseGradeCalculator) -> - module "Gradebook#calculateStudentGrade", +], (GradeCalculatorSpecHelper, Gradebook, DataLoader, _, tz, SubmissionDetailsDialog, CourseGradeCalculator) -> + exampleGradebookOptions = + settings: + show_concluded_enrollments: 'true' + show_inactive_enrollments: 'true' + sections: [] + + createExampleGrades = GradeCalculatorSpecHelper.createCourseGradesWithGradingPeriods + + 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) + + module 'Gradebook#calculateStudentGrade', setupThis:(options = {}) -> assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }] submissions = [{ assignment_id: 201, score: 10 }] defaults = { - gradingPeriodsEnabled: true + 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 @@ -25,49 +74,92 @@ define [ setup: -> @calculate = Gradebook.prototype.calculateStudentGrade - test "calculates grades using properties from the gradebook", -> + test 'calculates grades using properties from the gradebook', -> self = @setupThis() - @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + @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.gradingPeriods) + equal(args[3], self.gradingPeriodSet) - test "scopes effective due dates to the user", -> + test 'scopes effective due dates to the user', -> self = @setupThis() - @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + @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 grading periods are disabled", -> - self = @setupThis(gradingPeriodsEnabled: false) - @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + 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(args[3], undefined) - equal(args[4], undefined) + equal(typeof args[3], 'undefined') + equal(typeof args[4], 'undefined') - test "does not calculate when the student is not loaded", -> - self = @setupThis(gradingPeriodsEnabled: false) - @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + 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(gradingPeriodsEnabled: false) - @stub(CourseGradeCalculator, 'calculate').returns(group_sums: []) + 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) - module "Gradebook#gradeSort" + module 'Gradebook#gradeSort' - test "gradeSort - total_grade", -> + test 'gradeSort - total_grade', -> gradeSort = (showTotalGradeAsPoints, a, b, field, asc) -> asc = true unless asc? @@ -79,31 +171,31 @@ 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' gradebookStubs = -> indexedOverrides: Gradebook.prototype.indexedOverrides indexedGradingPeriods: _.indexBy(@gradingPeriods, 'id') - module "Gradebook#hideAggregateColumns", + module 'Gradebook#hideAggregateColumns', setupThis: (options) -> customOptions = options || {} defaults = @@ -161,9 +253,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 = [] @@ -194,17 +286,17 @@ 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') - module "Gradebook#submissionsForStudent", + 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 = { @@ -217,32 +309,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 (multiple grading periods disabled)', -> 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", + 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" + gradingPeriodToShow: '2' ) submissions = @submissionsForStudent.call(self, @student) - propEqual _.pluck(submissions, "assignment_id"), ["2"] + propEqual _.pluck(submissions, 'assignment_id'), ['2'] module 'Gradebook#studentsUrl', setupThis:(options) -> @@ -271,9 +363,61 @@ define [ self = @setupThis(showConcludedEnrollments: true, showInactiveEnrollments: true) equal @studentsUrl.call(self), 'students_with_concluded_and_inactive_enrollments_url' + 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 + + 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 + + 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 + module 'Gradebook#showNotesColumn', setup: -> - @loadNotes = @stub(DataLoader, "getDataForColumn") + @loadNotes = @stub(DataLoader, 'getDataForColumn') setupShowNotesColumn: (opts) -> defaultOptions = @@ -323,7 +467,7 @@ define [ } teardown: -> - @fixtureParent.innerHTML = "" + @fixtureParent.innerHTML = '' @fixture = undefined @fakeSubmissionDetailsDialog.restore() diff --git a/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee b/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee index b7960fdb638..55e14f84747 100644 --- a/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee +++ b/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee @@ -13,7 +13,6 @@ define [ 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)' @previousWindowENV = window.ENV _.extend(window.ENV, defaults) @@ -55,7 +54,6 @@ define [ 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)' @previousWindowENV = window.ENV _.extend(window.ENV, defaults) @@ -87,7 +85,6 @@ define [ 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' @previousWindowENV = window.ENV _.extend(window.ENV, defaults) diff --git a/spec/coffeescripts/gradezilla/GradebookSpec.coffee b/spec/coffeescripts/gradezilla/GradebookSpec.coffee index 2c71d18c6f8..34e90d6da3c 100644 --- a/spec/coffeescripts/gradezilla/GradebookSpec.coffee +++ b/spec/coffeescripts/gradezilla/GradebookSpec.coffee @@ -1,14 +1,166 @@ +# +# 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 . +# + define [ + 'spec/jsx/gradebook/GradeCalculatorSpecHelper' 'compiled/gradezilla/Gradebook' 'jsx/gradezilla/DataLoader' 'underscore' 'timezone' 'compiled/SubmissionDetailsDialog' -], (Gradebook, DataLoader, _, tz, SubmissionDetailsDialog) -> + 'jsx/gradebook/CourseGradeCalculator' +], (GradeCalculatorSpecHelper, Gradebook, DataLoader, _, tz, SubmissionDetailsDialog, CourseGradeCalculator) -> + exampleGradebookOptions = + settings: + show_concluded_enrollments: 'true' + show_inactive_enrollments: 'true' + sections: [] - module "Gradebook#gradeSort" + createExampleGrades = GradeCalculatorSpecHelper.createCourseGradesWithGradingPeriods - test "gradeSort - total_grade", -> + 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) + + module 'Gradebook#calculateStudentGrade', + setupThis:(options = {}) -> + assignments = [{ id: 201, points_possible: 10, omit_from_final_grade: false }] + submissions = [{ assignment_id: 201, score: 10 }] + defaults = { + gradingPeriodsEnabled: true + 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(gradingPeriodsEnabled: false) + @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(gradingPeriodsEnabled: false) + @stub(CourseGradeCalculator, 'calculate').returns(createExampleGrades()) + @calculate.call(self, id: '101', loaded: true, initialized: false) + notOk(CourseGradeCalculator.calculate.called) + + module 'Gradebook#gradeSort' + + test 'gradeSort - total_grade', -> gradeSort = (showTotalGradeAsPoints, a, b, field, asc) -> asc = true unless asc? @@ -20,31 +172,31 @@ 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' gradebookStubs = -> indexedOverrides: Gradebook.prototype.indexedOverrides indexedGradingPeriods: _.indexBy(@gradingPeriods, 'id') - module "Gradebook#hideAggregateColumns", + module 'Gradebook#hideAggregateColumns', setupThis: (options) -> customOptions = options || {} defaults = @@ -102,9 +254,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 = [] @@ -135,17 +287,17 @@ 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') - module "Gradebook#submissionsForStudent", + 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 = { @@ -158,32 +310,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 (multiple grading periods disabled)', -> 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", + 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" + gradingPeriodToShow: '2' ) submissions = @submissionsForStudent.call(self, @student) - propEqual _.pluck(submissions, "assignment_id"), ["2"] + propEqual _.pluck(submissions, 'assignment_id'), ['2'] module 'Gradebook#studentsUrl', setupThis:(options) -> @@ -212,9 +364,61 @@ define [ self = @setupThis(showConcludedEnrollments: true, showInactiveEnrollments: true) equal @studentsUrl.call(self), 'students_with_concluded_and_inactive_enrollments_url' + 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 + + 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 + + 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 + module 'Gradebook#showNotesColumn', setup: -> - @loadNotes = @stub(DataLoader, "getDataForColumn") + @loadNotes = @stub(DataLoader, 'getDataForColumn') setupShowNotesColumn: (opts) -> defaultOptions = @@ -264,7 +468,7 @@ define [ } teardown: -> - @fixtureParent.innerHTML = "" + @fixtureParent.innerHTML = '' @fixture = undefined @fakeSubmissionDetailsDialog.restore() diff --git a/spec/coffeescripts/gradezilla/SubmissionDetailsDialogSpec.coffee b/spec/coffeescripts/gradezilla/SubmissionDetailsDialogSpec.coffee index b7960fdb638..55e14f84747 100644 --- a/spec/coffeescripts/gradezilla/SubmissionDetailsDialogSpec.coffee +++ b/spec/coffeescripts/gradezilla/SubmissionDetailsDialogSpec.coffee @@ -13,7 +13,6 @@ define [ 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)' @previousWindowENV = window.ENV _.extend(window.ENV, defaults) @@ -55,7 +54,6 @@ define [ 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)' @previousWindowENV = window.ENV _.extend(window.ENV, defaults) @@ -87,7 +85,6 @@ define [ 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' @previousWindowENV = window.ENV _.extend(window.ENV, defaults) diff --git a/spec/controllers/gradebooks_controller_spec.rb b/spec/controllers/gradebooks_controller_spec.rb index 85653487d52..a19a6d3eb36 100644 --- a/spec/controllers/gradebooks_controller_spec.rb +++ b/spec/controllers/gradebooks_controller_spec.rb @@ -316,8 +316,16 @@ describe GradebooksController do end context "Multiple 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 @@ -333,11 +341,35 @@ describe GradebooksController do expect(assigns[:exclude_total]).to eq false end - it "assigns values for grade calculator to ENV" do + 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 - expect(assigns[:js_env][:grading_periods]).not_to be_nil - expect(assigns[:js_env][:effective_due_dates]).not_to be_nil + 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 @@ -541,6 +573,50 @@ describe GradebooksController do expect(assigns[:js_env][:STUDENT_CONTEXT_CARDS_ENABLED]).to eq true end end + + context "with multiple 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 + + 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 diff --git a/spec/javascripts/jsx/grade_summary.spec.jsx b/spec/javascripts/jsx/grade_summary.spec.jsx index fcb2315dea7..5ed77b1b825 100644 --- a/spec/javascripts/jsx/grade_summary.spec.jsx +++ b/spec/javascripts/jsx/grade_summary.spec.jsx @@ -20,9 +20,14 @@ define([ 'lodash', 'jquery', 'helpers/fakeENV', + 'spec/jsx/gradebook/GradeCalculatorSpecHelper', 'jsx/gradebook/CourseGradeCalculator', 'grade_summary' -], (_, $, fakeENV, CourseGradeCalculator, grade_summary) => { // eslint-disable-line camelcase +], ( + _, $, fakeENV, GradeCalculatorSpecHelper, CourseGradeCalculator, grade_summary // eslint-disable-line camelcase +) => { + let exampleGrades; + function createAssignmentGroups () { return [ { id: '301', assignments: [{ id: '201', muted: false }, { id: '202', muted: true }] }, @@ -37,9 +42,38 @@ define([ ]; } - module('grade_summary#calculateTotals', { + module('grade_summary.getGradingPeriodSet', { setup () { fakeENV.setup(); + }, + + teardown () { + fakeENV.teardown(); + } + }); + + test('normalizes the grading period set from the env', function () { + ENV.grading_period_set = { + id: 1501, + grading_periods: [{ id: 701, weight: 50 }, { id: 702, weight: 50 }], + weighted: true + }; + const gradingPeriodSet = grade_summary.getGradingPeriodSet(); + deepEqual(gradingPeriodSet.id, '1501'); + equal(gradingPeriodSet.gradingPeriods.length, 2); + deepEqual(_.map(gradingPeriodSet.gradingPeriods, 'id'), ['701', '702']); + }); + + test('returns null when the grading period set is not defined in the env', function () { + ENV.grading_period_set = undefined; + const gradingPeriodSet = grade_summary.getGradingPeriodSet(); + deepEqual(gradingPeriodSet, null); + }); + + module('grade_summary.calculateTotals', { + setup () { + fakeENV.setup(); + ENV.assignment_groups = createAssignmentGroups(); this.screenReaderFlashMessageExclusive = this.stub($, 'screenReaderFlashMessageExclusive'); $('#fixtures').html('
'); @@ -47,102 +81,9 @@ define([ this.currentOrFinal = 'current'; this.groupWeightingScheme = null; this.calculatedGrades = { - group_sums: [ - { - group: { - id: '1', - rules: {}, - group_weight: 0, - assignments: [ - { - id: '4', - submission_types: ['none'], - points_possible: 10, - due_at: '2017-01-03T06:59:00Z', - omit_from_final_grade: false - }, { - id: '3', - submission_types: ['none'], - points_possible: 10, - due_at: '2016-12-26T06:59:00Z', - omit_from_final_grade: false - } - ] - }, - current: { - possible: 0, - score: 0, - submission_count: 0, - submissions: [ - { - percent: 0, - possible: 10, - score: 0, - submission: { - assignment_id: '4', - score: null, - excused: false, - workflow_state: 'unsubmitted' - }, - submitted: false - }, - { - percent: 0, - possible: 10, - score: 0, - submission: { - assignment_id: '3', - score: null, - excused: false, - workflow_state: 'unsubmitted' - }, - submitted: false - } - ], - weight: 0 - }, - final: { - possible: 20, - score: 0, - submission_count: 0, - submissions: [ - { - percent: 0, - possible: 10, - score: 0, - submission: { - assignment_id: '4', - score: null, - excused: false, - workflow_state: 'unsubmitted' - }, - submitted: false - }, - { - percent: 0, - possible: 10, - score: 0, - submission: { - assignment_id: '3', - score: null, - excused: false, - workflow_state: 'unsubmitted' - }, - submitted: false - } - ], - weight: 0 - } - } - ], - current: { - score: 0, - possible: 0 - }, - final: { - score: 0, - possible: 20 - } + assignmentGroups: {}, + current: { score: 0, possible: 0 }, + final: { score: 0, possible: 20 } }; }, @@ -153,14 +94,12 @@ define([ test('generates a screenreader-only alert when grades have been changed', function () { grade_summary.calculateTotals(this.calculatedGrades, this.currentOrFinal, this.groupWeightingScheme); - ok(this.screenReaderFlashMessageExclusive.calledOnce); }); test('does not generate a screenreader-only alert when grades are unchanged', function () { $('#fixtures').html(''); grade_summary.calculateTotals(this.calculatedGrades, this.currentOrFinal, this.groupWeightingScheme); - notOk(this.screenReaderFlashMessageExclusive.called); }); @@ -211,10 +150,15 @@ define([ ENV.submissions = createSubmissions(); ENV.assignment_groups = createAssignmentGroups(); ENV.group_weighting_scheme = 'points'; - ENV.grading_periods = [{ id: 701, weight: 50 }, { id: 702, weight: 50 }]; + ENV.grading_period_set = { + id: 1501, + grading_periods: [{ id: 701, weight: 50 }, { id: 702, weight: 50 }], + weighted: true + }; ENV.effective_due_dates = { 201: { 101: { grading_period_id: '701' } } }; ENV.student_id = '101'; - this.stub(CourseGradeCalculator, 'calculate').returns('expected'); + exampleGrades = GradeCalculatorSpecHelper.createCourseGradesWithGradingPeriods(); + this.stub(CourseGradeCalculator, 'calculate').returns(exampleGrades); }, teardown () { @@ -223,37 +167,47 @@ define([ }); test('calculates grades using data in the env', function () { - this.stub(CourseGradeCalculator, 'calculate').returns('expected'); grade_summary.calculateGrades(); const args = CourseGradeCalculator.calculate.getCall(0).args; equal(args[0], ENV.submissions); deepEqual(_.map(args[1], 'id'), ['301', '302']); equal(args[2], ENV.group_weighting_scheme); - equal(args[3], ENV.grading_periods); }); - test('returns the result of grade calculation from the grade calculator', function () { - const grades = grade_summary.calculateGrades(); - equal(grades, 'expected'); + test('normalizes the grading period set before calculation', function () { + grade_summary.calculateGrades(); + const gradingPeriodSet = CourseGradeCalculator.calculate.getCall(0).args[3]; + deepEqual(gradingPeriodSet.id, '1501'); + equal(gradingPeriodSet.gradingPeriods.length, 2); + deepEqual(_.map(gradingPeriodSet.gradingPeriods, 'id'), ['701', '702']); }); test('scopes effective due dates to the user', function () { - this.stub(CourseGradeCalculator, 'calculate'); grade_summary.calculateGrades(); const dueDates = CourseGradeCalculator.calculate.getCall(0).args[4]; deepEqual(dueDates, { 201: { grading_period_id: '701' } }); }); - test('calculates grades without grading period data when effective due dates are not defined', function () { - delete ENV.effective_due_dates; - this.stub(CourseGradeCalculator, 'calculate'); + test('calculates grades without grading period data when the grading period set is not defined', function () { + delete ENV.grading_period_set; grade_summary.calculateGrades(); const args = CourseGradeCalculator.calculate.getCall(0).args; equal(args[0], ENV.submissions); equal(args[1], ENV.assignment_groups); equal(args[2], ENV.group_weighting_scheme); - equal(args[3], undefined); - equal(args[4], undefined); + equal(typeof args[3], 'undefined'); + equal(typeof args[4], 'undefined'); + }); + + test('calculates grades without grading period data when effective due dates are not defined', function () { + delete ENV.effective_due_dates; + grade_summary.calculateGrades(); + const args = CourseGradeCalculator.calculate.getCall(0).args; + equal(args[0], ENV.submissions); + equal(args[1], ENV.assignment_groups); + equal(args[2], ENV.group_weighting_scheme); + equal(typeof args[3], 'undefined'); + equal(typeof args[4], 'undefined'); }); test('includes muted assignments where "What-If" grades exist', function () { @@ -264,4 +218,33 @@ define([ equal(assignmentGroups[0].assignments.length, 2); equal(assignmentGroups[1].assignments.length, 1); }); + + test('returns course grades when no grading period id is provided', function () { + this.stub(grade_summary, 'getGradingPeriodIdFromUrl').returns(null); + const grades = grade_summary.calculateGrades(); + equal(grades, exampleGrades); + }); + + test('scopes grades to the provided grading period id', function () { + this.stub(grade_summary, 'getGradingPeriodIdFromUrl').returns('701'); + const grades = grade_summary.calculateGrades(); + equal(grades, exampleGrades.gradingPeriods[701]); + }); + + module('grade_summary.getGradingPeriodIdFromUrl'); + + test('returns the value for grading_period_id in the url', function () { + const url = 'example.com/course/1/grades?grading_period_id=701'; + equal(grade_summary.getGradingPeriodIdFromUrl(url), '701'); + }); + + test('returns null when grading_period_id is set to "0"', function () { + const url = 'example.com/course/1/grades?grading_period_id=0'; + deepEqual(grade_summary.getGradingPeriodIdFromUrl(url), null); + }); + + test('returns null when grading_period_id is not present in the url', function () { + const url = 'example.com/course/1/grades'; + deepEqual(grade_summary.getGradingPeriodIdFromUrl(url), null); + }); }); diff --git a/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx b/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx index 5d935df8edc..4fc83d0a1e8 100644 --- a/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx +++ b/spec/javascripts/jsx/gradebook/AssignmentGroupGradeCalculatorSpec.jsx @@ -27,22 +27,33 @@ define([ module('AssignmentGroupGradeCalculator.calculate with no submissions and no assignments', { setup () { submissions = []; - assignmentGroup = { id: 301, rules: {}, assignments: [] }; + assignmentGroup = { id: 301, rules: {}, assignments: [], group_weight: 100 }; } }); - test('returns a current and final score of 0', () => { + test('returns a current and final score of 0', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 0); equal(grades.final.score, 0); }); - test('includes 0 points possible', () => { + test('includes 0 points possible', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 0); equal(grades.final.possible, 0); }); + test('includes assignment group attributes', function () { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); + equal(grades.assignmentGroupId, 301); + equal(grades.assignmentGroupWeight, 100); + }); + + test('uses a score unit of "points"', function () { + const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); + equal(grades.scoreUnit, 'points'); + }); + module('AssignmentGroupGradeCalculator.calculate with no submissions and some assignments', { setup () { submissions = []; @@ -57,13 +68,13 @@ define([ } }); - test('returns a current and final score of 0', () => { + test('returns a current and final score of 0', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 0); equal(grades.final.score, 0); }); - test('include the sum of points possible', () => { + test('include the sum of points possible', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 0); equal(grades.final.possible, 1284); @@ -89,82 +100,82 @@ define([ } }); - test('adds all scores for current and final grades', () => { + test('adds all scores for current and final grades', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 159); equal(grades.final.score, 159); }); - test('excludes assignment points on ungraded submissions for the current grade', () => { + test('excludes assignment points on ungraded submissions for the current grade', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 284); equal(grades.final.possible, 1284); }); - test('ignores hidden submissions', () => { + test('ignores hidden submissions', function () { submissions[1].hidden = true; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 117); equal(grades.final.score, 117); }); - test('excludes assignment points on hidden submissions', () => { + test('excludes assignment points on hidden submissions', function () { submissions[1].hidden = true; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 193); equal(grades.final.possible, 1193); }); - test('ignores excused submissions', () => { + test('ignores excused submissions', function () { submissions[1].excused = true; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 117); equal(grades.final.score, 117); }); - test('excludes assignment points on excused submissions', () => { + test('excludes assignment points on excused submissions', function () { submissions[1].excused = true; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 193); equal(grades.final.possible, 1193); }); - test('excludes submissions "pending review" from the current grade', () => { + test('excludes submissions "pending review" from the current grade', function () { submissions[1].workflow_state = 'pending_review'; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 117); equal(grades.current.possible, 193); }); - test('includes submissions "pending review" in the final grade', () => { + test('includes submissions "pending review" in the final grade', function () { submissions[1].workflow_state = 'pending_review'; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 159); equal(grades.final.possible, 1284); }); - test('excludes assignments "omitted from final grade" from the current grade', () => { + test('excludes assignments "omitted from final grade" from the current grade', function () { assignmentGroup.assignments[2].omit_from_final_grade = true; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 145); equal(grades.current.possible, 229); }); - test('excludes assignments "omitted from final grade" from the final grade', () => { + test('excludes assignments "omitted from final grade" from the final grade', function () { assignmentGroup.assignments[2].omit_from_final_grade = true; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 145); equal(grades.final.possible, 1229); }); - test('excludes ungraded assignments "omitted from final grade" from the final grade', () => { + test('excludes ungraded assignments "omitted from final grade" from the final grade', function () { assignmentGroup.assignments[4].omit_from_final_grade = true; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 159); equal(grades.final.possible, 284); }); - test('eliminates multiple submissions for the same assignment', () => { + test('eliminates multiple submissions for the same assignment', function () { submissions.push({ ...submissions[0] }); const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 159); @@ -187,7 +198,7 @@ define([ } }); - test('includes scores for submissions on unpointed assignments', () => { + test('includes scores for submissions on unpointed assignments', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 20); equal(grades.final.score, 20); @@ -209,7 +220,7 @@ define([ } }); - test('drops one submission to maximize overall percentage grade', () => { + test('drops one submission to maximize overall percentage grade', function () { // drop 31/40, keep 17/24, keep 6/10 = 23/34 = 67.6% // keep 31/40, drop 17/24, keep 6/10 = 37/50 = 74.0% // keep 31/40, keep 17/24, drop 6/10 = 48/64 = 75.0% @@ -220,7 +231,7 @@ define([ ok(grades.final.submissions[2].drop); }); - test('drops pointed assignments over unpointed assignments', () => { + test('drops pointed assignments over unpointed assignments', function () { assignmentGroup.assignments[0].points_possible = 0; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 37); @@ -229,27 +240,27 @@ define([ ok(grades.final.submissions[1].drop); }); - test('excludes points possible from the assignment for the dropped submission', () => { + test('excludes points possible from the assignment for the dropped submission', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 64); equal(grades.final.possible, 64); }); - test('ignores ungraded submissions for the current grade', () => { + test('ignores ungraded submissions for the current grade', function () { submissions[2].score = null; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 31); equal(grades.final.score, 48); }); - test('excludes points possible for assignments with ungraded submissions for the current grade', () => { + test('excludes points possible for assignments with ungraded submissions for the current grade', function () { submissions[2].score = null; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 40); equal(grades.final.possible, 64); }); - test('accounts for impact on overall grade rather than score alone', () => { + test('accounts for impact on overall grade rather than score alone', function () { submissions[2].score = 7; // drop 31/40, keep 17/24, keep 7/10 = 24/34 = 70.6% @@ -264,7 +275,7 @@ define([ ok(grades.final.submissions[1].drop); }); - test('does not drop submissions or assignments when drop_lowest is 0', () => { + test('does not drop submissions or assignments when drop_lowest is 0', function () { assignmentGroup.rules.drop_lowest = 0; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 54, 'current score includes all submission scores'); @@ -300,7 +311,7 @@ define([ } }); - test('drops multiple submissions to maximize overall percentage grade', () => { + test('drops multiple submissions to maximize overall percentage grade', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); // drop 100/100, drop 42/91, keep 14/55, keep 3/38, ignore -/1000 = 17/93 = 18.3% @@ -330,7 +341,7 @@ define([ ok(grades.final.submissions[4].drop); }); - test('drops all but one score when drop_lowest is equal to the number of submissions', () => { + test('drops all but one score when drop_lowest is equal to the number of submissions', function () { assignmentGroup.rules = { drop_lowest: 4 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 100); @@ -352,7 +363,7 @@ define([ // This test is here because the reductive algorithm used for grading can // potentially enter into an infinite loop. While this setup data is indeed // ridiculous, its presence guarantees that the algorithm will always finish. - test('works in ridiculous circumstances', () => { + test('works in ridiculous circumstances', function () { submissions[0].score = null; submissions[1].score = 3; submissions[2].score = null; @@ -387,7 +398,7 @@ define([ } }); - test('drops one submission to minimize overall percentage grade', () => { + test('drops one submission to minimize overall percentage grade', function () { // drop 31/40, keep 17/24, keep 6/10 = 23/34 = 67.6% // keep 31/40, drop 17/24, keep 6/10 = 37/50 = 74.0% // keep 31/40, keep 17/24, drop 6/10 = 48/64 = 75.0% @@ -398,27 +409,27 @@ define([ ok(grades.final.submissions[0].drop); }); - test('excludes points possible from the assignment for the dropped submission', () => { + test('excludes points possible from the assignment for the dropped submission', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 34); equal(grades.final.possible, 34); }); - test('ignores ungraded submissions for the current grade', () => { + test('ignores ungraded submissions for the current grade', function () { submissions[0].score = null; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 6); equal(grades.final.score, 6); }); - test('excludes points possible for assignments with ungraded submissions for the current grade', () => { + test('excludes points possible for assignments with ungraded submissions for the current grade', function () { submissions[0].score = null; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.possible, 10); equal(grades.final.possible, 50); }); - test('accounts for impact on overall grade rather than score alone', () => { + test('accounts for impact on overall grade rather than score alone', function () { submissions[2].score = 10; // drop 31/40, keep 17/24, keep 10/10 = 27/34 = 79.4% @@ -433,7 +444,7 @@ define([ ok(grades.final.submissions[2].drop); }); - test('does not drop submissions or assignments when drop_highest is 0', () => { + test('does not drop submissions or assignments when drop_highest is 0', function () { assignmentGroup.rules.drop_highest = 0; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 54, 'current score includes all submission scores'); @@ -462,7 +473,7 @@ define([ } }); - test('drops multiple submissions to minimize overall percentage grade', () => { + test('drops multiple submissions to minimize overall percentage grade', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); // drop 100/100, drop 42/91, keep 14/55, keep 30/38, ignore -/1000 = 34/93 = 36.6% @@ -495,7 +506,7 @@ define([ // This behavior was explicitly written into the grade calculator. While // possibly unintended, this test is here to ensure this behavior is protected // until a decision is made to change it. - test('does not drop any scores when drop_highest is equal to the number of submissions', () => { + test('does not drop any scores when drop_highest is equal to the number of submissions', function () { assignmentGroup.rules = { drop_highest: 4 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 186); @@ -517,7 +528,7 @@ define([ // This behavior was explicitly written into the grade calculator. While // possibly unintended, this test is here to ensure this behavior is protected // until a decision is made to change it. - test('does not drop any scores when drop_highest is greater than the number of submissions', () => { + test('does not drop any scores when drop_highest is greater than the number of submissions', function () { assignmentGroup.rules = { drop_highest: 5 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 186); @@ -556,7 +567,7 @@ define([ } }); - test('drops the most and least favorable scores', () => { + test('drops the most and least favorable scores', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 56); equal(grades.current.possible, 146); @@ -571,7 +582,7 @@ define([ // This behavior was explicitly written into the grade calculator. While // possibly unintended, this test is here to ensure this behavior is protected // until a decision is made to change it. - test('does not drop higher scores when combined drop rules match the number of submissions', () => { + test('does not drop higher scores when combined drop rules match the number of submissions', function () { assignmentGroup.rules = { drop_lowest: 2, drop_highest: 2 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 103); @@ -593,7 +604,7 @@ define([ // This behavior was explicitly written into the grade calculator. While // possibly unintended, this test is here to ensure this behavior is protected // until a decision is made to change it. - test('does not drop higher scores when combined drop rules exceed the number of submissions', () => { + test('does not drop higher scores when combined drop rules exceed the number of submissions', function () { assignmentGroup.rules = { drop_lowest: 2, drop_highest: 3 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 103); @@ -630,7 +641,7 @@ define([ } }); - test('drops the same low-score submission regardless of submission order', () => { + test('drops the same low-score submission regardless of submission order', function () { assignmentGroup.rules = { drop_lowest: 1 }; let grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission1 = _.find(grades.current.submissions, 'drop'); @@ -640,7 +651,7 @@ define([ equal(droppedSubmission1.assignment_id, droppedSubmission2.assignment_id); }); - test('drops the same high-score submission regardless of submission order', () => { + test('drops the same high-score submission regardless of submission order', function () { assignmentGroup.rules = { drop_highest: 1 }; let grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); const droppedSubmission1 = _.find(grades.current.submissions, 'drop'); @@ -650,7 +661,7 @@ define([ equal(droppedSubmission1.assignment_id, droppedSubmission2.assignment_id); }); - test('drops the same low-score submission for unpointed assignments', () => { + test('drops the same low-score submission for unpointed assignments', function () { assignmentGroup.rules = { drop_lowest: 1 }; assignmentGroup.assignments[0].points_possible = 0; assignmentGroup.assignments[1].points_possible = 0; @@ -664,7 +675,7 @@ define([ equal(droppedSubmission1.assignment_id, droppedSubmission2.assignment_id); }); - test('drops the same high-score submission for unpointed assignments', () => { + test('drops the same high-score submission for unpointed assignments', function () { assignmentGroup.rules = { drop_highest: 1 }; assignmentGroup.assignments[0].points_possible = 0; assignmentGroup.assignments[1].points_possible = 0; @@ -696,21 +707,21 @@ define([ } }); - test('drops the submission with the lowest score when drop_lowest is 1', () => { + test('drops the submission with the lowest score when drop_lowest is 1', function () { assignmentGroup.rules = { drop_lowest: 1 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 35, 'all scores above the 0 are included'); ok(grades.current.submissions[3].drop); }); - test('drops the submission with the highest score when drop_highest is 1', () => { + test('drops the submission with the highest score when drop_highest is 1', function () { assignmentGroup.rules = { drop_highest: 1 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 15, 'all scores below the 20 are included'); ok(grades.current.submissions[2].drop); }); - test('drops submissions that match the given rules', () => { + test('drops submissions that match the given rules', function () { assignmentGroup.rules = { drop_highest: 1, drop_lowest: 2 }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 10, 'only the score of 10 is included'); @@ -735,13 +746,13 @@ define([ } }); - test('sets current score as 0', () => { + test('sets current score as 0', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 0, 'current score is 0 points when all submissions are excluded'); equal(grades.current.possible, 0, 'current possible is 0 when all assignment points are excluded'); }); - test('sets final score as 0', () => { + test('sets final score as 0', function () { const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.final.score, 0, 'final score is 0 points when all submissions are excluded'); equal(grades.final.possible, 35, 'final possible is sum of all assignment points'); @@ -770,7 +781,7 @@ define([ } }); - test('prevents submissions from being dropped for low scores', () => { + test('prevents submissions from being dropped for low scores', function () { assignmentGroup.rules.drop_lowest = 1; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 56); @@ -787,7 +798,7 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('prevents submissions from being dropped for high scores', () => { + test('prevents submissions from being dropped for high scores', function () { assignmentGroup.rules = { drop_highest: 1, never_drop: [201] }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 49); @@ -804,7 +815,7 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('considers multiple assignments', () => { + test('considers multiple assignments', function () { assignmentGroup.rules = { drop_lowest: 1, never_drop: [203, 204] }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 49); @@ -824,7 +835,7 @@ define([ // This behavior was explicitly written into the grade calculator. While // possibly unintended, this test is here to ensure this behavior is protected // until a decision is made to change it. - test('does not drop any scores when drop_lowest is equal to the number of droppable submissions', () => { + test('does not drop any scores when drop_lowest is equal to the number of droppable submissions', function () { assignmentGroup.rules = { drop_lowest: 1, never_drop: [202, 203, 204] }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 68); @@ -844,7 +855,7 @@ define([ // This behavior was explicitly written into the grade calculator. While // possibly unintended, this test is here to ensure this behavior is protected // until a decision is made to change it. - test('does not drop any scores when drop_highest is equal to the number of droppable submissions', () => { + test('does not drop any scores when drop_highest is equal to the number of droppable submissions', function () { assignmentGroup.rules = { drop_highest: 1, never_drop: [202, 203, 204] }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); equal(grades.current.score, 68); @@ -861,7 +872,7 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('does not drop any low score submissions when all assignments are listed as "never drop"', () => { + test('does not drop any low score submissions when all assignments are listed as "never drop"', function () { assignmentGroup.rules = { drop_lowest: 1, never_drop: [201, 202, 203, 204] }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); notOk(grades.current.submissions[0].drop); @@ -874,7 +885,7 @@ define([ notOk(grades.final.submissions[3].drop); }); - test('does not drop any high score submissions when all assignments are listed as "never drop"', () => { + test('does not drop any high score submissions when all assignments are listed as "never drop"', function () { assignmentGroup.rules = { drop_highest: 1, never_drop: [201, 202, 203, 204] }; const grades = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup); notOk(grades.current.submissions[0].drop); diff --git a/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx b/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx index 8f3cc1b27ad..167328da2ae 100644 --- a/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx +++ b/spec/javascripts/jsx/gradebook/CourseGradeCalculatorSpec.jsx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Instructure, Inc. + * Copyright (C) 2016 - 2017 Instructure, Inc. * * This file is part of Canvas. * @@ -23,6 +23,7 @@ define([ let submissions; let assignments; let assignmentGroups; + let gradingPeriodSet; let gradingPeriods; let effectiveDueDates; @@ -34,7 +35,7 @@ define([ function calculateWithGradingPeriods (weightingScheme) { return CourseGradeCalculator.calculate( - submissions, assignmentGroups, weightingScheme, gradingPeriods, effectiveDueDates + submissions, assignmentGroups, weightingScheme, gradingPeriodSet, effectiveDueDates ); } @@ -47,6 +48,14 @@ define([ } }); + test('includes assignment group grades', function () { + const grades = calculateWithoutGradingPeriods('points'); + equal(grades.assignmentGroups[301].current.score, 0); + equal(grades.assignmentGroups[301].final.score, 0); + equal(grades.assignmentGroups[301].current.possible, 0); + equal(grades.assignmentGroups[301].final.possible, 0); + }); + test('returns a current and final score of 0 when weighting scheme is points', function () { const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 0, 'current score is 0 when there are no submissions'); @@ -71,6 +80,16 @@ define([ equal(grades.final.possible, 100, 'percent possible is 100'); }); + test('uses a score unit of "points" when weighting scheme is not percent', function () { + const grades = calculateWithoutGradingPeriods('points'); + equal(grades.scoreUnit, 'points'); + }); + + test('uses a score unit of "percentage" when weighting scheme is percent', function () { + const grades = calculateWithoutGradingPeriods('percent'); + equal(grades.scoreUnit, 'percentage'); + }); + module('CourseGradeCalculator.calculate with no submissions and some assignments', { setup () { submissions = []; @@ -87,6 +106,18 @@ define([ } }); + test('includes assignment group grades', function () { + const grades = calculateWithoutGradingPeriods('points'); + equal(grades.assignmentGroups[301].current.score, 0); + equal(grades.assignmentGroups[301].final.score, 0); + equal(grades.assignmentGroups[301].current.possible, 0); + equal(grades.assignmentGroups[301].final.possible, 15); + equal(grades.assignmentGroups[302].current.score, 0); + equal(grades.assignmentGroups[302].final.score, 0); + equal(grades.assignmentGroups[302].current.possible, 0); + equal(grades.assignmentGroups[302].final.possible, 20); + }); + test('sets scores to 0 when weighting scheme is points', function () { const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 0, 'current score is 0 when there are no submissions'); @@ -145,6 +176,32 @@ define([ } }); + test('includes assignment group grades', function () { + const grades = calculateWithoutGradingPeriods('points'); + equal(grades.assignmentGroups[301].current.score, 142); + equal(grades.assignmentGroups[301].final.score, 142); + equal(grades.assignmentGroups[301].current.possible, 191); + equal(grades.assignmentGroups[301].final.possible, 191); + equal(grades.assignmentGroups[302].current.score, 17); + equal(grades.assignmentGroups[302].final.score, 17); + equal(grades.assignmentGroups[302].current.possible, 93); + equal(grades.assignmentGroups[302].final.possible, 1093); + }); + + test('includes all assignment group grades regardless of weight', function () { + assignmentGroups[0].group_weight = 200; + assignmentGroups[1].group_weight = null; + const grades = calculateWithoutGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 142); + equal(grades.assignmentGroups[301].final.score, 142); + equal(grades.assignmentGroups[301].current.possible, 191); + equal(grades.assignmentGroups[301].final.possible, 191); + equal(grades.assignmentGroups[302].current.score, 17); + equal(grades.assignmentGroups[302].final.score, 17); + equal(grades.assignmentGroups[302].current.possible, 93); + equal(grades.assignmentGroups[302].final.possible, 1093); + }); + test('adds all scores for current and final grades when weighting scheme is points', function () { const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 159, 'current score is sum of all graded submission scores'); @@ -170,6 +227,7 @@ define([ }); test('up-scales group weights which do not add up to exactly 100 percent', function () { + // 5 / (5+5) = 50% assignmentGroups[0].group_weight = 5; assignmentGroups[1].group_weight = 5; const grades = calculateWithoutGradingPeriods('percent'); @@ -234,6 +292,18 @@ define([ } }); + test('includes all assignment group grades regardless of points possible', function () { + const grades = calculateWithoutGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 15); + equal(grades.assignmentGroups[301].final.score, 15); + equal(grades.assignmentGroups[301].current.possible, 0); + equal(grades.assignmentGroups[301].final.possible, 0); + equal(grades.assignmentGroups[302].current.score, 20); + equal(grades.assignmentGroups[302].final.score, 20); + equal(grades.assignmentGroups[302].current.possible, 0); + equal(grades.assignmentGroups[302].final.possible, 0); + }); + test('adds all scores for current and final grades when weighting scheme is points', function () { const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 35, 'current score is sum of all submission scores'); @@ -276,6 +346,14 @@ define([ } }); + test('includes all assignment group grades regardless of submissions graded', function () { + const grades = calculateWithoutGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 0); + equal(grades.assignmentGroups[301].final.score, 0); + equal(grades.assignmentGroups[301].current.possible, 0); + equal(grades.assignmentGroups[301].final.possible, 35); + }); + test('sets current score to 0 when weighting scheme is points', function () { const grades = calculateWithoutGradingPeriods('points'); equal(grades.current.score, 0, 'current score is 0 points when all submissions are excluded'); @@ -307,6 +385,199 @@ define([ equal(grades.final.score, null, 'final score cannot be calculated without group weight'); }); + module('CourseGradeCalculator.calculate with unweighted grading periods', { + setup () { + submissions = [ + { assignment_id: 201, score: 10 }, + { assignment_id: 202, score: 5 }, + { assignment_id: 203, score: 12 }, + { assignment_id: 204, score: 16 } + ]; + assignments = [ + { id: 201, points_possible: 10, omit_from_final_grade: false }, + { id: 202, points_possible: 10, omit_from_final_grade: false }, + { id: 203, points_possible: 20, omit_from_final_grade: false }, + { id: 204, points_possible: 40, omit_from_final_grade: false } + ]; + assignmentGroups = [ + { id: 301, group_weight: 60, rules: {}, assignments: assignments.slice(0, 2) }, + { id: 302, group_weight: 20, rules: {}, assignments: assignments.slice(2, 3) }, + { id: 303, group_weight: 20, rules: {}, assignments: assignments.slice(3, 4) } + ]; + gradingPeriods = [ + { id: '701', weight: 50 }, + { id: '702', weight: 50 } + ]; + gradingPeriodSet = { gradingPeriods, weighted: false }; + effectiveDueDates = { + 201: { grading_period_id: '701' }, + 202: { grading_period_id: '701' }, + 203: { grading_period_id: '702' }, + 204: { grading_period_id: '702' } + }; + } + }); + + test('includes assignment group grades', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 15); + equal(grades.assignmentGroups[301].final.score, 15); + equal(grades.assignmentGroups[301].current.possible, 20); + equal(grades.assignmentGroups[301].final.possible, 20); + equal(grades.assignmentGroups[302].current.score, 12); + equal(grades.assignmentGroups[302].final.score, 12); + equal(grades.assignmentGroups[302].current.possible, 20); + equal(grades.assignmentGroups[302].final.possible, 20); + equal(grades.assignmentGroups[303].current.score, 16); + equal(grades.assignmentGroups[303].final.score, 16); + equal(grades.assignmentGroups[303].current.possible, 40); + equal(grades.assignmentGroups[303].final.possible, 40); + }); + + test('includes grading period weights in gradingPeriods', function () { + const grades = calculateWithGradingPeriods('percent'); + ok(grades.gradingPeriods); + equal(grades.gradingPeriods[701].gradingPeriodWeight, 50); + equal(grades.gradingPeriods[702].gradingPeriodWeight, 50); + }); + + test('includes assignment groups point scores in grading period grades', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].assignmentGroups[301].current.score, 15); + equal(grades.gradingPeriods[701].assignmentGroups[301].final.score, 15); + equal(grades.gradingPeriods[702].assignmentGroups[302].current.score, 12); + equal(grades.gradingPeriods[702].assignmentGroups[302].final.score, 12); + equal(grades.gradingPeriods[702].assignmentGroups[303].current.score, 16); + equal(grades.gradingPeriods[702].assignmentGroups[303].final.score, 16); + }); + + test('calculates current and final percent grades within grading periods', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].current.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].final.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].current.possible, 100, 'current possible is 100 percent'); + equal(grades.gradingPeriods[701].final.possible, 100, 'final possible is 100 percent'); + equal(grades.gradingPeriods[702].current.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].final.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].current.possible, 100, 'current possible is 100 percent'); + equal(grades.gradingPeriods[702].final.possible, 100, 'final possible is 100 percent'); + }); + + test('does not weight assignment groups within grading periods when weighting scheme is not percent', function () { + const grades = calculateWithGradingPeriods('points'); + equal(grades.gradingPeriods[701].current.score, 15, 'current score is sum of scores in grading period 701'); + equal(grades.gradingPeriods[701].final.score, 15, 'final score is sum of scores in grading period 701'); + equal(grades.gradingPeriods[701].current.possible, 20, 'current possible is sum of points in grading period 701'); + equal(grades.gradingPeriods[701].final.possible, 20, 'final possible is sum of points in grading period 701'); + equal(grades.gradingPeriods[702].current.score, 28, 'current score is sum of scores in grading period 702'); + equal(grades.gradingPeriods[702].final.score, 28, 'final score is sum of scores in grading period 702'); + equal(grades.gradingPeriods[702].current.possible, 60, 'current possible is sum of points in grading period 702'); + equal(grades.gradingPeriods[702].final.possible, 60, 'final possible is sum of points in grading period 702'); + }); + + test('combines all assignment groups for the course grade', function () { + // 15/20 * 60% = 45% + // 12/20 * 20% = 12% + // 16/40 * 20% = 8% + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 65, 'each assignment group is weighted only by its group_weight'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 65, 'each assignment group is weighted only by its group_weight'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('ignores grading period weights', function () { + // 15/20 * 60% = 45% + // 12/20 * 20% = 12% + // 16/40 * 20% = 8% + gradingPeriods[0].weight = 25; + gradingPeriods[1].weight = 75; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 65, 'each assignment group is weighted only by its group_weight'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 65, 'each assignment group is weighted only by its group_weight'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('does not weight assignment groups for course grade when weighting scheme is not percent', function () { + const grades = calculateWithGradingPeriods('points'); + equal(grades.current.score, 43, 'assignment group scores are totaled per grading period as points'); + equal(grades.current.possible, 80, 'current possible is sum of all assignment points'); + equal(grades.final.score, 43, 'assignment group scores are totaled per grading period as points'); + equal(grades.final.possible, 80, 'final possible is sum of all assignment points'); + }); + + test('up-scales group weights which do not add up to exactly 100 percent', function () { + // 6 / (6+2+2) = 60% + // 2 / (6+2+2) = 20% + assignmentGroups[0].group_weight = 6; + assignmentGroups[1].group_weight = 2; + assignmentGroups[2].group_weight = 2; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 65, 'each assignment group is weighted only by its group_weight'); + equal(grades.final.score, 65, 'each assignment group is weighted only by its group_weight'); + }); + + test('does not down-scale group weights which add up to over 100 percent', function () { + assignmentGroups[0].group_weight = 120; + assignmentGroups[1].group_weight = 40; + assignmentGroups[2].group_weight = 40; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 130, 'current score is effectively double the weight'); + equal(grades.final.score, 130, 'final score is effectively double the weight'); + equal(grades.current.possible, 100, 'current possible remains 100 percent'); + equal(grades.final.possible, 100, 'final possible remains 100 percent'); + }); + + test('includes assignment groups outside of grading periods', function () { + effectiveDueDates[201].grading_period_id = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 65, 'assignment 201 is included'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 65, 'assignment 201 is included'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('does not divide assignment groups crossing grading periods', function () { + effectiveDueDates[202].grading_period_id = '702'; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 65, 'assignment group 302 is not divided'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 65, 'assignment group 302 is not divided'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('includes assignment group grades without division', function () { + effectiveDueDates[202].grading_period_id = '702'; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 15); + equal(grades.assignmentGroups[301].final.score, 15); + equal(grades.assignmentGroups[301].current.possible, 20); + equal(grades.assignmentGroups[301].final.possible, 20); + equal(grades.assignmentGroups[302].current.score, 12); + equal(grades.assignmentGroups[302].final.score, 12); + equal(grades.assignmentGroups[302].current.possible, 20); + equal(grades.assignmentGroups[302].final.possible, 20); + equal(grades.assignmentGroups[303].current.score, 16); + equal(grades.assignmentGroups[303].final.score, 16); + equal(grades.assignmentGroups[303].current.possible, 40); + equal(grades.assignmentGroups[303].final.possible, 40); + }); + + test('uses a score unit of "points" when weighting scheme is not percent', function () { + const grades = calculateWithGradingPeriods('points'); + equal(grades.scoreUnit, 'points'); + equal(grades.gradingPeriods[701].scoreUnit, 'points'); + equal(grades.gradingPeriods[702].scoreUnit, 'points'); + }); + + test('uses a score unit of "percentage" when weighting scheme is percent', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.scoreUnit, 'percentage'); + equal(grades.gradingPeriods[701].scoreUnit, 'percentage'); + equal(grades.gradingPeriods[702].scoreUnit, 'percentage'); + }); + module('CourseGradeCalculator.calculate with weighted grading periods', { setup () { submissions = [ @@ -327,9 +598,10 @@ define([ { id: 303, group_weight: 20, rules: {}, assignments: assignments.slice(3, 4) } ]; gradingPeriods = [ - { id: 701, weight: 50 }, - { id: 702, weight: 50 } + { id: '701', weight: 50 }, + { id: '702', weight: 50 } ]; + gradingPeriodSet = { gradingPeriods, weighted: true }; effectiveDueDates = { 201: { grading_period_id: '701' }, 202: { grading_period_id: '701' }, @@ -339,11 +611,13 @@ define([ } }); - test('includes grading period weights in gradingPeriods', function () { + test('includes grading period attributes in gradingPeriods', function () { const grades = calculateWithGradingPeriods('percent'); ok(grades.gradingPeriods); - equal(grades.gradingPeriods[701].weight, 50); - equal(grades.gradingPeriods[702].weight, 50); + equal(grades.gradingPeriods[701].gradingPeriodId, 701); + equal(grades.gradingPeriods[702].gradingPeriodId, 702); + equal(grades.gradingPeriods[701].gradingPeriodWeight, 50); + equal(grades.gradingPeriods[702].gradingPeriodWeight, 50); }); test('includes assignment groups point scores in grading period grades', function () { @@ -397,6 +671,7 @@ define([ }); test('up-scales grading period weights which do not add up to exactly 100 percent', function () { + // 5 / (5+5) = 50% gradingPeriods[0].weight = 5; gradingPeriods[1].weight = 5; const grades = calculateWithGradingPeriods('percent'); @@ -416,6 +691,61 @@ define([ equal(grades.final.possible, 100, 'final possible remains 100 percent'); }); + test('uses zero weight for grading periods with null weight', function () { + // 5 / (0+5) = 100% + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = 5; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 50, 'grading period 702 has a current score of 50 percent'); + equal(grades.current.possible, 100); + equal(grades.final.score, 50, 'grading period 702 has a final score of 50 percent'); + equal(grades.final.possible, 100); + }); + + test('sets scores to zero when all grading period weights are zero', function () { + gradingPeriods[0].weight = 0; + gradingPeriods[1].weight = 0; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 0, 'all grading periods have zero weight'); + equal(grades.current.possible, 100, 'current possible remains 100 percent'); + equal(grades.final.score, 0, 'all grading periods have zero weight'); + equal(grades.final.possible, 100, 'final possible remains 100 percent'); + }); + + test('sets scores to zero when all grading period weights are null', function () { + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 0, 'all grading periods have zero weight'); + equal(grades.current.possible, 100, 'current possible remains 100 percent'); + equal(grades.final.score, 0, 'all grading periods have zero weight'); + equal(grades.final.possible, 100, 'final possible remains 100 percent'); + }); + + test('excludes assignments outside of grading periods', function () { + effectiveDueDates[201].grading_period_id = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.current.score, 50, 'assignment 201 is excluded'); + equal(grades.current.possible, 100, 'current possible is 100 percent'); + equal(grades.final.score, 50, 'assignment 201 is excluded'); + equal(grades.final.possible, 100, 'final possible is 100 percent'); + }); + + test('excludes assignments outside of grading periods for assignment group grades', function () { + effectiveDueDates[201].grading_period_id = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 5); + equal(grades.assignmentGroups[301].final.score, 5); + equal(grades.assignmentGroups[301].current.possible, 10); + equal(grades.assignmentGroups[301].final.possible, 10); + }); + + test('excludes grades for assignment groups outside of grading periods', function () { + effectiveDueDates[203].grading_period_id = null; + const grades = calculateWithGradingPeriods('percent'); + equal(typeof grades.assignmentGroups[302], 'undefined'); + }); + test('weights grading periods with unequal grading period weights', function () { gradingPeriods[0].weight = 25; gradingPeriods[1].weight = 75; @@ -451,45 +781,81 @@ define([ equal(grades.final.possible, 100, 'final possible remains 100 percent'); }); - test('sums assignment group scores as points when no grading periods have weight', function () { - gradingPeriods[0].weight = null; - gradingPeriods[1].weight = null; - const grades = calculateWithGradingPeriods('points'); - equal(grades.current.score, 43, 'assignment group scores are totaled per grading period as points'); - equal(grades.current.possible, 80, 'current possible is sum of all assignment points'); - equal(grades.final.score, 43, 'assignment group scores are totaled per grading period as points'); - equal(grades.final.possible, 80, 'final possible is sum of all assignment points'); - }); - - test('combines weighted assignment group scores as percent when no grading periods have weight', function () { + test('evaluates null grading period weights as 0 when no grading periods have weight', function () { gradingPeriods[0].weight = null; gradingPeriods[1].weight = null; const grades = calculateWithGradingPeriods('percent'); - equal(grades.current.score, 65, 'all assignment groups are weighted together'); - equal(grades.current.possible, 100, 'current possible is 100 percent with weighted groups'); - equal(grades.final.score, 65, 'all assignment groups are weighted together'); - equal(grades.final.possible, 100, 'final possible is 100 percent with weighted groups'); + equal(grades.current.score, 0, 'grading period 702 score of 50 effectively has 0 percent weight'); + equal(grades.current.possible, 100, 'current possible remains 100 percent'); + equal(grades.final.score, 0, 'grading period 702 score of 50 effectively has 0 percent weight'); + equal(grades.final.possible, 100, 'final possible remains 100 percent'); + }); + + test('sets null weights as 0 in gradingPeriods', function () { + gradingPeriods[0].weight = null; + gradingPeriods[1].weight = null; + const grades = calculateWithGradingPeriods('percent'); + ok(grades.gradingPeriods); + equal(grades.gradingPeriods[701].gradingPeriodWeight, 0); + equal(grades.gradingPeriods[702].gradingPeriodWeight, 0); }); test('combines weighted assignment group scores as percent in grading periods without weight', function () { gradingPeriods[0].weight = null; gradingPeriods[1].weight = null; const grades = calculateWithGradingPeriods('percent'); - equal(grades.gradingPeriods[701].current.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].current.score, 75, 'one assignment group is in grading period 701'); equal(grades.gradingPeriods[701].current.possible, 100, 'current possible is 100 percent'); - equal(grades.gradingPeriods[701].final.score, 75, 'one assignment group is in this grading period'); + equal(grades.gradingPeriods[701].final.score, 75, 'one assignment group is in grading period 701'); equal(grades.gradingPeriods[701].final.possible, 100, 'final possible is 100 percent'); - equal(grades.gradingPeriods[702].current.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].current.score, 50, 'two assignment groups are in grading period 702'); equal(grades.gradingPeriods[702].current.possible, 100, 'current possible is 100 percent'); - equal(grades.gradingPeriods[702].final.score, 50, 'two assignment groups are in this grading period'); + equal(grades.gradingPeriods[702].final.score, 50, 'two assignment groups are in grading period 702'); equal(grades.gradingPeriods[702].final.possible, 100, 'final possible is 100 percent'); }); + test('includes assignment group grades regardless of grading period weight', function () { + gradingPeriods[0].weight = 200; + gradingPeriods[1].weight = null; + const grades = calculateWithGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 15); + equal(grades.assignmentGroups[301].final.score, 15); + equal(grades.assignmentGroups[301].current.possible, 20); + equal(grades.assignmentGroups[301].final.possible, 20); + equal(grades.assignmentGroups[302].current.score, 12); + equal(grades.assignmentGroups[302].final.score, 12); + equal(grades.assignmentGroups[302].current.possible, 20); + equal(grades.assignmentGroups[302].final.possible, 20); + equal(grades.assignmentGroups[303].current.score, 16); + equal(grades.assignmentGroups[303].final.score, 16); + equal(grades.assignmentGroups[303].current.possible, 40); + equal(grades.assignmentGroups[303].final.possible, 40); + }); + + test('uses a score unit of "percentage" for course grade', function () { + let grades = calculateWithGradingPeriods('points'); + equal(grades.scoreUnit, 'percentage'); + grades = calculateWithGradingPeriods('percent'); + equal(grades.scoreUnit, 'percentage'); + }); + + test('uses a score unit of "points" for grading period grades when weighting scheme is not percent', function () { + const grades = calculateWithGradingPeriods('points'); + equal(grades.gradingPeriods[701].scoreUnit, 'points'); + equal(grades.gradingPeriods[702].scoreUnit, 'points'); + }); + + test('uses a score unit of "percentage" for grading period grades when weighting scheme is percent', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.gradingPeriods[701].scoreUnit, 'percentage'); + equal(grades.gradingPeriods[702].scoreUnit, 'percentage'); + }); + // This is a use case that is STRONGLY discouraged to users, but is still not // prevented. Assignment group rules must never be applied to multiple grading // periods in combination. Doing so would impact grades in closed grading // periods, which must never occur. - module('CourseGradeCalculator.calculate with assignment groups across multiple grading periods', { + module('CourseGradeCalculator.calculate with assignment groups across multiple weighted grading periods', { setup () { submissions = [ { assignment_id: 201, score: 10 }, @@ -506,9 +872,10 @@ define([ { id: 302, group_weight: 50, rules: {}, assignments: assignments.slice(2, 3) } ]; gradingPeriods = [ - { id: 701, weight: 50 }, - { id: 702, weight: 50 } + { id: '701', weight: 50 }, + { id: '702', weight: 50 } ]; + gradingPeriodSet = { gradingPeriods, weighted: true }; effectiveDueDates = { 201: { grading_period_id: '701' }, // in first assignment group and first grading period 202: { grading_period_id: '702' }, // in first assignment group and second grading period @@ -517,6 +884,14 @@ define([ } }); + test('recombines assignment group grades of divided assignment groups', function () { + const grades = calculateWithGradingPeriods('percent'); + equal(grades.assignmentGroups[301].current.score, 15); + equal(grades.assignmentGroups[301].final.score, 15); + equal(grades.assignmentGroups[301].current.possible, 20); + equal(grades.assignmentGroups[301].final.possible, 20); + }); + test('divides assignment groups across related grading periods', function () { const grades = calculateWithGradingPeriods('percent'); equal(grades.gradingPeriods[701].assignmentGroups[301].current.score, 10); @@ -570,21 +945,4 @@ define([ equal(grades.final.score, 65, 'assignment 202 is not assigned to the student'); equal(grades.final.possible, 100, 'final possible is 100 percent'); }); - - // When assignment groups cross multiple grading periods, the rules and - // weights are consequently duplicated and apply to the assignments in each - // grading period. This means weights can inadvertently exceed 100 in total. - test('duplicates weights of duplicated assignment groups when no grading periods have weight', function () { - // grading period 701 + assignment group 301: 10/10 * weight of 50 (50/100%) - // grading period 702 + assignment group 301: 5/10 * weight of 50 (25/100%) - // grading period 702 + assignment group 302: 3/10 * weight of 50 (15/100%) - // total: 50% + 25% + 15% = 90% - gradingPeriods[0].weight = null; - gradingPeriods[1].weight = null; - const grades = calculateWithGradingPeriods('percent'); - equal(grades.current.score, 90, 'all assignment groups are weighted together'); - equal(grades.current.possible, 100, 'current possible is 100 percent with weighted groups'); - equal(grades.final.score, 90, 'all assignment groups are weighted together'); - equal(grades.final.possible, 100, 'final possible is 100 percent with weighted groups'); - }); }); diff --git a/spec/javascripts/jsx/gradebook/GradeCalculatorSpecHelper.jsx b/spec/javascripts/jsx/gradebook/GradeCalculatorSpecHelper.jsx new file mode 100644 index 00000000000..0d7353c594e --- /dev/null +++ b/spec/javascripts/jsx/gradebook/GradeCalculatorSpecHelper.jsx @@ -0,0 +1,60 @@ +define(() => { + const GradeCalculatorSpecHelper = { + createCourseGradesWithGradingPeriods () { + return { + assignmentGroups: { + 301: { + assignmentGroupId: 301, + assignmentGroupWeight: 40, + current: { score: 5, possible: 10, submissions: [] }, + final: { score: 5, possible: 20, submissions: [] } + }, + + 302: { + assignmentGroupId: 302, + assignmentGroupWeight: 60, + current: { score: 12, possible: 15, submissions: [] }, + final: { score: 12, possible: 25, submissions: [] } + } + }, + + gradingPeriods: { + 701: { + gradingPeriodId: 701, + gradingPeriodWeight: 25, + assignmentGroups: { + 301: { + assignmentGroupId: 301, + assignmentGroupWeight: 40, + current: { score: 5, possible: 10, submissions: [] }, + final: { score: 5, possible: 20, submissions: [] } + } + }, + current: { score: 5, possible: 10, submissions: [] }, + final: { score: 5, possible: 20, submissions: [] } + }, + + 702: { + gradingPeriodId: 702, + gradingPeriodWeight: 75, + assignmentGroups: { + 302: { + assignmentGroupId: 302, + assignmentGroupWeight: 60, + current: { score: 12, possible: 15, submissions: [] }, + final: { score: 12, possible: 25, submissions: [] } + }, + }, + current: { score: 12, possible: 15, submissions: [] }, + final: { score: 12, possible: 25, submissions: [] } + }, + }, + + current: { score: 17, possible: 25, submissions: [] }, + final: { score: 17, possible: 45, submissions: [] } + }; + } + }; + + return GradeCalculatorSpecHelper; +}); From 7298b8749f9bb674470a05fc27cc0693c6efc4c5 Mon Sep 17 00:00:00 2001 From: Neil Gupta Date: Mon, 16 Jan 2017 19:18:24 -0600 Subject: [PATCH 8/8] Get rid of multiple_grading_periods feature flag Fixes CNVS-27109 Test plan: Regression test everything related to multiple grading periods. In particular, make sure: * there is no mention of the multiple grading periods feature flag on the account or course features pages * make sure grading periods UI always shows up on the account grading standards page * make sure the grading periods UI only shows on the course grading standards page if legacy course grading periods exist for that course (see below) * grading period dropdowns only show up everywhere else if there are grading periods to show. Otherwise, it should behave as if MGP is "disabled" * Grade calculation should work as expected with and without grading periods To create legacy course grading periods, you have to use rails console: 1. Find your course: course = Course.find() 2. Create a grading period group: group = course.grading_period_groups.create!(title: "2017") 2. Create a grading period: group.grading_periods.create!( weight: 1, title: 'Fall', start_date: 5.days.ago, end_date: 10.days.from_now ) Here's the list of known places grading periods can be seen in the UI: Instructor Assignments page (https://community.canvaslms.com/docs/DOC-2615) Student Assignments page (https://community.canvaslms.com/docs/DOC-3123) Gradebook (https://community.canvaslms.com/docs/DOC-2785) Gradebook Individual View (https://community.canvaslms.com/docs/DOC-2788) Course Settings grading schemes page (https://community.canvaslms.com/docs/DOC-4042) Student Grades page (https://community.canvaslms.com/docs/DOC-1291) Dashboard global Grades page (https://community.canvaslms.com/docs/DOC-5464) Change-Id: Iefac4b08120bd1699d4ed98bcb418089eec9b3b8 Reviewed-on: https://gerrit.instructure.com/99744 Reviewed-by: Spencer Olson Tested-by: Jenkins QA-Review: KC Naegle Product-Review: Keith T. Garner --- .../bundles/account_grading_standards.coffee | 1 - .../bundles/assignment_index.coffee | 2 +- .../bundles/course_grading_standards.coffee | 3 +- .../screenreader_gradebook_controller.coffee | 14 +- .../settings/grading_period_select.hbs | 4 +- .../tests/components/grading_cell.spec.coffee | 2 +- .../screenreader_gradebook.spec.coffee | 12 +- app/coffeescripts/gradebook/Gradebook.coffee | 22 +- app/coffeescripts/gradezilla/Gradebook.coffee | 22 +- app/coffeescripts/util/DateValidator.coffee | 4 +- .../assignments/CreateAssignmentView.coffee | 2 +- .../views/assignments/DueDateOverride.coffee | 6 +- .../views/assignments/IndexView.coffee | 2 +- app/controllers/application_controller.rb | 23 +- .../assignment_groups_api_controller.rb | 4 +- .../assignment_groups_controller.rb | 13 +- app/controllers/assignments_controller.rb | 4 +- app/controllers/courses_controller.rb | 16 +- .../discussion_topics_controller.rb | 4 +- app/controllers/enrollments_api_controller.rb | 27 +- app/controllers/filters/grading_periods.rb | 11 - app/controllers/gradebooks_controller.rb | 14 +- .../grading_period_sets_controller.rb | 3 - app/controllers/grading_periods_controller.rb | 11 +- .../grading_standards_controller.rb | 2 +- app/controllers/quizzes/quizzes_controller.rb | 4 +- app/controllers/submissions_api_controller.rb | 4 +- app/controllers/users_controller.rb | 2 +- app/jsx/due_dates/DueDates.jsx | 6 +- app/jsx/gradebook/SubmissionStateMap.jsx | 10 +- app/jsx/gradezilla/SubmissionStateMap.jsx | 10 +- app/jsx/grading/AccountTabContainer.jsx | 20 +- app/jsx/grading/CourseTabContainer.jsx | 6 +- app/jsx/grading/gradingPeriodCollection.jsx | 23 +- app/models/course.rb | 12 + app/models/feature_flag.rb | 5 +- app/models/grading_period_group.rb | 19 -- app/models/group.rb | 4 + app/views/gradebooks/grade_summary.html.erb | 2 +- app/views/gradebooks/gradebook.html.erb | 2 +- app/views/gradebooks/gradezilla.html.erb | 2 +- .../jst/assignments/IndexView.handlebars | 2 +- ..._multiple_grading_periods_feature_flags.rb | 16 + lib/api/v1/course_json.rb | 21 +- ..._multiple_grading_periods_feature_flags.rb | 7 + lib/effective_due_dates.rb | 21 +- lib/feature.rb | 13 +- lib/grade_calculator.rb | 2 +- lib/gradebook_exporter.rb | 18 +- lib/gradebook_importer.rb | 6 +- lib/gradebook_transformer.rb | 28 -- lib/submittables_grading_period_protection.rb | 8 +- spec/apis/v1/assignment_groups_api_spec.rb | 22 +- spec/apis/v1/assignments_api_spec.rb | 6 +- spec/apis/v1/courses_api_spec.rb | 38 +-- spec/apis/v1/enrollments_api_spec.rb | 127 ++++--- spec/apis/v1/grading_periods_api_spec.rb | 180 +++++----- spec/apis/v1/quizzes/quizzes_api_spec.rb | 6 +- spec/apis/v1/submissions_api_spec.rb | 3 +- .../gradebook/GradebookHeaderMenuSpec.coffee | 6 +- .../gradebook/GradebookSpec.coffee | 18 +- .../SubmissionDetailsDialogSpec.coffee | 6 +- .../gradezilla/GradebookHeaderMenuSpec.coffee | 6 +- .../gradezilla/GradebookSpec.coffee | 23 +- .../SubmissionDetailsDialogSpec.coffee | 6 +- .../jsx/due_dates/DueDatesSpec.coffee | 8 +- .../GradingPeriodCollectionSpec.coffee | 65 +--- .../CreateAssignmentViewSpec.coffee | 2 +- .../assignment_groups_controller_spec.rb | 19 +- .../assignments_controller_spec.rb | 3 +- .../discussion_topics_controller_spec.rb | 6 +- .../controllers/gradebooks_controller_spec.rb | 6 +- .../grading_period_sets_controller_spec.rb | 2 - .../grading_periods_controller_spec.rb | 54 +-- .../quizzes/quizzes_controller_spec.rb | 8 +- spec/controllers/users_controller_spec.rb | 50 +-- spec/factories/grading_period_factory.rb | 8 +- .../jsx/SubmissionDetailsDialogSpec.jsx | 2 +- .../SubmissionStateMapGradeVisibilitySpec.jsx | 20 +- .../SubmissionStateMapLockingSpec.jsx | 20 +- .../SubmissionStateMapTooltipSpec.jsx | 20 +- .../SubmissionStateMapGradeVisibilitySpec.jsx | 20 +- .../SubmissionStateMapLockingSpec.jsx | 20 +- .../SubmissionStateMapTooltipSpec.jsx | 20 +- .../jsx/grading/AccountTabContainerSpec.jsx | 37 +-- .../jsx/grading/CourseTabContainerSpec.jsx | 32 +- .../jsx/util/DateValidatorSpec.jsx | 4 +- .../data_fixup/populate_scores_table_spec.rb | 35 +- spec/lib/effective_due_dates_spec.rb | 25 +- spec/lib/gradebook_exporter_spec.rb | 29 +- spec/lib/gradebook_importer_spec.rb | 3 +- spec/models/assignment_group_spec.rb | 14 +- spec/models/assignment_spec.rb | 14 +- spec/models/course_spec.rb | 25 ++ spec/models/grading_period_group_spec.rb | 68 ---- .../models/grading_period_permissions_spec.rb | 38 --- spec/models/quizzes/quiz_spec.rb | 14 +- spec/models/submission_spec.rb | 14 +- .../account_admin_grading_schemes_spec.rb | 62 ++-- .../admin/account_admin_terms_spec.rb | 11 +- .../sub_accounts/grading_schemes_spec.rb | 107 +++--- .../gradebook/gradebook_grade_edit_spec.rb | 3 +- .../grades/gradebook/gradebook_mgp_spec.rb | 4 +- .../gradezilla/gradebook_grade_edit_spec.rb | 3 +- .../grades/gradezilla/gradebook_mgp_spec.rb | 4 +- .../grading_periods_course_spec.rb | 22 +- .../grading_standards_mgp_spec.rb | 7 +- .../grading_standards_spec.rb | 314 +++++++++--------- spec/selenium/grades/setup/gradebook_setup.rb | 4 +- .../speedgrader/speedgrader_mgp_spec.rb | 8 +- .../grades/speedgrader/speedgrader_spec.rb | 2 - .../selenium/grades/srgb/srgb_grading_spec.rb | 4 +- .../student_grades_page_spec.rb | 8 +- .../helpers/grading_schemes_common.rb | 3 + .../selenium/multiple_grading_periods_spec.rb | 16 +- ...tiple_grading_periods_within_controller.rb | 10 +- 116 files changed, 869 insertions(+), 1376 deletions(-) delete mode 100644 app/controllers/filters/grading_periods.rb create mode 100644 db/migrate/20170116190327_clear_any_multiple_grading_periods_feature_flags.rb create mode 100644 lib/data_fixup/clear_any_multiple_grading_periods_feature_flags.rb delete mode 100644 lib/gradebook_transformer.rb diff --git a/app/coffeescripts/bundles/account_grading_standards.coffee b/app/coffeescripts/bundles/account_grading_standards.coffee index e2b51a66960..f6de6c87b4c 100644 --- a/app/coffeescripts/bundles/account_grading_standards.coffee +++ b/app/coffeescripts/bundles/account_grading_standards.coffee @@ -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 diff --git a/app/coffeescripts/bundles/assignment_index.coffee b/app/coffeescripts/bundles/assignment_index.coffee index 86fbfbf583a..38a08ce5339 100644 --- a/app/coffeescripts/bundles/assignment_index.coffee +++ b/app/coffeescripts/bundles/assignment_index.coffee @@ -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 diff --git a/app/coffeescripts/bundles/course_grading_standards.coffee b/app/coffeescripts/bundles/course_grading_standards.coffee index b99f4bf789e..195a8612c06 100644 --- a/app/coffeescripts/bundles/course_grading_standards.coffee +++ b/app/coffeescripts/bundles/course_grading_standards.coffee @@ -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") ) diff --git a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee index 09e907263e9..c1232d1da4a 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee @@ -97,7 +97,7 @@ 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: (-> @@ -305,19 +305,19 @@ define [ gradingPeriodSet = @getGradingPeriodSet() effectiveDueDates = @get('effectiveDueDates.content') - usingGradingPeriods = gradingPeriodSet and effectiveDueDates + hasGradingPeriods = gradingPeriodSet and effectiveDueDates CourseGradeCalculator.calculate( submissions, assignmentGroups, weightingScheme, - gradingPeriodSet if usingGradingPeriods, - EffectiveDueDates.scopeToUser(effectiveDueDates, student.id) if usingGradingPeriods + 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' @@ -366,7 +366,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', []) @@ -694,7 +694,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") ) diff --git a/app/coffeescripts/ember/screenreader_gradebook/templates/settings/grading_period_select.hbs b/app/coffeescripts/ember/screenreader_gradebook/templates/settings/grading_period_select.hbs index 4fe7f4e1fd2..4a7a551e9f8 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/templates/settings/grading_period_select.hbs +++ b/app/coffeescripts/ember/screenreader_gradebook/templates/settings/grading_period_select.hbs @@ -1,4 +1,4 @@ -{{#if mgpEnabled}} +{{#if has_grading_periods}}
-{{/if}} \ No newline at end of file +{{/if}} diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee index c44034e2e42..66b84dac397 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee @@ -17,7 +17,7 @@ define [ App = startApp() @component = App.GradingCellComponent.create() - ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled = true + ENV.GRADEBOOK_OPTIONS.has_grading_periods = true ENV.current_user_roles = [] setType = (type) => diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee index 4507f769ac8..d1fdf55fba7 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee @@ -231,7 +231,7 @@ define [ ) defaults = { - mgpEnabled: false, + has_grading_periods: false, 'selectedGradingPeriod.id': null, effectiveDueDates } @@ -250,22 +250,22 @@ define [ 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'] 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'] test 'only returns submissions due for the student in the selected grading period', -> self = @setupThis( - mgpEnabled: true, + has_grading_periods: true, 'selectedGradingPeriod.id': '2' ) submissions = @srgb.submissionsForStudent.call(self, @student) @@ -292,7 +292,7 @@ define [ strictEqual @srgb.get('selectedSubmission'), null test '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']) diff --git a/app/coffeescripts/gradebook/Gradebook.coffee b/app/coffeescripts/gradebook/Gradebook.coffee index a7fb6106bb9..944e25cb1d5 100644 --- a/app/coffeescripts/gradebook/Gradebook.coffee +++ b/app/coffeescripts/gradebook/Gradebook.coffee @@ -120,7 +120,7 @@ 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 + @hasGradingPeriods = @options.has_grading_periods @gradingPeriods = GradingPeriodsApi.deserializePeriods(@options.active_grading_periods) if @options.grading_period_set @gradingPeriodSet = GradingPeriodSetsApi.deserializeSet(@options.grading_period_set) @@ -128,7 +128,7 @@ define [ @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 @@ -142,7 +142,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) -> @@ -157,7 +157,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 @@ -257,7 +257,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 @@ -751,7 +751,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) => @@ -760,14 +760,14 @@ define [ calculateStudentGrade: (student) => if student.loaded and student.initialized - usingGradingPeriods = @gradingPeriodSet and @effectiveDueDates + hasGradingPeriods = @gradingPeriodSet and @effectiveDueDates grades = CourseGradeCalculator.calculate( @submissionsForStudent(student), @assignmentGroups, @options.group_weighting_scheme, - @gradingPeriodSet if usingGradingPeriods, - EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if usingGradingPeriods + @gradingPeriodSet if hasGradingPeriods, + EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if hasGradingPeriods ) if @gradingPeriodToShow && !@isAllGradingPeriods(@gradingPeriodToShow) @@ -1072,7 +1072,7 @@ define [ initHeader: => @drawSectionSelectButton() if @sections_enabled - @drawGradingPeriodSelectButton() if @gradingPeriodsEnabled + @drawGradingPeriodSelectButton() if @hasGradingPeriods $settingsMenu = $('.gradebook_dropdown') showConcludedEnrollmentsEl = $settingsMenu.find("#show_concluded_enrollments") @@ -1696,7 +1696,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) diff --git a/app/coffeescripts/gradezilla/Gradebook.coffee b/app/coffeescripts/gradezilla/Gradebook.coffee index 98b282be64a..2c26774b72d 100644 --- a/app/coffeescripts/gradezilla/Gradebook.coffee +++ b/app/coffeescripts/gradezilla/Gradebook.coffee @@ -123,7 +123,7 @@ 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 + @hasGradingPeriods = @options.has_grading_periods @gradingPeriods = GradingPeriodsApi.deserializePeriods(@options.active_grading_periods) if @options.grading_period_set @gradingPeriodSet = GradingPeriodSetsApi.deserializeSet(@options.grading_period_set) @@ -131,7 +131,7 @@ define [ @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 +145,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 +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 @@ -258,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 @@ -731,7 +731,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) => @@ -740,14 +740,14 @@ define [ calculateStudentGrade: (student) => if student.loaded and student.initialized - usingGradingPeriods = @gradingPeriodSet and @effectiveDueDates + hasGradingPeriods = @gradingPeriodSet and @effectiveDueDates grades = CourseGradeCalculator.calculate( @submissionsForStudent(student), @assignmentGroups, @options.group_weighting_scheme, - @gradingPeriodSet if usingGradingPeriods, - EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if usingGradingPeriods + @gradingPeriodSet if hasGradingPeriods, + EffectiveDueDates.scopeToUser(@effectiveDueDates, student.id) if hasGradingPeriods ) if @gradingPeriodToShow && !@isAllGradingPeriods(@gradingPeriodToShow) @@ -1043,7 +1043,7 @@ define [ initHeader: => @drawSectionSelectButton() if @sections_enabled - @drawGradingPeriodSelectButton() if @gradingPeriodsEnabled + @drawGradingPeriodSelectButton() if @hasGradingPeriods $settingsMenu = $('.gradebook_dropdown') showConcludedEnrollmentsEl = $settingsMenu.find("#show_concluded_enrollments") @@ -1710,7 +1710,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) diff --git a/app/coffeescripts/util/DateValidator.coffee b/app/coffeescripts/util/DateValidator.coffee index 244fbc338b1..dfcb324e43a 100644 --- a/app/coffeescripts/util/DateValidator.coffee +++ b/app/coffeescripts/util/DateValidator.coffee @@ -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 @@ -72,7 +72,7 @@ define [ type: "due" } - if @multipleGradingPeriodsEnabled && !@userIsAdmin && @data.persisted == false + if @hasGradingPeriods && !@userIsAdmin && @data.persisted == false datetimesToValidate.push { date: dueAt, range: "grading_period_range", diff --git a/app/coffeescripts/views/assignments/CreateAssignmentView.coffee b/app/coffeescripts/views/assignments/CreateAssignmentView.coffee index 5f86cc292e3..4fb65adcc33 100644 --- a/app/coffeescripts/views/assignments/CreateAssignmentView.coffee +++ b/app/coffeescripts/views/assignments/CreateAssignmentView.coffee @@ -154,7 +154,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() ) diff --git a/app/coffeescripts/views/assignments/DueDateOverride.coffee b/app/coffeescripts/views/assignments/DueDateOverride.coffee index dcea652ffc6..b79d14f04cf 100644 --- a/app/coffeescripts/views/assignments/DueDateOverride.coffee +++ b/app/coffeescripts/views/assignments/DueDateOverride.coffee @@ -31,7 +31,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")) }) @@ -40,7 +40,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 @@ -56,7 +56,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") }) diff --git a/app/coffeescripts/views/assignments/IndexView.coffee b/app/coffeescripts/views/assignments/IndexView.coffee index ab952eddbdf..fb150687a44 100644 --- a/app/coffeescripts/views/assignments/IndexView.coffee +++ b/app/coffeescripts/views/assignments/IndexView.coffee @@ -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) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ac7fc5f8625..358b8e31ba2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -220,11 +220,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) @@ -249,18 +248,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). # @@ -2018,7 +2005,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), @@ -2029,7 +2016,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 diff --git a/app/controllers/assignment_groups_api_controller.rb b/app/controllers/assignment_groups_api_controller.rb index f2ff5c07006..a84191ad03f 100644 --- a/app/controllers/assignment_groups_api_controller.rb +++ b/app/controllers/assignment_groups_api_controller.rb @@ -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') diff --git a/app/controllers/assignment_groups_controller.rb b/app/controllers/assignment_groups_controller.rb index c03bfc7e0d5..5c32a4ce196 100644 --- a/app/controllers/assignment_groups_controller.rb +++ b/app/controllers/assignment_groups_controller.rb @@ -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? @@ -385,7 +384,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 @@ -426,7 +425,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) diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 2805161a73a..81c47072600 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -434,7 +434,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, SECTION_LIST: @context.course_sections.active.map do |section| @@ -462,7 +462,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) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 211dd555c43..b0326135609 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -354,14 +354,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 @@ -469,14 +469,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 @@ -2431,9 +2431,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) # @@ -2757,7 +2757,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 diff --git a/app/controllers/discussion_topics_controller.rb b/app/controllers/discussion_topics_controller.rb index f3610a760ff..c7d1c6847f1 100644 --- a/app/controllers/discussion_topics_controller.rb +++ b/app/controllers/discussion_topics_controller.rb @@ -447,7 +447,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 } } } @@ -470,7 +470,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 diff --git a/app/controllers/enrollments_api_controller.rb b/app/controllers/enrollments_api_controller.rb index 561016848d7..250c21d65da 100644 --- a/app/controllers/enrollments_api_controller.rb +++ b/app/controllers/enrollments_api_controller.rb @@ -197,8 +197,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" # }, @@ -208,32 +208,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" # } @@ -252,7 +252,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' } @@ -361,20 +360,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 diff --git a/app/controllers/filters/grading_periods.rb b/app/controllers/filters/grading_periods.rb deleted file mode 100644 index 669590aceaa..00000000000 --- a/app/controllers/filters/grading_periods.rb +++ /dev/null @@ -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 diff --git a/app/controllers/gradebooks_controller.rb b/app/controllers/gradebooks_controller.rb index 44205a73171..1fee570c117 100644 --- a/app/controllers/gradebooks_controller.rb +++ b/app/controllers/gradebooks_controller.rb @@ -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,7 +59,7 @@ 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 @@ -127,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 @@ -196,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 @@ -367,7 +367,7 @@ class GradebooksController < ApplicationController 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?, + 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, @@ -750,7 +750,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 @@ -785,7 +785,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 diff --git a/app/controllers/grading_period_sets_controller.rb b/app/controllers/grading_period_sets_controller.rb index 7f8d1f11b6c..73296ea9dd3 100644 --- a/app/controllers/grading_period_sets_controller.rb +++ b/app/controllers/grading_period_sets_controller.rb @@ -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] diff --git a/app/controllers/grading_periods_controller.rb b/app/controllers/grading_periods_controller.rb index b57f3bcaacb..9834cb8ea69 100644 --- a/app/controllers/grading_periods_controller.rb +++ b/app/controllers/grading_periods_controller.rb @@ -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 diff --git a/app/controllers/grading_standards_controller.rb b/app/controllers/grading_standards_controller.rb index 505c26e4d03..255432dc794 100644 --- a/app/controllers/grading_standards_controller.rb +++ b/app/controllers/grading_standards_controller.rb @@ -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) } diff --git a/app/controllers/quizzes/quizzes_controller.rb b/app/controllers/quizzes/quizzes_controller.rb index fd47f8dd6d2..0e8fac65e0a 100644 --- a/app/controllers/quizzes/quizzes_controller.rb +++ b/app/controllers/quizzes/quizzes_controller.rb @@ -306,10 +306,10 @@ 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? } - if @context.feature_enabled?(:multiple_grading_periods) + if @context.grading_periods? hash[:active_grading_periods] = GradingPeriod.json_for(@context, @current_user) end diff --git a/app/controllers/submissions_api_controller.rb b/app/controllers/submissions_api_controller.rb index 6313dc946bb..afbeea12fa0 100644 --- a/app/controllers/submissions_api_controller.rb +++ b/app/controllers/submissions_api_controller.rb @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 801171edffe..edad13ca003 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2134,7 +2134,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 && diff --git a/app/jsx/due_dates/DueDates.jsx b/app/jsx/due_dates/DueDates.jsx index c2735d43672..c47c5be88f3 100644 --- a/app/jsx/due_dates/DueDates.jsx +++ b/app/jsx/due_dates/DueDates.jsx @@ -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 } diff --git a/app/jsx/gradebook/SubmissionStateMap.jsx b/app/jsx/gradebook/SubmissionStateMap.jsx index 2035b77df12..e1c9cea3a00 100644 --- a/app/jsx/gradebook/SubmissionStateMap.jsx +++ b/app/jsx/gradebook/SubmissionStateMap.jsx @@ -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 ]; diff --git a/app/jsx/gradezilla/SubmissionStateMap.jsx b/app/jsx/gradezilla/SubmissionStateMap.jsx index 2035b77df12..e1c9cea3a00 100644 --- a/app/jsx/gradezilla/SubmissionStateMap.jsx +++ b/app/jsx/gradezilla/SubmissionStateMap.jsx @@ -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 ]; diff --git a/app/jsx/grading/AccountTabContainer.jsx b/app/jsx/grading/AccountTabContainer.jsx index d376fa3b38e..c71b2026761 100644 --- a/app/jsx/grading/AccountTabContainer.jsx +++ b/app/jsx/grading/AccountTabContainer.jsx @@ -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 (
{ this.tabContainer = el; }}>

{I18n.t('Grading')}

@@ -52,22 +50,6 @@ define([
); } - - renderStandards () { - return ( -
{ this.gradingStandards = el; }}> -

{I18n.t('Grading Schemes')}

- -
- ); - } - - render () { - if (this.props.multipleGradingPeriodsEnabled) { - return this.renderSetsAndStandards(); - } - return this.renderStandards(); - } } return AccountTabContainer; diff --git a/app/jsx/grading/CourseTabContainer.jsx b/app/jsx/grading/CourseTabContainer.jsx index 04d96af6106..25c2d8b41c0 100644 --- a/app/jsx/grading/CourseTabContainer.jsx +++ b/app/jsx/grading/CourseTabContainer.jsx @@ -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(); diff --git a/app/jsx/grading/gradingPeriodCollection.jsx b/app/jsx/grading/gradingPeriodCollection.jsx index c22a785a3d1..afb150e5d92 100644 --- a/app/jsx/grading/gradingPeriodCollection.jsx +++ b/app/jsx/grading/gradingPeriodCollection.jsx @@ -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 ( - - {I18n.t('You can disable this feature ')} - {I18n.t('here.')} - ); - } - }, - 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 (
); @@ -257,9 +241,6 @@ function(React, update, GradingPeriod, $, I18n, _, ConvertCase) { render: function () { return (
-
- {this.renderLinkToSettingsPage()} -
{this.renderGradingPeriods()}
diff --git a/app/models/course.rb b/app/models/course.rb index df2a5428500..a54a4341992 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -3086,6 +3086,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 + private def effective_due_dates diff --git a/app/models/feature_flag.rb b/app/models/feature_flag.rb index 9579c78e970..ad55779ecee 100644 --- a/app/models/feature_flag.rb +++ b/app/models/feature_flag.rb @@ -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 diff --git a/app/models/grading_period_group.rb b/app/models/grading_period_group.rb index 6d65e7a8c35..c4571708fe4 100644 --- a/app/models/grading_period_group.rb +++ b/app/models/grading_period_group.rb @@ -31,27 +31,23 @@ class GradingPeriodGroup < ActiveRecord::Base 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 @@ -63,10 +59,6 @@ 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 @@ -91,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 diff --git a/app/models/group.rb b/app/models/group.rb index 70c00e809a4..bf3e5742031 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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), diff --git a/app/views/gradebooks/grade_summary.html.erb b/app/views/gradebooks/grade_summary.html.erb index f4164adb6ae..42766bc778b 100644 --- a/app/views/gradebooks/grade_summary.html.erb +++ b/app/views/gradebooks/grade_summary.html.erb @@ -54,7 +54,7 @@