Only allow non-negative points

refs OUT-2240

test plan:
  - enable the non-scoring rubric feature flag if not enabled
  - load the accounts rubrics page
  - click the "Learning Mastery" tab
  - confirm that when attempting to save ratings with
    negative points, an error message is displayed below
    the points field

Change-Id: Ic41b013cbc6499e3c1783fed2283fe6ed02ba2f0
Reviewed-on: https://gerrit.instructure.com/153069
Reviewed-by: Michael Brewer-Davis <mbd@instructure.com>
Reviewed-by: Matt Berns <mberns@instructure.com>
Tested-by: Jenkins
QA-Review: Michael Brewer-Davis <mbd@instructure.com>
Product-Review: Sidharth Oberoi <soberoi@instructure.com>
This commit is contained in:
Augusto Callejas 2018-06-08 07:49:13 -04:00
parent 9864f1c2ac
commit 754c955fc9
4 changed files with 52 additions and 3 deletions

View File

@ -142,7 +142,7 @@ export default class ProficiencyTable extends React.Component {
handlePointsChange = _.memoize((index) => (value) => {
const parsed = NumberHelper.parse(value)
let rows = this.state.rows
if (!this.invalidPoints(parsed)) {
if (!this.invalidPoints(parsed) && parsed >= 0) {
rows = rows.removeIn([index, 'pointsError'])
}
rows = rows.setIn([index, 'points'], parsed)
@ -164,7 +164,7 @@ export default class ProficiencyTable extends React.Component {
})
isStateValid = () => !this.state.rows.some(row =>
this.invalidPoints(row.get('points')) || this.invalidDescription(row.get('description')))
this.invalidPoints(row.get('points')) || row.get('points') < 0 || this.invalidDescription(row.get('description')))
stateToConfig = () => ({
@ -208,6 +208,12 @@ export default class ProficiencyTable extends React.Component {
r = r.set('focusField', 'points')
firstError = false
}
} else if (row.get('points') < 0) {
r = r.set('pointsError', I18n.t('Negative points'))
if (firstError) {
r = r.set('focusField', 'points')
firstError = false
}
}
return r
})

View File

@ -73,6 +73,42 @@ describe('default proficiency', () => {
})
})
it('setting blank description sets error', () => {
const wrapper = mount(<ProficiencyTable {...defaultProps()}/>)
return promise.then(() => {
wrapper.instance().handleDescriptionChange(0)("")
wrapper.find('Button').last().simulate('click')
expect(wrapper.find('ProficiencyRating').first().prop('descriptionError')).toBe('Missing required description')
})
})
it('setting blank points sets error', () => {
const wrapper = mount(<ProficiencyTable {...defaultProps()}/>)
return promise.then(() => {
wrapper.instance().handlePointsChange(0)("")
wrapper.find('Button').last().simulate('click')
expect(wrapper.find('ProficiencyRating').first().prop('pointsError')).toBe('Invalid points')
})
})
it('setting invalid points sets error', () => {
const wrapper = mount(<ProficiencyTable {...defaultProps()}/>)
return promise.then(() => {
wrapper.instance().handlePointsChange(0)("1.1.1")
wrapper.find('Button').last().simulate('click')
expect(wrapper.find('ProficiencyRating').first().prop('pointsError')).toBe('Invalid points')
})
})
it('setting negative points sets error', () => {
const wrapper = mount(<ProficiencyTable {...defaultProps()}/>)
return promise.then(() => {
wrapper.instance().handlePointsChange(0)("-1")
wrapper.find('Button').last().simulate('click')
expect(wrapper.find('ProficiencyRating').first().prop('pointsError')).toBe('Negative points')
})
})
it('sends POST on submit', () => {
const postSpy = jest.spyOn(axios,'post').mockImplementation(() => Promise.resolve({status: 200}))
const wrapper = mount(<ProficiencyTable {...defaultProps()}/>)
@ -154,3 +190,9 @@ it('invalid rating points leaves state invalid', () => {
wrapper.instance().handlePointsChange(0)("1.1.1")
expect(wrapper.instance().isStateValid()).toBe(false)
})
it('negative rating points leaves state invalid', () => {
const wrapper = shallow(<ProficiencyTable {...defaultProps()}/>)
wrapper.instance().handlePointsChange(0)("-1")
expect(wrapper.instance().isStateValid()).toBe(false)
})

View File

@ -20,7 +20,7 @@ class OutcomeProficiencyRating < ApplicationRecord
belongs_to :outcome_proficiency, inverse_of: :outcome_proficiency_ratings
validates :description, presence: true
validates :points, presence: true
validates :points, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :color, presence: true, format: /\A([A-Fa-f0-9]{6})\z/i
def as_json(_options={})

View File

@ -26,6 +26,7 @@ describe OutcomeProficiencyRating, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of :description }
it { is_expected.to validate_presence_of :points }
it { is_expected.to validate_numericality_of(:points).is_greater_than_or_equal_to(0) }
it { is_expected.to allow_value('0F160a').for(:color) }
it { is_expected.not_to allow_value('#0F160a').for(:color) }
it { is_expected.not_to allow_value('').for(:color) }