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:
parent
052aa21606
commit
b8c2cece19
|
@ -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(),
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue