diff --git a/app/controllers/gradebook_csvs_controller.rb b/app/controllers/gradebook_csvs_controller.rb
index 74c26527a3e..708f1d09e75 100644
--- a/app/controllers/gradebook_csvs_controller.rb
+++ b/app/controllers/gradebook_csvs_controller.rb
@@ -30,7 +30,9 @@ class GradebookCsvsController < ApplicationController
csv_options = {
include_sis_id: @context.grants_any_right?(@current_user, session, :read_sis, :manage_sis),
- grading_period_id: params[:grading_period_id]
+ grading_period_id: params[:grading_period_id],
+ student_order: params[:student_order],
+ current_view: params[:current_view]
}
if params[:assignment_order]
diff --git a/lib/gradebook_exporter.rb b/lib/gradebook_exporter.rb
index b52dc149f6f..81926c0e54c 100644
--- a/lib/gradebook_exporter.rb
+++ b/lib/gradebook_exporter.rb
@@ -90,7 +90,11 @@ class GradebookExporter
end
# remove duplicate enrollments for students enrolled in multiple sections
- student_enrollments = student_enrollments.uniq(&:user_id)
+ student_enrollments = if @options[:current_view]
+ student_enrollments.select { |s| @options[:student_order].include?(s[:user_id]) }.uniq(&:user_id)
+ else
+ student_enrollments.uniq(&:user_id)
+ end
# TODO: Stop using the grade calculator and instead use the scores table entirely.
# This cannot be done until we are storing points values in the scores table, which
@@ -105,8 +109,12 @@ class GradebookExporter
submissions = {}
calc.submissions.each { |s| submissions[[s.user_id, s.assignment_id]] = s }
+ assignments = if @options[:current_view]
+ calc.gradable_assignments.select { |a| @options[:assignment_order].include?(a[:id]) }
+ else
+ calc.gradable_assignments
+ end
- assignments = select_in_grading_period(calc.gradable_assignments).to_a
Assignment.preload_unposted_anonymous_submissions(assignments)
ActiveRecord::Associations.preload(assignments, :assignment_group)
@@ -390,6 +398,7 @@ class GradebookExporter
end
def show_totals?
+ return false if !@options[:current_view] && @course.grading_periods?
return true unless @course.grading_periods?
return true if @options[:grading_period_id].try(:to_i) != 0
diff --git a/spec/javascripts/jsx/gradebook/default_gradebook/components/ActionMenuSpec.js b/spec/javascripts/jsx/gradebook/default_gradebook/components/ActionMenuSpec.js
index 4b4ce3800e4..07f73a148d6 100644
--- a/spec/javascripts/jsx/gradebook/default_gradebook/components/ActionMenuSpec.js
+++ b/spec/javascripts/jsx/gradebook/default_gradebook/components/ActionMenuSpec.js
@@ -25,6 +25,7 @@ import ActionMenu from 'ui/features/gradebook/react/default_gradebook/components
const workingMenuProps = () => ({
getAssignmentOrder() {},
+ getStudentOrder() {},
gradebookIsEditable: true,
contextAllowsGradebookUploads: true,
gradebookImportUrl: 'http://gradebookImportUrl',
@@ -86,10 +87,17 @@ test('renders the Import menu item', () => {
equal(specificMenuItem.textContent, 'Import')
})
-test('renders the Export menu item', () => {
+test('renders the Export Current Gradebook View menu item', () => {
const specificMenuItem = document.querySelector('[role="menuitem"] [data-menu-id="export"]')
- equal(specificMenuItem.textContent, 'Export')
+ equal(specificMenuItem.textContent, 'Export Current Gradebook View')
+ equal(specificMenuItem.parentElement.getAttribute('aria-disabled'), null)
+})
+
+test('renders the Export Entire Gradebook menu item', () => {
+ const specificMenuItem = document.querySelector('[role="menuitem"] [data-menu-id="export-all"]')
+
+ equal(specificMenuItem.textContent, 'Export Entire Gradebook')
equal(specificMenuItem.parentElement.getAttribute('aria-disabled'), null)
})
@@ -117,7 +125,7 @@ test('renders no Post Grades feature menu item when disabled', () => {
strictEqual(specificMenuItem, null)
})
-test('renders the Post Grades feature menu item when enabled', function() {
+test('renders the Post Grades feature menu item when enabled', function () {
this.wrapper.unmount()
const props = workingMenuProps()
props.postGradesFeature.enabled = true
@@ -131,7 +139,7 @@ test('renders the Post Grades feature menu item when enabled', function() {
equal(specificMenuItem.textContent, 'Sync to SIS')
})
-test('renders the Post Grades feature menu item with label when sis handle is set', function() {
+test('renders the Post Grades feature menu item with label when sis handle is set', function () {
this.wrapper.unmount()
const props = workingMenuProps()
props.postGradesFeature.enabled = true
@@ -156,7 +164,7 @@ QUnit.module('ActionMenu - getExistingExport', {
}
})
-test('returns an export hash with workflowState when progressId and attachment.id are present', function() {
+test('returns an export hash with workflowState when progressId and attachment.id are present', function () {
const propsWithPreviousExport = {
...workingMenuProps(),
...previousExportProps()
@@ -172,11 +180,11 @@ test('returns an export hash with workflowState when progressId and attachment.i
deepEqual(this.wrapper.instance().getExistingExport(), expectedExport)
})
-test('returns undefined when lastExport is undefined', function() {
+test('returns undefined when lastExport is undefined', function () {
equal(this.wrapper.instance().getExistingExport(), undefined)
})
-test("returns undefined when lastExport's attachment is undefined", function() {
+test("returns undefined when lastExport's attachment is undefined", function () {
const propsWithPreviousExport = {
...workingMenuProps(),
...previousExportProps()
@@ -188,7 +196,7 @@ test("returns undefined when lastExport's attachment is undefined", function() {
equal(this.wrapper.instance().getExistingExport(), undefined)
})
-test('returns undefined when lastExport is missing progressId', function() {
+test('returns undefined when lastExport is missing progressId', function () {
const propsWithPreviousExport = {
...workingMenuProps(),
...previousExportProps()
@@ -200,7 +208,7 @@ test('returns undefined when lastExport is missing progressId', function() {
equal(this.wrapper.instance().getExistingExport(), undefined)
})
-test("returns undefined when lastExport's attachment is missing its id", function() {
+test("returns undefined when lastExport's attachment is missing its id", function () {
const propsWithPreviousExport = {
...workingMenuProps(),
...previousExportProps()
@@ -221,7 +229,7 @@ QUnit.module('ActionMenu - handleExport', {
})
}
- return Promise.reject('Export failure reason')
+ return Promise.reject(new Error('Export failure reason'))
},
setup() {
@@ -256,7 +264,7 @@ QUnit.module('ActionMenu - handleExport', {
}
})
-test('clicking on the export menu option calls the handleExport function', function() {
+test('clicking on the export menu option calls the handleExport function', function () {
this.spies.handleExport = sandbox.stub(ActionMenu.prototype, 'handleExport')
this.menuItem.click()
@@ -264,7 +272,7 @@ test('clicking on the export menu option calls the handleExport function', funct
equal(this.spies.handleExport.callCount, 1)
})
-test('shows a message to the user indicating the export is in progress', function() {
+test('shows a message to the user indicating the export is in progress', function () {
const exportResult = this.getPromise('resolved')
this.spies.startExport.returns(exportResult)
this.spies.flashMessage = sandbox.stub(window.$, 'flashMessage')
@@ -277,7 +285,7 @@ test('shows a message to the user indicating the export is in progress', functio
return exportResult
})
-test('changes the "Export" menu item to indicate the export is in progress', function() {
+test('changes the "Export" menu item to indicate the export is in progress', function () {
const exportResult = this.getPromise('resolved')
this.spies.startExport.returns(exportResult)
@@ -298,7 +306,7 @@ test('changes the "Export" menu item to indicate the export is in progress', fun
return exportResult
})
-test('starts the export using the GradebookExportManager instance', function() {
+test('starts the export using the GradebookExportManager instance', function () {
const exportResult = this.getPromise('resolved')
this.spies.startExport.returns(exportResult)
@@ -309,7 +317,7 @@ test('starts the export using the GradebookExportManager instance', function() {
return exportResult
})
-test('passes the grading period to the GradebookExportManager', function() {
+test('passes the grading period to the GradebookExportManager', function () {
const exportResult = this.getPromise('resolved')
this.spies.startExport.returns(exportResult)
@@ -321,7 +329,7 @@ test('passes the grading period to the GradebookExportManager', function() {
return exportResult
})
-test('on success, takes the user to the newly completed export', function() {
+test('on success, takes the user to the newly completed export', function () {
const exportResult = this.getPromise('resolved')
this.spies.startExport.returns(exportResult)
@@ -336,7 +344,7 @@ test('on success, takes the user to the newly completed export', function() {
})
})
-test('on success, re-enables the "Export" menu item', function() {
+test('on success, re-enables the "Export Current Gradebook View" and "Export Entire Gradbook" menu items', function () {
const exportResult = this.getPromise('resolved')
this.spies.startExport.returns(exportResult)
@@ -350,12 +358,17 @@ test('on success, re-enables the "Export" menu item', function() {
this.menuItem = document.querySelector('[role="menuitem"] [data-menu-id="export"]')
- equal(this.menuItem.textContent, 'Export')
+ equal(this.menuItem.textContent, 'Export Current Gradebook View')
+ equal(this.menuItem.parentElement.getAttribute('aria-disabled'), null)
+
+ this.menuItem = document.querySelector('[role="menuitem"] [data-menu-id="export-all"]')
+
+ equal(this.menuItem.textContent, 'Export Entire Gradebook')
equal(this.menuItem.parentElement.getAttribute('aria-disabled'), null)
})
})
-test('on success, shows the "New Export" menu item', function() {
+test('on success, shows the "New Export" menu item', function () {
const exportResult = this.getPromise('resolved')
this.spies.startExport.returns(exportResult)
@@ -375,7 +388,7 @@ test('on success, shows the "New Export" menu item', function() {
})
})
-test('on failure, shows a message to the user indicating the export failed', function() {
+test('on failure, shows a message to the user indicating the export failed', function () {
const exportResult = this.getPromise('rejected')
this.spies.startExport.returns(exportResult)
this.spies.flashError = sandbox.stub(window.$, 'flashError')
@@ -387,12 +400,12 @@ test('on failure, shows a message to the user indicating the export failed', fun
equal(this.spies.flashError.callCount, 1)
equal(
this.spies.flashError.getCall(0).args[0],
- 'Gradebook Export Failed: Export failure reason'
+ 'Gradebook Export Failed: Error: Export failure reason'
)
})
})
-test('on failure, renables the "Export" menu item', function() {
+test('on failure, renables the "Export Current Gradebook View" and "Export Entire Gradbook" menu items', function () {
const exportResult = this.getPromise('rejected')
this.spies.startExport.returns(exportResult)
@@ -406,7 +419,12 @@ test('on failure, renables the "Export" menu item', function() {
this.menuItem = document.querySelector('[role="menuitem"] [data-menu-id="export"]')
- equal(this.menuItem.textContent, 'Export')
+ equal(this.menuItem.textContent, 'Export Current Gradebook View')
+ equal(this.menuItem.parentElement.getAttribute('aria-disabled'), null)
+
+ this.menuItem = document.querySelector('[role="menuitem"] [data-menu-id="export-all"]')
+
+ equal(this.menuItem.textContent, 'Export Entire Gradebook')
equal(this.menuItem.parentElement.getAttribute('aria-disabled'), null)
})
})
@@ -428,7 +446,7 @@ QUnit.module('ActionMenu - handleImport', {
}
})
-test('clicking on the import menu option calls the handleImport function', function() {
+test('clicking on the import menu option calls the handleImport function', function () {
const handleImportSpy = sandbox.spy(ActionMenu.prototype, 'handleImport')
this.menuItem.click()
@@ -436,7 +454,7 @@ test('clicking on the import menu option calls the handleImport function', funct
equal(handleImportSpy.callCount, 1)
})
-test('it takes you to the new imports page', function() {
+test('it takes you to the new imports page', function () {
this.menuItem.click()
equal(this.spies.gotoUrl.callCount, 1)
@@ -448,7 +466,7 @@ QUnit.module('ActionMenu - disableImports', {
}
})
-test('is called once when the component renders', function() {
+test('is called once when the component renders', function () {
const disableImportsSpy = sandbox.spy(ActionMenu.prototype, 'disableImports')
this.wrapper = mount()
@@ -457,12 +475,12 @@ test('is called once when the component renders', function() {
equal(disableImportsSpy.callCount, 1)
})
-test('returns false when gradebook is editable and context allows gradebook uploads', function() {
+test('returns false when gradebook is editable and context allows gradebook uploads', function () {
this.wrapper = mount()
strictEqual(this.wrapper.instance().disableImports(), false)
})
-test('returns true when gradebook is not editable and context allows gradebook uploads', function() {
+test('returns true when gradebook is not editable and context allows gradebook uploads', function () {
const newImportProps = {
...workingMenuProps(),
gradebookIsEditable: false
@@ -472,7 +490,7 @@ test('returns true when gradebook is not editable and context allows gradebook u
strictEqual(this.wrapper.instance().disableImports(), true)
})
-test('returns true when gradebook is editable but context does not allow gradebook uploads', function() {
+test('returns true when gradebook is editable but context does not allow gradebook uploads', function () {
const newImportProps = {
...workingMenuProps(),
contextAllowsGradebookUploads: false
@@ -492,7 +510,7 @@ QUnit.module('ActionMenu - lastExportFromProps', {
}
})
-test('returns the lastExport hash if props have a completed last export', function() {
+test('returns the lastExport hash if props have a completed last export', function () {
const propsWithPreviousExport = {
...workingMenuProps(),
...previousExportProps()
@@ -503,11 +521,11 @@ test('returns the lastExport hash if props have a completed last export', functi
deepEqual(this.wrapper.instance().lastExportFromProps(), propsWithPreviousExport.lastExport)
})
-test('returns undefined if props have no lastExport', function() {
+test('returns undefined if props have no lastExport', function () {
equal(this.wrapper.instance().lastExportFromProps(), undefined)
})
-test('returns undefined if props have a lastExport but it is not completed', function() {
+test('returns undefined if props have a lastExport but it is not completed', function () {
const propsWithPreviousExport = {
...workingMenuProps(),
...previousExportProps()
@@ -529,7 +547,7 @@ QUnit.module('ActionMenu - lastExportFromState', {
}
})
-test('returns the previous export if state has a previousExport defined', function() {
+test('returns the previous export if state has a previousExport defined', function () {
const expectedPreviousExport = {
label: 'previous export label',
attachmentUrl: 'http://attachmentUrl'
@@ -540,13 +558,13 @@ test('returns the previous export if state has a previousExport defined', functi
deepEqual(this.wrapper.instance().lastExportFromState(), expectedPreviousExport)
})
-test('returns undefined if an export is already in progress', function() {
+test('returns undefined if an export is already in progress', function () {
this.wrapper.instance().setExportInProgress(true)
equal(this.wrapper.instance().lastExportFromState(), undefined)
})
-test('returns undefined if no previous export is set in the state', function() {
+test('returns undefined if no previous export is set in the state', function () {
this.wrapper.instance().setExportInProgress(false)
equal(this.wrapper.instance().lastExportFromState(), undefined)
@@ -567,7 +585,7 @@ QUnit.module('ActionMenu - previousExport', {
}
})
-test('returns the previous export stored in the state if it is available', function() {
+test('returns the previous export stored in the state if it is available', function () {
const stateExport = {
label: 'previous export label',
attachmentUrl: 'http://attachmentUrl'
@@ -580,7 +598,7 @@ test('returns the previous export stored in the state if it is available', funct
equal(lastExportFromState.callCount, 1)
})
-test('returns the previous export stored in the props if nothing is available in state', function() {
+test('returns the previous export stored in the props if nothing is available in state', function () {
const expectedPreviousExport = {
attachmentUrl: 'http://downloadUrl',
label: 'Previous Export (Jan 20, 2009 at 5pm)'
@@ -598,7 +616,7 @@ test('returns the previous export stored in the props if nothing is available in
equal(lastExportFromProps.callCount, 1)
})
-test('returns undefined if state has nothing and props have nothing', function() {
+test('returns undefined if state has nothing and props have nothing', function () {
const lastExportFromState = sandbox
.stub(ActionMenu.prototype, 'lastExportFromState')
.returns(undefined)
@@ -621,13 +639,13 @@ QUnit.module('ActionMenu - exportInProgress', {
}
})
-test('returns true if exportInProgress is set', function() {
+test('returns true if exportInProgress is set', function () {
this.wrapper.instance().setExportInProgress(true)
strictEqual(this.wrapper.instance().exportInProgress(), true)
})
-test('returns false if exportInProgress is set to false', function() {
+test('returns false if exportInProgress is set to false', function () {
this.wrapper.instance().setExportInProgress(false)
strictEqual(this.wrapper.instance().exportInProgress(), false)
@@ -647,7 +665,7 @@ QUnit.module('ActionMenu - Post Grade Ltis', {
}
})
-test('Invokes the onSelect prop when selected', function() {
+test('Invokes the onSelect prop when selected', function () {
document.querySelector('[data-menu-id="post_grades_lti_1"]').click()
strictEqual(this.props.postGradesLtis[0].onSelect.called, true)
@@ -703,7 +721,7 @@ test('Does not render menu item when isEnabled is false and publishToSisUrl is u
equal(menuItem, null)
})
-test('Does not render menu item when isEnabled is true and publishToSisUrl is undefined', function() {
+test('Does not render menu item when isEnabled is true and publishToSisUrl is undefined', function () {
this.wrapper.setProps({
publishGradesToSis: {
isEnabled: true
@@ -716,7 +734,7 @@ test('Does not render menu item when isEnabled is true and publishToSisUrl is un
equal(menuItem, null)
})
-test('Renders menu item when isEnabled is true and publishToSisUrl is defined', function() {
+test('Renders menu item when isEnabled is true and publishToSisUrl is defined', function () {
this.wrapper.setProps({
publishGradesToSis: {
isEnabled: true,
@@ -730,7 +748,7 @@ test('Renders menu item when isEnabled is true and publishToSisUrl is defined',
ok(menuItem)
})
-test('Calls gotoUrl with publishToSisUrl when clicked', function() {
+test('Calls gotoUrl with publishToSisUrl when clicked', function () {
this.wrapper.setProps({
publishGradesToSis: {
isEnabled: true,
diff --git a/spec/javascripts/jsx/gradebook/shared/GradebookExportManagerSpec.js b/spec/javascripts/jsx/gradebook/shared/GradebookExportManagerSpec.js
index 6d5ed4b8dd1..2e8d928a525 100644
--- a/spec/javascripts/jsx/gradebook/shared/GradebookExportManagerSpec.js
+++ b/spec/javascripts/jsx/gradebook/shared/GradebookExportManagerSpec.js
@@ -18,7 +18,7 @@
import moxios from 'moxios'
-import GradebookExportManager from 'ui/features/gradebook/react/shared/GradebookExportManager.js'
+import GradebookExportManager from 'ui/features/gradebook/react/shared/GradebookExportManager'
const currentUserId = 42
const exportingUrl = 'http://exportingUrl'
@@ -165,7 +165,8 @@ test('sets show_student_first_last_name setting if requested', function () {
}
const getAssignmentOrder = () => []
- return this.subject.startExport(undefined, getAssignmentOrder, true).then(() => {
+ const getStudentOrder = () => []
+ return this.subject.startExport(undefined, getAssignmentOrder, true, getStudentOrder).then(() => {
const postData = JSON.parse(moxios.requests.mostRecent().config.data)
propEqual(postData.show_student_first_last_name, true)
})
@@ -178,10 +179,13 @@ test('does not set show_student_first_last_name setting by default', function ()
}
const getAssignmentOrder = () => []
- return this.subject.startExport(undefined, getAssignmentOrder).then(() => {
- const postData = JSON.parse(moxios.requests.mostRecent().config.data)
- propEqual(postData.show_student_first_last_name, false)
- })
+ const getStudentOrder = () => []
+ return this.subject
+ .startExport(undefined, getAssignmentOrder, false, getStudentOrder)
+ .then(() => {
+ const postData = JSON.parse(moxios.requests.mostRecent().config.data)
+ propEqual(postData.show_student_first_last_name, false)
+ })
})
test('includes assignment_order if getAssignmentOrder returns some assignments', function () {
@@ -191,10 +195,13 @@ test('includes assignment_order if getAssignmentOrder returns some assignments',
}
const getAssignmentOrder = () => ['1', '2', '3']
- return this.subject.startExport(undefined, getAssignmentOrder).then(() => {
- const postData = JSON.parse(moxios.requests.mostRecent().config.data)
- propEqual(postData.assignment_order, ['1', '2', '3'])
- })
+ const getStudentOrder = () => []
+ return this.subject
+ .startExport(undefined, getAssignmentOrder, false, getStudentOrder)
+ .then(() => {
+ const postData = JSON.parse(moxios.requests.mostRecent().config.data)
+ propEqual(postData.assignment_order, ['1', '2', '3'])
+ })
})
test('does not include assignment_order if getAssignmentOrder returns no assignments', function () {
@@ -204,10 +211,13 @@ test('does not include assignment_order if getAssignmentOrder returns no assignm
}
const getAssignmentOrder = () => []
- return this.subject.startExport(undefined, getAssignmentOrder).then(() => {
- const postData = JSON.parse(moxios.requests.mostRecent().config.data)
- equal(postData.assignment_order, undefined)
- })
+ const getStudentOrder = () => []
+ return this.subject
+ .startExport(undefined, getAssignmentOrder, false, getStudentOrder)
+ .then(() => {
+ const postData = JSON.parse(moxios.requests.mostRecent().config.data)
+ equal(postData.assignment_order, undefined)
+ })
})
test('returns a rejected promise if the manager has no exportingUrl set', function () {
@@ -215,7 +225,12 @@ test('returns a rejected promise if the manager has no exportingUrl set', functi
this.subject.exportingUrl = undefined
return this.subject
- .startExport(undefined, () => [])
+ .startExport(
+ undefined,
+ () => [],
+ false,
+ () => []
+ )
.catch(reason => {
equal(reason, 'No way to export gradebooks provided!')
})
@@ -225,7 +240,12 @@ test('returns a rejected promise if the manager already has an export going', fu
this.subject = new GradebookExportManager(exportingUrl, currentUserId, workingExport)
return this.subject
- .startExport(undefined, () => [])
+ .startExport(
+ undefined,
+ () => [],
+ false,
+ () => []
+ )
.catch(reason => {
equal(reason, 'An export is already in progress.')
})
@@ -243,7 +263,12 @@ test('sets a new existing export and returns a fulfilled promise', function () {
}
return this.subject
- .startExport(undefined, () => [])
+ .startExport(
+ undefined,
+ () => [],
+ false,
+ () => []
+ )
.then(() => {
deepEqual(this.subject.export, expectedExport)
})
@@ -254,7 +279,12 @@ test('clears any new export and returns a rejected promise if no monitoring is p
this.subject = new GradebookExportManager(exportingUrl, currentUserId)
return this.subject
- .startExport(undefined, () => [])
+ .startExport(
+ undefined,
+ () => [],
+ false,
+ () => []
+ )
.catch(reason => {
equal(reason, 'No way to monitor gradebook exports provided!')
equal(this.subject.export, undefined)
@@ -275,7 +305,12 @@ test('starts polling for progress and returns a rejected promise on progress fai
})
return this.subject
- .startExport(undefined, () => [])
+ .startExport(
+ undefined,
+ () => [],
+ false,
+ () => []
+ )
.catch(reason => {
equal(reason, 'Error exporting gradebook: Arbitrary failure')
})
@@ -295,7 +330,12 @@ test('starts polling for progress and returns a rejected promise on unknown prog
})
return this.subject
- .startExport(undefined, () => [])
+ .startExport(
+ undefined,
+ () => [],
+ false,
+ () => []
+ )
.catch(reason => {
equal(reason, 'Error exporting gradebook: Pattern buffer degradation')
})
@@ -323,7 +363,12 @@ test('starts polling for progress and returns a fulfilled promise on progress co
})
return this.subject
- .startExport(undefined, () => [])
+ .startExport(
+ undefined,
+ () => [],
+ false,
+ () => {}
+ )
.then(resolution => {
equal(this.subject.export, undefined)
diff --git a/spec/lib/gradebook_exporter_spec.rb b/spec/lib/gradebook_exporter_spec.rb
index c3f8f9101f2..3506539870a 100644
--- a/spec/lib/gradebook_exporter_spec.rb
+++ b/spec/lib/gradebook_exporter_spec.rb
@@ -558,55 +558,62 @@ describe GradebookExporter do
describe "with grading periods" do
describe "assignments in the selected grading period are exported" do
- before do
- @csv = exporter(grading_period_id: @last_period.id).to_csv
- @rows = CSV.parse(@csv, headers: true)
- @headers = @rows.headers
+ describe "export entire gradebook" do
+ before do
+ @csv = exporter(grading_period_id: @last_period.id).to_csv
+ @rows = CSV.parse(@csv, headers: true)
+ @headers = @rows.headers
+ end
+
+ it "exports assignments from all grading periods" do
+ expect(@headers).to include @no_due_date_assignment.title_with_id,
+ @current_assignment.title_with_id,
+ @past_assignment.title_with_id,
+ @future_assignment.title_with_id
+ end
+
+ it "does not export totals columns" do
+ expect(@headers).to_not include "Final Score (#{@last_period.title})"
+ end
end
- it "exports selected grading period's assignments" do
- expect(@headers).to include @no_due_date_assignment.title_with_id,
- @current_assignment.title_with_id
- final_grade = @rows[1]["Final Score (#{@last_period.title})"].try(:to_f)
- expect(final_grade).to eq 20
- end
+ describe "export current gradebook view" do
+ before do
+ exporter_options = {
+ grading_period_id: @last_period.id,
+ current_view: true,
+ assignment_order: @course.assignments.pluck(:id),
+ student_order: @course.student_enrollments.pluck(:user_id)
+ }
+ @csv = exporter(exporter_options).to_csv
+ @rows = CSV.parse(@csv, headers: true)
+ @headers = @rows.headers
+ end
- it "exports assignments without due dates if exporting last grading period" do
- expect(@headers).to include @current_assignment.title_with_id,
- @no_due_date_assignment.title_with_id
- final_grade = @rows[1]["Final Score (#{@last_period.title})"].try(:to_f)
- expect(final_grade).to eq 20
- end
+ it "exports filtered grading period's assignments with totals columns" do
+ expect(@headers).to include @no_due_date_assignment.title_with_id,
+ @current_assignment.title_with_id
+ final_grade = @rows[1]["Final Score (#{@last_period.title})"].try(:to_f)
+ expect(final_grade).to eq 20
+ end
- it "does not export assignments without due date" do
- @grading_period_id = @first_period.id
- @csv = exporter(grading_period_id: @grading_period_id).to_csv
- @rows = CSV.parse(@csv, headers: true)
- @headers = @rows.headers
+ it "exports all visible assignments in the gradebook" do
+ exporter_options = {
+ grading_period_id: @first_period.id,
+ current_view: true,
+ assignment_order: [@no_due_date_assignment.id, @future_assignment.id],
+ student_order: @course.student_enrollments.pluck(:user_id).map(&:to_s)
+ }
+ @csv = exporter(exporter_options).to_csv
+ @rows = CSV.parse(@csv, headers: true)
+ @headers = @rows.headers
- expect(@headers).to_not include @no_due_date_assignment.title_with_id
- end
+ expect(@headers).to include @no_due_date_assignment.title_with_id,
+ @future_assignment.title_with_id
- it "does not export assignments in other grading periods" do
- expect(@headers).to_not include @past_assignment.title_with_id,
- @future_assignment.title_with_id
- end
-
- it "does not export future assignments" do
- expect(@headers).to_not include @future_assignment.title_with_id
- end
-
- it "exports the entire gradebook when grading_period_id is 0" do
- @grading_period_id = 0
- @csv = exporter(grading_period_id: @grading_period_id).to_csv
- @rows = CSV.parse(@csv, headers: true)
- @headers = @rows.headers
-
- expect(@headers).to include @past_assignment.title_with_id,
- @current_assignment.title_with_id,
- @future_assignment.title_with_id,
- @no_due_date_assignment.title_with_id
- expect(@headers).not_to include "Final Score"
+ expect(@headers).to_not include @current_assignment.title_with_id,
+ @past_assignment.title_with_id
+ end
end
end
end
@@ -694,7 +701,13 @@ describe GradebookExporter do
end
let(:exporter) do
- GradebookExporter.new(@course, @teacher, { grading_period_id: @last_grading_period.id })
+ exporter_options = {
+ grading_period_id: @last_grading_period.id,
+ current_view: true,
+ assignment_order: @course.assignments.pluck(:id),
+ student_order: @course.student_enrollments.pluck(:user_id).map(&:to_s)
+ }
+ GradebookExporter.new(@course, @teacher, exporter_options)
end
let(:exported_headers) { CSV.parse(exporter.to_csv, headers: true).headers }
@@ -863,7 +876,15 @@ describe GradebookExporter do
start_date: 1.week.ago, end_date: 1.week.from_now, title: "test period"
)
end
- let(:exporter) { GradebookExporter.new(@course, @teacher, { grading_period_id: grading_period.id }) }
+ let(:exporter) do
+ exporter_options = {
+ grading_period_id: grading_period.id,
+ current_view: true,
+ assignment_order: @course.assignments.pluck(:id),
+ student_order: @course.student_enrollments.pluck(:user_id)
+ }
+ GradebookExporter.new(@course, @teacher, exporter_options)
+ end
before do
allow(exporter).to receive(:enrollments_for_csv).and_return([enrollment])
diff --git a/ui/features/gradebook/react/default_gradebook/Gradebook.tsx b/ui/features/gradebook/react/default_gradebook/Gradebook.tsx
index 66892050a25..d44aca78bff 100644
--- a/ui/features/gradebook/react/default_gradebook/Gradebook.tsx
+++ b/ui/features/gradebook/react/default_gradebook/Gradebook.tsx
@@ -2021,6 +2021,7 @@ class Gradebook extends React.Component {
publishToSisUrl: this.options.publish_to_sis_url
},
gradingPeriodId: this.state.gradingPeriodId,
+ getStudentOrder: this.getStudentOrder,
getAssignmentOrder: this.getAssignmentOrder
}
const progressData = this.options.gradebook_csv_progress
diff --git a/ui/features/gradebook/react/default_gradebook/components/ActionMenu.tsx b/ui/features/gradebook/react/default_gradebook/components/ActionMenu.tsx
index 7044e212c96..06c00252a9b 100644
--- a/ui/features/gradebook/react/default_gradebook/components/ActionMenu.tsx
+++ b/ui/features/gradebook/react/default_gradebook/components/ActionMenu.tsx
@@ -38,6 +38,7 @@ export type ActionMenuProps = {
gradebookIsEditable: boolean
contextAllowsGradebookUploads: boolean
getAssignmentOrder: any
+ getStudentOrder: any
gradebookImportUrl: string
showStudentFirstLastName: boolean
lastExport?: {
@@ -128,7 +129,7 @@ class ActionMenu extends React.Component {
this.setState({exportInProgress: !!status})
}
- handleExport() {
+ handleExport(currentView) {
this.setExportInProgress(true)
$.flashMessage(I18n.t('Gradebook export started'))
@@ -136,7 +137,9 @@ class ActionMenu extends React.Component {
?.startExport(
this.props.gradingPeriodId,
this.props.getAssignmentOrder,
- this.props.showStudentFirstLastName
+ this.props.showStudentFirstLastName,
+ this.props.getStudentOrder,
+ currentView
)
.then(resolution => {
this.setExportInProgress(false)
@@ -324,11 +327,26 @@ class ActionMenu extends React.Component {
+
+
diff --git a/ui/features/gradebook/react/default_gradebook/components/EnhancedActionMenu.js b/ui/features/gradebook/react/default_gradebook/components/EnhancedActionMenu.js
index 366e5bf781d..c8754f1fdd9 100644
--- a/ui/features/gradebook/react/default_gradebook/components/EnhancedActionMenu.js
+++ b/ui/features/gradebook/react/default_gradebook/components/EnhancedActionMenu.js
@@ -71,12 +71,18 @@ export default function EnhancedActionMenu(props) {
}
}
- const handleExport = () => {
+ const handleExport = currentView => {
setExportInProgress(true)
$.flashMessage(I18n.t('Gradebook export started'))
return exportManager.current
- .startExport(props.gradingPeriodId, props.getAssignmentOrder, props.showStudentFirstLastName)
+ .startExport(
+ props.gradingPeriodId,
+ props.getAssignmentOrder,
+ props.showStudentFirstLastName,
+ props.getStudentOrder,
+ currentView
+ )
.then(resolution => {
setExportInProgress(false)
@@ -84,7 +90,7 @@ export default function EnhancedActionMenu(props) {
const updatedAt = new Date(resolution.updatedAt)
const previousExportValue = {
- label: `${I18n.t('New Export')} (${DateHelper.formatDatetimeForDisplay(updatedAt)})`,
+ label: `${I18n.t('Previous Export')} (${DateHelper.formatDatetimeForDisplay(updatedAt)})`,
attachmentUrl
}
@@ -288,11 +294,24 @@ export default function EnhancedActionMenu(props) {
{
- handleExport()
+ handleExport(true)
}}
>
- {exportInProgress ? I18n.t('Export in progress') : I18n.t('New Export')}
+ {exportInProgress
+ ? I18n.t('Export in progress')
+ : I18n.t('Export Current Gradebook View')}
+
+
+
+ {
+ handleExport(false)
+ }}
+ >
+
+ {exportInProgress ? I18n.t('Export in progress') : I18n.t('Export Entire Gradebook')}
@@ -306,6 +325,7 @@ EnhancedActionMenu.propTypes = {
gradebookIsEditable: bool.isRequired,
contextAllowsGradebookUploads: bool.isRequired,
getAssignmentOrder: func.isRequired,
+ getStudentOrder: func.isRequired,
gradebookImportUrl: string.isRequired,
currentUserId: string.isRequired,
diff --git a/ui/features/gradebook/react/default_gradebook/components/__tests__/EnhancedActionMenu.test.js b/ui/features/gradebook/react/default_gradebook/components/__tests__/EnhancedActionMenu.test.js
index ad800238919..c09ebb9fb7d 100644
--- a/ui/features/gradebook/react/default_gradebook/components/__tests__/EnhancedActionMenu.test.js
+++ b/ui/features/gradebook/react/default_gradebook/components/__tests__/EnhancedActionMenu.test.js
@@ -36,6 +36,7 @@ const getPromise = (type, object = defaultResult) => {
const workingMenuProps = () => ({
getAssignmentOrder() {},
+ getStudentOrder() {},
gradebookIsEditable: true,
contextAllowsGradebookUploads: true,
gradebookImportUrl: 'http://gradebookImportUrl',
@@ -130,11 +131,20 @@ describe('EnhancedActionMenu', () => {
expect(specificMenuItem).toHaveTextContent('Import')
})
- it('renders the New Export menu item', async () => {
+ it('renders the Export Current Gradebook View menu item', async () => {
component = renderComponent(props)
clickOnDropdown('Export')
const specificMenuItem = component.getByRole('menuitem', {
- name: 'New Export'
+ name: 'Export Current Gradebook View'
+ })
+ expect(specificMenuItem).toBeInTheDocument()
+ })
+
+ it('renders the Export Entire Gradebook menu item', async () => {
+ component = renderComponent(props)
+ clickOnDropdown('Export')
+ const specificMenuItem = component.getByRole('menuitem', {
+ name: 'Export Entire Gradebook'
})
expect(specificMenuItem).toBeInTheDocument()
})
@@ -148,7 +158,7 @@ describe('EnhancedActionMenu', () => {
expect(specificMenuItem).toBeInTheDocument()
})
- it('updates the New Export date when export success', async () => {
+ it('updates the Previous Export date when export success', async () => {
const exportResult = getPromise('resolved', {
...defaultResult,
updatedAt: '2021-05-12T13:00:00Z'
@@ -157,12 +167,12 @@ describe('EnhancedActionMenu', () => {
startExport.mockReturnValue(exportResult)
component = renderComponent(props)
clickOnDropdown('Export')
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Entire Gradebook')
await waitFor(() => {
clickOnDropdown('Export')
})
const specificMenuItem = component.getByRole('menuitem', {
- name: 'New Export (May 12, 2021 at 1pm)'
+ name: 'Previous Export (May 12, 2021 at 1pm)'
})
expect(specificMenuItem).toBeInTheDocument()
})
@@ -219,7 +229,7 @@ describe('EnhancedActionMenu', () => {
startExport.mockReturnValue(exportResult)
const spy = jest.spyOn(window.$, 'flashMessage').mockReturnValue(true)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
await waitFor(() => {
expect(spy).toHaveBeenCalled()
@@ -227,17 +237,19 @@ describe('EnhancedActionMenu', () => {
})
})
- it('changes the "Export" menu item to indicate the export is in progress', async () => {
+ it('changes the "Export Current Gradebook View" and "Export Entire Gradebook" menu items to indicate the export is in progress', async () => {
const exportResult = getPromise('resolved')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
clickOnDropdown('Export')
- const specificMenuItem = component.getByRole('menuitem', {name: /Export in progress/})
+ const specificMenuItems = component.getAllByRole('menuitem', {name: /Export in progress/})
await waitFor(() => {
- expect(specificMenuItem).toBeInTheDocument()
- expect(specificMenuItem).toHaveAttribute('aria-disabled', 'true')
+ expect(specificMenuItems[0]).toBeInTheDocument()
+ expect(specificMenuItems[1]).toBeInTheDocument()
+ expect(specificMenuItems[0]).toHaveAttribute('aria-disabled', 'true')
+ expect(specificMenuItems[1]).toHaveAttribute('aria-disabled', 'true')
})
})
@@ -245,7 +257,7 @@ describe('EnhancedActionMenu', () => {
const exportResult = getPromise('resolved')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
await waitFor(() => {
expect(startExport).toHaveBeenCalled()
@@ -256,7 +268,7 @@ describe('EnhancedActionMenu', () => {
const exportResult = getPromise('resolved')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
await waitFor(() => {
expect(startExport.mock.calls[0][0]).toEqual('1234')
@@ -267,7 +279,7 @@ describe('EnhancedActionMenu', () => {
const exportResult = getPromise('resolved')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
await waitFor(() => {
expect(startExport.mock.calls[0][2]).toEqual(true)
@@ -278,25 +290,43 @@ describe('EnhancedActionMenu', () => {
const exportResult = getPromise('resolved')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
await waitFor(() => expect(window.location.href).toEqual(defaultResult.attachmentUrl))
})
- it('on success, re-enables the "New Export" menu item', async () => {
+ it('on success, re-enables the "Export Entire Gradebook" menu item', async () => {
const exportResult = getPromise('resolved')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Entire Gradebook')
})
await waitFor(() => {
clickOnDropdown('Export')
})
const specificMenuItem = document
- .querySelector('[data-menu-id="previous-export"]')
+ .querySelector('[data-menu-id="export-all"]')
.closest('[role="menuitem"]')
await waitFor(() => {
- expect(specificMenuItem).toHaveTextContent('New Export')
+ expect(specificMenuItem).toHaveTextContent('Export Entire Gradebook')
+ expect(specificMenuItem).not.toHaveAttribute('aria-disabled', 'true')
+ })
+ })
+
+ it('on success, re-enables the "Export Current Gradebook View" menu item', async () => {
+ const exportResult = getPromise('resolved')
+ startExport.mockReturnValue(exportResult)
+ act(() => {
+ selectDropdownOption('Export Current Gradebook View')
+ })
+ await waitFor(() => {
+ clickOnDropdown('Export')
+ })
+ const specificMenuItem = document
+ .querySelector('[data-menu-id="export"]')
+ .closest('[role="menuitem"]')
+ await waitFor(() => {
+ expect(specificMenuItem).toHaveTextContent('Export Current Gradebook View')
expect(specificMenuItem).not.toHaveAttribute('aria-disabled', 'true')
})
})
@@ -306,7 +336,7 @@ describe('EnhancedActionMenu', () => {
const exportResult = getPromise('rejected')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
await waitFor(() => {
expect(spy).toHaveBeenCalled()
@@ -316,21 +346,26 @@ describe('EnhancedActionMenu', () => {
})
})
- it('on failure, renables the "New Export" menu item', async () => {
+ it('on failure, renables the "Export Current Gradebook View" and "Export Entire Gradebook" menu items', async () => {
const exportResult = getPromise('rejected')
startExport.mockReturnValue(exportResult)
act(() => {
- selectDropdownOption('New Export')
+ selectDropdownOption('Export Current Gradebook View')
})
await waitFor(() => {
clickOnDropdown('Export')
})
- const specificMenuItem = document
+ const exportMenuItem = document
.querySelector('[data-menu-id="export"]')
.closest('[role="menuitem"]')
+ const exportAllMenuItem = document
+ .querySelector('[data-menu-id="export-all"]')
+ .closest('[role="menuitem"]')
await waitFor(() => {
- expect(specificMenuItem).toHaveTextContent('New Export')
- expect(specificMenuItem).not.toHaveAttribute('aria-disabled', 'true')
+ expect(exportMenuItem).toHaveTextContent('Export Current Gradebook View')
+ expect(exportMenuItem).not.toHaveAttribute('aria-disabled', 'true')
+ expect(exportAllMenuItem).toHaveTextContent('Export Entire Gradebook')
+ expect(exportAllMenuItem).not.toHaveAttribute('aria-disabled', 'true')
})
})
})
diff --git a/ui/features/gradebook/react/shared/GradebookExportManager.js b/ui/features/gradebook/react/shared/GradebookExportManager.js
index 0257665d500..f3a0f5dc5b5 100644
--- a/ui/features/gradebook/react/shared/GradebookExportManager.js
+++ b/ui/features/gradebook/react/shared/GradebookExportManager.js
@@ -119,7 +119,13 @@ class GradebookExportManager {
}, this.pollingInterval)
}
- startExport(gradingPeriodId, getAssignmentOrder, showStudentFirstLastName = false) {
+ startExport(
+ gradingPeriodId,
+ getAssignmentOrder,
+ showStudentFirstLastName = false,
+ getStudentOrder,
+ currentView = false
+ ) {
if (!this.exportingUrl) {
return Promise.reject(I18n.t('No way to export gradebooks provided!'))
}
@@ -130,7 +136,9 @@ class GradebookExportManager {
}
const params = {
- grading_period_id: gradingPeriodId
+ grading_period_id: gradingPeriodId,
+ show_student_first_last_name: showStudentFirstLastName,
+ current_view: currentView
}
const assignmentOrder = getAssignmentOrder()
@@ -138,7 +146,10 @@ class GradebookExportManager {
params.assignment_order = assignmentOrder
}
- params.show_student_first_last_name = showStudentFirstLastName
+ const studentOrder = getStudentOrder()
+ if (studentOrder && studentOrder.length > 0) {
+ params.student_order = studentOrder.map(Number)
+ }
return axios.post(this.exportingUrl, params).then(response => {
this.export = {