Add student group filter to new gradebook
Add a filter for student groups to new gradebook. It can be enabled from the View menu (as with the other filters) and behaves generally as they do, with the small difference of showing groups/group categories hierarchically. Also add the option to show a student's groups as their "secondary info" below their name. closes GRADE-2122 Test plan: - Have a course (using New Gradebook) and some students - Create some student groups for the course, and assign students to them - Open New Gradebook - The "View" menu should have a "Student Groups" filter in addition to the existing filters - Enabling it should make a filter appear that lets you choose between the student groups for the course (or show all groups) - Selecting a particular group should show only students belonging to that group - Selecting "All Student Groups" should show all students - Your selection should persist between page views - The ellipsis menu in the Student Name column should have a new "Group" option under "Secondary Info" - Selecting it should set the secondary text below a student's name to the groups that student is part of Change-Id: Id6b1c62699f1778e60545ba85f945ebecfc3e98a Reviewed-on: https://gerrit.instructure.com/194096 Reviewed-by: Keith Garner <kgarner@instructure.com> Reviewed-by: Jeremy Neander <jneander@instructure.com> Tested-by: Jenkins QA-Review: Derek Bender <djbender@instructure.com> Product-Review: Matthew Goodwin <mattg@instructure.com>
This commit is contained in:
parent
45fb184f9d
commit
c858092ceb
|
@ -77,6 +77,7 @@ import OutlierScoreHelper from 'jsx/grading/helpers/OutlierScoreHelper'
|
|||
import LatePolicyApplicator from 'jsx/grading/LatePolicyApplicator'
|
||||
import Button from '@instructure/ui-buttons/lib/components/Button'
|
||||
import IconSettingsSolid from '@instructure/ui-icons/lib/Solid/IconSettings'
|
||||
import StudentGroupFilter from 'jsx/gradezilla/default_gradebook/components/StudentGroupFilter'
|
||||
import * as FlashAlert from 'jsx/shared/FlashAlert'
|
||||
import 'jquery.ajaxJSON'
|
||||
import 'jquery.instructure_date_and_time'
|
||||
|
@ -162,6 +163,7 @@ export default do ->
|
|||
|
||||
filterRowsBy =
|
||||
sectionId: null
|
||||
studentGroupId: null
|
||||
|
||||
if settings.filter_rows_by?
|
||||
Object.assign(filterRowsBy, ConvertCase.camelize(settings.filter_rows_by))
|
||||
|
@ -239,6 +241,13 @@ export default do ->
|
|||
anonymousSpeedGraderAlertMountPoint = () ->
|
||||
document.querySelector("[data-component='AnonymousSpeedGraderAlert']")
|
||||
|
||||
formatStudentGroupsForFilter = (groupCategoryList) ->
|
||||
groupCategoryList.map((category) => {
|
||||
children: category.groups.sort((a, b) => a.id - b.id),
|
||||
id: category.id,
|
||||
name: category.name
|
||||
})
|
||||
|
||||
class Gradebook
|
||||
columnWidths =
|
||||
assignment:
|
||||
|
@ -364,6 +373,8 @@ export default do ->
|
|||
else
|
||||
@gridDisplaySettings.selectedSecondaryInfo = 'none'
|
||||
|
||||
@setStudentGroups(@options.student_groups)
|
||||
|
||||
bindGridEvents: =>
|
||||
@gradebookGrid.events.onColumnsReordered.subscribe (_event, columns) =>
|
||||
# determine if assignment columns or custom columns were reordered
|
||||
|
@ -1148,6 +1159,42 @@ export default do ->
|
|||
showSections: ->
|
||||
@sections_enabled
|
||||
|
||||
showStudentGroups: ->
|
||||
@studentGroupsEnabled
|
||||
|
||||
updateStudentGroupFilterVisibility: ->
|
||||
mountPoint = document.getElementById('student-group-filter-container')
|
||||
|
||||
if @showStudentGroups() and 'studentGroups' in @gridDisplaySettings.selectedViewOptionsFilters
|
||||
groupCategoryList = Object.values(@studentGroupCategories).sort((a, b) => (a.id - b.id))
|
||||
|
||||
props =
|
||||
items: formatStudentGroupsForFilter(groupCategoryList)
|
||||
onSelect: @updateCurrentStudentGroup
|
||||
selectedItemId: @getStudentGroupToShow()
|
||||
disabled: !@contentLoadStates.studentsLoaded
|
||||
|
||||
@studentGroupFilterMenu = renderComponent(StudentGroupFilter, mountPoint, props)
|
||||
else
|
||||
@updateCurrentStudentGroup(null)
|
||||
if @studentGroupFilterMenu
|
||||
ReactDOM.unmountComponentAtNode(mountPoint)
|
||||
@studentGroupFilterMenu = null
|
||||
|
||||
getStudentGroupToShow: () =>
|
||||
groupId = @getFilterRowsBySetting('studentGroupId') || '0'
|
||||
|
||||
if groupId in Object.keys(@studentGroups) then groupId else '0'
|
||||
|
||||
updateCurrentStudentGroup: (groupId) =>
|
||||
groupId = if groupId == '0' then null else groupId
|
||||
if @getFilterRowsBySetting('studentGroupId') != groupId
|
||||
@setFilterRowsBySetting('studentGroupId', groupId)
|
||||
@saveSettings({}, =>
|
||||
@updateStudentGroupFilterVisibility()
|
||||
@reloadStudentData()
|
||||
)
|
||||
|
||||
assignmentGroupList: ->
|
||||
return [] unless @assignmentGroups
|
||||
Object.values(@assignmentGroups).sort((a, b) => (a.position - b.position))
|
||||
|
@ -1415,6 +1462,7 @@ export default do ->
|
|||
# available, whereas assignment groups and context modules are fetched via the DataLoader,
|
||||
# so we need to wait until they are loaded to set their filter visibility.
|
||||
@updateSectionFilterVisibility()
|
||||
@updateStudentGroupFilterVisibility()
|
||||
@updateAssignmentGroupFilterVisibility() if @contentLoadStates.assignmentGroupsLoaded
|
||||
@updateGradingPeriodFilterVisibility()
|
||||
@updateModulesFilterVisibility() if @contentLoadStates.contextModulesLoaded
|
||||
|
@ -2612,6 +2660,7 @@ export default do ->
|
|||
filters.push('gradingPeriods') if @gradingPeriodSet?
|
||||
filters.push('modules') if @listContextModules().length > 0
|
||||
filters.push('sections') if @sections_enabled
|
||||
filters.push('studentGroups') if @studentGroupsEnabled
|
||||
filters
|
||||
|
||||
setSelectedViewOptionsFilters: (filters) =>
|
||||
|
@ -2690,6 +2739,13 @@ export default do ->
|
|||
@sections = _.indexBy(sections, 'id')
|
||||
@sections_enabled = sections.length > 1
|
||||
|
||||
setStudentGroups: (groupCategories) =>
|
||||
@studentGroupCategories = _.indexBy(groupCategories.map(htmlEscape), 'id')
|
||||
|
||||
studentGroupList = _.flatten(_.pluck(groupCategories, 'groups')).map(htmlEscape)
|
||||
@studentGroups = _.indexBy(studentGroupList, 'id')
|
||||
@studentGroupsEnabled = studentGroupList.length > 0
|
||||
|
||||
setAssignments: (assignmentMap) =>
|
||||
@assignments = assignmentMap
|
||||
|
||||
|
|
|
@ -44,7 +44,8 @@ class GradebookSettingsController < ApplicationController
|
|||
:assignment_group_id
|
||||
],
|
||||
filter_rows_by: [
|
||||
:section_id
|
||||
:section_id,
|
||||
:student_group_id
|
||||
],
|
||||
selected_view_options_filters: []
|
||||
},
|
||||
|
|
|
@ -21,6 +21,7 @@ class GradebooksController < ApplicationController
|
|||
include GradebooksHelper
|
||||
include KalturaHelper
|
||||
include Api::V1::AssignmentGroup
|
||||
include Api::V1::GroupCategory
|
||||
include Api::V1::Submission
|
||||
include Api::V1::CustomGradebookColumn
|
||||
include Api::V1::Section
|
||||
|
@ -411,6 +412,7 @@ class GradebooksController < ApplicationController
|
|||
sections: sections_json(@context.active_course_sections, @current_user, session, [], allow_sis_ids: true),
|
||||
settings_update_url: api_v1_course_gradebook_settings_update_url(@context),
|
||||
settings: gradebook_settings.fetch(@context.id, {}),
|
||||
student_groups: group_categories_json(@context.group_categories, @current_user, session, {include: ['groups']}),
|
||||
login_handle_name: @context.root_account.settings[:login_handle_name],
|
||||
sis_name: @context.root_account.settings[:sis_name],
|
||||
version: params.fetch(:version, nil),
|
||||
|
@ -680,6 +682,7 @@ class GradebooksController < ApplicationController
|
|||
if new_gradebook_enabled?
|
||||
env[:selected_section_id] = gradebook_settings.dig(@context.id, 'filter_rows_by', 'section_id')
|
||||
env[:post_policies_enabled] = true if @context.feature_enabled?(:post_policies)
|
||||
env[:selected_student_group_id] = gradebook_settings.dig(@context.id, 'filter_rows_by', 'student_group_id')
|
||||
end
|
||||
|
||||
if @assignment.quiz
|
||||
|
@ -809,6 +812,13 @@ class GradebooksController < ApplicationController
|
|||
end
|
||||
helper_method :multiple_assignment_groups?
|
||||
|
||||
def student_groups?
|
||||
return @student_groups if defined?(@student_groups)
|
||||
|
||||
@student_groups = @context.groups.any?
|
||||
end
|
||||
helper_method :student_groups?
|
||||
|
||||
private
|
||||
|
||||
def outcome_proficiency
|
||||
|
|
|
@ -25,6 +25,12 @@ function getSecondaryDisplayInfo(student, secondaryInfo, options) {
|
|||
const sectionNames = student.sections.map(sectionId => options.getSection(sectionId).name)
|
||||
return $.toSentence(sectionNames.sort())
|
||||
}
|
||||
|
||||
if (options.shouldShowGroups() && secondaryInfo === 'group') {
|
||||
const groupNames = student.group_ids.map(groupId => options.getGroup(groupId).name)
|
||||
return $.toSentence(groupNames.sort())
|
||||
}
|
||||
|
||||
return {
|
||||
login_id: student.login_id,
|
||||
sis_id: student.sis_user_id,
|
||||
|
@ -82,6 +88,9 @@ export default class StudentCellFormatter {
|
|||
getSection(sectionId) {
|
||||
return gradebook.sections[sectionId]
|
||||
},
|
||||
getGroup(groupId) {
|
||||
return gradebook.studentGroups[groupId]
|
||||
},
|
||||
getSelectedPrimaryInfo() {
|
||||
return gradebook.getSelectedPrimaryInfo()
|
||||
},
|
||||
|
@ -90,6 +99,9 @@ export default class StudentCellFormatter {
|
|||
},
|
||||
shouldShowSections() {
|
||||
return gradebook.showSections()
|
||||
},
|
||||
shouldShowGroups() {
|
||||
return gradebook.showStudentGroups()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ export default class StudentColumnHeader extends ColumnHeader {
|
|||
sisName: string,
|
||||
selectedSecondaryInfo: oneOf(studentRowHeaderConstants.secondaryInfoKeys).isRequired,
|
||||
sectionsEnabled: bool.isRequired,
|
||||
studentGroupsEnabled: bool.isRequired,
|
||||
onSelectSecondaryInfo: func.isRequired,
|
||||
sortBySetting: shape({
|
||||
direction: string.isRequired,
|
||||
|
@ -84,6 +85,10 @@ export default class StudentColumnHeader extends ColumnHeader {
|
|||
this.onSelectSecondaryInfo('login_id')
|
||||
}
|
||||
|
||||
onShowGroup = () => {
|
||||
this.onSelectSecondaryInfo('group')
|
||||
}
|
||||
|
||||
onShowFirstLastNames = () => {
|
||||
this.onSelectPrimaryInfo('first_last')
|
||||
}
|
||||
|
@ -271,6 +276,16 @@ export default class StudentColumnHeader extends ColumnHeader {
|
|||
studentRowHeaderConstants.secondaryInfoLabels.login_id}
|
||||
</MenuItem>
|
||||
|
||||
{this.props.studentGroupsEnabled && (
|
||||
<MenuItem
|
||||
key="group"
|
||||
selected={this.props.selectedSecondaryInfo === 'group'}
|
||||
onSelect={this.onShowGroup}
|
||||
>
|
||||
{studentRowHeaderConstants.secondaryInfoLabels.group}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
key="none"
|
||||
selected={this.props.selectedSecondaryInfo === 'none'}
|
||||
|
|
|
@ -55,7 +55,8 @@ function getProps(gradebook, options) {
|
|||
gradebook.setSortRowsBySetting(columnId, 'sortable_name', 'descending')
|
||||
},
|
||||
settingKey: sortRowsBySetting.settingKey
|
||||
}
|
||||
},
|
||||
studentGroupsEnabled: gradebook.showStudentGroups()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ export function createGradebook(options = {}) {
|
|||
},
|
||||
settings_update_url: '/path/to/settingsUpdateUrl',
|
||||
speed_grader_enabled: true,
|
||||
student_groups: [],
|
||||
...options
|
||||
})
|
||||
|
||||
|
@ -93,6 +94,7 @@ export function setFixtureHtml($fixture) {
|
|||
<div id="grading-periods-filter-container"></div>
|
||||
<div id="modules-filter-container"></div>
|
||||
<div id="sections-filter-container"></div>
|
||||
<div id="student-group-filter-container"></div>
|
||||
<div id="search-filter-container">
|
||||
<input type="text" />
|
||||
</div>
|
||||
|
|
|
@ -22,10 +22,33 @@ import Select from '@instructure/ui-core/lib/components/Select'
|
|||
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
|
||||
import I18n from 'i18n!gradezilla'
|
||||
|
||||
function renderItem(item) {
|
||||
return (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
)
|
||||
}
|
||||
|
||||
function renderItemAndChildren(item) {
|
||||
return (
|
||||
<optgroup label={item.name} key={`group_${item.id}`}>
|
||||
{item.children.map(child => renderItem(child))}
|
||||
</optgroup>
|
||||
)
|
||||
}
|
||||
|
||||
class GradebookFilter extends React.Component {
|
||||
static propTypes = {
|
||||
items: arrayOf(
|
||||
shape({
|
||||
/* groups can only ever be a single level deep */
|
||||
children: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired
|
||||
})
|
||||
),
|
||||
id: string.isRequired,
|
||||
name: string.isRequired
|
||||
})
|
||||
|
@ -59,11 +82,10 @@ class GradebookFilter extends React.Component {
|
|||
<option key="0" value="0">
|
||||
{this.props.allItemsLabel}
|
||||
</option>
|
||||
{this.props.items.map(item => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
|
||||
{this.props.items.map(item =>
|
||||
item.children ? renderItemAndChildren(item) : renderItem(item)
|
||||
)}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import I18n from 'i18n!gradebook'
|
||||
import GradebookFilter from './GradebookFilter'
|
||||
|
||||
export default class StudentGroupFilter extends GradebookFilter {
|
||||
static defaultProps = {
|
||||
allItemsLabel: I18n.t('All Student Groups'),
|
||||
disabled: false,
|
||||
filterLabel: I18n.t('Student Group Filter')
|
||||
}
|
||||
}
|
|
@ -22,7 +22,8 @@ export const filterLabels = {
|
|||
assignmentGroups: I18n.t('Assignment Groups'),
|
||||
gradingPeriods: I18n.t('Grading Periods'),
|
||||
modules: I18n.t('Modules'),
|
||||
sections: I18n.t('Sections')
|
||||
sections: I18n.t('Sections'),
|
||||
studentGroups: I18n.t('Student Groups')
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -27,6 +27,7 @@ const primaryInfoKeys = ['first_last', 'last_first']
|
|||
const defaultPrimaryInfo = 'first_last'
|
||||
|
||||
const secondaryInfoLabels = {
|
||||
group: I18n.t('Group'),
|
||||
section: I18n.t('Section'),
|
||||
sis_id: I18n.t('SIS ID'),
|
||||
integration_id: I18n.t('Integration ID'),
|
||||
|
@ -34,7 +35,7 @@ const secondaryInfoLabels = {
|
|||
none: I18n.t('None')
|
||||
}
|
||||
|
||||
const secondaryInfoKeys = ['section', 'sis_id', 'integration_id', 'login_id', 'none']
|
||||
const secondaryInfoKeys = ['section', 'sis_id', 'integration_id', 'login_id', 'group', 'none']
|
||||
const defaultSecondaryInfo = 'none'
|
||||
const sectionSecondaryInfo = 'section'
|
||||
|
||||
|
|
|
@ -65,6 +65,10 @@
|
|||
<div class="gradebook-filter-container" id="sections-filter-container"></div>
|
||||
<% end %>
|
||||
|
||||
<% if student_groups? %>
|
||||
<div class="gradebook-filter-container" id="student-group-filter-container"></div>
|
||||
<% end %>
|
||||
|
||||
<div class="gradebook-filter-container gradebook_filter" id="search-filter-container">
|
||||
<% placeholder = t('Search…') %>
|
||||
<input
|
||||
|
|
|
@ -20,6 +20,7 @@ module Api::V1::GroupCategory
|
|||
include Api::V1::Json
|
||||
include Api::V1::Context
|
||||
include Api::V1::Progress
|
||||
include Api::V1::Group
|
||||
|
||||
API_GROUP_CATEGORY_JSON_OPTS = {
|
||||
:only => %w(id name role self_signup group_limit auto_leader)
|
||||
|
@ -66,7 +67,14 @@ module Api::V1::GroupCategory
|
|||
if includes.include?('unassigned_users_count')
|
||||
hash['unassigned_users_count'] = group_category.unassigned_users.count(:all)
|
||||
end
|
||||
if includes.include?('groups')
|
||||
hash['groups'] = group_category.groups.map { |group| group_json(group, user, session) }
|
||||
end
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
def group_categories_json(group_categories, user, session, options = {})
|
||||
group_categories.map { |group_category| group_category_json(group_category, user, session, options) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,7 @@ class GradebookUserIds
|
|||
@sort_by = settings[:sort_rows_by_setting_key] || "name"
|
||||
@selected_grading_period_id = settings.dig(:filter_columns_by, :grading_period_id)
|
||||
@selected_section_id = settings.dig(:filter_rows_by, :section_id)
|
||||
@selected_student_group_id = settings.dig(:filter_rows_by, :student_group_id)
|
||||
@direction = settings[:sort_rows_by_direction] || "ascending"
|
||||
end
|
||||
|
||||
|
@ -141,7 +142,14 @@ class GradebookUserIds
|
|||
end
|
||||
|
||||
def students
|
||||
User.left_joins(:enrollments).merge(student_enrollments_scope)
|
||||
students = User.left_joins(:enrollments).merge(student_enrollments_scope)
|
||||
|
||||
if student_group_id.present?
|
||||
students.joins(group_memberships: :group).
|
||||
where(group_memberships: {group: student_group_id, workflow_state: :accepted})
|
||||
else
|
||||
students
|
||||
end
|
||||
end
|
||||
|
||||
def sort_by_scores(type = :total_grade, id = nil)
|
||||
|
@ -194,7 +202,12 @@ class GradebookUserIds
|
|||
end
|
||||
|
||||
def section_id
|
||||
return nil if @selected_section_id.nil? || @selected_section_id == "null" || @section_section_id == "0"
|
||||
return nil if @selected_section_id.nil? || @selected_section_id == "null" || @selected_section_id == "0"
|
||||
@selected_section_id
|
||||
end
|
||||
|
||||
def student_group_id
|
||||
return nil if @selected_student_group_id.nil? || ["0", "null"].include?(@selected_student_group_id)
|
||||
@selected_student_group_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,7 +38,8 @@ RSpec.describe GradebookSettingsController, type: :controller do
|
|||
"assignment_group_id" => "888"
|
||||
},
|
||||
"filter_rows_by" => {
|
||||
"section_id" => "null"
|
||||
"section_id" => "null",
|
||||
"student_group_id" => "null"
|
||||
},
|
||||
"selected_view_options_filters" => ["assignmentGroups"],
|
||||
"show_inactive_enrollments" => "true", # values must be strings
|
||||
|
@ -60,7 +61,7 @@ RSpec.describe GradebookSettingsController, type: :controller do
|
|||
end
|
||||
|
||||
let(:gradebook_settings_massaged) do
|
||||
gradebook_settings.merge('filter_rows_by' => { 'section_id' => nil })
|
||||
gradebook_settings.merge('filter_rows_by' => { 'section_id' => nil, 'student_group_id' => nil })
|
||||
end
|
||||
|
||||
let(:valid_params) do
|
||||
|
@ -85,7 +86,7 @@ RSpec.describe GradebookSettingsController, type: :controller do
|
|||
it { expect(response).to be_ok }
|
||||
it { is_expected.to include 'enter_grades_as' => {'2301' => 'points'} }
|
||||
it { is_expected.to include 'filter_columns_by' => {'grading_period_id' => '1401', 'assignment_group_id' => '888'} }
|
||||
it { is_expected.to include 'filter_rows_by' => {'section_id' => nil} }
|
||||
it { is_expected.to include 'filter_rows_by' => {'section_id' => nil, 'student_group_id' => nil} }
|
||||
it { is_expected.to include 'selected_view_options_filters' => ['assignmentGroups'] }
|
||||
it { is_expected.to include 'show_inactive_enrollments' => 'true' }
|
||||
it { is_expected.to include 'show_concluded_enrollments' => 'false' }
|
||||
|
|
|
@ -981,6 +981,30 @@ describe GradebooksController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "student_groups" do
|
||||
let(:category) { @course.group_categories.create!(name: "category") }
|
||||
let(:category2) { @course.group_categories.create!(name: "another category") }
|
||||
|
||||
let(:group_categories_json) { assigns[:js_env][:GRADEBOOK_OPTIONS][:student_groups] }
|
||||
|
||||
before(:each) do
|
||||
category.create_groups(2)
|
||||
category2.create_groups(2)
|
||||
end
|
||||
|
||||
it "includes the student group categories for the course" do
|
||||
get :show, params: {course_id: @course.id}
|
||||
expect(group_categories_json.pluck("id")).to contain_exactly(category.id, category2.id)
|
||||
end
|
||||
|
||||
it "includes the groups within each category" do
|
||||
get :show, params: {course_id: @course.id}
|
||||
|
||||
category2_json = group_categories_json.find { |category_json| category_json["id"] == category2.id }
|
||||
expect(category2_json["groups"].pluck("id")).to match_array(category2.groups.pluck(:id))
|
||||
end
|
||||
end
|
||||
|
||||
context "publish_to_sis_enabled" do
|
||||
before(:once) do
|
||||
@course.sis_source_id = 'xyz'
|
||||
|
|
|
@ -2189,6 +2189,7 @@ QUnit.module('Gradebook#getFilterSettingsViewOptionsMenuProps', {
|
|||
this.gradebook.gradingPeriodSet = {id: '1501'}
|
||||
this.gradebook.setContextModules([{id: '2601'}, {id: '2602'}])
|
||||
this.gradebook.sections_enabled = true
|
||||
this.gradebook.studentGroupsEnabled = true
|
||||
sandbox.stub(this.gradebook, 'renderViewOptionsMenu')
|
||||
sandbox.stub(this.gradebook, 'renderFilters')
|
||||
sandbox.stub(this.gradebook, 'saveSettings')
|
||||
|
@ -2197,37 +2198,49 @@ QUnit.module('Gradebook#getFilterSettingsViewOptionsMenuProps', {
|
|||
|
||||
test('includes available filters', function() {
|
||||
const props = this.gradebook.getFilterSettingsViewOptionsMenuProps()
|
||||
deepEqual(props.available, ['assignmentGroups', 'gradingPeriods', 'modules', 'sections'])
|
||||
deepEqual(props.available, [
|
||||
'assignmentGroups',
|
||||
'gradingPeriods',
|
||||
'modules',
|
||||
'sections',
|
||||
'studentGroups'
|
||||
])
|
||||
})
|
||||
|
||||
test('available filters exclude assignment groups when only one exists', function() {
|
||||
this.gradebook.setAssignmentGroups({301: {name: 'Assignments'}})
|
||||
const props = this.gradebook.getFilterSettingsViewOptionsMenuProps()
|
||||
deepEqual(props.available, ['gradingPeriods', 'modules', 'sections'])
|
||||
deepEqual(props.available, ['gradingPeriods', 'modules', 'sections', 'studentGroups'])
|
||||
})
|
||||
|
||||
test('available filters exclude assignment groups when not loaded', function() {
|
||||
this.gradebook.setAssignmentGroups(undefined)
|
||||
const props = this.gradebook.getFilterSettingsViewOptionsMenuProps()
|
||||
deepEqual(props.available, ['gradingPeriods', 'modules', 'sections'])
|
||||
deepEqual(props.available, ['gradingPeriods', 'modules', 'sections', 'studentGroups'])
|
||||
})
|
||||
|
||||
test('available filters exclude grading periods when no grading period set exists', function() {
|
||||
this.gradebook.gradingPeriodSet = null
|
||||
const props = this.gradebook.getFilterSettingsViewOptionsMenuProps()
|
||||
deepEqual(props.available, ['assignmentGroups', 'modules', 'sections'])
|
||||
deepEqual(props.available, ['assignmentGroups', 'modules', 'sections', 'studentGroups'])
|
||||
})
|
||||
|
||||
test('available filters exclude modules when none exist', function() {
|
||||
this.gradebook.setContextModules([])
|
||||
const props = this.gradebook.getFilterSettingsViewOptionsMenuProps()
|
||||
deepEqual(props.available, ['assignmentGroups', 'gradingPeriods', 'sections'])
|
||||
deepEqual(props.available, ['assignmentGroups', 'gradingPeriods', 'sections', 'studentGroups'])
|
||||
})
|
||||
|
||||
test('available filters exclude sections when only one exists', function() {
|
||||
this.gradebook.sections_enabled = false
|
||||
const props = this.gradebook.getFilterSettingsViewOptionsMenuProps()
|
||||
deepEqual(props.available, ['assignmentGroups', 'gradingPeriods', 'modules'])
|
||||
deepEqual(props.available, ['assignmentGroups', 'gradingPeriods', 'modules', 'studentGroups'])
|
||||
})
|
||||
|
||||
test('available filters exclude student groups when none exist', function() {
|
||||
this.gradebook.studentGroupsEnabled = false
|
||||
const props = this.gradebook.getFilterSettingsViewOptionsMenuProps()
|
||||
deepEqual(props.available, ['assignmentGroups', 'gradingPeriods', 'modules', 'sections'])
|
||||
})
|
||||
|
||||
test('includes selected filters', function() {
|
||||
|
@ -2944,6 +2957,134 @@ test('does not render when filter is not selected', function() {
|
|||
strictEqual(this.container.children.length, 0, 'rendered elements have been removed')
|
||||
})
|
||||
|
||||
QUnit.module('Gradebook#updateStudentGroupFilterVisibility', hooks => {
|
||||
let gradebook
|
||||
let container
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
const studentGroupFilterContainerSelector = 'student-group-filter-container'
|
||||
$fixtures.innerHTML = `<div id="${studentGroupFilterContainerSelector}"></div>`
|
||||
container = $fixtures.querySelector(`#${studentGroupFilterContainerSelector}`)
|
||||
|
||||
const studentGroups = [
|
||||
{
|
||||
groups: [{id: '1', name: 'First Group Set 1'}, {id: '2', name: 'First Group Set 2'}],
|
||||
id: '1',
|
||||
name: 'First Group Set'
|
||||
},
|
||||
{
|
||||
groups: [{id: '3', name: 'Second Group Set 1'}, {id: '4', name: 'Second Group Set 2'}],
|
||||
id: '2',
|
||||
name: 'Second Group Set'
|
||||
}
|
||||
]
|
||||
|
||||
gradebook = createGradebook({student_groups: studentGroups})
|
||||
gradebook.studentGroupsEnabled = true
|
||||
gradebook.setSelectedViewOptionsFilters(['studentGroups'])
|
||||
})
|
||||
|
||||
hooks.afterEach(() => {
|
||||
$fixtures.innerHTML = ''
|
||||
})
|
||||
|
||||
test('renders the section select when not already rendered', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
ok(container.children.length > 0, 'student group menu was rendered')
|
||||
})
|
||||
|
||||
test('stores a reference to the section select when it is rendered', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
ok(gradebook.studentGroupFilterMenu, 'student group menu reference has been stored')
|
||||
})
|
||||
|
||||
test('does not render when there are no student groups', () => {
|
||||
gradebook.studentGroupsEnabled = false
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
notOk(gradebook.studentGroupFilterMenu, 'student group menu reference has not been stored')
|
||||
strictEqual(container.children.length, 0, 'nothing was rendered')
|
||||
})
|
||||
|
||||
test('does not render when filter is not selected', () => {
|
||||
gradebook.setSelectedViewOptionsFilters(['assignmentGroups'])
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
notOk(gradebook.studentGroupFilterMenu, 'student group menu reference has been removed')
|
||||
strictEqual(container.children.length, 0, 'rendered elements have been removed')
|
||||
})
|
||||
|
||||
test('renders the group select with group categories at the top level', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
const studentGroupCategories = gradebook.studentGroupFilterMenu.props.items
|
||||
deepEqual(studentGroupCategories.map(group => group.id), ['1', '2'])
|
||||
})
|
||||
|
||||
test('renders the group select with all groups', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
const studentGroupCategories = gradebook.studentGroupFilterMenu.props.items
|
||||
const studentGroups = _.flatten(studentGroupCategories.map(category => category.children))
|
||||
deepEqual(studentGroups.map(group => group.id), ['1', '2', '3', '4'])
|
||||
})
|
||||
|
||||
test('unescapes student group category names', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
const studentGroupCategories = gradebook.studentGroupFilterMenu.props.items
|
||||
deepEqual(studentGroupCategories.map(section => section.name), [
|
||||
'First Group Set',
|
||||
'Second Group Set'
|
||||
])
|
||||
})
|
||||
|
||||
test('unescapes student group names', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
const studentGroupCategories = gradebook.studentGroupFilterMenu.props.items
|
||||
const studentGroups = _.flatten(studentGroupCategories.map(category => category.children))
|
||||
deepEqual(studentGroups.map(section => section.name), [
|
||||
'First Group Set 1',
|
||||
'First Group Set 2',
|
||||
'Second Group Set 1',
|
||||
'Second Group Set 2'
|
||||
])
|
||||
})
|
||||
|
||||
test('sets the student group select to show the saved "filter rows by" setting', () => {
|
||||
gradebook.setFilterRowsBySetting('studentGroupId', '4')
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
strictEqual(gradebook.studentGroupFilterMenu.props.selectedItemId, '4')
|
||||
})
|
||||
|
||||
test('sets the student group select as disabled when students are not loaded', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
strictEqual(gradebook.studentGroupFilterMenu.props.disabled, true)
|
||||
})
|
||||
|
||||
test('sets the section select as not disabled when students are loaded', () => {
|
||||
gradebook.setStudentsLoaded(true)
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
strictEqual(gradebook.studentGroupFilterMenu.props.disabled, false)
|
||||
})
|
||||
|
||||
test('updates the disabled state of the rendered section select', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
gradebook.setStudentsLoaded(true)
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
strictEqual(gradebook.studentGroupFilterMenu.props.disabled, false)
|
||||
})
|
||||
|
||||
test('renders only one section select when updated', () => {
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
ok(gradebook.studentGroupFilterMenu, 'student group menu reference has been stored')
|
||||
strictEqual(container.children.length, 1, 'only one section select is rendered')
|
||||
})
|
||||
|
||||
test('removes the section select when filter is deselected', () => {
|
||||
gradebook.setSelectedViewOptionsFilters(['assignmentGroups'])
|
||||
gradebook.updateStudentGroupFilterVisibility()
|
||||
notOk(gradebook.studentGroupFilterMenu, 'student group menu reference has been stored')
|
||||
strictEqual(container.children.length, 0, 'nothing was rendered')
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('Menus', {
|
||||
setup() {
|
||||
this.gradebook = createGradebook({
|
||||
|
|
|
@ -43,8 +43,22 @@ QUnit.module('GradebookGrid StudentCellFormatter', hooks => {
|
|||
{id: '2004', name: 'Seniors'}
|
||||
])
|
||||
|
||||
gradebook.setStudentGroups([
|
||||
{
|
||||
groups: [{id: '1', name: 'First Category 1'}, {id: '2', name: 'First Category 2'}],
|
||||
id: '1',
|
||||
name: 'First Category'
|
||||
},
|
||||
{
|
||||
groups: [{id: '3', name: 'Second Category 1'}, {id: '4', name: 'Second Category 2'}],
|
||||
id: '2',
|
||||
name: 'Second Category'
|
||||
}
|
||||
])
|
||||
|
||||
student = {
|
||||
enrollments: [{grades: {html_url: 'http://example.com/grades/1101'}}],
|
||||
group_ids: ['1', '4'],
|
||||
id: '1101',
|
||||
isConcluded: false,
|
||||
isInactive: false,
|
||||
|
@ -154,6 +168,41 @@ QUnit.module('GradebookGrid StudentCellFormatter', hooks => {
|
|||
equal(renderCell().querySelector('.secondary-info').innerText, student.integration_id)
|
||||
})
|
||||
|
||||
test('renders student group names when secondary info is "group"', () => {
|
||||
gradebook.setSelectedSecondaryInfo('group')
|
||||
equal(
|
||||
renderCell().querySelector('.secondary-info').innerText,
|
||||
'First Category 1 and Second Category 2'
|
||||
)
|
||||
})
|
||||
|
||||
test('does not escape html in the student group names', () => {
|
||||
gradebook.setStudentGroups([
|
||||
{
|
||||
groups: [{id: '1', name: '<span>First Category 1</span>'}],
|
||||
id: '1',
|
||||
name: 'First Category'
|
||||
},
|
||||
{
|
||||
groups: [{id: '4', name: 'Second Category 2'}],
|
||||
id: '1',
|
||||
name: 'Second Category'
|
||||
}
|
||||
])
|
||||
|
||||
gradebook.setSelectedSecondaryInfo('group')
|
||||
equal(
|
||||
renderCell().querySelector('.secondary-info').innerText,
|
||||
'<span>First Category 1</span> and Second Category 2'
|
||||
)
|
||||
})
|
||||
|
||||
test('does not render student group names when groups should not be visible', () => {
|
||||
gradebook.setStudentGroups([])
|
||||
gradebook.setSelectedSecondaryInfo('group')
|
||||
strictEqual(renderCell().querySelector('.secondary-info'), null)
|
||||
})
|
||||
|
||||
test('does not render secondary info when any secondary info is null and secondary info is not "none"', () => {
|
||||
student.login_id = null
|
||||
gradebook.setSelectedSecondaryInfo('login_id', true) // skipRedraw
|
||||
|
|
|
@ -179,6 +179,23 @@ QUnit.module('GradebookGrid StudentColumnHeaderRenderer', suiteHooks => {
|
|||
strictEqual(component.props.sectionsEnabled, false)
|
||||
})
|
||||
|
||||
test('sets studentGroupsEnabled to true when student groups are present', () => {
|
||||
gradebook.setStudentGroups([
|
||||
{
|
||||
groups: [{id: '1', name: 'Default Group 1'}, {id: '2', name: 'Default Group 2'}],
|
||||
id: '1',
|
||||
name: 'Default Group'
|
||||
}
|
||||
])
|
||||
render()
|
||||
strictEqual(component.props.studentGroupsEnabled, true)
|
||||
})
|
||||
|
||||
test('sets studentGroupsEnabled to false when student groups are not present', () => {
|
||||
render()
|
||||
strictEqual(component.props.studentGroupsEnabled, false)
|
||||
})
|
||||
|
||||
test('includes the selected enrollment filters', () => {
|
||||
gradebook.toggleEnrollmentFilter('concluded')
|
||||
render()
|
||||
|
|
|
@ -63,7 +63,8 @@ QUnit.module('GradebookGrid StudentColumnHeader', suiteHooks => {
|
|||
onSortBySortableNameAscending() {},
|
||||
onSortBySortableNameDescending() {},
|
||||
settingKey: 'sortable_name'
|
||||
}
|
||||
},
|
||||
studentGroupsEnabled: true
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -476,6 +477,68 @@ QUnit.module('GradebookGrid StudentColumnHeader', suiteHooks => {
|
|||
})
|
||||
})
|
||||
|
||||
QUnit.module('"Group" option', () => {
|
||||
test('is present when the course has student groups', () => {
|
||||
mountAndOpenOptionsMenu()
|
||||
ok(getSecondaryInfoOption('Group'))
|
||||
})
|
||||
|
||||
test('is not present when the course has no student groups', () => {
|
||||
props.studentGroupsEnabled = false
|
||||
mountAndOpenOptionsMenu()
|
||||
notOk(getSecondaryInfoOption('Group'))
|
||||
})
|
||||
|
||||
test('is selected when displaying student groups for secondary info', () => {
|
||||
props.selectedSecondaryInfo = 'group'
|
||||
mountAndOpenOptionsMenu()
|
||||
strictEqual(getSecondaryInfoOption('Group').getAttribute('aria-checked'), 'true')
|
||||
})
|
||||
|
||||
test('is not selected when displaying different secondary info', () => {
|
||||
props.selectedSecondaryInfo = 'sis_id'
|
||||
mountAndOpenOptionsMenu()
|
||||
strictEqual(getSecondaryInfoOption('Group').getAttribute('aria-checked'), 'false')
|
||||
})
|
||||
|
||||
QUnit.module('when clicked', contextHooks => {
|
||||
contextHooks.beforeEach(() => {
|
||||
props.onSelectSecondaryInfo = sinon.stub()
|
||||
})
|
||||
|
||||
test('calls the .onSelectSecondaryInfo callback', () => {
|
||||
mountAndOpenOptionsMenu()
|
||||
getSecondaryInfoOption('Group').click()
|
||||
strictEqual(props.onSelectSecondaryInfo.callCount, 1)
|
||||
})
|
||||
|
||||
test('includes "group" when calling the .onSelectSecondaryInfo callback', () => {
|
||||
mountAndOpenOptionsMenu()
|
||||
getSecondaryInfoOption('Group').click()
|
||||
const [secondaryInfoType] = props.onSelectSecondaryInfo.lastCall.args
|
||||
equal(secondaryInfoType, 'group')
|
||||
})
|
||||
|
||||
test('returns focus to the "Options" menu trigger', () => {
|
||||
mountAndOpenOptionsMenu()
|
||||
getSecondaryInfoOption('Group').focus()
|
||||
getSecondaryInfoOption('Group').click()
|
||||
strictEqual(document.activeElement, getOptionsMenuTrigger())
|
||||
})
|
||||
|
||||
// TODO: GRADE-____
|
||||
QUnit.skip(
|
||||
'does not call the .onSelectSecondaryInfo callback when already selected',
|
||||
() => {
|
||||
props.selectedSecondaryInfo = 'group'
|
||||
mountAndOpenOptionsMenu()
|
||||
getSecondaryInfoOption('Group').click()
|
||||
strictEqual(props.onSelectSecondaryInfo.callCount, 0)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('"SIS ID" option', () => {
|
||||
test('displays the configured SIS name', () => {
|
||||
props.sisName = 'Powerschool'
|
||||
|
|
|
@ -95,3 +95,31 @@ test('selecting an option while the control is disabled does not call the onSele
|
|||
|
||||
strictEqual(props.onSelect.callCount, 0)
|
||||
})
|
||||
|
||||
test('options containing child options are rendered as optgroups', function() {
|
||||
const itemWithChildItems = {
|
||||
id: '10',
|
||||
name: 'Hierarchical',
|
||||
children: [{id: '1001', name: 'Child 1'}, {id: '1002', name: 'Child 2'}]
|
||||
}
|
||||
|
||||
const props = {...defaultProps(), items: [itemWithChildItems]}
|
||||
this.wrapper = mount(<GradebookFilter {...props} />)
|
||||
ok(this.wrapper.exists('optgroup[label="Hierarchical"]'))
|
||||
})
|
||||
|
||||
test('child options are rendered within their parent optgroup', function() {
|
||||
const itemWithChildItems = {
|
||||
id: '10',
|
||||
name: 'Hierarchical',
|
||||
children: [{id: '1001', name: 'Child 1'}, {id: '1002', name: 'Child 2'}]
|
||||
}
|
||||
const items = [itemWithChildItems]
|
||||
|
||||
const props = {...defaultProps(), items}
|
||||
this.wrapper = mount(<GradebookFilter {...props} />)
|
||||
deepEqual(
|
||||
this.wrapper.find('optgroup[label="Hierarchical"] option').map(option => option.props().value),
|
||||
['1001', '1002']
|
||||
)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import StudentGroupFilter from 'jsx/gradezilla/default_gradebook/components/StudentGroupFilter'
|
||||
|
||||
QUnit.module('Student Group Filter - subclass functionality', hooks => {
|
||||
let $container
|
||||
let props
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
$container = document.createElement('div')
|
||||
document.body.appendChild($container)
|
||||
|
||||
props = {
|
||||
items: [
|
||||
{
|
||||
children: [{id: '1', name: 'First Group Set 1'}, {id: '2', name: 'First Group Set 2'}],
|
||||
id: '1',
|
||||
name: 'First Group Set'
|
||||
},
|
||||
{
|
||||
children: [{id: '3', name: 'Second Group Set 1'}, {id: '4', name: 'Second Group Set 2'}],
|
||||
id: '2',
|
||||
name: 'Second Group Set'
|
||||
}
|
||||
],
|
||||
onSelect: () => {},
|
||||
selectedItemId: '0'
|
||||
}
|
||||
})
|
||||
|
||||
hooks.afterEach(() => {
|
||||
$container.remove()
|
||||
})
|
||||
|
||||
test('renders a screenreader-friendly label', () => {
|
||||
ReactDOM.render(<StudentGroupFilter {...props} />, $container)
|
||||
|
||||
ok($container.querySelector('label').innerText.includes('Student Group Filter'))
|
||||
})
|
||||
|
||||
test('the options are displayed in the same order as they were sent in', () => {
|
||||
ReactDOM.render(<StudentGroupFilter {...props} />, $container)
|
||||
|
||||
// "0" is the value for "All Student Groups"
|
||||
const expectedOptionValues = ['0', '1', '2', '3', '4']
|
||||
deepEqual(
|
||||
[...$container.querySelectorAll('option')].map(option => option.value),
|
||||
expectedOptionValues
|
||||
)
|
||||
})
|
||||
|
||||
test('options are displayed within their respective groups', () => {
|
||||
ReactDOM.render(<StudentGroupFilter {...props} />, $container)
|
||||
|
||||
const firstGroupOptions = [
|
||||
...$container.querySelectorAll('optgroup[label="First Group Set"] option')
|
||||
]
|
||||
deepEqual(firstGroupOptions.map(option => option.value), ['1', '2'])
|
||||
})
|
||||
})
|
|
@ -74,6 +74,7 @@ QUnit.module('Gradebook Enrollment Filter Change Data Loading', suiteHooks => {
|
|||
},
|
||||
selected_view_options_filters: []
|
||||
},
|
||||
student_groups: [],
|
||||
students_stateless_url: '/students-url',
|
||||
submissions_url: '/submissions-url'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,433 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import DataLoader from 'jsx/gradezilla/DataLoader'
|
||||
import {
|
||||
createGradebook,
|
||||
setFixtureHtml
|
||||
} from 'jsx/gradezilla/default_gradebook/__tests__/GradebookSpecHelper'
|
||||
import {createExampleStudents} from './DataLoadingSpecHelpers'
|
||||
import DataLoadingWrapper from './DataLoadingWrapper'
|
||||
|
||||
QUnit.module('Gradebook Student Group Filter Change Data Loading', suiteHooks => {
|
||||
let $container
|
||||
let dataLoadingWrapper
|
||||
let gradebook
|
||||
let gradebookOptions
|
||||
|
||||
let initialData
|
||||
let nextData
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
$container = document.body.appendChild(document.createElement('div'))
|
||||
setFixtureHtml($container)
|
||||
|
||||
initialData = {
|
||||
contextModules: [
|
||||
{id: '2601', position: 3, name: 'English'},
|
||||
{id: '2602', position: 1, name: 'Math'},
|
||||
{id: '2603', position: 2, name: 'Science'}
|
||||
],
|
||||
gradingPeriodAssignments: {
|
||||
1401: ['2301'],
|
||||
1402: ['2302']
|
||||
},
|
||||
studentIds: ['1101', '1102', '1103'],
|
||||
students: createExampleStudents().slice(0, 3)
|
||||
}
|
||||
|
||||
nextData = {
|
||||
gradingPeriodAssignments: {
|
||||
1401: ['2301', '2303'],
|
||||
1402: ['2302', '2304']
|
||||
},
|
||||
studentIds: ['1101', '1102', '1103', '1104'],
|
||||
students: createExampleStudents().slice(3)
|
||||
}
|
||||
|
||||
gradebookOptions = {
|
||||
api_max_per_page: 50,
|
||||
assignment_groups_url: '/assignment-groups',
|
||||
chunk_size: 10,
|
||||
context_id: '1201',
|
||||
context_modules_url: '/context-modules',
|
||||
custom_column_data_url: '/custom-column-data',
|
||||
sections: [{id: '2001', name: 'Freshmen'}, {id: '2002', name: 'Sophomores'}],
|
||||
settings: {
|
||||
filter_rows_by: {
|
||||
section_id: null
|
||||
},
|
||||
selected_view_options_filters: ['sections', 'studentGroups']
|
||||
},
|
||||
student_groups: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Default Set',
|
||||
groups: [{id: '1', name: 'Default Set 1'}, {id: '2', name: 'Default Set 2'}]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Alternate Set',
|
||||
groups: [{id: '3', name: 'Alternate Set 1'}, {id: '4', name: 'Alternate Set 2'}]
|
||||
}
|
||||
],
|
||||
students_stateless_url: '/students-url',
|
||||
submissions_url: '/submissions-url'
|
||||
}
|
||||
|
||||
dataLoadingWrapper = new DataLoadingWrapper()
|
||||
dataLoadingWrapper.setup()
|
||||
})
|
||||
|
||||
suiteHooks.afterEach(() => {
|
||||
gradebook.destroy()
|
||||
$container.remove()
|
||||
dataLoadingWrapper.teardown()
|
||||
})
|
||||
|
||||
function initializeAndLoadGradebook(options = {}) {
|
||||
if (options.includeGradingPeriodSet) {
|
||||
gradebookOptions.grading_period_set = {
|
||||
grading_periods: [
|
||||
{id: '1401', startDate: new Date('2015-09-01'), title: 'Q1'},
|
||||
{id: '1402', startDate: new Date('2015-10-15'), title: 'Q2'}
|
||||
],
|
||||
id: '1301'
|
||||
}
|
||||
}
|
||||
|
||||
gradebook = createGradebook(gradebookOptions)
|
||||
sinon.stub(gradebook, 'saveSettings').callsFake((settings, onSuccess = () => {}) => {
|
||||
onSuccess(settings)
|
||||
})
|
||||
|
||||
gradebook.initialize()
|
||||
|
||||
// Load Grid Data
|
||||
dataLoadingWrapper.loadStudentIds(initialData.studentIds)
|
||||
dataLoadingWrapper.loadGradingPeriodAssignments(initialData.gradingPeriodAssignments)
|
||||
dataLoadingWrapper.loadAssignmentGroups([])
|
||||
dataLoadingWrapper.loadContextModules()
|
||||
dataLoadingWrapper.loadCustomColumns()
|
||||
|
||||
// Load Student Data
|
||||
dataLoadingWrapper.loadStudents(initialData.students)
|
||||
dataLoadingWrapper.loadSubmissions([])
|
||||
|
||||
dataLoadingWrapper.finishLoadingStudents()
|
||||
dataLoadingWrapper.finishLoadingSubmissions()
|
||||
dataLoadingWrapper.finishLoadingCustomColumnData()
|
||||
|
||||
DataLoader.loadGradebookData.resetHistory()
|
||||
}
|
||||
|
||||
function changeStudentGroupFilter() {
|
||||
gradebook.updateCurrentStudentGroup('3')
|
||||
}
|
||||
|
||||
QUnit.module('when the student group filter changes', () => {
|
||||
test('sets the students as not loaded', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
strictEqual(gradebook.contentLoadStates.studentsLoaded, false)
|
||||
})
|
||||
|
||||
test('sets the submissions as not loaded', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
strictEqual(gradebook.contentLoadStates.submissionsLoaded, false)
|
||||
})
|
||||
|
||||
test('calls DataLoader.loadGradebookData()', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
strictEqual(DataLoader.loadGradebookData.callCount, 1)
|
||||
})
|
||||
|
||||
QUnit.module('when calling DataLoader.loadGradebookData()', () => {
|
||||
test('includes the course id', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(options.courseId, '1201')
|
||||
})
|
||||
|
||||
test('includes the per page api request setting', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(options.perPage, 50)
|
||||
})
|
||||
|
||||
test('requests assignment groups', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(typeof options.assignmentGroupsURL, 'undefined')
|
||||
})
|
||||
|
||||
test('does not request context modules', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(typeof options.contextModulesURL, 'undefined')
|
||||
})
|
||||
|
||||
test('requests data for hidden custom columns', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
strictEqual(options.customColumnDataParams.include_hidden, true)
|
||||
})
|
||||
|
||||
test('includes the students url', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(options.studentsURL, '/students-url')
|
||||
})
|
||||
|
||||
test('includes the students page callback', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
strictEqual(options.studentsPageCb, gradebook.gotChunkOfStudents)
|
||||
})
|
||||
|
||||
test('includes students params', () => {
|
||||
initializeAndLoadGradebook()
|
||||
const exampleStudentsParams = {example: true}
|
||||
sandbox.stub(gradebook, 'studentsParams').returns(exampleStudentsParams)
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(options.studentsParams, exampleStudentsParams)
|
||||
})
|
||||
|
||||
test('includes the loaded student ids', () => {
|
||||
initializeAndLoadGradebook()
|
||||
const studentIds = ['1101', '1102', '1103']
|
||||
gradebook.courseContent.students.setStudentIds(studentIds)
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
deepEqual(options.loadedStudentIds, studentIds)
|
||||
})
|
||||
|
||||
test('includes the submissions url', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(options.submissionsURL, '/submissions-url')
|
||||
})
|
||||
|
||||
test('includes the submissions chunk callback', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
strictEqual(options.submissionsChunkCb, gradebook.gotSubmissionsChunk)
|
||||
})
|
||||
|
||||
test('includes the submissions chunk size', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(options.submissionsChunkSize, 10)
|
||||
})
|
||||
|
||||
test('includes the stored custom columns', () => {
|
||||
initializeAndLoadGradebook()
|
||||
gradebook.gotCustomColumns([{id: '2401'}, {id: '2402'}])
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
deepEqual(options.customColumnIds, ['2401', '2402'])
|
||||
})
|
||||
|
||||
test('includes the custom column data url', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
equal(options.customColumnDataURL, '/custom-column-data')
|
||||
})
|
||||
|
||||
test('includes the custom column data page callback', () => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
strictEqual(options.customColumnDataPageCb, gradebook.gotCustomColumnDataChunk)
|
||||
})
|
||||
|
||||
test('does not request grading period assignments', () => {
|
||||
initializeAndLoadGradebook({includeGradingPeriodSet: true})
|
||||
changeStudentGroupFilter()
|
||||
const [options] = DataLoader.loadGradebookData.lastCall.args
|
||||
strictEqual(options.getGradingPeriodAssignments, false)
|
||||
})
|
||||
})
|
||||
|
||||
test('re-renders the filters', () => {
|
||||
initializeAndLoadGradebook()
|
||||
sandbox.spy(gradebook, 'renderFilters')
|
||||
changeStudentGroupFilter()
|
||||
strictEqual(gradebook.renderFilters.callCount, 1)
|
||||
})
|
||||
|
||||
test('re-renders the filters after students load status is updated', () => {
|
||||
initializeAndLoadGradebook()
|
||||
sandbox.stub(gradebook, 'renderFilters').callsFake(() => {
|
||||
strictEqual(gradebook.contentLoadStates.studentsLoaded, false)
|
||||
})
|
||||
changeStudentGroupFilter()
|
||||
})
|
||||
|
||||
test('re-renders the filters after submissions load status is updated', () => {
|
||||
initializeAndLoadGradebook()
|
||||
sandbox.stub(gradebook, 'renderFilters').callsFake(() => {
|
||||
strictEqual(gradebook.contentLoadStates.submissionsLoaded, false)
|
||||
})
|
||||
changeStudentGroupFilter()
|
||||
})
|
||||
|
||||
QUnit.module('when student ids finish loading', contextHooks => {
|
||||
contextHooks.beforeEach(() => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
})
|
||||
|
||||
test('stores the loaded student ids in the Gradebook', () => {
|
||||
dataLoadingWrapper.loadStudentIds(nextData.studentIds)
|
||||
deepEqual(gradebook.courseContent.students.listStudentIds(), nextData.studentIds)
|
||||
})
|
||||
|
||||
test('updates grid rows for the loaded student ids', () => {
|
||||
dataLoadingWrapper.loadStudentIds(nextData.studentIds)
|
||||
strictEqual(document.body.querySelectorAll('.canvas_0 .slick-row').length, 4)
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('loading students', hooks => {
|
||||
hooks.beforeEach(() => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
dataLoadingWrapper.loadStudentIds(nextData.studentIds)
|
||||
})
|
||||
|
||||
QUnit.module('when a chunk of students have loaded', () => {
|
||||
test('adds the loaded students to the Gradebook', () => {
|
||||
dataLoadingWrapper.loadStudents(nextData.students)
|
||||
const studentNames = gradebook.courseContent.students
|
||||
.listStudents()
|
||||
.map(student => student.name)
|
||||
deepEqual(studentNames.sort(), ['Adam Jones', 'Betty Ford', 'Charlie Xi', 'Dana Young'])
|
||||
})
|
||||
|
||||
test('updates the rows for loaded students', () => {
|
||||
dataLoadingWrapper.loadStudents(nextData.students)
|
||||
const $studentNames = document.body.querySelectorAll('.slick-row .student-name')
|
||||
const studentNames = [...$studentNames].map($name => $name.textContent.trim())
|
||||
deepEqual(studentNames, ['Adam Jones', 'Betty Ford', 'Charlie Xi', 'Dana Young'])
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('when students finish loading', () => {
|
||||
test('sets the students as loaded', () => {
|
||||
dataLoadingWrapper.finishLoadingStudents()
|
||||
strictEqual(gradebook.contentLoadStates.studentsLoaded, true)
|
||||
})
|
||||
|
||||
test('re-renders the column headers', () => {
|
||||
sinon.spy(gradebook, 'updateColumnHeaders')
|
||||
dataLoadingWrapper.finishLoadingStudents()
|
||||
strictEqual(gradebook.updateColumnHeaders.callCount, 1)
|
||||
})
|
||||
|
||||
test('re-renders the column headers after updating students load state', () => {
|
||||
sinon.stub(gradebook, 'updateColumnHeaders').callsFake(() => {
|
||||
// students load state was already updated
|
||||
strictEqual(gradebook.contentLoadStates.studentsLoaded, true)
|
||||
})
|
||||
dataLoadingWrapper.finishLoadingStudents()
|
||||
})
|
||||
|
||||
test('re-renders the filters', () => {
|
||||
sinon.spy(gradebook, 'renderFilters')
|
||||
dataLoadingWrapper.finishLoadingStudents()
|
||||
strictEqual(gradebook.renderFilters.callCount, 1)
|
||||
})
|
||||
|
||||
test('re-renders the filters after updating students load state', () => {
|
||||
sinon.stub(gradebook, 'renderFilters').callsFake(() => {
|
||||
// students load state was already updated
|
||||
strictEqual(gradebook.contentLoadStates.studentsLoaded, true)
|
||||
})
|
||||
dataLoadingWrapper.finishLoadingStudents()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('loading submissions', hooks => {
|
||||
hooks.beforeEach(() => {
|
||||
initializeAndLoadGradebook()
|
||||
changeStudentGroupFilter()
|
||||
|
||||
dataLoadingWrapper.loadStudentIds(nextData.studentIds)
|
||||
dataLoadingWrapper.loadAssignmentGroups([])
|
||||
dataLoadingWrapper.loadContextModules()
|
||||
dataLoadingWrapper.loadCustomColumns()
|
||||
dataLoadingWrapper.loadStudents(nextData.students)
|
||||
})
|
||||
|
||||
QUnit.module('when submissions finish loading', contextHooks => {
|
||||
contextHooks.beforeEach(() => {
|
||||
dataLoadingWrapper.finishLoadingStudents()
|
||||
})
|
||||
|
||||
test('sets the submissions as loaded', () => {
|
||||
dataLoadingWrapper.finishLoadingSubmissions()
|
||||
strictEqual(gradebook.contentLoadStates.submissionsLoaded, true)
|
||||
})
|
||||
|
||||
test('re-renders the column headers', () => {
|
||||
sandbox.spy(gradebook, 'updateColumnHeaders')
|
||||
dataLoadingWrapper.finishLoadingSubmissions()
|
||||
strictEqual(gradebook.updateColumnHeaders.callCount, 1)
|
||||
})
|
||||
|
||||
test('re-renders the column headers after updating submissions load state', () => {
|
||||
sandbox.stub(gradebook, 'updateColumnHeaders').callsFake(() => {
|
||||
// submissions load state was already updated
|
||||
strictEqual(gradebook.contentLoadStates.submissionsLoaded, true)
|
||||
})
|
||||
dataLoadingWrapper.finishLoadingSubmissions()
|
||||
})
|
||||
|
||||
test('re-renders the filters', () => {
|
||||
sandbox.spy(gradebook, 'renderFilters')
|
||||
dataLoadingWrapper.finishLoadingSubmissions()
|
||||
strictEqual(gradebook.renderFilters.callCount, 1)
|
||||
})
|
||||
|
||||
test('re-renders the filters after updating submissions load state', () => {
|
||||
sandbox.stub(gradebook, 'renderFilters').callsFake(() => {
|
||||
// submissions load state was already updated
|
||||
strictEqual(gradebook.contentLoadStates.submissionsLoaded, true)
|
||||
})
|
||||
dataLoadingWrapper.finishLoadingSubmissions()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -82,5 +82,26 @@ describe "Api::V1::GroupCategory" do
|
|||
end
|
||||
end
|
||||
|
||||
describe "groups within the category" do
|
||||
let(:course) { Course.create! }
|
||||
let(:category) { course.group_categories.create!(name: "category") }
|
||||
let(:user) { course.enroll_teacher(User.create!, enrollment_state: "active").user }
|
||||
|
||||
before(:each) do
|
||||
category.create_groups(2)
|
||||
end
|
||||
|
||||
it "are included when 'groups' is specified as an include key" do
|
||||
json = CategoryHarness.new.group_category_json(category, user, nil, {include: ['groups']})
|
||||
json_group_ids = json["groups"].map { |group| group["id"] }
|
||||
|
||||
expect(json_group_ids).to match_array(category.groups.pluck(:id))
|
||||
end
|
||||
|
||||
it "are not included when 'groups' is not specified as an include key" do
|
||||
json = CategoryHarness.new.group_category_json(category, user, nil)
|
||||
expect(json).not_to have_key("groups")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -159,6 +159,33 @@ describe GradebookUserIds do
|
|||
end
|
||||
end
|
||||
|
||||
describe "filtering by student group" do
|
||||
let_once(:category) do
|
||||
category = @course.group_categories.create!(name: "whatever")
|
||||
category.create_groups(2)
|
||||
|
||||
category.groups.first.add_user(@student1)
|
||||
category.groups.second.add_user(@student2)
|
||||
category
|
||||
end
|
||||
let_once(:group) { category.groups.first }
|
||||
|
||||
it "only returns students in the selected group when one is specified" do
|
||||
@teacher.preferences[:gradebook_settings] = {
|
||||
@course.id => {
|
||||
filter_rows_by: {
|
||||
student_group_id: group.id
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(gradebook_user_ids.user_ids).to contain_exactly(@student1.id)
|
||||
end
|
||||
|
||||
it "returns students in all groups when no group is specified" do
|
||||
expect(gradebook_user_ids.user_ids).to match_array(@course.students.pluck(:id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pg_collkey installed' do
|
||||
before do
|
||||
skip 'requires pg_collkey installed SD-2747' unless has_pg_collkey
|
||||
|
|
|
@ -116,4 +116,33 @@ describe "Filter" do
|
|||
expect(Gradezilla::Cells.get_grade(@student_2, @first_assignment)).to eq '1'
|
||||
end
|
||||
end
|
||||
|
||||
context "by Student Group" do
|
||||
before(:once) do
|
||||
gradebook_data_setup
|
||||
show_student_groups_filter(@teacher)
|
||||
|
||||
@category = @course.group_categories.create!(name: "a group category")
|
||||
@category.create_groups(2)
|
||||
|
||||
@category.groups.first.add_user(@student_1)
|
||||
@category.groups.second.add_user(@student_2)
|
||||
end
|
||||
|
||||
before(:each) { user_session(@teacher) }
|
||||
|
||||
it "should allow showing only a specific student group", priority: "1" do
|
||||
Gradezilla.visit(@course)
|
||||
Gradezilla.select_student_group("All Student Groups")
|
||||
|
||||
Gradezilla::Cells.edit_grade(@student_1, @first_assignment, 0)
|
||||
Gradezilla::Cells.edit_grade(@student_2, @first_assignment, 1)
|
||||
|
||||
group2 = @category.groups.second
|
||||
Gradezilla.select_student_group(group2)
|
||||
expect(Gradezilla.student_group_dropdown).to include_text(group2.name)
|
||||
|
||||
expect(Gradezilla::Cells.get_grade(@student_2, @first_assignment)).to eq '1'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -320,6 +320,12 @@ module Gradezilla
|
|||
wait_for_ajaximations
|
||||
end
|
||||
|
||||
def self.select_student_group(student_group)
|
||||
student_group = student_group.name if student_group.is_a?(Group)
|
||||
click_option(student_group_dropdown, student_group, :text)
|
||||
wait_for_ajaximations
|
||||
end
|
||||
|
||||
def self.show_notes
|
||||
view_menu = open_gradebook_menu('View')
|
||||
select_gradebook_menu_option('Notes', container: view_menu, role: 'menuitemcheckbox')
|
||||
|
@ -517,6 +523,10 @@ module Gradezilla
|
|||
f('#modules-filter-container select')
|
||||
end
|
||||
|
||||
def self.student_group_dropdown
|
||||
f('#student-group-filter-container select')
|
||||
end
|
||||
|
||||
def self.filter_menu_item(menu_item_name)
|
||||
fj("option:contains(\"#{menu_item_name}\")")
|
||||
end
|
||||
|
|
|
@ -104,6 +104,10 @@ module GradebookSetup
|
|||
set_filter_visibility(user, 'modules', true)
|
||||
end
|
||||
|
||||
def show_student_groups_filter(user)
|
||||
set_filter_visibility(user, 'studentGroups', true)
|
||||
end
|
||||
|
||||
def set_filter_visibility(user, filter, visible)
|
||||
filters = user.preferences.dig(:gradebook_settings, @course.id, :selected_view_options_filters) || []
|
||||
if visible && !filters.include?(filter)
|
||||
|
|
Loading…
Reference in New Issue