add locked view for mastery scales and proficiency calculation

Add read-only view of mastery scale and of proficiency calculation

closes OUT-3767
flag=account_level_mastery_scales

test plan:
- as Admin, enable "Account-level Mastery Scales"
- log in using an account with permissions
  "Outcome Proficiency Calculations - add / edit" and
  "Outcome Proficiency Scales - add / edit" disabled
- Go to outcomes, Mastery tab, you should not see a form, only the data
- Go to outcomes, Calculation tab, you should not see a form, only the data

Change-Id: I3d6e7fefcd56ceb567dc59c3238ce8cc27553054
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/248943
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Pat Renner <prenner@instructure.com>
Reviewed-by: Augusto Callejas <acallejas@instructure.com>
QA-Review: Pat Renner <prenner@instructure.com>
Product-Review: Jody Sailor
This commit is contained in:
Manoel Quirino Neto 2020-09-30 13:52:21 -03:00 committed by Manoel Quirino
parent 718eb4a736
commit dc68ff2f37
16 changed files with 720 additions and 265 deletions

View File

@ -45,7 +45,9 @@ class OutcomesController < ApplicationController
:manage_outcomes => @context.grants_right?(@current_user, session, :manage_outcomes),
:manage_rubrics => @context.grants_right?(@current_user, session, :manage_rubrics),
:manage_courses => @context.grants_right?(@current_user, session, :manage_courses),
:import_outcomes => @context.grants_right?(@current_user, session, :import_outcomes)
:import_outcomes => @context.grants_right?(@current_user, session, :import_outcomes),
:manage_proficiency_scales => @context.grants_right?(@current_user, session, :manage_proficiency_scales),
:manage_proficiency_calculations => @context.grants_right?(@current_user, session, :manage_proficiency_calculations)
})
set_tutorial_js_env

View File

@ -109,7 +109,9 @@ const Display = ({calculationInt, currentMethod}) => {
</Text>
{currentMethod.validRange && (
<>
<Heading level="h4">{I18n.t('Parameter')}</Heading>
<Heading margin="medium none none" level="h4">
{I18n.t('Parameter')}
</Heading>
<Text color="primary" weight="normal">
{calculationInt}
</Text>
@ -176,8 +178,8 @@ const Example = ({currentMethod}) => {
)
}
const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
const {calculationMethod: initialMethodKey, calculationInt: initialInt, locked} = method
const ProficiencyCalculation = ({method, update: rawUpdate, updateError, canManage}) => {
const {calculationMethod: initialMethodKey, calculationInt: initialInt} = method
const [calculationMethodKey, setCalculationMethodKey] = useState(initialMethodKey)
const [calculationInt, setCalculationInt] = useState(initialInt)
@ -216,18 +218,9 @@ const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
return (
<View as="div">
{locked && (
<Heading level="h5" margin="medium 0">
{I18n.t(
'The proficiency calculation was set by your admin and applies to all courses in this account.'
)}
</Heading>
)}
<Flex alignItems="start" wrap="wrap">
<Flex.Item size="240px" padding="small">
{locked ? (
<Display currentMethod={currentMethod} calculationInt={calculationInt} />
) : (
{canManage ? (
<Form
calculationMethodKey={calculationMethodKey}
calculationInt={calculationInt}
@ -236,6 +229,8 @@ const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
updateCalculationMethod={updateCalculationMethod}
setCalculationInt={setCalculationInt}
/>
) : (
<Display currentMethod={currentMethod} calculationInt={calculationInt} />
)}
</Flex.Item>
<Flex.Item size="240px" shouldGrow padding="small">
@ -249,9 +244,9 @@ const ProficiencyCalculation = ({method, update: rawUpdate, updateError}) => {
ProficiencyCalculation.propTypes = {
method: PropTypes.shape({
calculationMethod: PropTypes.string.isRequired,
calculationInt: PropTypes.number,
locked: PropTypes.bool
calculationInt: PropTypes.number
}),
canManage: PropTypes.bool,
update: PropTypes.func.isRequired,
updateError: PropTypes.string
}
@ -259,8 +254,7 @@ ProficiencyCalculation.propTypes = {
ProficiencyCalculation.defaultProps = {
method: {
calculationMethod: 'decaying_average',
calculationInt: 65,
locked: false
calculationInt: 65
},
updateError: null
}

View File

@ -43,20 +43,18 @@ describe('ProficiencyCalculation', () => {
const makeProps = (overrides = {}) => ({
update: Function.prototype,
canManage: true,
...overrides,
method: {
calculationMethod: 'decaying_average',
calculationInt: 75,
locked: false,
...(overrides.method || {})
}
})
describe('locked', () => {
it('renders method and int', () => {
const {getByText} = render(
<ProficiencyCalculation {...makeProps({method: {locked: true}})} />
)
const {getByText} = render(<ProficiencyCalculation {...makeProps({canManage: false})} />)
expect(getByText('Decaying Average')).not.toBeNull()
expect(getByText('75')).not.toBeNull()
})
@ -65,7 +63,8 @@ describe('ProficiencyCalculation', () => {
const {getByText, queryByText} = render(
<ProficiencyCalculation
{...makeProps({
method: {calculationMethod: 'latest', calculationInt: null, locked: true}
canManage: false,
method: {calculationMethod: 'latest', calculationInt: null}
})}
/>
)
@ -77,7 +76,7 @@ describe('ProficiencyCalculation', () => {
const {getByText} = render(
<ProficiencyCalculation
{...makeProps({
method: {locked: true}
canManage: false
})}
/>
)
@ -89,7 +88,8 @@ describe('ProficiencyCalculation', () => {
const {getByText} = render(
<ProficiencyCalculation
{...makeProps({
method: {calculationMethod: 'latest', calculationInt: null, locked: true}
canManage: false,
method: {calculationMethod: 'latest', calculationInt: null}
})}
/>
)

View File

@ -17,7 +17,7 @@
*/
import React from 'react'
import {render, wait, fireEvent} from '@testing-library/react'
import {render, wait, fireEvent, waitForElementToBeRemoved} from '@testing-library/react'
import {MockedProvider} from '@apollo/react-testing'
import {OUTCOME_PROFICIENCY_QUERY, SET_OUTCOME_CALCULATION_METHOD} from '../api'
import MasteryCalculation from '../index'
@ -27,7 +27,10 @@ describe('MasteryCalculation', () => {
window.ENV = {
PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES: [
{id: '1', role: 'AccountAdmin', label: 'Account Admin', base_role_type: 'AccountMembership'}
]
],
PERMISSIONS: {
manage_proficiency_calculations: true
}
}
})
@ -53,8 +56,7 @@ describe('MasteryCalculation', () => {
contextType: 'Account',
contextId: 1,
calculationMethod: 'decaying_average',
calculationInt: 65,
locked: false
calculationInt: 65
}
}
}
@ -63,35 +65,35 @@ describe('MasteryCalculation', () => {
]
it('loads proficiency data', async () => {
const {getByText, getByDisplayValue} = render(
const {getByText, queryByText, getByDisplayValue} = render(
<MockedProvider mocks={mocks}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
expect(getByText('Loading')).not.toEqual(null)
await wait()
await waitForElementToBeRemoved(() => queryByText('Loading'))
expect(getByDisplayValue(/65/)).not.toEqual(null)
})
it('loads role list', async () => {
const {getByText} = render(
const {getByText, queryByText} = render(
<MockedProvider mocks={mocks}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
expect(getByText('Loading')).not.toEqual(null)
await wait()
await waitForElementToBeRemoved(() => queryByText('Loading'))
expect(getByText(/Permission to change this mastery calculation/)).not.toEqual(null)
expect(getByText(/Account Admin/)).not.toEqual(null)
})
it('displays an error on failed request', async () => {
const {getByText} = render(
const {getByText, queryByText} = render(
<MockedProvider mocks={[]}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
await wait()
await waitForElementToBeRemoved(() => queryByText('Loading'))
expect(getByText(/An error occurred/)).not.toEqual(null)
})
@ -114,12 +116,12 @@ describe('MasteryCalculation', () => {
}
}
]
const {getByText} = render(
const {getByText, queryByText} = render(
<MockedProvider mocks={emptyMocks}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
await wait()
await waitForElementToBeRemoved(() => queryByText('Loading'))
expect(getByText('Proficiency Calculation')).not.toBeNull()
})
@ -164,4 +166,28 @@ describe('MasteryCalculation', () => {
await wait(() => expect(updateCall).toHaveBeenCalled())
})
})
describe('locked', () => {
beforeEach(() => {
window.ENV.PERMISSIONS = {
manage_proficiency_calculations: false
}
})
afterEach(() => {
window.ENV.PERMISSIONS = null
})
it('hides role list', async () => {
const {getByText, queryByText} = render(
<MockedProvider mocks={mocks}>
<MasteryCalculation contextType="Account" contextId="11" />
</MockedProvider>
)
expect(getByText('Loading')).not.toEqual(null)
await waitForElementToBeRemoved(() => queryByText('Loading'))
expect(queryByText(/Permission to change this mastery calculation/)).not.toBeInTheDocument()
expect(queryByText(/Account Admin/)).not.toBeInTheDocument()
})
})
})

View File

@ -58,20 +58,24 @@ const MasteryCalculation = ({contextType, contextId}) => {
}
const {outcomeCalculationMethod} = data.account
const roles = ENV.PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES || []
const canManage = ENV.PERMISSIONS.manage_proficiency_calculations
return (
<>
{canManage && (
<RoleList
description={I18n.t(
'Permission to change this mastery calculation is enabled at the account level for:'
)}
roles={roles}
/>
)}
<ProficiencyCalculation
contextType={contextType}
contextId={contextId}
method={outcomeCalculationMethod || undefined} // send undefined when value is null
update={setCalculationMethod}
updateError={setCalculationMethodError}
canManage={canManage}
/>
</>
)

View File

@ -24,7 +24,9 @@ import {IconTrashLine} from '@instructure/ui-icons'
import {Popover} from '@instructure/ui-overlays'
import {RadioInput} from '@instructure/ui-forms'
import {TextInput} from '@instructure/ui-text-input'
import {ScreenReaderContent} from '@instructure/ui-a11y'
import {ScreenReaderContent, PresentationContent} from '@instructure/ui-a11y'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-layout'
import {Flex} from '@instructure/ui-flex'
import ColorPicker, {PREDEFINED_COLORS} from '../../shared/ColorPicker'
@ -51,13 +53,15 @@ class ProficiencyRating extends React.Component {
points: PropTypes.string.isRequired,
pointsError: PropTypes.string,
isMobileView: PropTypes.bool,
position: PropTypes.number.isRequired
position: PropTypes.number.isRequired,
canManage: PropTypes.bool
}
static defaultProps = {
descriptionError: null,
focusField: null,
pointsError: null,
canManage: window.ENV?.PERMISSIONS ? ENV.PERMISSIONS.manage_proficiency_scales : true,
isMobileView: false
}
@ -71,12 +75,14 @@ class ProficiencyRating extends React.Component {
}
componentDidMount() {
if (this.props.focusField === 'mastery') {
const {canManage, focusField} = this.props
if (focusField === 'mastery' && canManage) {
this.radioInput.focus()
}
}
componentDidUpdate() {
if (this.props.canManage) {
if (this.props.focusField === 'trash') {
setTimeout(
() => (this.props.disableDelete ? this.colorButton.focus() : this.trashButton.focus()),
@ -88,6 +94,7 @@ class ProficiencyRating extends React.Component {
this.pointsInput.focus()
}
}
}
setDescriptionRef = element => {
this.descriptionInput = element
@ -136,9 +143,10 @@ class ProficiencyRating extends React.Component {
}
renderDescription = () => {
const {description, descriptionError, position} = this.props
const {description, descriptionError, position, canManage} = this.props
return (
<div className="description">
{canManage ? (
<TextInput
width="100%"
messages={this.errorMessage(descriptionError)}
@ -151,14 +159,27 @@ class ProficiencyRating extends React.Component {
inputRef={element => this.setDescriptionRef(element)}
defaultValue={description}
/>
) : (
<Text>
<ScreenReaderContent>
{I18n.t(`Description for proficiency rating %{position}: %{description}`, {
position,
description
})}
</ScreenReaderContent>
<PresentationContent>{description}</PresentationContent>
</Text>
)}
</div>
)
}
renderMastery = () => {
const {mastery, position} = this.props
const {mastery, position, canManage} = this.props
return (
<div className="mastery">
<div className={`mastery ${canManage ? null : 'view-only'}`}>
{(mastery || canManage) && (
<RadioInput
label={
<ScreenReaderContent>
@ -170,16 +191,20 @@ class ProficiencyRating extends React.Component {
}
ref={input => (this.radioInput = input)}
checked={mastery}
readOnly={!canManage}
onChange={this.handleMasteryChange}
/>
)}
</div>
)
}
renderPointsInput = () => {
const {points, pointsError, position, isMobileView} = this.props
const {points, pointsError, position, isMobileView, canManage} = this.props
return (
<div className="points">
{canManage ? (
<>
<TextInput
type="text"
inputRef={this.setPointsRef}
@ -193,17 +218,36 @@ class ProficiencyRating extends React.Component {
defaultValue={I18n.n(points)}
width={isMobileView ? '7rem' : '4rem'}
/>
<div className="pointsDescription" aria-hidden="true">
{I18n.t('points')}
</div>
</>
) : (
<View margin={`0 0 0 ${isMobileView ? '0' : 'small'}`}>
<ScreenReaderContent>
{I18n.t(`Points for proficiency rating %{position}: %{points}`, {
position,
points
})}
</ScreenReaderContent>
<PresentationContent>
{I18n.n(points)}
<div className="pointsDescription view-only">{I18n.t('points')}</div>
</PresentationContent>
</View>
)}
</div>
)
}
renderColorPicker = () => {
const {color, position} = this.props
const {color, position, canManage, isMobileView} = this.props
return (
<div className="color">
{canManage ? (
<Popover on="click" show={this.state.showColorPopover} onToggle={this.handleMenuToggle}>
<Popover.Trigger>
<Button ref={this.setColorRef} variant="link">
@ -235,6 +279,24 @@ class ProficiencyRating extends React.Component {
/>
</Popover.Content>
</Popover>
) : (
<>
<span
className="colorPickerIcon"
style={{
background: formatColor(color),
marginLeft: isMobileView ? 0 : '2rem'
}}
>
<ScreenReaderContent>
{I18n.t(`Color %{color} for proficiency rating %{position}`, {
color: ColorPicker.getColorName(color) || formatColor(color),
position
})}
</ScreenReaderContent>
</span>
</>
)}
</div>
)
}
@ -259,7 +321,7 @@ class ProficiencyRating extends React.Component {
errorMessage = error => (error ? [{text: error, type: 'error'}] : null)
render() {
const {isMobileView} = this.props
const {isMobileView, canManage} = this.props
return (
<Flex
padding={`${isMobileView ? '0 0 small 0' : '0 small small small'}`}
@ -274,9 +336,9 @@ class ProficiencyRating extends React.Component {
{isMobileView && (
<>
{this.renderPointsInput()}
<div className="mobileRow">
<div className={`mobileRow ${canManage ? null : 'view-only'}`}>
{this.renderColorPicker()}
{this.renderDeleteButton()}
{canManage && this.renderDeleteButton()}
</div>
</>
)}
@ -287,9 +349,11 @@ class ProficiencyRating extends React.Component {
{this.renderPointsInput()}
</Flex.Item>
<Flex.Item>{this.renderColorPicker()}</Flex.Item>
{canManage && (
<Flex.Item size="10%" padding="0 small 0 small">
{this.renderDeleteButton()}
</Flex.Item>
)}
</>
)}
</Flex>

View File

@ -57,13 +57,13 @@ const configToState = data => {
return {
masteryIndex,
rows,
locked: data.locked,
allowSave: false
}
}
class ProficiencyTable extends React.Component {
static propTypes = {
proficiency: PropTypes.object,
canManage: PropTypes.bool.isRequired,
update: PropTypes.func.isRequired,
focusTab: PropTypes.func,
breakpoints: breakpointsShape
@ -81,6 +81,7 @@ class ProficiencyTable extends React.Component {
]
}
},
canManage: window.ENV?.PERMISSIONS ? ENV.PERMISSIONS.manage_proficiency_scales : true,
focusTab: null,
breakpoints: {}
}
@ -282,7 +283,7 @@ class ProficiencyTable extends React.Component {
render() {
const {allowSave, masteryIndex} = this.state
const {breakpoints} = this.props
const {breakpoints, canManage} = this.props
const isMobileView = breakpoints.mobileOnly
return (
<>
@ -331,10 +332,14 @@ class ProficiencyTable extends React.Component {
onPointsChange={this.handlePointsChange(index)}
position={index + 1}
isMobileView={isMobileView}
canManage={canManage}
/>
{this.renderBorder()}
</React.Fragment>
))}
{canManage && (
<>
<View
width="100%"
textAlign="start"
@ -356,6 +361,8 @@ class ProficiencyTable extends React.Component {
</Button>
</div>
</>
)}
</>
)
}
}

View File

@ -35,8 +35,10 @@ const defaultProps = (props = {}) => ({
...props
})
describe('ProficiencyRating', () => {
describe('can not manage', () => {
it('renders the ProficiencyRating component', () => {
const wrapper = shallow(<ProficiencyRating {...defaultProps()} />)
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
expect(wrapper).toMatchSnapshot()
})
@ -44,7 +46,87 @@ it('mastery checkbox is checked if mastery', () => {
const wrapper = shallow(
<ProficiencyRating
{...defaultProps({
mastery: true
mastery: true,
canManage: false
})}
/>
)
const radio = wrapper.find('RadioInput')
expect(radio.props().checked).toBe(true)
})
it('mastery checkbox does not appear if not mastery', () => {
const wrapper = shallow(
<ProficiencyRating
{...defaultProps({
mastery: false,
canManage: false
})}
/>
)
const radio = wrapper.find('RadioInput')
expect(radio.exists()).toBeFalsy()
})
it('mastery checkbox does not receive focus', () => {
const wrapper = mount(
<div>
<ProficiencyRating
{...defaultProps({focusField: 'mastery', canManage: false, mastery: true})}
/>
</div>
)
expect(
wrapper
.find('RadioInput')
.find('input')
.instance()
).not.toBe(document.activeElement)
})
it('clicking mastery checkbox does not trigger change', () => {
const onMasteryChange = jest.fn()
const wrapper = mount(
<ProficiencyRating {...defaultProps({onMasteryChange, mastery: true, canManage: false})} />
)
wrapper
.find('RadioInput')
.find('input')
.simulate('change')
expect(onMasteryChange).not.toHaveBeenCalled()
})
it('does not render TextInput', () => {
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
expect(wrapper.find('TextInput').exists()).toBeFalsy()
})
it('does not render delete button', () => {
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
expect(wrapper.find('.deleteButton').exists()).toBeFalsy()
})
it('includes the points', () => {
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: false})} />)
const content = wrapper
.find('.points')
.find('PresentationContent')
.at(0)
expect(content.childAt(0).text()).toBe('10')
})
})
describe('can manage', () => {
it('renders the ProficiencyRating component', () => {
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: true})} />)
expect(wrapper).toMatchSnapshot()
})
it('mastery checkbox is checked if mastery', () => {
const wrapper = shallow(
<ProficiencyRating
{...defaultProps({
mastery: true,
canManage: true
})}
/>
)
@ -55,7 +137,7 @@ it('mastery checkbox is checked if mastery', () => {
it('mastery checkbox receives focus', () => {
const wrapper = mount(
<div>
<ProficiencyRating {...defaultProps({focusField: 'mastery'})} />
<ProficiencyRating {...defaultProps({focusField: 'mastery', canManage: true})} />
</div>
)
expect(
@ -68,7 +150,9 @@ it('mastery checkbox receives focus', () => {
it('clicking mastery checkbox triggers change', () => {
const onMasteryChange = jest.fn()
const wrapper = mount(<ProficiencyRating {...defaultProps({onMasteryChange})} />)
const wrapper = mount(
<ProficiencyRating {...defaultProps({onMasteryChange, canManage: true})} />
)
wrapper
.find('RadioInput')
.find('input')
@ -77,14 +161,16 @@ it('clicking mastery checkbox triggers change', () => {
})
it('includes the rating description', () => {
const wrapper = shallow(<ProficiencyRating {...defaultProps()} />)
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: true})} />)
const input = wrapper.find('TextInput').at(0)
expect(input.prop('defaultValue')).toBe('Stellar')
})
it('changing description triggers change', () => {
const onDescriptionChange = jest.fn()
const wrapper = mount(<ProficiencyRating {...defaultProps({onDescriptionChange})} />)
const wrapper = mount(
<ProficiencyRating {...defaultProps({onDescriptionChange, canManage: true})} />
)
wrapper
.find('TextInput')
.at(0)
@ -94,14 +180,16 @@ it('changing description triggers change', () => {
})
it('includes the points', () => {
const wrapper = shallow(<ProficiencyRating {...defaultProps()} />)
const wrapper = shallow(<ProficiencyRating {...defaultProps({canManage: true})} />)
const input = wrapper.find('TextInput').at(1)
expect(input.prop('defaultValue')).toBe('10')
})
it('changing points triggers change', () => {
const onPointsChange = jest.fn()
const wrapper = mount(<ProficiencyRating {...defaultProps({onPointsChange})} />)
const wrapper = mount(
<ProficiencyRating {...defaultProps({onPointsChange, canManage: true})} />
)
wrapper
.find('TextInput')
.at(1)
@ -112,7 +200,7 @@ it('changing points triggers change', () => {
it('clicking delete button triggers delete', () => {
const onDelete = jest.fn()
const wrapper = mount(<ProficiencyRating {...defaultProps({onDelete})} />)
const wrapper = mount(<ProficiencyRating {...defaultProps({onDelete, canManage: true})} />)
wrapper
.find('IconButton')
.at(0)
@ -126,7 +214,8 @@ it('clicking disabled delete button does not triggers delete', () => {
<ProficiencyRating
{...defaultProps({
onDelete,
disableDelete: true
disableDelete: true,
canManage: true
})}
/>
)
@ -136,3 +225,5 @@ it('clicking disabled delete button does not triggers delete', () => {
.simulate('click')
expect(onDelete).toHaveBeenCalledTimes(0)
})
})
})

View File

@ -252,4 +252,39 @@ describe('custom proficiency', () => {
expect(deleteButtons.length).toEqual(1)
expect(deleteButtons[0].disabled).toEqual(true)
})
describe('can not manage', () => {
const props = {
...defaultProps,
canManage: false,
proficiency: {
proficiencyRatingsConnection: {
nodes: [
{
description: 'Great',
points: 10,
color: '0000ff',
mastery: true
},
{
description: 'Poor',
points: 0,
color: 'ff0000',
mastery: false
}
]
}
}
}
it('does not render Save button', () => {
const {queryByText} = render(<ProficiencyTable {...props} />)
expect(queryByText('Save Mastery Scale')).not.toBeInTheDocument()
})
it('does not render Add button', () => {
const {queryByText} = render(<ProficiencyTable {...props} />)
expect(queryByText('Add Proficiency Level')).not.toBeInTheDocument()
})
})
})

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders the ProficiencyRating component 1`] = `
exports[`ProficiencyRating can manage renders the ProficiencyRating component 1`] = `
<Flex
alignItems="start"
as="span"
@ -23,7 +23,7 @@ exports[`renders the ProficiencyRating component 1`] = `
textAlign="center"
>
<div
className="mastery"
className="mastery null"
>
<RadioInput
checked={false}
@ -324,3 +324,132 @@ exports[`renders the ProficiencyRating component 1`] = `
</Item>
</Flex>
`;
exports[`ProficiencyRating can not manage renders the ProficiencyRating component 1`] = `
<Flex
alignItems="start"
as="span"
direction="row"
display="flex"
elementRef={[Function]}
justifyItems="start"
padding="0 small small small"
width="100%"
withVisualDebug={false}
wrap="no-wrap"
>
<Item
as="span"
elementRef={[Function]}
padding="0 medium 0 0"
shouldGrow={false}
shouldShrink={false}
size="15%"
textAlign="center"
>
<div
className="mastery view-only"
/>
</Item>
<Item
align="start"
as="span"
elementRef={[Function]}
padding="0 small 0 0"
shouldGrow={false}
shouldShrink={false}
size="40%"
>
<div
className="description"
>
<Text
as="span"
letterSpacing="normal"
size="medium"
wrap="normal"
>
<ScreenReaderContent
as="span"
>
Description for proficiency rating 1: Stellar
</ScreenReaderContent>
<PresentationContent
as="span"
>
Stellar
</PresentationContent>
</Text>
</div>
</Item>
<Item
align="start"
as="span"
elementRef={[Function]}
padding="0 small 0 0"
shouldGrow={false}
shouldShrink={false}
size="15%"
>
<div
className="points"
>
<View
borderColor="default"
debug={false}
display="auto"
focusColor="info"
focusPosition="offset"
focused={false}
margin="0 0 0 small"
overflowX="visible"
overflowY="visible"
position="static"
shouldAnimateFocus={true}
>
<ScreenReaderContent
as="span"
>
Points for proficiency rating 1: 10.0
</ScreenReaderContent>
<PresentationContent
as="span"
>
10
<div
className="pointsDescription view-only"
>
points
</div>
</PresentationContent>
</View>
</div>
</Item>
<Item
as="span"
elementRef={[Function]}
shouldGrow={false}
shouldShrink={false}
>
<div
className="color"
>
<span
className="colorPickerIcon"
style={
Object {
"background": "#00ff00",
"marginLeft": "2rem",
}
}
>
<ScreenReaderContent
as="span"
>
Color #00ff00 for proficiency rating 1
</ScreenReaderContent>
</span>
</div>
</Item>
</Flex>
`;

View File

@ -28,7 +28,10 @@ describe('MasteryScale', () => {
window.ENV = {
PROFICIENCY_SCALES_ENABLED_ROLES: [
{id: '1', role: 'AccountAdmin', label: 'Account Admin', base_role_type: 'AccountMembership'}
]
],
PERMISSIONS: {
manage_proficiency_scales: true
}
}
})
@ -168,4 +171,28 @@ describe('MasteryScale', () => {
})
})
})
describe('can not manage', () => {
beforeEach(() => {
window.ENV.PERMISSIONS = {
manage_proficiency_scales: false
}
})
afterEach(() => {
window.ENV.PERMISSIONS = null
})
it('hides role list', async () => {
const {getByText, queryByText} = render(
<MockedProvider mocks={mocks}>
<MasteryScale contextType="Account" contextId="11" />
</MockedProvider>
)
expect(getByText('Loading')).not.toEqual(null)
await wait()
expect(queryByText(/Permission to change this mastery calculation/)).not.toBeInTheDocument()
expect(queryByText(/Account Admin/)).not.toBeInTheDocument()
})
})
})

View File

@ -72,8 +72,11 @@ const MasteryScale = ({contextType, contextId}) => {
}
const {outcomeProficiency} = data.account
const roles = ENV.PROFICIENCY_SCALES_ENABLED_ROLES || []
const canManage = ENV.PERMISSIONS.manage_proficiency_scales
return (
<div>
{canManage && (
<>
<p>
<Text>
{I18n.t(
@ -81,12 +84,16 @@ const MasteryScale = ({contextType, contextId}) => {
)}
</Text>
</p>
<RoleList
description={I18n.t(
'Permission to change this mastery scale is enabled at the account level for:'
)}
roles={roles}
/>
</>
)}
<ProficiencyTable
proficiency={outcomeProficiency || undefined} // send undefined when value is null
update={updateProficiencyRatings}

View File

@ -574,4 +574,17 @@ const ColorPicker = createReactClass({
return this.props.nonModal ? body : this.modalWrapping(body)
}
})
ColorPicker.getColorName = colorHex => {
const colorWithoutHash = colorHex.replace('#', '')
const definedColor = PREDEFINED_COLORS.find(
color => color.hexcode.replace('#', '') === colorWithoutHash
)
if (definedColor) {
return definedColor.name
}
}
export default ColorPicker

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2016 - 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 ColorPicker from '../ColorPicker'
describe('ColorPicker', () => {
describe('getColorName', () => {
it('returns name', () => {
expect(ColorPicker.getColorName('#BD3C14')).toBe('Brick')
})
it('returns name without hash', () => {
expect(ColorPicker.getColorName('BD3C14')).toBe('Brick')
})
it('returns undefined if color does not exists in PREDEFINED_COLORS', () => {
expect(ColorPicker.getColorName('#111111')).toBeUndefined()
})
})
})

View File

@ -15,6 +15,12 @@
padding-#{direction(left)}: 16px;
padding-top: 10px;
}
&.view-only {
@include desktop-only {
padding-top: 2px;
}
}
}
.delete {
@ -65,6 +71,12 @@
padding-#{direction(left)}: 8px;
padding-top: 6px;
}
&.view-only {
@include mobile-only {
padding-top: 0;
}
}
}
.header {
@ -85,4 +97,10 @@
justify-content: space-between;
display: flex;
}
&.view-only {
@include mobile-only {
margin-#{direction(left)}: -1px;
}
}
}

View File

@ -78,8 +78,11 @@ describe OutcomesController do
user_session(@admin)
get 'index', params: {:account_id => @account.id}
permissions = assigns[:js_env][:PERMISSIONS]
[:manage_outcomes, :manage_rubrics, :manage_courses, :import_outcomes].each do |permission|
expect(permissions.key?(permission)).to be_truthy
[
:manage_outcomes, :manage_rubrics, :manage_courses, :import_outcomes, :manage_proficiency_scales,
:manage_proficiency_calculations
].each do |permission|
expect(permissions).to have_key(permission)
end
end