enhance individual gradebook student section
this is an iterative commit for the enhanced individual gradebook work currently under feature flag. this commit modifies the graphql queries and sets the template for the student section for the rework refs EVAL-3127 flag=individual_gradebook_enhancements test plan: - none Change-Id: I09afeecbf1ed7d3091f63ce84946ccea41382bbb Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/318319 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Aaron Shafovaloff <ashafovaloff@instructure.com> QA-Review: Aaron Shafovaloff <ashafovaloff@instructure.com> Product-Review: Aaron Shafovaloff <ashafovaloff@instructure.com>
This commit is contained in:
parent
7746f95086
commit
aca8e7fd3e
|
@ -390,4 +390,6 @@ module Interfaces::SubmissionInterface
|
|||
def assigned_assessments
|
||||
load_association(:assigned_assessments)
|
||||
end
|
||||
|
||||
field :assignment_id, ID, null: false
|
||||
end
|
||||
|
|
|
@ -34,6 +34,7 @@ describe Types::SubmissionType do
|
|||
expect(submission_type.resolve("user { _id }")).to eq @student.id.to_s
|
||||
expect(submission_type.resolve("excused")).to be false
|
||||
expect(submission_type.resolve("assignment { _id }")).to eq @assignment.id.to_s
|
||||
expect(submission_type.resolve("assignmentId")).to eq @assignment.id.to_s
|
||||
end
|
||||
|
||||
it "requires read permission" do
|
||||
|
|
|
@ -21,7 +21,7 @@ import gql from 'graphql-tag'
|
|||
export const GRADEBOOK_QUERY = gql`
|
||||
query GradebookQuery($courseId: ID!) {
|
||||
course(id: $courseId) {
|
||||
enrollmentsConnection(filter: {types: StudentEnrollment}) {
|
||||
enrollmentsConnection(filter: {types: [StudentEnrollment, StudentViewEnrollment]}) {
|
||||
nodes {
|
||||
user {
|
||||
id: _id
|
||||
|
@ -35,16 +35,67 @@ export const GRADEBOOK_QUERY = gql`
|
|||
grade
|
||||
id: _id
|
||||
score
|
||||
assignment {
|
||||
id: _id
|
||||
assignmentId
|
||||
}
|
||||
}
|
||||
}
|
||||
assignmentsConnection {
|
||||
assignmentGroupsConnection {
|
||||
nodes {
|
||||
id: _id
|
||||
name
|
||||
groupWeight
|
||||
rules {
|
||||
dropHighest
|
||||
dropLowest
|
||||
}
|
||||
state
|
||||
position
|
||||
assignmentsConnection {
|
||||
nodes {
|
||||
pointsPossible
|
||||
id: _id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const GRADEBOOK_STUDENT_QUERY = gql`
|
||||
query GradebookStudentQuery($courseId: ID!, $userIds: [ID!]) {
|
||||
course(id: $courseId) {
|
||||
usersConnection(
|
||||
filter: {
|
||||
enrollmentTypes: [StudentEnrollment, StudentViewEnrollment]
|
||||
enrollmentStates: active
|
||||
userIds: $userIds
|
||||
}
|
||||
) {
|
||||
nodes {
|
||||
enrollments {
|
||||
id: _id
|
||||
grades {
|
||||
unpostedCurrentGrade
|
||||
unpostedCurrentScore
|
||||
unpostedFinalGrade
|
||||
unpostedFinalScore
|
||||
}
|
||||
section {
|
||||
id: _id
|
||||
name
|
||||
}
|
||||
}
|
||||
loginId
|
||||
name
|
||||
}
|
||||
}
|
||||
submissionsConnection(studentIds: $userIds) {
|
||||
nodes {
|
||||
grade
|
||||
id: _id
|
||||
score
|
||||
assignmentId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
import React from 'react'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {AssignmentConnectionResponse, SubmissionConnectionResponse} from '../types'
|
||||
import {AssignmentConnection, SubmissionConnectionResponse} from '../types'
|
||||
|
||||
const I18n = useI18nScope('enhanced_individual_gradebook')
|
||||
|
||||
type Props = {
|
||||
assignment?: AssignmentConnectionResponse
|
||||
assignment?: AssignmentConnection
|
||||
submissions?: SubmissionConnectionResponse[]
|
||||
}
|
||||
|
||||
|
|
|
@ -19,18 +19,18 @@
|
|||
import React, {useEffect, useState} from 'react'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
|
||||
import {AssignmentConnectionResponse, UserConnectionResponse} from '../types'
|
||||
import {AssignmentConnection, UserConnectionResponse} from '../types'
|
||||
import {View} from '@instructure/ui-view'
|
||||
|
||||
const I18n = useI18nScope('enhanced_individual_gradebook')
|
||||
|
||||
type Props = {
|
||||
assignments: AssignmentConnectionResponse[]
|
||||
assignments: AssignmentConnection[]
|
||||
students: UserConnectionResponse[]
|
||||
selectedStudentId?: string | null
|
||||
selectedAssignmentId?: string | null
|
||||
onStudentChange: (student?: UserConnectionResponse) => void
|
||||
onAssignmentChange: (assignment?: AssignmentConnectionResponse) => void
|
||||
onStudentChange: (studentId?: string) => void
|
||||
onAssignmentChange: (assignment?: AssignmentConnection) => void
|
||||
}
|
||||
|
||||
// TODO: might want a map for quicker lookups
|
||||
|
@ -41,7 +41,7 @@ type DropDownOption<T> = {
|
|||
}
|
||||
|
||||
type StudentDropdownOption = DropDownOption<UserConnectionResponse>[]
|
||||
type AssignmentDropdownOption = DropDownOption<AssignmentConnectionResponse>[]
|
||||
type AssignmentDropdownOption = DropDownOption<AssignmentConnection>[]
|
||||
|
||||
const DEFAULT_STUDENT_DROPDOWN_TEXT = I18n.t('No Student Selected')
|
||||
const DEFAULT_ASSIGNMENT_DROPDOWN_TEXT = I18n.t('No Assignment Selected')
|
||||
|
@ -106,7 +106,7 @@ export default function ContentSelection({
|
|||
const selectedIndex = (event ? event.target.selectedIndex : newIndex) ?? 0
|
||||
setSelectedStudentIndex(selectedIndex)
|
||||
const selectedStudent = studentDropdownOptions[selectedIndex]?.data
|
||||
onStudentChange(selectedStudent)
|
||||
onStudentChange(selectedStudent?.id)
|
||||
}
|
||||
|
||||
const handleChangeAssignment = (
|
||||
|
|
|
@ -22,13 +22,14 @@ import {useSearchParams} from 'react-router-dom'
|
|||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {View} from '@instructure/ui-view'
|
||||
|
||||
import {AssignmentGroupCriteriaMap} from '../../../shared/grading/grading.d'
|
||||
import AssignmentInformation from './AssignmentInformation'
|
||||
import ContentSelection from './ContentSelection'
|
||||
import GlobalSettings from './GlobalSettings'
|
||||
import GradingResults from './GradingResults'
|
||||
import StudentInformation from './StudentInformation'
|
||||
import {
|
||||
AssignmentConnectionResponse,
|
||||
AssignmentConnection,
|
||||
GradebookQueryResponse,
|
||||
SubmissionConnectionResponse,
|
||||
UserConnectionResponse,
|
||||
|
@ -43,15 +44,20 @@ const ASSIGNMENT_SEARCH_PARAM = 'assignment'
|
|||
export default function EnhancedIndividualGradebook() {
|
||||
const [submissions, setSubmissions] = useState<SubmissionConnectionResponse[]>([])
|
||||
const [students, setStudents] = useState<UserConnectionResponse[]>([])
|
||||
const [assignments, setAssignments] = useState<AssignmentConnectionResponse[]>([])
|
||||
const [assignments, setAssignments] = useState<AssignmentConnection[]>([])
|
||||
|
||||
const [selectedStudent, setSelectedStudent] = useState<UserConnectionResponse>()
|
||||
const [selectedAssignment, setSelectedAssignment] = useState<AssignmentConnectionResponse>()
|
||||
const [selectedAssignment, setSelectedAssignment] = useState<AssignmentConnection>()
|
||||
const [selectedSubmissions, setSelectedSubmissions] = useState<SubmissionConnectionResponse[]>([])
|
||||
|
||||
const courseId = ENV.GRADEBOOK_OPTIONS?.context_id // TODO: get from somewhere else?
|
||||
const courseId = ENV.GRADEBOOK_OPTIONS?.context_id || '' // TODO: get from somewhere else?
|
||||
const [searchParams, setSearhParams] = useSearchParams()
|
||||
const selectedStudentId = searchParams.get(STUDENT_SEARCH_PARAM)
|
||||
const studentIdQueryParam = searchParams.get(STUDENT_SEARCH_PARAM)
|
||||
const [selectedStudentId, setSelectedStudentId] = useState<string | null | undefined>(
|
||||
studentIdQueryParam
|
||||
)
|
||||
|
||||
const [assignmentGroupMap, setAssignmentGroupMap] = useState<AssignmentGroupCriteriaMap>({})
|
||||
|
||||
const selectedAssignmentId = searchParams.get(ASSIGNMENT_SEARCH_PARAM)
|
||||
|
||||
const {data, error} = useQuery<GradebookQueryResponse>(GRADEBOOK_QUERY, {
|
||||
|
@ -66,9 +72,43 @@ export default function EnhancedIndividualGradebook() {
|
|||
}
|
||||
|
||||
if (data?.course) {
|
||||
const {assignmentsConnection, enrollmentsConnection, submissionsConnection} = data.course
|
||||
const {assignmentGroupsConnection, enrollmentsConnection, submissionsConnection} = data.course
|
||||
|
||||
setAssignments(assignmentsConnection.nodes)
|
||||
const {assignments, assignmentGroupMap} = assignmentGroupsConnection.nodes.reduce(
|
||||
(prev, curr) => {
|
||||
prev.assignments.push(...curr.assignmentsConnection.nodes)
|
||||
|
||||
prev.assignmentGroupMap[curr.id] = {
|
||||
name: curr.name,
|
||||
assignments: curr.assignmentsConnection.nodes.map(assignment => {
|
||||
return {
|
||||
id: assignment.id,
|
||||
name: assignment.name,
|
||||
points_possible: assignment.pointsPossible,
|
||||
submission_types: assignment.submissionTypes,
|
||||
anonymize_students: assignment.anonymizeStudents,
|
||||
omit_from_final_grade: assignment.omitFromFinalGrade,
|
||||
workflow_state: assignment.workflowState,
|
||||
}
|
||||
}),
|
||||
group_weight: curr.groupWeight,
|
||||
rules: curr.rules,
|
||||
id: curr.id,
|
||||
position: curr.position,
|
||||
integration_data: {},
|
||||
sis_source_id: null,
|
||||
}
|
||||
|
||||
return prev
|
||||
},
|
||||
{
|
||||
assignments: [] as AssignmentConnection[],
|
||||
assignmentGroupMap: {} as AssignmentGroupCriteriaMap,
|
||||
}
|
||||
)
|
||||
|
||||
setAssignmentGroupMap(assignmentGroupMap)
|
||||
setAssignments(assignments)
|
||||
setSubmissions(submissionsConnection.nodes)
|
||||
|
||||
const studentEnrollments = enrollmentsConnection.nodes.map(enrollment => enrollment.user)
|
||||
|
@ -79,17 +119,17 @@ export default function EnhancedIndividualGradebook() {
|
|||
}
|
||||
}, [data, error])
|
||||
|
||||
const handleStudentChange = (student?: UserConnectionResponse) => {
|
||||
setSelectedStudent(student)
|
||||
if (student) {
|
||||
searchParams.set(STUDENT_SEARCH_PARAM, student?.id)
|
||||
const handleStudentChange = (studentId?: string) => {
|
||||
setSelectedStudentId(studentId)
|
||||
if (studentId) {
|
||||
searchParams.set(STUDENT_SEARCH_PARAM, studentId)
|
||||
setSearhParams(searchParams)
|
||||
} else {
|
||||
searchParams.delete(STUDENT_SEARCH_PARAM)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssignmentChange = (assignment?: AssignmentConnectionResponse) => {
|
||||
const handleAssignmentChange = (assignment?: AssignmentConnection) => {
|
||||
setSelectedAssignment(assignment)
|
||||
setSelectedSubmissions(submissions.filter(s => s.assignment.id === assignment?.id))
|
||||
if (assignment) {
|
||||
|
@ -130,7 +170,11 @@ export default function EnhancedIndividualGradebook() {
|
|||
|
||||
<div className="hr" style={{margin: 10, padding: 10, borderBottom: '1px solid #eee'}} />
|
||||
|
||||
<StudentInformation student={selectedStudent} />
|
||||
<StudentInformation
|
||||
courseId={courseId}
|
||||
studentId={selectedStudentId}
|
||||
assignmentGroupMap={assignmentGroupMap}
|
||||
/>
|
||||
|
||||
<div className="hr" style={{margin: 10, padding: 10, borderBottom: '1px solid #eee'}} />
|
||||
|
||||
|
|
|
@ -16,20 +16,49 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {useQuery} from 'react-apollo'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import CourseGradeCalculator from '@canvas/grading/CourseGradeCalculator'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
|
||||
import {UserConnectionResponse} from '../types'
|
||||
import {
|
||||
GradebookStudentDetails,
|
||||
GradebookStudentQueryResponse,
|
||||
GradebookUserSubmissionDetails,
|
||||
} from '../types'
|
||||
import {GRADEBOOK_STUDENT_QUERY} from '../queries/Queries'
|
||||
import {AssignmentGroupCriteriaMap, SubmissionGradeCriteria} from '@canvas/grading/grading'
|
||||
|
||||
const I18n = useI18nScope('enhanced_individual_gradebook')
|
||||
|
||||
type Props = {
|
||||
student?: UserConnectionResponse
|
||||
courseId: string
|
||||
studentId?: string | null
|
||||
assignmentGroupMap: AssignmentGroupCriteriaMap
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function StudentInformation({student}: Props) {
|
||||
export default function StudentInformation({courseId, studentId, assignmentGroupMap}: Props) {
|
||||
const [selectedStudent, setSelectedStudent] = useState<GradebookStudentDetails>()
|
||||
const [studentSubmissions, setStudentSubmissions] = useState<GradebookUserSubmissionDetails[]>([])
|
||||
const {data, error} = useQuery<GradebookStudentQueryResponse>(GRADEBOOK_STUDENT_QUERY, {
|
||||
variables: {courseId, userIds: studentId},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
skip: !studentId,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
// TODO: handle error
|
||||
}
|
||||
|
||||
if (data?.course) {
|
||||
setSelectedStudent(data.course.usersConnection.nodes[0])
|
||||
setStudentSubmissions(data.course.submissionsConnection.nodes)
|
||||
}
|
||||
}, [data, error])
|
||||
|
||||
if (!selectedStudent) {
|
||||
return (
|
||||
<View as="div">
|
||||
<View as="div" className="row-fluid">
|
||||
|
@ -44,4 +73,86 @@ export default function StudentInformation({student}: Props) {
|
|||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const submissions: SubmissionGradeCriteria[] = studentSubmissions.map(submission => {
|
||||
return {
|
||||
assignment_id: submission.assignmentId,
|
||||
excused: false,
|
||||
grade: submission.grade,
|
||||
score: submission.score,
|
||||
workflow_state: 'graded',
|
||||
id: submission.id,
|
||||
}
|
||||
})
|
||||
CourseGradeCalculator.calculate(submissions, assignmentGroupMap, 'points', true)
|
||||
|
||||
return (
|
||||
<div id="student_information">
|
||||
<div className="row-fluid">
|
||||
<div className="span4">
|
||||
<h2>Student Information</h2>
|
||||
</div>
|
||||
<div className="span8">
|
||||
<h3 className="student_selection">
|
||||
<a href="studentUrl"> {selectedStudent.name}</a>
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<strong>
|
||||
Secondary ID:
|
||||
<span className="secondary_id"> {selectedStudent.loginId}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<strong>
|
||||
Sections:{' '}
|
||||
{selectedStudent.enrollments.map(enrollment => enrollment.section.name).join(', ')}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<h4>Grades</h4>
|
||||
|
||||
<div className="ic-Table-responsive-x-scroll">
|
||||
<table className="ic-Table">
|
||||
<thead>
|
||||
<tr>
|
||||
{/* {{#if subtotal_by_period}}
|
||||
<th scope="col">{{#t}}Grading Period{</th>
|
||||
{{else}}
|
||||
<th scope="col">{{#t}}Assignment Group{</th>
|
||||
{{/if}} */}
|
||||
<th scope="col">Assignment Group</th>
|
||||
<th scope="col">Grade</th>
|
||||
<th scope="col">Letter Grade</th>
|
||||
<th scope="col">% of Grade</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* {{#each assignment_subtotal in assignment_subtotals}}
|
||||
{{
|
||||
assignment-subtotal-grades
|
||||
subtotal=assignment_subtotal
|
||||
student=selectedStudent
|
||||
weightingScheme=weightingScheme
|
||||
gradingStandard=ENV.GRADEBOOK_OPTIONS.grading_standard
|
||||
}}
|
||||
{{/each}} */}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
Final Grade:
|
||||
<span className="total-grade">
|
||||
{selectedStudent.enrollments[0].grades.unpostedCurrentScore}% (
|
||||
{studentSubmissions.reduce((a, b) => a + b.score ?? 0, 0)} / )
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
{/* {{partial "student_information/assignment_subtotals"}} */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {WorkflowState} from '../../../api.d'
|
||||
|
||||
/**
|
||||
* Temporarily generate types for GraphQL queries until we have automated generation setup
|
||||
*/
|
||||
|
@ -34,9 +36,30 @@ export type UserConnectionResponse = {
|
|||
sortableName: string
|
||||
}
|
||||
|
||||
export type AssignmentConnectionResponse = {
|
||||
export type AssignmentConnection = {
|
||||
id: string
|
||||
name: string
|
||||
pointsPossible: number
|
||||
submissionTypes: string[]
|
||||
anonymizeStudents: boolean
|
||||
omitFromFinalGrade: boolean
|
||||
workflowState: WorkflowState
|
||||
}
|
||||
|
||||
export type AssignmentGroupConnection = {
|
||||
id: string
|
||||
name: string
|
||||
groupWeight: number
|
||||
rules: {
|
||||
drop_lowest?: number
|
||||
drop_highest?: number
|
||||
never_drop?: string[]
|
||||
}
|
||||
state: string
|
||||
position: number
|
||||
assignmentsConnection: {
|
||||
nodes: AssignmentConnection[]
|
||||
}
|
||||
}
|
||||
|
||||
export type SubmissionConnectionResponse = {
|
||||
|
@ -49,6 +72,13 @@ export type SubmissionConnectionResponse = {
|
|||
grade: string
|
||||
}
|
||||
|
||||
export type EnrollmentGrades = {
|
||||
unpostedCurrentGrade: number
|
||||
unpostedCurrentScore: number
|
||||
unpostedFinalGrade: number
|
||||
unpostedFinalScore: number
|
||||
}
|
||||
|
||||
export type GradebookQueryResponse = {
|
||||
course: {
|
||||
enrollmentsConnection: {
|
||||
|
@ -59,8 +89,40 @@ export type GradebookQueryResponse = {
|
|||
submissionsConnection: {
|
||||
nodes: SubmissionConnectionResponse[]
|
||||
}
|
||||
assignmentsConnection: {
|
||||
nodes: AssignmentConnectionResponse[]
|
||||
assignmentGroupsConnection: {
|
||||
nodes: AssignmentGroupConnection[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type GradebookStudentDetails = {
|
||||
enrollments: {
|
||||
id: string
|
||||
grades: EnrollmentGrades
|
||||
section: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}[]
|
||||
loginId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type GradebookUserSubmissionDetails = {
|
||||
grade: string
|
||||
id: string
|
||||
score: number
|
||||
assignmentId: string
|
||||
workflowState: string
|
||||
}
|
||||
|
||||
export type GradebookStudentQueryResponse = {
|
||||
course: {
|
||||
usersConnection: {
|
||||
nodes: GradebookStudentDetails[]
|
||||
}
|
||||
submissionsConnection: {
|
||||
nodes: GradebookUserSubmissionDetails[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue