load students and graders into moderation table
closes GRADE-964 closes GRADE-1024 closes GRADE-1059 refs GRADE-877 refs GRADE-1166 test plan: A. Setup 1. Enable Anonymous Moderated Marking 2. Select or create a course a. with Anonymous Marking enabled b. with three instructors (A, B, and C) c. with 51 or more students * this is for multiple pages of students 3. Select or create an assignment a. with Moderated Grading b. with Anonymous Grading c. with instructor A as the final grader 4. As each instructor (B and C) a. give at least one student a provisional grade on each assignment (using SpeedGrader) * This SHOULD create a new moderation_grader for each user and for each assignment B. Verify 1. Enable grader_names_visible_to_final_grader assignment.grader_names_visible_to_final_grader = true assignment.save! 2. Log in or act as instructor A 3. Visit the moderation page for the assignment 4. Verify both instructors B an C are named in the table 5. Verify all students are loaded into the table 6. Verify the correct grades are displayed in the table 7. Verify grades as associated with the correct graders 8. Verify ungraded student/grader pairs show as "–" C. Verify with Other Graders Anonymous 1. Disable grader_names_visible_to_final_grader assignment.grader_names_visible_to_final_grader = false assignment.save! 2. Log in or act as instructor A 3. Visit the moderation page for the assignment 4. Verify column headers display anonymous grader names * Students are not yet anonymized. Change-Id: I155567cf461380f7018c964e74d775ed182c9954 Reviewed-on: https://gerrit.instructure.com/151269 Tested-by: Jenkins Reviewed-by: Adrian Packel <apackel@instructure.com> Reviewed-by: Keith T. Garner <kgarner@instructure.com> QA-Review: Indira Pai <ipai@instructure.com> Product-Review: Sidharth Oberoi <soberoi@instructure.com>
This commit is contained in:
parent
31bae72fe9
commit
c8229d721e
|
@ -858,7 +858,7 @@ class SubmissionsApiController < ApplicationController
|
|||
student_scope = student_scope.order(:id)
|
||||
students = Api.paginate(student_scope, self, api_v1_course_assignment_gradeable_students_url(@context, @assignment))
|
||||
if (include_pg = includes.include?('provisional_grades'))
|
||||
return unless authorized_action(@context, @current_user, :moderate_grades)
|
||||
render_unauthorized_action and return unless @assignment.permits_moderation?(@current_user)
|
||||
submissions = @assignment.submissions.where(user_id: students).preload(:provisional_grades).index_by(&:user_id)
|
||||
selections = @assignment.moderated_grading_selections.where(student_id: students).index_by(&:student_id)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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, {Component} from 'react'
|
||||
import {arrayOf, shape, string} from 'prop-types'
|
||||
import Text from '@instructure/ui-elements/lib/components/Text'
|
||||
import I18n from 'i18n!assignment_grade_summary'
|
||||
|
||||
import GridRow from './GridRow'
|
||||
|
||||
export default class Grid extends Component {
|
||||
static propTypes = {
|
||||
graders: arrayOf(
|
||||
shape({
|
||||
graderName: string,
|
||||
graderId: string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
grades: shape({}).isRequired,
|
||||
rows: arrayOf(
|
||||
shape({
|
||||
studentId: string.isRequired,
|
||||
studentName: string.isRequired
|
||||
}).isRequired
|
||||
).isRequired
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return Object.keys(nextProps).some(key => this.props[key] !== nextProps[key])
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="GradesGrid">
|
||||
<table>
|
||||
<thead className="GradesGrid__Header">
|
||||
<tr className="GradesGrid__HeaderRow">
|
||||
<th className="GradesGrid__StudentColumnHeader" scope="col">
|
||||
<Text>{I18n.t('Student')}</Text>
|
||||
</th>
|
||||
|
||||
{this.props.graders.map((grader, index) => (
|
||||
<th className="GradesGrid__GraderHeader" key={grader.graderId} scope="col">
|
||||
<Text>
|
||||
{grader.graderName ||
|
||||
I18n.t('Grader %{graderNumber}', {graderNumber: I18n.n(index + 1)})}
|
||||
</Text>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="GradesGrid__Body">
|
||||
{this.props.rows.map((row, index) => (
|
||||
<GridRow
|
||||
graders={this.props.graders}
|
||||
grades={this.props.grades[row.studentId]}
|
||||
key={index}
|
||||
row={row}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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, {Component} from 'react'
|
||||
import {arrayOf, shape, string} from 'prop-types'
|
||||
import Text from '@instructure/ui-elements/lib/components/Text'
|
||||
import I18n from 'i18n!assignment_grade_summary'
|
||||
|
||||
function getGrade(graderId, grades) {
|
||||
const gradeInfo = grades[graderId]
|
||||
return gradeInfo && gradeInfo.score != null ? I18n.n(gradeInfo.score) : '–'
|
||||
}
|
||||
|
||||
export default class GridRow extends Component {
|
||||
static propTypes = {
|
||||
graders: arrayOf(
|
||||
shape({
|
||||
graderName: string,
|
||||
graderId: string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
grades: shape({}),
|
||||
row: shape({
|
||||
studentId: string.isRequired,
|
||||
studentName: string.isRequired
|
||||
}).isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
grades: {}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return Object.keys(nextProps).some(key => this.props[key] !== nextProps[key])
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<tr className={`GradesGrid__BodyRow student_${this.props.row.studentId}`}>
|
||||
<th className="GradesGrid__BodyRowHeader" scope="row">
|
||||
<Text>{this.props.row.studentName}</Text>
|
||||
</th>
|
||||
|
||||
{this.props.graders.map(grader => {
|
||||
const classNames = ['GradesGrid__ProvisionalGradeCell', `grader_${grader.graderId}`]
|
||||
|
||||
return (
|
||||
<td className={classNames.join(' ')} key={grader.graderId}>
|
||||
<Text>{getGrade(grader.graderId, this.props.grades)}</Text>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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, {Component} from 'react'
|
||||
import {arrayOf, shape, string} from 'prop-types'
|
||||
import I18n from 'i18n!assignment_grade_summary'
|
||||
|
||||
import Grid from './Grid'
|
||||
|
||||
function studentToRow(student, index) {
|
||||
return {
|
||||
studentId: student.id,
|
||||
studentName:
|
||||
student.displayName || I18n.t('Student %{studentNumber}', {studentNumber: I18n.n(index + 1)})
|
||||
}
|
||||
}
|
||||
|
||||
export default class GradesGrid extends Component {
|
||||
static propTypes = {
|
||||
graders: arrayOf(
|
||||
shape({
|
||||
graderName: string,
|
||||
graderId: string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
grades: shape({}).isRequired,
|
||||
students: arrayOf(
|
||||
shape({
|
||||
displayName: string,
|
||||
id: string.isRequired
|
||||
}).isRequired
|
||||
).isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
rows: this.props.students.map(studentToRow)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.students !== this.props.students) {
|
||||
this.setState({
|
||||
rows: nextProps.students.map(studentToRow)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="GradesGridContainer">
|
||||
<Grid graders={this.props.graders} grades={this.props.grades} rows={this.state.rows} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,51 +16,96 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {arrayOf, shape, string} from 'prop-types'
|
||||
import React, {Component} from 'react'
|
||||
import {arrayOf, func, shape, string} from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import Text from '@instructure/ui-elements/lib/components/Text'
|
||||
import Spinner from '@instructure/ui-elements/lib/components/Spinner'
|
||||
import View from '@instructure/ui-layout/lib/components/View'
|
||||
import I18n from 'i18n!assignment_grade_summary'
|
||||
|
||||
import '../../../context_cards/StudentContextCardTrigger'
|
||||
import {loadStudents} from '../students/StudentActions'
|
||||
import GradesGrid from './GradesGrid'
|
||||
import Header from './Header'
|
||||
|
||||
function Layout(props) {
|
||||
if (props.graders.length === 0) {
|
||||
class Layout extends Component {
|
||||
static propTypes = {
|
||||
assignment: shape({
|
||||
title: string.isRequired
|
||||
}).isRequired,
|
||||
graders: arrayOf(
|
||||
shape({
|
||||
graderId: string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
loadStudents: func.isRequired,
|
||||
provisionalGrades: shape({}).isRequired,
|
||||
students: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired
|
||||
})
|
||||
).isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.graders.length) {
|
||||
this.props.loadStudents()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.graders.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<Header assignment={this.props.assignment} />
|
||||
|
||||
<View as="div" margin="medium 0 0 0">
|
||||
<Text color="warning">
|
||||
{I18n.t(
|
||||
'Moderation is unable to occur at this time due to grades not being submitted.'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header assignment={props.assignment} />
|
||||
<Header assignment={this.props.assignment} />
|
||||
|
||||
<Text>
|
||||
{I18n.t('Moderation is unable to occur at this time due to grades not being submitted.')}
|
||||
</Text>
|
||||
<View as="div" margin="large 0 0 0">
|
||||
{this.props.students.length > 0 ? (
|
||||
<GradesGrid
|
||||
graders={this.props.graders}
|
||||
grades={this.props.provisionalGrades}
|
||||
students={this.props.students}
|
||||
/>
|
||||
) : (
|
||||
<Spinner title={I18n.t('Students are loading')} />
|
||||
)}
|
||||
</View>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header assignment={props.assignment} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
assignment: shape({
|
||||
title: string.isRequired
|
||||
}).isRequired,
|
||||
graders: arrayOf(
|
||||
shape({
|
||||
graderId: string.isRequired
|
||||
})
|
||||
).isRequired
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
assignment: state.context.assignment,
|
||||
graders: state.context.graders
|
||||
graders: state.context.graders,
|
||||
provisionalGrades: state.grades.provisionalGrades,
|
||||
students: state.students.list
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Layout)
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
loadStudents() {
|
||||
dispatch(loadStudents())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Layout)
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
import {applyMiddleware, combineReducers, createStore} from 'redux'
|
||||
import ReduxThunk from 'redux-thunk'
|
||||
|
||||
import gradesReducer from './grades/gradesReducer'
|
||||
import studentsReducer from './students/studentsReducer'
|
||||
|
||||
const createStoreWithMiddleware = applyMiddleware(ReduxThunk)(createStore)
|
||||
|
||||
export default function configureStore(env) {
|
||||
|
@ -29,7 +32,9 @@ export default function configureStore(env) {
|
|||
}
|
||||
|
||||
const reducer = combineReducers({
|
||||
context: contextReducer
|
||||
context: contextReducer,
|
||||
grades: gradesReducer,
|
||||
students: studentsReducer
|
||||
})
|
||||
|
||||
return createStoreWithMiddleware(reducer)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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/>.
|
||||
*/
|
||||
|
||||
export const ADD_PROVISIONAL_GRADES = 'ADD_PROVISIONAL_GRADES'
|
||||
|
||||
export function addProvisionalGrades(provisionalGrades) {
|
||||
return {type: ADD_PROVISIONAL_GRADES, payload: {provisionalGrades}}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 buildReducer from '../buildReducer'
|
||||
import {ADD_PROVISIONAL_GRADES} from './GradeActions'
|
||||
|
||||
function addProvisionalGrades(state, grades) {
|
||||
const provisionalGrades = {...state.provisionalGrades}
|
||||
grades.forEach(grade => {
|
||||
provisionalGrades[grade.studentId] = provisionalGrades[grade.studentId] || {}
|
||||
provisionalGrades[grade.studentId][grade.graderId] = grade
|
||||
})
|
||||
return {...state, provisionalGrades}
|
||||
}
|
||||
|
||||
const handlers = {}
|
||||
|
||||
handlers[ADD_PROVISIONAL_GRADES] = (state, action) =>
|
||||
addProvisionalGrades(state, action.payload.provisionalGrades)
|
||||
|
||||
export default buildReducer(handlers, {
|
||||
provisionalGrades: {}
|
||||
})
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 {addProvisionalGrades} from '../grades/GradeActions'
|
||||
import * as StudentsApi from './StudentsApi'
|
||||
|
||||
export const ADD_STUDENTS = 'ADD_STUDENTS'
|
||||
export const FAILURE = 'FAILURE'
|
||||
export const SET_LOAD_STUDENTS_STATUS = 'SET_LOAD_STUDENTS_STATUS'
|
||||
export const STARTED = 'STARTED'
|
||||
export const SUCCESS = 'SUCCESS'
|
||||
|
||||
export function addStudents(students) {
|
||||
return {type: ADD_STUDENTS, payload: {students}}
|
||||
}
|
||||
|
||||
export function setLoadStudentsStatus(status) {
|
||||
return {type: SET_LOAD_STUDENTS_STATUS, payload: {status}}
|
||||
}
|
||||
|
||||
export function loadStudents() {
|
||||
return (dispatch, getState) => {
|
||||
const {assignment} = getState().context
|
||||
|
||||
dispatch(setLoadStudentsStatus(STARTED))
|
||||
|
||||
StudentsApi.loadStudents(assignment.courseId, assignment.id, {
|
||||
onAllPagesLoaded() {
|
||||
dispatch(setLoadStudentsStatus(SUCCESS))
|
||||
},
|
||||
onFailure() {
|
||||
dispatch(setLoadStudentsStatus(FAILURE))
|
||||
},
|
||||
onPageLoaded({provisionalGrades, students}) {
|
||||
dispatch(addStudents(students))
|
||||
dispatch(addProvisionalGrades(provisionalGrades))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 axios from 'axios'
|
||||
|
||||
import parseLinkHeader from '../../../shared/helpers/parseLinkHeader'
|
||||
|
||||
const STUDENTS_PER_PAGE = 50
|
||||
|
||||
function normalizeStudentPage(data) {
|
||||
const students = []
|
||||
const provisionalGrades = []
|
||||
|
||||
data.forEach(studentDatum => {
|
||||
const studentId = studentDatum.id
|
||||
|
||||
students.push({
|
||||
displayName: studentDatum.display_name,
|
||||
id: studentId
|
||||
})
|
||||
|
||||
studentDatum.provisional_grades.forEach(gradeDatum => {
|
||||
provisionalGrades.push({
|
||||
grade: gradeDatum.grade,
|
||||
graderId: gradeDatum.scorer_id || gradeDatum.anonymous_grader_id,
|
||||
id: gradeDatum.provisional_grade_id,
|
||||
score: gradeDatum.score,
|
||||
selected: studentDatum.selected_provisional_grade_id === gradeDatum.provisional_grade_id,
|
||||
studentId
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return {provisionalGrades, students}
|
||||
}
|
||||
|
||||
function getAllStudentsPages(url, callbacks) {
|
||||
axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
callbacks.onPageLoaded(normalizeStudentPage(response.data))
|
||||
const linkHeaders = parseLinkHeader(response)
|
||||
if (linkHeaders.next) {
|
||||
getAllStudentsPages(linkHeaders.next, callbacks)
|
||||
} else {
|
||||
callbacks.onAllPagesLoaded()
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
callbacks.onFailure(response)
|
||||
return Promise.reject(response) // allow for shared error handling
|
||||
})
|
||||
}
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export function loadStudents(courseId, assignmentId, callbacks) {
|
||||
const queryParams = `include[]=provisional_grades&per_page=${STUDENTS_PER_PAGE}`
|
||||
const url = `/api/v1/courses/${courseId}/assignments/${assignmentId}/gradeable_students?${queryParams}`
|
||||
|
||||
getAllStudentsPages(url, callbacks)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 buildReducer from '../buildReducer'
|
||||
import {ADD_STUDENTS, SET_LOAD_STUDENTS_STATUS} from './StudentActions'
|
||||
|
||||
function addStudents(state, students) {
|
||||
return {...state, list: [...state.list, ...students]}
|
||||
}
|
||||
|
||||
function setLoadStudentsStatus(state, loadStudentsStatus) {
|
||||
return {...state, loadStudentsStatus}
|
||||
}
|
||||
|
||||
const handlers = {}
|
||||
|
||||
handlers[ADD_STUDENTS] = (state, {payload}) => addStudents(state, payload.students)
|
||||
|
||||
handlers[SET_LOAD_STUDENTS_STATUS] = (state, {payload}) =>
|
||||
setLoadStudentsStatus(state, payload.status)
|
||||
|
||||
export default buildReducer(handlers, {
|
||||
list: [],
|
||||
loadStudentsStatus: null
|
||||
})
|
|
@ -18,6 +18,12 @@
|
|||
|
||||
@import "base/environment";
|
||||
|
||||
$headerHeight: 50px;
|
||||
$rowHeight: 50px;
|
||||
$graderColumnWidth: 116px;
|
||||
$studentColumnWidth: 200px;
|
||||
$maxGridBodyHeight: $rowHeight * 10;
|
||||
|
||||
.ic-Layout-columns {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -27,3 +33,77 @@
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.GradesGridContainer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* GradesGrid */
|
||||
|
||||
.GradesGrid {
|
||||
overflow-x: auto;
|
||||
|
||||
&, * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
tr {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th, td {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.GradesGrid__Header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.GradesGrid__HeaderRow {
|
||||
height: $headerHeight;
|
||||
line-height: $headerHeight;
|
||||
}
|
||||
|
||||
.GradesGrid__GraderHeader {
|
||||
overflow: hidden;
|
||||
padding: 0 $ic-sp;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
width: $graderColumnWidth;
|
||||
}
|
||||
|
||||
.GradesGrid__StudentColumnHeader {
|
||||
padding: 0 $ic-sp;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
width: $studentColumnWidth;
|
||||
}
|
||||
|
||||
.GradesGrid__Body {
|
||||
display: block;
|
||||
max-height: $maxGridBodyHeight;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.GradesGrid__BodyRow {
|
||||
height: $rowHeight;
|
||||
line-height: $rowHeight;
|
||||
}
|
||||
|
||||
.GradesGrid__BodyRowHeader {
|
||||
font-weight: normal;
|
||||
padding: 0 $ic-sp;
|
||||
text-align: start;
|
||||
width: $studentColumnWidth;
|
||||
}
|
||||
|
||||
.GradesGrid__ProvisionalGradeCell {
|
||||
text-align: center;
|
||||
width: $graderColumnWidth;
|
||||
}
|
||||
|
||||
/* End GradesGrid */
|
||||
|
|
|
@ -4241,6 +4241,28 @@ describe 'Submissions API', type: :request do
|
|||
api_call_as_user(@ta, :get, @path, @params, {}, {}, { :expected_status => 401 })
|
||||
end
|
||||
|
||||
context "when Anonymous Moderated Marking is enabled" do
|
||||
before(:once) { @course.root_account.enable_feature!(:anonymous_moderated_marking) }
|
||||
|
||||
it "is unauthorized when the user is not the assigned final grader" do
|
||||
api_call_as_user(@teacher, :get, @path, @params, {}, {}, expected_status: 401)
|
||||
end
|
||||
|
||||
it "is unauthorized when the user is an account admin without 'Select Final Grade for Moderation' permission" do
|
||||
@course.account.role_overrides.create!(role: admin_role, enabled: false, permission: :select_final_grade)
|
||||
api_call_as_user(account_admin_user, :get, @path, @params, {}, {}, expected_status: 401)
|
||||
end
|
||||
|
||||
it "is authorized when the user is the final grader" do
|
||||
@assignment.update!(final_grader: @teacher, grader_count: 2)
|
||||
api_call_as_user(@teacher, :get, @path, @params, {}, {}, expected_status: 200)
|
||||
end
|
||||
|
||||
it "is authorized when the user is an account admin with 'Select Final Grade for Moderation' permission" do
|
||||
api_call_as_user(account_admin_user, :get, @path, @params, {}, {}, expected_status: 200)
|
||||
end
|
||||
end
|
||||
|
||||
it "includes provisional grades with selections" do
|
||||
sub = @assignment.grade_student(@student1, :score => 90, :grader => @ta, :provisional => true).first
|
||||
pg = sub.provisional_grades.first
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 {mount} from 'enzyme'
|
||||
|
||||
import GridRow from 'jsx/assignments/GradeSummary/components/GradesGrid/GridRow'
|
||||
|
||||
QUnit.module('GradeSummary GridRow', suiteHooks => {
|
||||
let props
|
||||
let wrapper
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
props = {
|
||||
graders: [
|
||||
{graderId: '1101', graderName: 'Miss Frizzle'},
|
||||
{graderId: '1102', graderName: 'Mr. Keating'}
|
||||
],
|
||||
grades: {
|
||||
1101: {
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4601',
|
||||
score: 10,
|
||||
selected: false,
|
||||
studentId: '1111'
|
||||
},
|
||||
1102: {
|
||||
grade: 'B',
|
||||
graderId: '1102',
|
||||
id: '4602',
|
||||
score: 8.9,
|
||||
selected: false,
|
||||
studentId: '1111'
|
||||
}
|
||||
},
|
||||
row: {
|
||||
studentId: '1111',
|
||||
studentName: 'Adam Jones'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
suiteHooks.afterEach(() => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
function mountComponent() {
|
||||
// React is unable to render partial table structures.
|
||||
wrapper = mount(
|
||||
<table>
|
||||
<tbody>
|
||||
<GridRow {...props} />
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
test('displays the student name in the row header', () => {
|
||||
mountComponent()
|
||||
const header = wrapper.find('th.GradesGrid__BodyRowHeader')
|
||||
equal(header.text(), 'Adam Jones')
|
||||
})
|
||||
|
||||
test('includes a cell for each grader', () => {
|
||||
mountComponent()
|
||||
strictEqual(wrapper.find('td.GradesGrid__ProvisionalGradeCell').length, 2)
|
||||
})
|
||||
|
||||
test('displays the score of a provisional grade in the matching cell', () => {
|
||||
mountComponent()
|
||||
const cell = wrapper.find('td.grader_1102')
|
||||
equal(cell.text(), '8.9')
|
||||
})
|
||||
|
||||
test('displays zero scores', () => {
|
||||
props.grades[1101].score = 0
|
||||
mountComponent()
|
||||
const cell = wrapper.find('td.grader_1101')
|
||||
equal(cell.text(), '0')
|
||||
})
|
||||
|
||||
test('displays "–" (en dash) when the student grade for a given grader was cleared', () => {
|
||||
props.grades[1101].score = null
|
||||
mountComponent()
|
||||
const cell = wrapper.find('td.grader_1101')
|
||||
equal(cell.text(), '–')
|
||||
})
|
||||
|
||||
test('displays "–" (en dash) when the student was not graded by a given grader', () => {
|
||||
delete props.grades[1101]
|
||||
mountComponent()
|
||||
const cell = wrapper.find('td.grader_1101')
|
||||
equal(cell.text(), '–')
|
||||
})
|
||||
|
||||
test('displays "–" (en dash) when the student has no provisional grades', () => {
|
||||
delete props.grades
|
||||
mountComponent()
|
||||
const cell = wrapper.find('td.grader_1101')
|
||||
equal(cell.text(), '–')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 {mount} from 'enzyme'
|
||||
|
||||
import Grid from 'jsx/assignments/GradeSummary/components/GradesGrid/Grid'
|
||||
|
||||
QUnit.module('GradeSummary Grid', suiteHooks => {
|
||||
let props
|
||||
let wrapper
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
props = {
|
||||
graders: [
|
||||
{graderId: '1101', graderName: 'Miss Frizzle'},
|
||||
{graderId: '1102', graderName: 'Mr. Keating'}
|
||||
],
|
||||
grades: {
|
||||
1111: {
|
||||
1101: {
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4601',
|
||||
score: 10,
|
||||
selected: false,
|
||||
studentId: '1111'
|
||||
},
|
||||
1102: {
|
||||
grade: 'B',
|
||||
graderId: '1102',
|
||||
id: '4602',
|
||||
score: 8.9,
|
||||
selected: false,
|
||||
studentId: '1111'
|
||||
}
|
||||
},
|
||||
1112: {
|
||||
1102: {
|
||||
grade: 'C',
|
||||
graderId: '1102',
|
||||
id: '4603',
|
||||
score: 7.8,
|
||||
selected: false,
|
||||
studentId: '1112'
|
||||
}
|
||||
},
|
||||
1113: {
|
||||
1101: {
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4604',
|
||||
score: 10,
|
||||
selected: false,
|
||||
studentId: '1113'
|
||||
}
|
||||
}
|
||||
},
|
||||
rows: [
|
||||
{studentId: '1111', studentName: 'Adam Jones'},
|
||||
{studentId: '1112', studentName: 'Betty Ford'},
|
||||
{studentId: '1113', studentName: 'Charlie Xi'},
|
||||
{studentId: '1114', studentName: 'Dana Smith'}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
suiteHooks.afterEach(() => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
function mountComponent() {
|
||||
wrapper = mount(<Grid {...props} />)
|
||||
}
|
||||
|
||||
test('includes a column header for each grader', () => {
|
||||
mountComponent()
|
||||
strictEqual(wrapper.find('th.GradesGrid__GraderHeader').length, 2)
|
||||
})
|
||||
|
||||
test('displays the grader names in the column headers', () => {
|
||||
mountComponent()
|
||||
const headers = wrapper.find('th.GradesGrid__GraderHeader')
|
||||
deepEqual(headers.map(header => header.text()), ['Miss Frizzle', 'Mr. Keating'])
|
||||
})
|
||||
|
||||
test('enumerates graders for names when graders are anonymous', () => {
|
||||
props.graders[0].graderName = null
|
||||
props.graders[1].graderName = null
|
||||
mountComponent()
|
||||
const headers = wrapper.find('th.GradesGrid__GraderHeader')
|
||||
deepEqual(headers.map(header => header.text()), ['Grader 1', 'Grader 2'])
|
||||
})
|
||||
|
||||
test('includes a GridRow for each student', () => {
|
||||
mountComponent()
|
||||
strictEqual(wrapper.find('GridRow').length, 4)
|
||||
})
|
||||
|
||||
test('sends graders to each GridRow', () => {
|
||||
mountComponent()
|
||||
wrapper.find('GridRow').forEach(gridRow => {
|
||||
strictEqual(gridRow.prop('graders'), props.graders)
|
||||
})
|
||||
})
|
||||
|
||||
test('sends student-specific grades to each GridRow', () => {
|
||||
mountComponent()
|
||||
const gridRow = wrapper.find('GridRow').at(1)
|
||||
strictEqual(gridRow.prop('grades'), props.grades[1112])
|
||||
})
|
||||
|
||||
test('sends the related row to each GridRow', () => {
|
||||
mountComponent()
|
||||
const gridRow = wrapper.find('GridRow').at(1)
|
||||
strictEqual(gridRow.prop('row'), props.rows[1])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 {mount} from 'enzyme'
|
||||
|
||||
import GradesGrid from 'jsx/assignments/GradeSummary/components/GradesGrid'
|
||||
|
||||
QUnit.module('GradeSummary GradesGrid', suiteHooks => {
|
||||
let props
|
||||
let wrapper
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
props = {
|
||||
graders: [
|
||||
{graderId: '1101', graderName: 'Miss Frizzle'},
|
||||
{graderId: '1102', graderName: 'Mr. Keating'}
|
||||
],
|
||||
grades: {
|
||||
1111: {
|
||||
1101: {
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4601',
|
||||
score: 10,
|
||||
selected: false,
|
||||
studentId: '1111'
|
||||
},
|
||||
1102: {
|
||||
grade: 'B',
|
||||
graderId: '1102',
|
||||
id: '4602',
|
||||
score: 8.9,
|
||||
selected: false,
|
||||
studentId: '1111'
|
||||
}
|
||||
},
|
||||
1112: {
|
||||
1102: {
|
||||
grade: 'C',
|
||||
graderId: '1102',
|
||||
id: '4603',
|
||||
score: 7.8,
|
||||
selected: false,
|
||||
studentId: '1112'
|
||||
}
|
||||
},
|
||||
1113: {
|
||||
1101: {
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4604',
|
||||
score: 10,
|
||||
selected: false,
|
||||
studentId: '1113'
|
||||
}
|
||||
}
|
||||
},
|
||||
students: [
|
||||
{id: '1111', displayName: 'Adam Jones'},
|
||||
{id: '1112', displayName: 'Betty Ford'},
|
||||
{id: '1113', displayName: 'Charlie Xi'},
|
||||
{id: '1114', displayName: 'Dana Smith'}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
suiteHooks.afterEach(() => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
function mountComponent() {
|
||||
wrapper = mount(<GradesGrid {...props} />)
|
||||
}
|
||||
|
||||
function getGraderNames() {
|
||||
const headers = wrapper.find('th.GradesGrid__GraderHeader')
|
||||
return headers.map(header => header.text())
|
||||
}
|
||||
|
||||
function getStudentNames() {
|
||||
const headers = wrapper.find('th.GradesGrid__BodyRowHeader')
|
||||
return headers.map(header => header.text())
|
||||
}
|
||||
|
||||
test('displays the grader names in the column headers', () => {
|
||||
mountComponent()
|
||||
deepEqual(getGraderNames(), ['Miss Frizzle', 'Mr. Keating'])
|
||||
})
|
||||
|
||||
test('enumerates graders for names when graders are anonymous', () => {
|
||||
props.graders[0].graderName = null
|
||||
props.graders[1].graderName = null
|
||||
mountComponent()
|
||||
deepEqual(getGraderNames(), ['Grader 1', 'Grader 2'])
|
||||
})
|
||||
|
||||
test('includes a row for each student', () => {
|
||||
mountComponent()
|
||||
strictEqual(wrapper.find('tr.GradesGrid__BodyRow').length, 4)
|
||||
})
|
||||
|
||||
test('adds rows as students are added', () => {
|
||||
const {students} = props
|
||||
props.students = students.slice(0, 2)
|
||||
mountComponent()
|
||||
wrapper.setProps({students})
|
||||
strictEqual(wrapper.find('tr.GradesGrid__BodyRow').length, 4)
|
||||
})
|
||||
|
||||
test('displays the student names in the row headers', () => {
|
||||
mountComponent()
|
||||
deepEqual(getStudentNames(), ['Adam Jones', 'Betty Ford', 'Charlie Xi', 'Dana Smith'])
|
||||
})
|
||||
|
||||
test('enumerates students for names when students are anonymous', () => {
|
||||
for (let i = 0; i < props.students.length; i++) {
|
||||
props.students[i].displayName = null
|
||||
}
|
||||
mountComponent()
|
||||
deepEqual(getStudentNames(), ['Student 1', 'Student 2', 'Student 3', 'Student 4'])
|
||||
})
|
||||
|
||||
test('enumerates additional students for names as they are added', () => {
|
||||
for (let i = 0; i < props.students.length; i++) {
|
||||
props.students[i].displayName = null
|
||||
}
|
||||
const {students} = props
|
||||
props.students = students.slice(0, 2)
|
||||
mountComponent()
|
||||
wrapper.setProps({students})
|
||||
deepEqual(getStudentNames(), ['Student 1', 'Student 2', 'Student 3', 'Student 4'])
|
||||
})
|
||||
})
|
|
@ -20,6 +20,7 @@ import React from 'react'
|
|||
import {mount} from 'enzyme'
|
||||
import {Provider} from 'react-redux'
|
||||
|
||||
import * as StudentActions from 'jsx/assignments/GradeSummary/students/StudentActions'
|
||||
import Layout from 'jsx/assignments/GradeSummary/components/Layout'
|
||||
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
|
||||
|
||||
|
@ -35,11 +36,18 @@ QUnit.module('GradeSummary Layout', suiteHooks => {
|
|||
id: '2301',
|
||||
title: 'Example Assignment'
|
||||
},
|
||||
graders: [{graderId: '1101'}, {graderId: '1102'}]
|
||||
graders: [
|
||||
{graderId: '1101', graderName: 'Miss Frizzle'},
|
||||
{graderId: '1102', graderName: 'Mr. Keating'}
|
||||
]
|
||||
}
|
||||
sinon
|
||||
.stub(StudentActions, 'loadStudents')
|
||||
.returns(StudentActions.setLoadStudentsStatus(StudentActions.LOAD_STUDENTS_STARTED))
|
||||
})
|
||||
|
||||
suiteHooks.afterEach(() => {
|
||||
StudentActions.loadStudents.restore()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
|
@ -57,14 +65,56 @@ QUnit.module('GradeSummary Layout', suiteHooks => {
|
|||
strictEqual(wrapper.find('Header').length, 1)
|
||||
})
|
||||
|
||||
test('includes a "no graders" message when there are no graders', () => {
|
||||
storeEnv.graders = []
|
||||
mountComponent()
|
||||
ok(wrapper.text().includes('Moderation is unable to occur'))
|
||||
})
|
||||
|
||||
test('excludes the "no graders" message when there are graders', () => {
|
||||
mountComponent()
|
||||
notOk(wrapper.text().includes('Moderation is unable to occur'))
|
||||
})
|
||||
|
||||
test('loads students upon mounting', () => {
|
||||
mountComponent()
|
||||
strictEqual(StudentActions.loadStudents.callCount, 1)
|
||||
})
|
||||
|
||||
QUnit.module('when there are no graders', hooks => {
|
||||
hooks.beforeEach(() => {
|
||||
storeEnv.graders = []
|
||||
})
|
||||
|
||||
test('includes a "no graders" message when there are no graders', () => {
|
||||
mountComponent()
|
||||
ok(wrapper.text().includes('Moderation is unable to occur'))
|
||||
})
|
||||
|
||||
test('does not load students', () => {
|
||||
mountComponent()
|
||||
strictEqual(StudentActions.loadStudents.callCount, 0)
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('when students have not yet loaded', () => {
|
||||
test('displays a spinner', () => {
|
||||
mountComponent()
|
||||
strictEqual(wrapper.find('Spinner').length, 1)
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('when some students have loaded', hooks => {
|
||||
let students
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
students = [{id: '1111', displayName: 'Adam Jones'}, {id: '1112', displayName: 'Betty Ford'}]
|
||||
})
|
||||
|
||||
test('renders the GradesGrid', () => {
|
||||
mountComponent()
|
||||
store.dispatch(StudentActions.addStudents(students))
|
||||
strictEqual(wrapper.find('GradesGrid').length, 1)
|
||||
})
|
||||
|
||||
test('does not display a spinner', () => {
|
||||
mountComponent()
|
||||
store.dispatch(StudentActions.addStudents(students))
|
||||
strictEqual(wrapper.find('Spinner').length, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -20,7 +20,7 @@ import fakeENV from 'helpers/fakeENV'
|
|||
|
||||
import getEnv from 'jsx/assignments/GradeSummary/getEnv'
|
||||
|
||||
QUnit.module('GradeSummary getEnv', suiteHooks => {
|
||||
QUnit.module('GradeSummary getEnv()', suiteHooks => {
|
||||
suiteHooks.beforeEach(() => {
|
||||
fakeENV.setup({
|
||||
ASSIGNMENT: {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 * as GradeActions from 'jsx/assignments/GradeSummary/grades/GradeActions'
|
||||
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
|
||||
|
||||
QUnit.module('GradeSummary GradeActions', suiteHooks => {
|
||||
let store
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
store = configureStore({
|
||||
assignment: {
|
||||
courseId: '1201',
|
||||
id: '2301',
|
||||
title: 'Example Assignment'
|
||||
},
|
||||
graders: [{graderId: '1101'}, {graderId: '1102'}]
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('.addProvisionalGrades()', () => {
|
||||
test('adds provisional grades to the store', () => {
|
||||
const provisionalGrades = [
|
||||
{
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4601',
|
||||
score: 10,
|
||||
studentId: '1111'
|
||||
},
|
||||
{
|
||||
grade: 'B',
|
||||
graderId: '1102',
|
||||
id: '4602',
|
||||
score: 9,
|
||||
studentId: '1112'
|
||||
}
|
||||
]
|
||||
store.dispatch(GradeActions.addProvisionalGrades(provisionalGrades))
|
||||
const grades = store.getState().grades.provisionalGrades
|
||||
deepEqual(grades[1112][1102], provisionalGrades[1])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 * as GradeActions from 'jsx/assignments/GradeSummary/grades/GradeActions'
|
||||
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
|
||||
|
||||
QUnit.module('GradeSummary gradesReducer()', suiteHooks => {
|
||||
let store
|
||||
let provisionalGrades
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
store = configureStore({
|
||||
assignment: {
|
||||
courseId: '1201',
|
||||
id: '2301',
|
||||
title: 'Example Assignment'
|
||||
},
|
||||
graders: [
|
||||
{graderId: '1101', graderName: 'Miss Frizzle'},
|
||||
{graderId: '1102', graderName: 'Mr. Keating'}
|
||||
]
|
||||
})
|
||||
|
||||
provisionalGrades = [
|
||||
{
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4601',
|
||||
score: 10,
|
||||
selected: true,
|
||||
studentId: '1111'
|
||||
},
|
||||
{
|
||||
grade: 'B',
|
||||
graderId: '1102',
|
||||
id: '4602',
|
||||
score: 9,
|
||||
selected: false,
|
||||
studentId: '1112'
|
||||
},
|
||||
{
|
||||
grade: 'C',
|
||||
graderId: '1102',
|
||||
id: '4603',
|
||||
score: 8,
|
||||
selected: false,
|
||||
studentId: '1111'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
function getProvisionalGrades() {
|
||||
return store.getState().grades.provisionalGrades
|
||||
}
|
||||
|
||||
QUnit.module('when handling "ADD_PROVISIONAL_GRADES"', () => {
|
||||
test('adds a key for each student among the provisional grades', () => {
|
||||
store.dispatch(GradeActions.addProvisionalGrades(provisionalGrades))
|
||||
deepEqual(Object.keys(getProvisionalGrades()).sort(), ['1111', '1112'])
|
||||
})
|
||||
|
||||
test('adds a key to a student for each grader who graded that student', () => {
|
||||
store.dispatch(GradeActions.addProvisionalGrades(provisionalGrades))
|
||||
deepEqual(Object.keys(getProvisionalGrades()[1111]).sort(), ['1101', '1102'])
|
||||
})
|
||||
|
||||
test('does not add a key to a student for a grader who has not graded that student', () => {
|
||||
store.dispatch(GradeActions.addProvisionalGrades(provisionalGrades))
|
||||
deepEqual(Object.keys(getProvisionalGrades()[1112]), ['1102'])
|
||||
})
|
||||
|
||||
test('keys a grade to the grader id within the student map', () => {
|
||||
store.dispatch(GradeActions.addProvisionalGrades(provisionalGrades))
|
||||
deepEqual(getProvisionalGrades()[1112][1102], provisionalGrades[1])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 * as StudentActions from 'jsx/assignments/GradeSummary/students/StudentActions'
|
||||
import * as StudentsApi from 'jsx/assignments/GradeSummary/students/StudentsApi'
|
||||
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
|
||||
|
||||
QUnit.module('GradeSummary StudentActions', suiteHooks => {
|
||||
let store
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
store = configureStore({
|
||||
assignment: {
|
||||
courseId: '1201',
|
||||
id: '2301',
|
||||
title: 'Example Assignment'
|
||||
},
|
||||
graders: [{graderId: '1101'}, {graderId: '1102'}]
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('.loadStudents()', hooks => {
|
||||
let args
|
||||
let provisionalGrades
|
||||
let students
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
sinon.stub(StudentsApi, 'loadStudents').callsFake((courseId, assignmentId, callbacks) => {
|
||||
args = {courseId, assignmentId, callbacks}
|
||||
})
|
||||
|
||||
provisionalGrades = [
|
||||
{
|
||||
grade: 'A',
|
||||
graderId: '1101',
|
||||
id: '4601',
|
||||
score: 10,
|
||||
studentId: '1111'
|
||||
},
|
||||
{
|
||||
grade: 'B',
|
||||
graderId: '1102',
|
||||
id: '4602',
|
||||
score: 9,
|
||||
studentId: '1112'
|
||||
}
|
||||
]
|
||||
|
||||
students = [{id: '1111', displayName: 'Adam Jones'}, {id: '1112', displayName: 'Betty Ford'}]
|
||||
})
|
||||
|
||||
hooks.afterEach(() => {
|
||||
args = null
|
||||
StudentsApi.loadStudents.restore()
|
||||
})
|
||||
|
||||
test('sets the "load students" status to "started"', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
const {loadStudentsStatus} = store.getState().students
|
||||
equal(loadStudentsStatus, StudentActions.STARTED)
|
||||
})
|
||||
|
||||
test('loads students through the api', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
strictEqual(StudentsApi.loadStudents.callCount, 1)
|
||||
})
|
||||
|
||||
test('includes the course id when loading students through the api', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
strictEqual(args.courseId, '1201')
|
||||
})
|
||||
|
||||
test('includes the assignment id when loading students through the api', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
strictEqual(args.assignmentId, '2301')
|
||||
})
|
||||
|
||||
test('adds students to the store when a page of students is loaded', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
args.callbacks.onPageLoaded({provisionalGrades, students})
|
||||
const storedStudents = store.getState().students.list
|
||||
deepEqual(storedStudents, students)
|
||||
})
|
||||
|
||||
test('adds provisional grades to the store when a page of students is loaded', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
args.callbacks.onPageLoaded({provisionalGrades, students})
|
||||
const grades = store.getState().grades.provisionalGrades
|
||||
deepEqual(grades[1112][1102], provisionalGrades[1])
|
||||
})
|
||||
|
||||
test('sets the "load students" status to "success" when all pages have loaded', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
args.callbacks.onAllPagesLoaded()
|
||||
const {loadStudentsStatus} = store.getState().students
|
||||
equal(loadStudentsStatus, StudentActions.SUCCESS)
|
||||
})
|
||||
|
||||
test('sets the "load students" status to "failure" when a failure occurs', () => {
|
||||
store.dispatch(StudentActions.loadStudents())
|
||||
args.callbacks.onFailure()
|
||||
const {loadStudentsStatus} = store.getState().students
|
||||
equal(loadStudentsStatus, StudentActions.FAILURE)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 * as StudentsApi from 'jsx/assignments/GradeSummary/students/StudentsApi'
|
||||
import FakeServer, {paramsFromRequest, pathFromRequest} from 'jsx/__tests__/FakeServer'
|
||||
|
||||
QUnit.module('GradeSummary StudentsApi', suiteHooks => {
|
||||
let qunitTimeout
|
||||
let server
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
qunitTimeout = QUnit.config.testTimeout
|
||||
QUnit.config.testTimeout = 500 // avoid accidental unresolved async
|
||||
server = new FakeServer()
|
||||
})
|
||||
|
||||
suiteHooks.afterEach(() => {
|
||||
server.teardown()
|
||||
QUnit.config.testTimeout = qunitTimeout
|
||||
})
|
||||
|
||||
QUnit.module('.loadStudents()', hooks => {
|
||||
const url = '/api/v1/courses/1201/assignments/2301/gradeable_students'
|
||||
|
||||
let loadedProvisionalGrades
|
||||
let loadedStudents
|
||||
let provisionalGradesData
|
||||
let studentsData
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
provisionalGradesData = [
|
||||
{
|
||||
grade: 'A',
|
||||
provisional_grade_id: '4601',
|
||||
score: 10,
|
||||
scorer_id: '1101'
|
||||
},
|
||||
{
|
||||
grade: 'B',
|
||||
provisional_grade_id: '4602',
|
||||
score: 9,
|
||||
scorer_id: '1102'
|
||||
},
|
||||
{
|
||||
grade: 'C',
|
||||
provisional_grade_id: '4603',
|
||||
score: 8,
|
||||
scorer_id: '1101'
|
||||
},
|
||||
{
|
||||
grade: 'B-',
|
||||
provisional_grade_id: '4604',
|
||||
score: 8.9,
|
||||
scorer_id: '1102'
|
||||
}
|
||||
]
|
||||
|
||||
studentsData = [
|
||||
{display_name: 'Adam Jones', id: '1111', provisional_grades: [provisionalGradesData[0]]},
|
||||
{
|
||||
display_name: 'Betty Ford',
|
||||
id: '1112',
|
||||
provisional_grades: provisionalGradesData.slice(1, 3),
|
||||
selected_provisional_grade_id: '4603'
|
||||
},
|
||||
{display_name: 'Charlie Xi', id: '1113', provisional_grades: []},
|
||||
{display_name: 'Dana Smith', id: '1114', provisional_grades: [provisionalGradesData[3]]}
|
||||
]
|
||||
|
||||
server
|
||||
.for(url)
|
||||
.respond([
|
||||
{status: 200, body: [studentsData[0]]},
|
||||
{status: 200, body: [studentsData[1]]},
|
||||
{status: 200, body: studentsData.slice(2)}
|
||||
])
|
||||
|
||||
loadedProvisionalGrades = []
|
||||
loadedStudents = []
|
||||
})
|
||||
|
||||
async function loadStudents() {
|
||||
let resolvePromise
|
||||
let rejectPromise
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
resolvePromise = resolve
|
||||
rejectPromise = reject
|
||||
})
|
||||
|
||||
StudentsApi.loadStudents('1201', '2301', {
|
||||
onAllPagesLoaded: resolvePromise,
|
||||
onFailure: rejectPromise,
|
||||
onPageLoaded({provisionalGrades, students}) {
|
||||
loadedProvisionalGrades.push(provisionalGrades)
|
||||
loadedStudents.push(students)
|
||||
}
|
||||
})
|
||||
|
||||
await promise
|
||||
}
|
||||
|
||||
function flatten(nestedArrays) {
|
||||
return [].concat(...nestedArrays)
|
||||
}
|
||||
|
||||
function sortBy(array, key) {
|
||||
return [].concat(array).sort((a, b) => {
|
||||
if (a[key] === b[key]) {
|
||||
return 0
|
||||
}
|
||||
return a[key] < b[key] ? -1 : 1
|
||||
})
|
||||
}
|
||||
|
||||
test('sends a request for students', async () => {
|
||||
await loadStudents()
|
||||
const request = server.receivedRequests[0]
|
||||
equal(pathFromRequest(request), '/api/v1/courses/1201/assignments/2301/gradeable_students')
|
||||
})
|
||||
|
||||
test('includes provisional grades', async () => {
|
||||
await loadStudents()
|
||||
const request = server.receivedRequests[0]
|
||||
deepEqual(paramsFromRequest(request).include, ['provisional_grades'])
|
||||
})
|
||||
|
||||
test('requests 50 students per page', async () => {
|
||||
await loadStudents()
|
||||
const request = server.receivedRequests[0]
|
||||
strictEqual(paramsFromRequest(request).per_page, '50')
|
||||
})
|
||||
|
||||
test('sends additional requests while additional pages are available', async () => {
|
||||
await loadStudents()
|
||||
strictEqual(server.receivedRequests.length, 3)
|
||||
})
|
||||
|
||||
test('calls onPageLoaded for each successful request', async () => {
|
||||
await loadStudents()
|
||||
strictEqual(loadedStudents.length, 3)
|
||||
})
|
||||
|
||||
test('includes students when calling onPageLoaded', async () => {
|
||||
await loadStudents()
|
||||
const studentCountPerPage = loadedStudents.map(pageStudents => pageStudents.length)
|
||||
deepEqual(studentCountPerPage, [1, 1, 2])
|
||||
})
|
||||
|
||||
test('normalizes student names', async () => {
|
||||
await loadStudents()
|
||||
const names = flatten(loadedStudents).map(student => student.displayName)
|
||||
deepEqual(names.sort(), ['Adam Jones', 'Betty Ford', 'Charlie Xi', 'Dana Smith'])
|
||||
})
|
||||
|
||||
test('includes ids on students', async () => {
|
||||
await loadStudents()
|
||||
const ids = flatten(loadedStudents).map(student => student.id)
|
||||
deepEqual(ids.sort(), ['1111', '1112', '1113', '1114'])
|
||||
})
|
||||
|
||||
test('includes provisional grades when calling onPageLoaded', async () => {
|
||||
await loadStudents()
|
||||
const gradeCountPerPage = loadedProvisionalGrades.map(pageGrades => pageGrades.length)
|
||||
deepEqual(gradeCountPerPage, [1, 2, 1])
|
||||
})
|
||||
|
||||
test('normalizes provisional grade grader ids', async () => {
|
||||
await loadStudents()
|
||||
const grades = sortBy(flatten(loadedProvisionalGrades), 'id')
|
||||
deepEqual(grades.map(grade => grade.graderId), ['1101', '1102', '1101', '1102'])
|
||||
})
|
||||
|
||||
test('uses anonymous grader id for provisional grades when graders are anonymous', async () => {
|
||||
for (let i = 0; i < provisionalGradesData.length; i++) {
|
||||
delete provisionalGradesData[i].scorer_id
|
||||
provisionalGradesData[i].anonymous_grader_id = `abcd${i + 1}`
|
||||
}
|
||||
await loadStudents()
|
||||
const grades = sortBy(flatten(loadedProvisionalGrades), 'id')
|
||||
deepEqual(grades.map(grade => grade.graderId), ['abcd1', 'abcd2', 'abcd3', 'abcd4'])
|
||||
})
|
||||
|
||||
test('includes provisional grade ids', async () => {
|
||||
await loadStudents()
|
||||
const grades = sortBy(flatten(loadedProvisionalGrades), 'id')
|
||||
deepEqual(grades.map(grade => grade.grade), ['A', 'B', 'C', 'B-'])
|
||||
})
|
||||
|
||||
test('includes provisional grade grades', async () => {
|
||||
await loadStudents()
|
||||
const grades = sortBy(flatten(loadedProvisionalGrades), 'id')
|
||||
deepEqual(grades.map(grade => grade.grade), ['A', 'B', 'C', 'B-'])
|
||||
})
|
||||
|
||||
test('includes provisional grade scores', async () => {
|
||||
await loadStudents()
|
||||
const grades = sortBy(flatten(loadedProvisionalGrades), 'id')
|
||||
deepEqual(grades.map(grade => grade.score), [10, 9, 8, 8.9])
|
||||
})
|
||||
|
||||
test('sets selection state on provisional grades', async () => {
|
||||
await loadStudents()
|
||||
const grades = sortBy(flatten(loadedProvisionalGrades), 'id')
|
||||
deepEqual(grades.map(grade => grade.selected), [false, false, true, false])
|
||||
})
|
||||
|
||||
test('includes associated student id', async () => {
|
||||
await loadStudents()
|
||||
const grades = sortBy(flatten(loadedProvisionalGrades), 'id')
|
||||
deepEqual(grades.map(grade => grade.studentId), ['1111', '1112', '1112', '1114'])
|
||||
})
|
||||
|
||||
test('calls onFailure when a request fails', async () => {
|
||||
server.unsetResponses(url)
|
||||
server
|
||||
.for(url)
|
||||
.respond([
|
||||
{status: 200, body: [studentsData[0]]},
|
||||
{status: 500, body: {error: 'server error'}}
|
||||
])
|
||||
|
||||
try {
|
||||
await loadStudents()
|
||||
} catch (e) {
|
||||
ok(e.message.includes('500'))
|
||||
}
|
||||
})
|
||||
|
||||
test('does not send additional requests when one fails', async () => {
|
||||
server.unsetResponses(url)
|
||||
server
|
||||
.for(url)
|
||||
.respond([
|
||||
{status: 200, body: [studentsData[0]]},
|
||||
{status: 500, body: {error: 'server error'}}
|
||||
])
|
||||
|
||||
try {
|
||||
await loadStudents()
|
||||
} catch (e) {
|
||||
strictEqual(server.receivedRequests.length, 2)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 {
|
||||
FAILURE,
|
||||
STARTED,
|
||||
SUCCESS,
|
||||
addStudents,
|
||||
setLoadStudentsStatus
|
||||
} from 'jsx/assignments/GradeSummary/students/StudentActions'
|
||||
import configureStore from 'jsx/assignments/GradeSummary/configureStore'
|
||||
|
||||
QUnit.module('GradeSummary studentsReducer()', suiteHooks => {
|
||||
let store
|
||||
let students
|
||||
|
||||
suiteHooks.beforeEach(() => {
|
||||
store = configureStore({
|
||||
assignment: {
|
||||
courseId: '1201',
|
||||
id: '2301',
|
||||
title: 'Example Assignment'
|
||||
},
|
||||
graders: [{graderId: '1101'}, {graderId: '1102'}]
|
||||
})
|
||||
|
||||
students = [
|
||||
{id: '1111', displayName: 'Adam Jones'},
|
||||
{id: '1112', displayName: 'Betty Ford'},
|
||||
{id: '1113', displayName: 'Charlie Xi'},
|
||||
{id: '1114', displayName: 'Dana Young'}
|
||||
]
|
||||
})
|
||||
|
||||
QUnit.module('when handling "ADD_STUDENTS"', () => {
|
||||
test('adds students to the store', () => {
|
||||
store.dispatch(addStudents(students))
|
||||
const storedStudents = store.getState().students.list
|
||||
deepEqual(storedStudents, students)
|
||||
})
|
||||
|
||||
test('appends students to the end of the current list of students', () => {
|
||||
store.dispatch(addStudents(students.slice(0, 2)))
|
||||
store.dispatch(addStudents(students.slice(2)))
|
||||
const storedStudents = store.getState().students.list
|
||||
deepEqual(storedStudents, students)
|
||||
})
|
||||
|
||||
test('preserves the order of students as they are added', () => {
|
||||
store.dispatch(addStudents(students.slice(2)))
|
||||
store.dispatch(addStudents(students.slice(0, 2)))
|
||||
const storedStudents = store.getState().students.list
|
||||
deepEqual(storedStudents.map(student => student.id), ['1113', '1114', '1111', '1112'])
|
||||
})
|
||||
})
|
||||
|
||||
QUnit.module('when handling "SET_LOAD_STUDENTS_STATUS"', () => {
|
||||
function getLoadStudentsStatus() {
|
||||
return store.getState().students.loadStudentsStatus
|
||||
}
|
||||
|
||||
test('optionally sets the "load students" status to "failure"', () => {
|
||||
store.dispatch(setLoadStudentsStatus(FAILURE))
|
||||
equal(getLoadStudentsStatus(), FAILURE)
|
||||
})
|
||||
|
||||
test('optionally sets the "load students" status to "started"', () => {
|
||||
store.dispatch(setLoadStudentsStatus(STARTED))
|
||||
equal(getLoadStudentsStatus(), STARTED)
|
||||
})
|
||||
|
||||
test('optionally sets the "load students" status to "success"', () => {
|
||||
store.dispatch(setLoadStudentsStatus(SUCCESS))
|
||||
equal(getLoadStudentsStatus(), SUCCESS)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue