add assessment summary to assessment audit tray

closes GRADE-1575

test plan:
 A. Setup
    1. Create an assignment
       * With moderated grading
       * Worth 10 points
    2. Grade at least one student
    3. Post grades
    4. Unmute

 B. Verify
    1. Log in as a user with "view audit trail" permission (admin)
    2. Open the assignment in SpeedGrader
    3. Verify the "score out of points possible" is correct
    4. Verify the "Posted to student" date is correct

 Notes:
   * The "Posted to student" date is not present for anonymous
     assignments (yet)

Change-Id: Iff383eda746dcba6bb5d9e21485572c4ae774e28
Reviewed-on: https://gerrit.instructure.com/166716
Tested-by: Jenkins
Reviewed-by: Adrian Packel <apackel@instructure.com>
Reviewed-by: Derek Bender <djbender@instructure.com>
QA-Review: Gary Mei <gmei@instructure.com>
Product-Review: Sidharth Oberoi <soberoi@instructure.com>
This commit is contained in:
Jeremy Neander 2018-10-02 11:32:46 -05:00
parent 052aa21606
commit b8c2cece19
8 changed files with 312 additions and 16 deletions

View File

@ -32,12 +32,14 @@ class FriendlyDatetime extends Component {
PropTypes.instanceOf(Date)
]).isRequired,
format: PropTypes.string,
prefix: PropTypes.string
prefix: PropTypes.string,
showTime: PropTypes.bool
}
static defaultProps = {
format: null,
prefix: ""
prefix: "",
showTime: false
}
// The original render function is really slow because of all
@ -45,6 +47,9 @@ class FriendlyDatetime extends Component {
// As long as @props.datetime stays same, we don't have to recompute our output.
// memoizing like this beat React.addons.PureRenderMixin 3x
render = _.memoize(() => {
// Separate props not used by the `time` element
const {showTime, ...timeElementProps} = this.props
let datetime = this.props.dateTime
if (!datetime) {
return (<time />)
@ -53,9 +58,16 @@ class FriendlyDatetime extends Component {
datetime = tz.parse(datetime)
}
const fudged = $.fudgeDateForProfileTimezone(datetime)
const friendly = this.props.format ? tz.format(datetime, this.props.format) : $.friendlyDatetime(fudged)
let friendly
if (this.props.format) {
friendly = tz.format(datetime, this.props.format)
} else if (showTime) {
friendly = $.datetimeString(datetime)
} else {
friendly = $.friendlyDatetime(fudged)
}
const timeProps = Object.assign({}, this.props, {
const timeProps = Object.assign({}, timeElementProps, {
title: $.datetimeString(datetime),
dateTime: datetime.toISOString(),
})

View File

@ -0,0 +1,96 @@
/*
* 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 {number, shape, string} from 'prop-types'
import PresentationContent from '@instructure/ui-a11y/lib/components/PresentationContent'
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
import Flex, {FlexItem} from '@instructure/ui-layout/lib/components/Flex'
import Text from '@instructure/ui-elements/lib/components/Text'
import I18n from 'i18n!speed_grader'
import FriendlyDatetime from '../../../shared/FriendlyDatetime'
export default function AssessmentSummary(props) {
const numberOptions = {precision: 2, strip_insignificant_zeros: true}
let score = ''
if (props.submission.score != null) {
score = I18n.n(props.submission.score, numberOptions)
}
const pointsPossible = I18n.n(props.assignment.pointsPossible, numberOptions)
const scoreText = I18n.t('%{score}/%{pointsPossible}', {pointsPossible, score})
return (
<Flex
as="section"
background="default"
borderRadius="medium"
borderWidth="small"
direction="column"
justifyItems="center"
padding="small"
textAlign="center"
>
<FlexItem>
<Text aria-labelledby="audit-tray-final-grade-label" weight="bold">
<Text as="div" size="x-large">
{scoreText}
</Text>
<PresentationContent>
<Text id="audit-tray-final-grade-label" as="div" size="small">
{I18n.t('Final Grade')}
</Text>
</PresentationContent>
<Text aria-labelledby="audit-tray-posted-date-label" fontStyle="italic" size="small">
<ScreenReaderContent>{I18n.t('Posted to student')}</ScreenReaderContent>
<FriendlyDatetime dateTime={props.assignment.gradesPublishedAt} showTime />
</Text>
</Text>
</FlexItem>
<FlexItem
as="div"
background="transparent"
borderWidth="none none small"
margin="small none"
padding="none"
/>
<FlexItem>
<Text as="div">{I18n.t('Posted to student')}</Text>
<Text fontStyle="italic" size="small" weight="bold">
<FriendlyDatetime dateTime={props.assignment.gradesPublishedAt} showTime />
</Text>
</FlexItem>
</Flex>
)
}
AssessmentSummary.propTypes = {
assignment: shape({
gradesPublishedAt: string,
pointsPossible: number
}).isRequired,
submission: shape({
score: number
}).isRequired
}

View File

@ -25,6 +25,8 @@ import Tray from '@instructure/ui-overlays/lib/components/Tray'
import View from '@instructure/ui-layout/lib/components/View'
import I18n from 'i18n!speed_grader'
import AssessmentSummary from './components/AssessmentSummary'
export default class AssessmentAuditTray extends Component {
static propTypes = {
onEntered: func,
@ -59,6 +61,10 @@ export default class AssessmentAuditTray extends Component {
}
render() {
if (!this.state.assignment) {
return null
}
const {onEntered, onExited} = this.props
return (
@ -70,7 +76,7 @@ export default class AssessmentAuditTray extends Component {
placement="end"
>
<View as="div" padding="small">
<Flex as="div" margin="0 0 small 0">
<Flex as="div" margin="0 0 medium 0">
<FlexItem>
<CloseButton onClick={this.dismiss}>{I18n.t('Close')}</CloseButton>
</FlexItem>
@ -81,6 +87,13 @@ export default class AssessmentAuditTray extends Component {
</Heading>
</FlexItem>
</Flex>
<View as="div" margin="small">
<AssessmentSummary
assignment={this.state.assignment}
submission={this.state.submission}
/>
</View>
</View>
</Tray>
)

View File

@ -1621,20 +1621,27 @@ EG = {
},
setUpAssessmentAuditTray() {
let auditTray
const bindRef = ref => {
auditTray = ref
EG.assessmentAuditTray = ref
}
const tray = <AssessmentAuditTray ref={bindRef} />
ReactDOM.render(tray, document.getElementById(ASSESSMENT_AUDIT_TRAY_MOUNT_POINT))
const onClick = () => {
auditTray.show({
assignmentId: ENV.assignment_id,
const {submission} = this.currentStudent
EG.assessmentAuditTray.show({
assignment: {
gradesPublishedAt: jsonData.grades_published_at,
id: ENV.assignment_id,
pointsPossible: jsonData.points_possible
},
courseId: ENV.course_id,
submissionId: this.currentStudent.submission.id
submission: {
id: submission.id,
score: submission.score
}
})
}
@ -1645,6 +1652,7 @@ EG = {
tearDownAssessmentAuditTray() {
ReactDOM.unmountComponentAtNode(document.getElementById(ASSESSMENT_AUDIT_TRAY_MOUNT_POINT))
ReactDOM.unmountComponentAtNode(document.getElementById(ASSESSMENT_AUDIT_BUTTON_MOUNT_POINT))
EG.assessmentAuditTray = null
},
setReadOnly: function(readonly) {

View File

@ -19,6 +19,7 @@
import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-dom/test-utils'
import $ from 'jquery'
import FriendlyDatetime from 'jsx/shared/FriendlyDatetime'
import I18n from 'i18nObj'
import I18nStubber from 'helpers/I18nStubber'
@ -108,3 +109,23 @@ test('will automatically put a space on the prefix if necessary', () => {
)
ReactDOM.unmountComponentAtNode(rendered.time.parentNode)
})
test('formats date with time when "showTime" is true', () => {
const fDT = React.createFactory(FriendlyDatetime)
const rendered = TestUtils.renderIntoDocument(fDT({dateTime: '1970-01-17', showTime: true}))
equal(
$(rendered.time)
.find('.visible-desktop')
.text(),
'Jan 17, 1970 at 12am',
'converts to readable format'
)
equal(
$(rendered.time)
.find('.hidden-desktop')
.text(),
'1/17/1970',
'converts to readable format'
)
ReactDOM.unmountComponentAtNode(rendered.time.parentNode)
})

View File

@ -36,9 +36,16 @@ QUnit.module('AssessmentAuditTray', suiteHooks => {
onExited = promiseProp('onExited')
context = {
assignmentId: '2301',
assignment: {
gradesPublishedAt: '2015-05-04T12:00:00.000Z',
id: '2301',
pointsPossible: 10
},
courseId: '1201',
submissionId: '2501'
submission: {
id: '2501',
score: 9.5
}
}
renderTray()

View File

@ -0,0 +1,101 @@
/*
* 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 ReactDOM from 'react-dom'
import AssessmentSummary from 'jsx/speed_grader/AssessmentAuditTray/components/AssessmentSummary'
QUnit.module('AssessmentSummary', suiteHooks => {
let $container
let props
suiteHooks.beforeEach(() => {
$container = document.body.appendChild(document.createElement('div'))
props = {
assignment: {
gradesPublishedAt: '2015-05-04T12:00:00.000Z',
pointsPossible: 10
},
submission: {
score: 9.5
}
}
})
suiteHooks.afterEach(() => {
ReactDOM.unmountComponentAtNode($container)
$container.remove()
})
function renderComponent() {
ReactDOM.render(<AssessmentSummary {...props} />, $container)
}
QUnit.module('"Final Grade"', () => {
test('shows the score out of points possible', () => {
renderComponent()
ok($container.textContent.includes('9.5/10'))
})
test('rounds the score to two decimal places', () => {
props.submission.score = 9.523
renderComponent()
ok($container.textContent.includes('9.52/10'))
})
test('rounds the points possible to two decimal places', () => {
props.assignment.pointsPossible = 10.017
renderComponent()
ok($container.textContent.includes('9.5/10.02'))
})
test('displays zero out of points possible when the score is zero', () => {
props.submission.score = 0
renderComponent()
ok($container.textContent.includes('0/10'))
})
test('displays score out of zero points possible when the assignment is worth zero points', () => {
props.assignment.pointsPossible = 0
renderComponent()
ok($container.textContent.includes('9.5/0'))
})
test('displays "" (en dash) for score when the submission is ungraded', () => {
props.submission.score = null
renderComponent()
ok($container.textContent.includes('/10'))
})
})
QUnit.module('"Posted to student"', () => {
test('displays the "grades published" date from the assignment', () => {
renderComponent()
const $time = $container.querySelector('time')
equal($time.getAttribute('datetime'), props.assignment.gradesPublishedAt)
})
test('includes the time on the visible date', () => {
renderComponent()
const $time = $container.querySelector('time')
ok($time.textContent.includes('12pm'))
})
})
})

View File

@ -1668,8 +1668,9 @@ QUnit.module('SpeedGrader', suiteHooks => {
name: 'Adam Jones',
submission_state: 'graded',
submission: {
score: 9.1,
grade: 'A',
id: '2501',
score: 9.1,
submission_comments: []
}
}
@ -1679,6 +1680,7 @@ QUnit.module('SpeedGrader', suiteHooks => {
window.jsonData = {
GROUP_GRADING_MODE: false,
anonymize_students: false,
grades_published_at: '2015-05-04T12:00:00.000Z',
gradingPeriods: {},
id: 27,
points_possible: 10,
@ -1703,10 +1705,46 @@ QUnit.module('SpeedGrader', suiteHooks => {
notOk(getAssessmentAuditButton())
})
test('opens the "Assessment Audit" tray when clicked', async () => {
test('opens the "Assessment Audit" tray when clicked', () => {
setUpSpeedGrader()
sandbox.stub(SpeedGrader.EG.assessmentAuditTray, 'show')
getAssessmentAuditButton().click()
ok(await waitForElement('[role="dialog"][aria-label="Assessment audit tray"]'))
strictEqual(SpeedGrader.EG.assessmentAuditTray.show.callCount, 1)
})
QUnit.module('when opening the "Assessment Audit" tray', contextHooks => {
let context
contextHooks.beforeEach(() => {
setUpSpeedGrader()
sandbox.stub(SpeedGrader.EG.assessmentAuditTray, 'show')
getAssessmentAuditButton().click()
context = SpeedGrader.EG.assessmentAuditTray.show.lastCall.args[0]
})
test('includes .assignment.gradesPublishedAt in the context', () => {
equal(context.assignment.gradesPublishedAt, '2015-05-04T12:00:00.000Z')
})
test('includes .assignment.id in the context', () => {
strictEqual(context.assignment.id, '2301')
})
test('includes .assignment.pointsPossible in the context', () => {
strictEqual(context.assignment.pointsPossible, 10)
})
test('includes .courseId in the context', () => {
strictEqual(context.courseId, '1201')
})
test('includes .submission.id in the context', () => {
strictEqual(context.submission.id, '2501')
})
test('includes .submission.score in the context', () => {
strictEqual(context.submission.score, 9.1)
})
})
})
})