Add filters for inactive/concluded enrollments to LMGB
flag=inactive_concluded_lmgb_filters closes OUT-3794 Test-plan: * FF: 'Student filters in LMGB' - Create 3+ students in a course - Create an assignment that has 2+ outcomes attached via rubric - For one of the other students, conclude their enrollment ('Concluded User') - For one of the other students, deactivate their user ('Deactivated User') - One user should remain that is an active enrollment ('Normal User') - Provide outcomes assessment on the new assignment via speedgrader for each of the above 3 students (Concluded/Deactivated/Normal) - Create another 20 students in the course (they don't have to have assessments) > 1.upto(20) {|i| u = User.create!(name: "Student #{i}"); Course.find(..).enroll_student(u) } - With the FF off - LMGB should behave as normal - Note: if "Hide students with unassessed outcomes" is enabled, Unassessed User should not appear in the outcome report and vice versa. - With the FF on - Click kebab filter in 'Students' header to toggle different options - For 'Show Unassessed students' enabled, unassessed users should appear and vice versa - For 'Show Concluded students' enabled, Concluded User should appear and vice versa - For 'Show Inactive students' enabled, Inactive User should appear and vice versa - The filter options should now apply to the export, i.e. the users that appear in the LMGB should appear in the CSV export (sorting will differ) - Verify that sorting by column headers works as expected - Verify that the column header averages apply only to the scores that are visible (total students, not limited to the current page) - Verify that pagination is correct (20 students by default) regardless of filter selections - Add an inactive and concluded user to a different section - Change a users' section and make sure that with the FF on and off, the LMGB behaves as expected. - Note: inactive + concluded users should be displayed in CSV with FF disabled but not in the UI. Unassessed users with active enrollments should always appear in the csv report if and only if they appear in the UI based on enabled or disabled filters. Change-Id: I5639dc28f1c5ebd43900de4e99006ecc3535468d Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/256110 QA-Review: Augusto Callejas <acallejas@instructure.com> Product-Review: Jody Sailor Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Augusto Callejas <acallejas@instructure.com> Reviewed-by: Pat Renner <prenner@instructure.com>
This commit is contained in:
parent
ec0cbda3bf
commit
9a5bc16cbd
|
@ -69,6 +69,8 @@ export default class OutcomeGradebookView extends View
|
|||
new CheckboxView(Dictionary.remedial)
|
||||
]
|
||||
|
||||
inactive_concluded_lmgb_filters: ENV.GRADEBOOK_OPTIONS?.inactive_concluded_lmgb_filters
|
||||
|
||||
ratings: []
|
||||
|
||||
events:
|
||||
|
@ -84,6 +86,7 @@ export default class OutcomeGradebookView extends View
|
|||
if ENV.GRADEBOOK_OPTIONS.outcome_proficiency?.ratings
|
||||
@ratings = ENV.GRADEBOOK_OPTIONS.outcome_proficiency.ratings
|
||||
@checkboxes = @ratings.map (rating) -> new CheckboxView({color: "\##{rating.color}", label: rating.description})
|
||||
Grid.gridRef = @
|
||||
|
||||
remove: ->
|
||||
super()
|
||||
|
@ -146,7 +149,8 @@ export default class OutcomeGradebookView extends View
|
|||
view.on('togglestate', @_createFilter("rating_#{i}")) for view, i in @checkboxes
|
||||
@updateExportLink(@learningMastery.getCurrentSectionId())
|
||||
@$('#no_results_outcomes').change(() -> _this._toggleOutcomesWithNoResults(this.checked))
|
||||
@$('#no_results_students').change(() -> _this._toggleStudentsWithNoResults(this.checked))
|
||||
if !@inactive_concluded_lmgb_filters
|
||||
@$('#no_results_students').change(() -> _this._toggleStudentsWithNoResults(this.checked))
|
||||
|
||||
_setFilterSetting: (name, value) ->
|
||||
filters = userSettings.contextGet('lmgb_filters')
|
||||
|
@ -167,8 +171,19 @@ export default class OutcomeGradebookView extends View
|
|||
@grid.setColumns(@columns)
|
||||
Grid.View.redrawHeader(@grid, Grid.averageFn)
|
||||
|
||||
_toggleStudentsWithNoResults: (enabled) ->
|
||||
_toggleStudentsWithNoResults: (enabled) =>
|
||||
@_setFilterSetting('students_no_results', enabled)
|
||||
@updateExportLink(@learningMastery.getCurrentSectionId())
|
||||
@_rerender()
|
||||
|
||||
_toggleStudentsWithInactiveEnrollments: (enabled) =>
|
||||
@_setFilterSetting('inactive_enrollments', enabled)
|
||||
@updateExportLink(@learningMastery.getCurrentSectionId())
|
||||
@_rerender()
|
||||
|
||||
_toggleStudentsWithConcludedEnrollments: (enabled) =>
|
||||
@_setFilterSetting('concluded_enrollments', enabled)
|
||||
@updateExportLink(@learningMastery.getCurrentSectionId())
|
||||
@_rerender()
|
||||
|
||||
_rerender: ->
|
||||
|
@ -179,7 +194,11 @@ export default class OutcomeGradebookView extends View
|
|||
@_loadOutcomes()
|
||||
|
||||
_toggleSort: (e, {grid, sortAsc, sortCol}) =>
|
||||
if sortCol.field == @sortField
|
||||
target = $(e.target).attr('data-component')
|
||||
# Don't sort if user clicks the enrollments filter kabob
|
||||
if target == 'lmgb-student-filter-trigger'
|
||||
return
|
||||
else if sortCol.field == @sortField
|
||||
# Change sort direction
|
||||
@sortOrderAsc = !@sortOrderAsc
|
||||
else
|
||||
|
@ -200,11 +219,12 @@ export default class OutcomeGradebookView extends View
|
|||
#
|
||||
# Returns an object.
|
||||
toJSON: ->
|
||||
_.extend({}, checkboxes: @checkboxes)
|
||||
_.extend({}, checkboxes: @checkboxes, inactive_concluded_lmgb_filters: @inactive_concluded_lmgb_filters)
|
||||
|
||||
_loadFilterSettings: ->
|
||||
@$('#no_results_outcomes').prop('checked', @._getFilterSetting('outcomes_no_results'))
|
||||
@$('#no_results_students').prop('checked', @._getFilterSetting('students_no_results'))
|
||||
if !@inactive_concluded_lmgb_filters
|
||||
@$('#no_results_students').prop('checked', @._getFilterSetting('students_no_results'))
|
||||
|
||||
# Public: Render the view once all needed data is loaded.
|
||||
#
|
||||
|
@ -308,10 +328,24 @@ export default class OutcomeGradebookView extends View
|
|||
"/api/v1/courses/#{course}/outcome_rollups?rating_percents=true&per_page=20&include[]=outcomes&include[]=users&include[]=outcome_paths#{excluding}&page=#{page}#{sortParams}#{sectionParam}"
|
||||
|
||||
_loadOutcomes: (page = 1) =>
|
||||
exclude = if @$('#no_results_students').prop('checked') then 'missing_user_rollups' else ''
|
||||
filter = @_getOutcomeFiltersParams()
|
||||
course = ENV.context_asset_string.split('_')[1]
|
||||
@$('.outcome-gradebook-wrapper').disableWhileLoading(@hasOutcomes)
|
||||
@_loadPage(@_rollupsUrl(course, exclude, page))
|
||||
@_loadPage(@_rollupsUrl(course, filter, page))
|
||||
|
||||
_getOutcomeFilters: ->
|
||||
outcome_filters = []
|
||||
if @inactive_concluded_lmgb_filters
|
||||
if !@._getFilterSetting('inactive_enrollments') then outcome_filters.push('inactive_enrollments')
|
||||
if !@._getFilterSetting('concluded_enrollments') then outcome_filters.push('concluded_enrollments')
|
||||
if !@._getFilterSetting('students_no_results') then outcome_filters.push('missing_user_rollups')
|
||||
else
|
||||
if @._getFilterSetting('students_no_results') then outcome_filters.push('missing_user_rollups')
|
||||
|
||||
return outcome_filters
|
||||
|
||||
_getOutcomeFiltersParams: ->
|
||||
return @_getOutcomeFilters().map((value) => "&exclude[]=#{value}").join('')
|
||||
|
||||
# Internal: Load a page of outcome results from the given URL.
|
||||
#
|
||||
|
@ -364,5 +398,11 @@ export default class OutcomeGradebookView extends View
|
|||
|
||||
updateExportLink: (section) =>
|
||||
url = "#{ENV.GRADEBOOK_OPTIONS.context_url}/outcome_rollups.csv"
|
||||
url += "?section_id=#{section}" if section and section != '0'
|
||||
params = "#{@_getOutcomeFiltersParams()}"
|
||||
|
||||
if section and section != '0'
|
||||
params += "&" if params != ''
|
||||
params += "section_id=#{section}"
|
||||
|
||||
url += "?#{params}" if params != ''
|
||||
$('.export-content').attr('href', url)
|
||||
|
|
|
@ -609,7 +609,8 @@ class GradebooksController < ApplicationController
|
|||
|
||||
def set_learning_mastery_env
|
||||
set_student_context_cards_js_env
|
||||
visible_sections = if @context.root_account.feature_enabled?(:limit_section_visibility_in_lmgb)
|
||||
root_account = @context.root_account
|
||||
visible_sections = if root_account.feature_enabled?(:limit_section_visibility_in_lmgb)
|
||||
@context.sections_visible_to(@current_user)
|
||||
else
|
||||
@context.active_course_sections
|
||||
|
@ -619,11 +620,12 @@ class GradebooksController < ApplicationController
|
|||
GRADEBOOK_OPTIONS: {
|
||||
context_id: @context.id.to_s,
|
||||
context_url: named_context_url(@context, :context_url),
|
||||
ACCOUNT_LEVEL_MASTERY_SCALES: @context.root_account.feature_enabled?(:account_level_mastery_scales),
|
||||
ACCOUNT_LEVEL_MASTERY_SCALES: root_account.feature_enabled?(:account_level_mastery_scales),
|
||||
outcome_proficiency: outcome_proficiency,
|
||||
sections: sections_json(visible_sections, @current_user, session, [], allow_sis_ids: true),
|
||||
settings: gradebook_settings(@context.global_id),
|
||||
settings_update_url: api_v1_course_gradebook_settings_update_url(@context)
|
||||
settings_update_url: api_v1_course_gradebook_settings_update_url(@context),
|
||||
inactive_concluded_lmgb_filters: root_account.feature_enabled?(:inactive_concluded_lmgb_filters)
|
||||
}
|
||||
})
|
||||
end
|
||||
|
|
|
@ -347,10 +347,33 @@ class OutcomeResultsController < ApplicationController
|
|||
|
||||
def user_rollups(opts = {})
|
||||
excludes = Api.value_to_array(params[:exclude]).uniq
|
||||
filter_users_by_excludes
|
||||
|
||||
@results = find_results(opts).preload(:user)
|
||||
outcome_results_rollups(results: @results, users: @users, excludes: excludes, context: @context)
|
||||
end
|
||||
|
||||
def filter_users_by_excludes(aggregate = false)
|
||||
excludes = Api.value_to_array(params[:exclude]).uniq
|
||||
# exclude users with no results (if being requested) before we paginate,
|
||||
# otherwise we end up with users in the pagination that may have no rollups,
|
||||
# which will inflate the pagination total count
|
||||
remove_users_with_no_results if excludes.include?('missing_user_rollups') && !aggregate
|
||||
|
||||
if @context.root_account.feature_enabled?(:inactive_concluded_lmgb_filters)
|
||||
exclude_concluded = excludes.include? 'concluded_enrollments'
|
||||
exclude_inactive = excludes.include? 'inactive_enrollments'
|
||||
return unless exclude_concluded || exclude_inactive
|
||||
|
||||
filters = []
|
||||
filters << 'completed' if exclude_concluded
|
||||
filters << 'inactive' if exclude_inactive
|
||||
|
||||
ActiveRecord::Associations::Preloader.new.preload(@users, :enrollments)
|
||||
@users = @users.reject {|u| u.enrollments.all? {|e| filters.include? e.workflow_state}}
|
||||
end
|
||||
end
|
||||
|
||||
def remove_users_with_no_results
|
||||
userids_with_results = find_results.pluck(:user_id).uniq
|
||||
@users = @users.select { |u| userids_with_results.include? u.id }
|
||||
|
@ -358,13 +381,9 @@ class OutcomeResultsController < ApplicationController
|
|||
|
||||
def user_rollups_json
|
||||
return user_rollups_sorted_by_score_json if params[:sort_by] == 'outcome' && params[:sort_outcome_id]
|
||||
excludes = Api.value_to_array(params[:exclude]).uniq
|
||||
# exclude users with no results (if being requested) before we paginate,
|
||||
# otherwise we end up with users in the pagination that may have no rollups,
|
||||
# which will inflate the pagination total count
|
||||
remove_users_with_no_results if excludes.include? 'missing_user_rollups'
|
||||
@users = Api.paginate(@users, self, api_v1_course_outcome_rollups_url(@context))
|
||||
|
||||
rollups = user_rollups
|
||||
@users = Api.paginate(@users, self, api_v1_course_outcome_rollups_url(@context))
|
||||
rollups = @users.map {|u| rollups.find {|r| r.context.id == u.id }}.compact if params[:sort_by] == 'student'
|
||||
json = outcome_results_rollups_json(rollups)
|
||||
json[:meta] = Api.jsonapi_meta(@users, self, api_v1_course_outcome_rollups_url(@context))
|
||||
|
@ -397,7 +416,8 @@ class OutcomeResultsController < ApplicationController
|
|||
def aggregate_rollups_json
|
||||
# calculating averages for all users in the context and only returning one
|
||||
# rollup, so don't paginate users in this method.
|
||||
@results = find_results.preload(:user)
|
||||
filter_users_by_excludes(true)
|
||||
@results = find_results(all_users: false).preload(:user)
|
||||
aggregate_rollups = [aggregate_outcome_results_rollup(@results, @context, params[:aggregate_stat])]
|
||||
json = aggregate_outcome_results_rollups_json(aggregate_rollups)
|
||||
# no pagination, so no meta field
|
||||
|
@ -572,7 +592,12 @@ class OutcomeResultsController < ApplicationController
|
|||
elsif params[:section_id]
|
||||
@section = @context.course_sections.where(id: params[:section_id].to_i).first
|
||||
reject! "invalid section id" unless @section
|
||||
@users = apply_sort_order(@section.students).to_a
|
||||
@users = if @context.root_account.feature_enabled?(:inactive_concluded_lmgb_filters)
|
||||
# include all enrollment types which will be filtered later
|
||||
apply_sort_order(@section.users).to_a
|
||||
else
|
||||
apply_sort_order(@section.students).to_a
|
||||
end
|
||||
end
|
||||
@users ||= users_for_outcome_context.to_a
|
||||
@users.sort! {|a,b| a.id <=> b.id} unless params[:sort_by]
|
||||
|
|
|
@ -20,10 +20,14 @@ import I18n from 'i18n!gradebookOutcomeGradebookGrid'
|
|||
import $ from 'jquery'
|
||||
import _ from 'underscore'
|
||||
import HeaderFilterView from 'jsx/gradebook/views/HeaderFilterView'
|
||||
import OutcomeFilterView from 'jsx/gradebook/views/OutcomeFilterView'
|
||||
import OutcomeColumnView from 'compiled/views/gradebook/OutcomeColumnView'
|
||||
import cellTemplate from 'jst/gradebook/outcome_gradebook_cell'
|
||||
import studentCellTemplate from 'jst/gradebook/outcome_gradebook_student_cell'
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
/*
|
||||
xsslint safeString.method cellHtml
|
||||
*/
|
||||
|
@ -35,6 +39,7 @@ const Grid = {
|
|||
section: undefined,
|
||||
dataSource: {},
|
||||
outcomes: [],
|
||||
gridRef: null,
|
||||
options: {
|
||||
headerRowHeight: 42,
|
||||
rowHeight: 42,
|
||||
|
@ -348,6 +353,7 @@ const Grid = {
|
|||
})
|
||||
}
|
||||
},
|
||||
// This only renders student rows, not column headers
|
||||
studentCell(_row, _cell, value, _columnDef, _dataContext) {
|
||||
return studentCellTemplate(
|
||||
_.extend(value, {
|
||||
|
@ -403,10 +409,6 @@ const Grid = {
|
|||
if (column.field === 'student') {
|
||||
return Grid.View.studentHeaderRowCell(node, column, grid)
|
||||
}
|
||||
const results = Grid.View.getColumnResults(grid.getData(), column)
|
||||
if (!results.length) {
|
||||
return $(node).empty()
|
||||
}
|
||||
return $(node)
|
||||
.empty()
|
||||
.append(Grid.View.cellHtml(score?.score, score?.hide_points, column, false))
|
||||
|
@ -414,7 +416,10 @@ const Grid = {
|
|||
_aggregateUrl(stat) {
|
||||
const course = ENV.context_asset_string.split('_')[1]
|
||||
const sectionParam = Grid.section && Grid.section !== '0' ? `§ion_id=${Grid.section}` : ''
|
||||
return `/api/v1/courses/${course}/outcome_rollups?aggregate=course&aggregate_stat=${stat}${sectionParam}`
|
||||
const filters = Grid.gridRef._getOutcomeFiltersParams()
|
||||
? `${Grid.gridRef._getOutcomeFiltersParams()}`
|
||||
: ''
|
||||
return `/api/v1/courses/${course}/outcome_rollups?aggregate=course&aggregate_stat=${stat}${sectionParam}${filters}`
|
||||
},
|
||||
redrawHeader(grid, fn = Grid.averageFn) {
|
||||
Grid.averageFn = fn
|
||||
|
@ -442,6 +447,22 @@ const Grid = {
|
|||
})
|
||||
})
|
||||
},
|
||||
addEnrollmentFilters(node) {
|
||||
const existingExclusions = Grid.gridRef._getOutcomeFilters()
|
||||
const menu = React.createElement(
|
||||
OutcomeFilterView,
|
||||
{
|
||||
showInactiveEnrollments: !existingExclusions.includes('inactive_enrollments'),
|
||||
showConcludedEnrollments: !existingExclusions.includes('concluded_enrollments'),
|
||||
showUnassessedStudents: !existingExclusions.includes('missing_user_rollups'),
|
||||
toggleInactiveEnrollments: Grid.gridRef._toggleStudentsWithInactiveEnrollments,
|
||||
toggleConcludedEnrollments: Grid.gridRef._toggleStudentsWithConcludedEnrollments,
|
||||
toggleUnassessedStudents: Grid.gridRef._toggleStudentsWithNoResults
|
||||
},
|
||||
null
|
||||
)
|
||||
ReactDOM.render(menu, node)
|
||||
},
|
||||
studentHeaderRowCell(node, _column, grid) {
|
||||
$(node).addClass('average-filter')
|
||||
const view = new HeaderFilterView({
|
||||
|
@ -453,6 +474,10 @@ const Grid = {
|
|||
},
|
||||
headerCell({node, column, grid}, _fn = Grid.averageFn) {
|
||||
if (column.field === 'student') {
|
||||
if (ENV.GRADEBOOK_OPTIONS?.inactive_concluded_lmgb_filters) {
|
||||
$(node).empty()
|
||||
this.addEnrollmentFilters(node)
|
||||
}
|
||||
return
|
||||
}
|
||||
const totalsFn = _.partial(Grid.View.calculateRatingsTotals, grid, column)
|
||||
|
|
|
@ -22,6 +22,12 @@ import 'compiled/jquery.kylemenu'
|
|||
import template from 'jst/gradebook/header_filter'
|
||||
|
||||
class HeaderFilterView extends View {
|
||||
toJSON() {
|
||||
return {
|
||||
inactive_concluded_lmgb_filters: ENV.GRADEBOOK_OPTIONS?.inactive_concluded_lmgb_filters
|
||||
}
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (C) 2017 - 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 {func, bool} from 'prop-types'
|
||||
import {IconMoreSolid} from '@instructure/ui-icons'
|
||||
import {IconButton} from '@instructure/ui-buttons'
|
||||
import {View} from '@instructure/ui-layout'
|
||||
import {Menu} from '@instructure/ui-menu'
|
||||
import {Text} from '@instructure/ui-elements'
|
||||
import I18n from 'i18n!gradebook'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
|
||||
export default class StudentColumnHeader extends React.Component {
|
||||
static propTypes = {
|
||||
showInactiveEnrollments: bool.isRequired,
|
||||
showConcludedEnrollments: bool.isRequired,
|
||||
showUnassessedStudents: bool.isRequired,
|
||||
toggleInactiveEnrollments: func.isRequired,
|
||||
toggleConcludedEnrollments: func.isRequired,
|
||||
toggleUnassessedStudents: func.isRequired
|
||||
}
|
||||
|
||||
toggleInactiveEnrollments = (_e, _menuItem, newValue) => {
|
||||
this.props.toggleInactiveEnrollments(newValue)
|
||||
}
|
||||
|
||||
toggleConcludedEnrollments = (_e, _menuItem, newValue) => {
|
||||
this.props.toggleConcludedEnrollments(newValue)
|
||||
}
|
||||
|
||||
toggleUnassessedStudents = (_e, _menuItem, newValue) => {
|
||||
this.props.toggleUnassessedStudents(newValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View textAlign="end">
|
||||
<Flex id="learning-mastery-gradebook-filter">
|
||||
<Flex.Item shouldGrow>
|
||||
<Text weight="bold" id="lmgb-student-filter-title">
|
||||
{I18n.t('Students')}
|
||||
</Text>
|
||||
</Flex.Item>
|
||||
<Flex.Item shouldShrink id="lmgb-student-filter-trigger">
|
||||
<Menu
|
||||
placement="bottom end"
|
||||
withArrow
|
||||
shouldHideOnSelect
|
||||
trigger={
|
||||
<IconButton
|
||||
data-component="lmgb-student-filter-trigger"
|
||||
renderIcon={IconMoreSolid}
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
size="medium"
|
||||
screenReaderLabel={I18n.t('Display Filter Options')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Menu.Group
|
||||
allowMultiple
|
||||
label={I18n.t('Show')}
|
||||
id="learning-mastery-gradebook-dropdown"
|
||||
>
|
||||
<Menu.Item
|
||||
value="display_inactive_enrollments"
|
||||
data-component="lmgb-student-filter-inactive-enrollments"
|
||||
selected={this.props.showInactiveEnrollments}
|
||||
onSelect={this.toggleInactiveEnrollments}
|
||||
>
|
||||
{I18n.t('Inactive enrollments')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
value="display_concluded_enrollments"
|
||||
data-component="lmgb-student-filter-concluded-enrollments"
|
||||
selected={this.props.showConcludedEnrollments}
|
||||
onSelect={this.toggleConcludedEnrollments}
|
||||
>
|
||||
{I18n.t('Concluded enrollments')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
value="no_results_students"
|
||||
data-component="lmgb-student-filter-unassessed-students"
|
||||
selected={this.props.showUnassessedStudents}
|
||||
onSelect={this.toggleUnassessedStudents}
|
||||
>
|
||||
{I18n.t('Unassessed students')}
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - 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 {render, fireEvent} from '@testing-library/react'
|
||||
import OutcomeFilterView from '../OutcomeFilterView'
|
||||
|
||||
const defaultProps = () => ({
|
||||
showInactiveEnrollments: false,
|
||||
showConcludedEnrollments: false,
|
||||
showUnassessedStudents: false,
|
||||
toggleInactiveEnrollments: () => {},
|
||||
toggleConcludedEnrollments: () => {},
|
||||
toggleUnassessedStudents: () => {}
|
||||
})
|
||||
|
||||
it('calls toggleInactiveEnrollments to enable displaying inactive enrollments', () => {
|
||||
const toggleInactiveEnrollments = jest.fn()
|
||||
const {getByText, getByRole} = render(
|
||||
<OutcomeFilterView {...defaultProps()} toggleInactiveEnrollments={toggleInactiveEnrollments} />
|
||||
)
|
||||
fireEvent.click(getByRole('button'))
|
||||
fireEvent.click(getByText('Inactive enrollments'))
|
||||
expect(toggleInactiveEnrollments).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('calls toggleInactiveEnrollments to disable displaying inactive enrollments', () => {
|
||||
const toggleInactiveEnrollments = jest.fn()
|
||||
const {getByText, getByRole} = render(
|
||||
<OutcomeFilterView
|
||||
{...defaultProps()}
|
||||
toggleInactiveEnrollments={toggleInactiveEnrollments}
|
||||
showInactiveEnrollments
|
||||
/>
|
||||
)
|
||||
fireEvent.click(getByRole('button'))
|
||||
fireEvent.click(getByText('Inactive enrollments'))
|
||||
expect(toggleInactiveEnrollments).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('calls toggleConcludedEnrollments to enable displaying Concluded enrollments', () => {
|
||||
const toggleConcludedEnrollments = jest.fn()
|
||||
const {getByText, getByRole} = render(
|
||||
<OutcomeFilterView
|
||||
{...defaultProps()}
|
||||
toggleConcludedEnrollments={toggleConcludedEnrollments}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(getByRole('button'))
|
||||
fireEvent.click(getByText('Concluded enrollments'))
|
||||
expect(toggleConcludedEnrollments).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('calls toggleConcludedEnrollments to disable displaying Concluded enrollments', () => {
|
||||
const toggleConcludedEnrollments = jest.fn()
|
||||
const {getByText, getByRole} = render(
|
||||
<OutcomeFilterView
|
||||
{...defaultProps()}
|
||||
toggleConcludedEnrollments={toggleConcludedEnrollments}
|
||||
showConcludedEnrollments
|
||||
/>
|
||||
)
|
||||
fireEvent.click(getByRole('button'))
|
||||
fireEvent.click(getByText('Concluded enrollments'))
|
||||
expect(toggleConcludedEnrollments).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('calls toggleUnassessedStudents to enable displaying Unassessed students', () => {
|
||||
const toggleUnassessedStudents = jest.fn()
|
||||
const {getByText, getByRole} = render(
|
||||
<OutcomeFilterView {...defaultProps()} toggleUnassessedStudents={toggleUnassessedStudents} />
|
||||
)
|
||||
fireEvent.click(getByRole('button'))
|
||||
fireEvent.click(getByText('Unassessed students'))
|
||||
expect(toggleUnassessedStudents).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('calls toggleUnassessedStudents to disable displaying Unassessed students', () => {
|
||||
const toggleUnassessedStudents = jest.fn()
|
||||
const {getByText, getByRole} = render(
|
||||
<OutcomeFilterView
|
||||
{...defaultProps()}
|
||||
toggleUnassessedStudents={toggleUnassessedStudents}
|
||||
showUnassessedStudents
|
||||
/>
|
||||
)
|
||||
fireEvent.click(getByRole('button'))
|
||||
fireEvent.click(getByText('Unassessed students'))
|
||||
expect(toggleUnassessedStudents).toHaveBeenCalledWith(false)
|
||||
})
|
|
@ -367,3 +367,11 @@ $page-dark: #ebeff2;
|
|||
&.near-mastery { background: $near-color; }
|
||||
&.remedial { background: $remedial-color; }
|
||||
}
|
||||
|
||||
#learning-mastery-gradebook-filter {
|
||||
margin-#{direction(right)}: 3px;
|
||||
}
|
||||
|
||||
#lmgb-average-filter {
|
||||
margin-#{direction(right)}: 10px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="inline-block">
|
||||
<a class="al-trigger" href="#" data-append-to-body="true" role="button">
|
||||
<a class="al-trigger" href="#" data-append-to-body="true" role="button" {{#if inactive_concluded_lmgb_filters}}id="lmgb-average-filter"{{/if}}>
|
||||
<span class="current-label">{{#t "course_average"}}Course average{{/t}}</span>
|
||||
<i class="icon-mini-arrow-down"></i>
|
||||
</a>
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
<input type="checkbox" id="no_results_outcomes"/>
|
||||
<label for="no_results_outcomes">{{#t}}Hide outcomes with no results{{/t}}</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="no_results_students"/>
|
||||
<label for="no_results_students">{{#t}}Hide students with no results{{/t}}</label>
|
||||
</li>
|
||||
{{#unless inactive_concluded_lmgb_filters}}
|
||||
<li>
|
||||
<input type="checkbox" id="no_results_students"/>
|
||||
<label for="no_results_students">{{#t}}Hide students with no results{{/t}}</label>
|
||||
</li>
|
||||
{{/unless}}
|
||||
</ul>
|
||||
<ul class="operations unstyled" >
|
||||
<li class="operation-view">
|
||||
|
|
|
@ -55,3 +55,8 @@ improved_outcomes_management:
|
|||
display_name: Improved Outcomes Management
|
||||
description: Helps administrators and teachers make more meaningful decisions as they import,
|
||||
organize, and edit outcomes in their account and courses.
|
||||
inactive_concluded_lmgb_filters:
|
||||
state: hidden
|
||||
applies_to: RootAccount
|
||||
display_name: Student filters in LMGB
|
||||
description: Allow inactive and/or concluded enrollments to be displayed in LMGB
|
||||
|
|
|
@ -189,7 +189,11 @@ module Api::V1::OutcomeResults
|
|||
section_ids_func = if @section
|
||||
->(user) { [@section.id] }
|
||||
else
|
||||
enrollments = @context.student_enrollments.active.where(:user_id => serialized_rollup_pairs.map{|pair| pair[0].context.id}).to_a
|
||||
enrollments = if @context.root_account.feature_enabled?(:inactive_concluded_lmgb_filters)
|
||||
@context.all_accepted_student_enrollments.where(:user_id => serialized_rollup_pairs.map{|pair| pair[0].context.id}).to_a
|
||||
else
|
||||
@context.student_enrollments.active.where(:user_id => serialized_rollup_pairs.map{|pair| pair[0].context.id}).to_a
|
||||
end
|
||||
->(user) { enrollments.select{|e| e.user_id == user.id}.map(&:course_section_id) }
|
||||
end
|
||||
|
||||
|
|
|
@ -315,6 +315,235 @@ describe "Outcome Results API", type: :request do
|
|||
END
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable RSpec/NestedGroups
|
||||
context 'Student filters in LMGB FF' do
|
||||
before do
|
||||
@concluded_student = User.create!(:name => 'Student - Concluded')
|
||||
@concluded_student.register!
|
||||
@course.enroll_student(@concluded_student)
|
||||
create_outcome_assessment(student: @concluded_student)
|
||||
@concluded_student.enrollments.first.conclude
|
||||
|
||||
@inactive_student = User.create!(:name => 'Student - Inactive')
|
||||
@inactive_student.register!
|
||||
@course.enroll_student(@inactive_student)
|
||||
create_outcome_assessment(student: @inactive_student)
|
||||
@inactive_student.enrollments.first.deactivate
|
||||
|
||||
@no_results_student = User.create!(:name => 'Student - No Results')
|
||||
@no_results_student.register!
|
||||
@course.enroll_student(@no_results_student)
|
||||
|
||||
outcome_result # Creates result for enrolled student outcome_student
|
||||
end
|
||||
|
||||
context 'is disabled' do
|
||||
it "doesn't display no results students when exclude[]=missing_user_rollups is present" do
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=missing_user_rollups"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it "does display no results students when no excludes are present" do
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it 'does not display concluded students in a specific section' do
|
||||
section1 = add_section 's1', course: outcome_course
|
||||
student_in_section section1, user: @concluded_student, allow_multiple_enrollments: true
|
||||
@concluded_student.enrollments.map(&:deactivate)
|
||||
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?section_id=#{section1.id}"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
CSV
|
||||
end
|
||||
|
||||
it 'does not display inactive students in a specific section' do
|
||||
section1 = add_section 's1', course: outcome_course
|
||||
student_in_section section1, user: @inactive_student, allow_multiple_enrollments: true
|
||||
@inactive_student.enrollments.map(&:deactivate)
|
||||
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?section_id=#{section1.id}"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
CSV
|
||||
end
|
||||
end
|
||||
|
||||
context 'is enabled' do
|
||||
before do
|
||||
@course.account.enable_feature!(:inactive_concluded_lmgb_filters)
|
||||
end
|
||||
|
||||
it "doesn't display concluded students when exclude[]=concluded_enrollments is present" do
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=concluded_enrollments"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it "doesn't display concluded students when exclude[]=concluded_enrollments and section_id is given" do
|
||||
section1 = add_section 's1', course: outcome_course
|
||||
student_in_section section1, user: outcome_student, allow_multiple_enrollments: true
|
||||
student_in_section section1, user: @concluded_student, allow_multiple_enrollments: true
|
||||
@concluded_student.enrollments.map(&:conclude)
|
||||
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=concluded_enrollments§ion_id=#{section1.id}"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it "doesn't display inactive students when exclude[]=inactive_enrollments is present" do
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=inactive_enrollments"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it "doesn't display concluded students when exclude[]=inactive_enrollments and section_id is given" do
|
||||
section1 = add_section 's1', course: outcome_course
|
||||
student_in_section section1, user: outcome_student, allow_multiple_enrollments: true
|
||||
student_in_section section1, user: @inactive_student, allow_multiple_enrollments: true
|
||||
@inactive_student.enrollments.map(&:deactivate)
|
||||
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=inactive_enrollments§ion_id=#{section1.id}"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it "doesn't display no results students when exclude[]=missing_user_rollups is present" do
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=missing_user_rollups"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it 'displays concluded, inactive, and no results students when they are not excluded' do
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
context 'users with multiple enrollments' do
|
||||
before do
|
||||
@section1 = add_section 's1', course: outcome_course
|
||||
student_in_section @section1, user: outcome_student, allow_multiple_enrollments: true
|
||||
student_in_section @section1, user: @inactive_student, allow_multiple_enrollments: true
|
||||
student_in_section @section1, user: @concluded_student, allow_multiple_enrollments: true
|
||||
end
|
||||
|
||||
it 'displays concluded, inactive, and no results students when they arent excluded in a section' do
|
||||
student_in_section @section1, user: @no_results_student, allow_multiple_enrollments: true
|
||||
@no_results_student.enrollments.first.accept
|
||||
@inactive_student.enrollments.map(&:deactivate)
|
||||
@concluded_student.enrollments.map(&:conclude)
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?section_id=#{@section1.id}"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it 'students with an active enrollment are always present' do
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=inactive_enrollments&exclude[]=concluded_enrollments"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it 'users with inactive and concluded enrollments do display when only one is excluded' do
|
||||
@inactive_student.enrollments.last.conclude
|
||||
@concluded_student.enrollments.last.deactivate
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=inactive_enrollments"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{@concluded_student.name},#{@concluded_student.id},3.0,3.0
|
||||
#{@inactive_student.name},#{@inactive_student.id},3.0,3.0
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
|
||||
it 'users with inactive and concluded enrollments dont display when both are excluded' do
|
||||
@inactive_student.enrollments.last.conclude
|
||||
@concluded_student.enrollments.last.deactivate
|
||||
user_session @user
|
||||
get "/courses/#{@course.id}/outcome_rollups.csv?exclude[]=inactive_enrollments&exclude[]=concluded_enrollments"
|
||||
expect(response).to be_successful
|
||||
expect(response.body).to eq <<~CSV
|
||||
Student name,Student ID,new outcome result,new outcome mastery points
|
||||
#{outcome_student.name},#{outcome_student.id},3.0,3.0
|
||||
#{@no_results_student.name},#{@no_results_student.id},,3.0
|
||||
CSV
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/NestedGroups
|
||||
end
|
||||
|
||||
describe "user_ids parameter" do
|
||||
|
|
|
@ -1563,6 +1563,22 @@ describe GradebooksController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'inactive_concluded_lmgb_filters' do
|
||||
it 'is false if the feature flag is off' do
|
||||
@course.root_account.disable_feature! :inactive_concluded_lmgb_filters
|
||||
get :show, params: {course_id: @course.id}
|
||||
gradebook_env = assigns[:js_env][:GRADEBOOK_OPTIONS]
|
||||
expect(gradebook_env[:inactive_concluded_lmgb_filters]).to be_falsey
|
||||
end
|
||||
|
||||
it 'is true if the feature flag is on' do
|
||||
@course.root_account.enable_feature! :inactive_concluded_lmgb_filters
|
||||
get :show, params: {course_id: @course.id}
|
||||
gradebook_env = assigns[:js_env][:GRADEBOOK_OPTIONS]
|
||||
expect(gradebook_env[:inactive_concluded_lmgb_filters]).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -341,6 +341,106 @@ describe OutcomeResultsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with the inactive_concluded_lmgb_filters FF' do
|
||||
context 'enabled' do
|
||||
before do
|
||||
@course.account.enable_feature!(:inactive_concluded_lmgb_filters)
|
||||
end
|
||||
|
||||
it 'displays rollups for concluded enrollments when they are included' do
|
||||
StudentEnrollment.find_by(user_id: @student2.id).conclude
|
||||
json = parse_response(get_rollups({}))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(1)
|
||||
expect(rollups.first['scores'][0]['score']).to eq 1.0
|
||||
end
|
||||
|
||||
it 'does not display rollups for concluded enrollments when they are not included' do
|
||||
StudentEnrollment.find_by(user_id: @student2.id).conclude
|
||||
json = parse_response(get_rollups(exclude: 'concluded_enrollments'))
|
||||
expect(json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'displays rollups for a student who has an active and a concluded enrolllment regardless of filter' do
|
||||
section1 = add_section 's1', course: outcome_course
|
||||
student_in_section section1, user: @student2, allow_multiple_enrollments: true
|
||||
StudentEnrollment.find_by(course_section_id: section1.id).conclude
|
||||
json = parse_response(get_rollups(exclude: 'concluded_enrollments'))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(2)
|
||||
expect(rollups.first['scores'][0]['score']).to eq 1.0
|
||||
expect(rollups.second['scores'][0]['score']).to eq 1.0
|
||||
end
|
||||
|
||||
it 'displays rollups for inactive enrollments when they are included' do
|
||||
StudentEnrollment.find_by(user_id: @student2.id).deactivate
|
||||
json = parse_response(get_rollups({}))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(1)
|
||||
expect(rollups.first['scores'][0]['score']).to eq 1.0
|
||||
end
|
||||
|
||||
it 'does not display rollups for inactive enrollments when they are not included' do
|
||||
StudentEnrollment.find_by(user_id: @student2.id).deactivate
|
||||
json = parse_response(get_rollups(exclude: 'inactive_enrollments'))
|
||||
expect(json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}.count).to eq(0)
|
||||
end
|
||||
|
||||
context 'users with enrollments of different enrollment states' do
|
||||
before do
|
||||
StudentEnrollment.find_by(user_id: @student2.id).deactivate
|
||||
@section1 = add_section 's1', course: outcome_course
|
||||
student_in_section @section1, user: @student2, allow_multiple_enrollments: true
|
||||
StudentEnrollment.find_by(course_section_id: @section1.id).conclude
|
||||
end
|
||||
|
||||
it 'users whose enrollments are all excluded are not included' do
|
||||
json = parse_response(get_rollups(exclude: ['concluded_enrollments', 'inactive_enrollments']))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'users whose enrollments are all excluded are not included in a specified section' do
|
||||
json = parse_response(get_rollups(exclude: ['concluded_enrollments', 'inactive_enrollments'],
|
||||
section_id: @section1.id))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'users who contain an active enrollment are always included' do
|
||||
section3 = add_section 's3', course: outcome_course
|
||||
student_in_section section3, user: @student2, allow_multiple_enrollments: true
|
||||
json = parse_response(get_rollups(exclude: ['concluded_enrollments', 'inactive_enrollments']))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(3)
|
||||
expect(rollups.first['scores'][0]['score']).to eq 1.0
|
||||
expect(rollups.second['scores'][0]['score']).to eq 1.0
|
||||
expect(rollups.third['scores'][0]['score']).to eq 1.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'disabled' do
|
||||
before do
|
||||
@course.account.disable_feature!(:inactive_concluded_lmgb_filters)
|
||||
end
|
||||
|
||||
it 'does not display rollups for concluded enrollments when they are included' do
|
||||
StudentEnrollment.find_by(user_id: @student2.id).conclude
|
||||
json = parse_response(get_rollups({}))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'does not display for inactive enrollments when they are included' do
|
||||
StudentEnrollment.find_by(user_id: @student2.id).deactivate
|
||||
json = parse_response(get_rollups({}))
|
||||
rollups = json['rollups'].select{|r| r['links']['user'] == @student2.id.to_s}
|
||||
expect(rollups.count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'sorting' do
|
||||
it 'should validate sort_by parameter' do
|
||||
get_rollups(sort_by: 'garbage')
|
||||
|
|
|
@ -273,7 +273,75 @@ describe "outcome gradebook" do
|
|||
expect(means).to contain_exactly("2", "3")
|
||||
end
|
||||
|
||||
context 'with learning mastery scales enabled' do
|
||||
context 'with inactive_concluded_lmgb_filters enabled' do
|
||||
before(:once) do
|
||||
Account.default.set_feature_flag!('inactive_concluded_lmgb_filters', 'on')
|
||||
end
|
||||
|
||||
it 'correctly displays inactive enrollments when the filter option is selected' do
|
||||
StudentEnrollment.find_by(user_id: @student_1.id).deactivate
|
||||
|
||||
get "/courses/#{@course.id}/gradebook"
|
||||
select_learning_mastery
|
||||
wait_for_ajax_requests
|
||||
|
||||
active_students = [@student_2.name, @student_3.name]
|
||||
student_names = ff('.outcome-student-cell-content').map {|cell| cell.text.split("\n")[0]}
|
||||
expect(student_names.sort).to eq(active_students)
|
||||
|
||||
f('button[data-component="lmgb-student-filter-trigger"]').click
|
||||
f('span[data-component="lmgb-student-filter-inactive-enrollments"]').click
|
||||
wait_for_ajax_requests
|
||||
|
||||
active_students = [@student_1.name, @student_2.name, @student_3.name]
|
||||
student_names = ff('.outcome-student-cell-content').map {|cell| cell.text.split("\n")[0]}
|
||||
expect(student_names.sort).to eq(active_students)
|
||||
end
|
||||
|
||||
it 'correctly displays concluded enrollments when the filter option is selected' do
|
||||
StudentEnrollment.find_by(user_id: @student_1.id).conclude
|
||||
|
||||
get "/courses/#{@course.id}/gradebook"
|
||||
select_learning_mastery
|
||||
wait_for_ajax_requests
|
||||
|
||||
active_students = [@student_2.name, @student_3.name]
|
||||
student_names = ff('.outcome-student-cell-content').map {|cell| cell.text.split("\n")[0]}
|
||||
expect(student_names.sort).to eq(active_students)
|
||||
|
||||
f('button[data-component="lmgb-student-filter-trigger"]').click
|
||||
f('span[data-component="lmgb-student-filter-concluded-enrollments"]').click
|
||||
wait_for_ajax_requests
|
||||
|
||||
active_students = [@student_1.name, @student_2.name, @student_3.name]
|
||||
student_names = ff('.outcome-student-cell-content').map {|cell| cell.text.split("\n")[0]}
|
||||
expect(student_names.sort).to eq(active_students)
|
||||
end
|
||||
|
||||
it 'correctly displays unassessed students when the filter option is selected' do
|
||||
student_4 = User.create!(:name => 'Unassessed Student')
|
||||
student_4.register!
|
||||
@course.enroll_student(student_4)
|
||||
|
||||
get "/courses/#{@course.id}/gradebook"
|
||||
select_learning_mastery
|
||||
wait_for_ajax_requests
|
||||
|
||||
active_students = [@student_1.name, @student_2.name, @student_3.name]
|
||||
student_names = ff('.outcome-student-cell-content').map {|cell| cell.text.split("\n")[0]}
|
||||
expect(student_names.sort).to eq(active_students)
|
||||
|
||||
f('button[data-component="lmgb-student-filter-trigger"]').click
|
||||
f('span[data-component="lmgb-student-filter-unassessed-students"]').click
|
||||
wait_for_ajax_requests
|
||||
|
||||
active_students = [@student_1.name, @student_2.name, @student_3.name, student_4.name]
|
||||
student_names = ff('.outcome-student-cell-content').map {|cell| cell.text.split("\n")[0]}
|
||||
expect(student_names.sort).to eq(active_students.sort)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with learning mastery scales enabled' do
|
||||
before(:once) do
|
||||
@rating1 = OutcomeProficiencyRating.new(description: 'best', points: 10, mastery: true, color: '00ff00')
|
||||
@rating2 = OutcomeProficiencyRating.new(description: 'worst', points: 0, mastery: false, color: 'ff0000')
|
||||
|
|
Loading…
Reference in New Issue