Display roles that can edit proficencies
closes OUT-3945 flag=account_level_mastery_scales test plan: - enable account levels mastery scales flag - create custom roles that have different outcome proficiency permissions (scales & calculation methods) - confirm the mastery and calculation tabs on an account outcomes page display the roles that have the respective permissions Change-Id: Ic677ca651c1825629609f00532598532313e002a Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/247692 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Brian Watson <bwatson@instructure.com> Product-Review: Jody Sailor Reviewed-by: Michael Brewer-Davis <mbd@instructure.com>
This commit is contained in:
parent
0ab3fbcb7f
commit
b4bf5427e2
|
@ -18,6 +18,7 @@
|
|||
|
||||
class OutcomesController < ApplicationController
|
||||
include Api::V1::Outcome
|
||||
include Api::V1::Role
|
||||
before_action :require_context, :except => [:build_outcomes]
|
||||
add_crumb(proc { t "#crumbs.outcomes", "Outcomes" }, :except => [:destroy, :build_outcomes]) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_outcomes_path }
|
||||
before_action { |c| c.active_tab = "outcomes" }
|
||||
|
@ -50,6 +51,7 @@ class OutcomesController < ApplicationController
|
|||
|
||||
set_tutorial_js_env
|
||||
mastery_scales_js_env
|
||||
proficiency_roles_js_env
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -311,4 +313,23 @@ class OutcomesController < ApplicationController
|
|||
def learning_outcome_params
|
||||
params.require(:learning_outcome).permit(:description, :short_description, :title, :display_name, :vendor_guid)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def proficiency_roles_js_env
|
||||
if @context.is_a?(Account) && @context.root_account.feature_enabled?(:account_level_mastery_scales)
|
||||
proficiency_calculation_roles = []
|
||||
@context.roles_with_enabled_permission(:manage_proficiency_calculations).each do |role|
|
||||
proficiency_calculation_roles << role_json(@context, role, @current_user, session, skip_permissions: true)
|
||||
end if @context.grants_right? @current_user, :manage_proficiency_calculations
|
||||
proficiency_scales_roles = []
|
||||
@context.roles_with_enabled_permission(:manage_proficiency_scales).each do |role|
|
||||
proficiency_scales_roles << role_json(@context, role, @current_user, session, skip_permissions: true)
|
||||
end if @context.grants_right? @current_user, :manage_proficiency_scales
|
||||
js_env(
|
||||
PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES: proficiency_calculation_roles,
|
||||
PROFICIENCY_SCALES_ENABLED_ROLES: proficiency_scales_roles
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,18 @@ import {OUTCOME_PROFICIENCY_QUERY, SET_OUTCOME_CALCULATION_METHOD} from '../api'
|
|||
import MasteryCalculation from '../index'
|
||||
|
||||
describe('MasteryCalculation', () => {
|
||||
beforeEach(() => {
|
||||
window.ENV = {
|
||||
PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES: [
|
||||
{id: '1', role: 'AccountAdmin', label: 'Account Admin', base_role_type: 'AccountMembership'}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
window.ENV = null
|
||||
})
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
|
@ -61,6 +73,18 @@ describe('MasteryCalculation', () => {
|
|||
expect(getByDisplayValue(/65/)).not.toEqual(null)
|
||||
})
|
||||
|
||||
it('loads role list', async () => {
|
||||
const {getByText} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MasteryCalculation contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
expect(getByText('Loading')).not.toEqual(null)
|
||||
await wait()
|
||||
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(
|
||||
<MockedProvider mocks={[]}>
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
|
||||
import React, {useCallback} from 'react'
|
||||
import I18n from 'i18n!MasteryScale'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {Spinner} from '@instructure/ui-spinner'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import ProficiencyCalculation from './ProficiencyCalculation'
|
||||
import RoleList from '../RoleList'
|
||||
import {OUTCOME_PROFICIENCY_QUERY, SET_OUTCOME_CALCULATION_METHOD} from './api'
|
||||
import {useQuery, useMutation} from 'react-apollo'
|
||||
|
||||
|
@ -57,11 +57,15 @@ const MasteryCalculation = ({contextType, contextId}) => {
|
|||
)
|
||||
}
|
||||
const {outcomeCalculationMethod} = data.account
|
||||
const roles = ENV.PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES || []
|
||||
return (
|
||||
<>
|
||||
<Heading level="h5" margin="medium 0">
|
||||
{I18n.t('Set the mastery scale to be used for all courses within this account.')}
|
||||
</Heading>
|
||||
<RoleList
|
||||
description={I18n.t(
|
||||
'Permission to change this mastery calculation is enabled at the account level for:'
|
||||
)}
|
||||
roles={roles}
|
||||
/>
|
||||
<ProficiencyCalculation
|
||||
contextType={contextType}
|
||||
contextId={contextId}
|
||||
|
|
|
@ -24,6 +24,18 @@ import {OUTCOME_PROFICIENCY_QUERY} from '../api'
|
|||
import MasteryScale from '../index'
|
||||
|
||||
describe('MasteryScale', () => {
|
||||
beforeEach(() => {
|
||||
window.ENV = {
|
||||
PROFICIENCY_SCALES_ENABLED_ROLES: [
|
||||
{id: '1', role: 'AccountAdmin', label: 'Account Admin', base_role_type: 'AccountMembership'}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
window.ENV = null
|
||||
})
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
|
@ -81,6 +93,18 @@ describe('MasteryScale', () => {
|
|||
expect(getByDisplayValue(/Rating A/)).not.toEqual(null)
|
||||
})
|
||||
|
||||
it('loads role list', async () => {
|
||||
const {getByText} = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MasteryScale contextType="Account" contextId="11" />
|
||||
</MockedProvider>
|
||||
)
|
||||
expect(getByText('Loading')).not.toEqual(null)
|
||||
await wait()
|
||||
expect(getByText(/Permission to change this mastery scale/)).not.toEqual(null)
|
||||
expect(getByText(/Account Admin/)).not.toEqual(null)
|
||||
})
|
||||
|
||||
it('displays an error on failed request', async () => {
|
||||
const {getByText} = render(
|
||||
<MockedProvider mocks={[]}>
|
||||
|
|
|
@ -18,13 +18,12 @@
|
|||
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import I18n from 'i18n!MasteryScale'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {Spinner} from '@instructure/ui-spinner'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import ProficiencyTable from './ProficiencyTable'
|
||||
import RoleList from '../RoleList'
|
||||
import {saveProficiency, OUTCOME_PROFICIENCY_QUERY} from './api'
|
||||
import {useQuery, useMutation} from 'react-apollo'
|
||||
import {useQuery} from 'react-apollo'
|
||||
|
||||
const MasteryScale = ({contextType, contextId}) => {
|
||||
const {loading, error, data} = useQuery(OUTCOME_PROFICIENCY_QUERY, {
|
||||
|
@ -72,11 +71,22 @@ const MasteryScale = ({contextType, contextId}) => {
|
|||
)
|
||||
}
|
||||
const {outcomeProficiency} = data.account
|
||||
const roles = ENV.PROFICIENCY_SCALES_ENABLED_ROLES || []
|
||||
return (
|
||||
<div>
|
||||
<Heading level="h5" margin="medium 0">
|
||||
{I18n.t('Set the mastery scale to be used for all courses within this account.')}
|
||||
</Heading>
|
||||
<p>
|
||||
<Text>
|
||||
{I18n.t(
|
||||
'This mastery scale will be used as the default for all courses within your account.'
|
||||
)}
|
||||
</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}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - 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 {arrayOf, string, shape} from 'prop-types'
|
||||
import {List} from '@instructure/ui-list'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {getSortedRoles} from '../permissions/helper/utils'
|
||||
|
||||
const RoleList = ({description, roles}) => {
|
||||
if (roles?.length > 0) {
|
||||
const accountAdmin = roles.find(element => element.role === 'AccountAdmin') || {}
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<Text>{description}</Text>
|
||||
</p>
|
||||
<List>
|
||||
{getSortedRoles(roles, accountAdmin).map(role => (
|
||||
<List.Item key={role.id}>{role.label}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
RoleList.propTypes = {
|
||||
description: string.isRequired,
|
||||
roles: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
role: string.isRequired,
|
||||
label: string.isRequired,
|
||||
base_role_type: string.isRequired
|
||||
})
|
||||
).isRequired
|
||||
}
|
||||
|
||||
export default RoleList
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - 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 {render, shallow} from 'enzyme'
|
||||
import RoleList from '../RoleList'
|
||||
|
||||
const defaultProps = (props = {}) => ({
|
||||
description: 'My RoleList',
|
||||
roles: [
|
||||
{id: '26', role: 'Custom Admin', label: 'Custom Admin', base_role_type: 'AccountMembership'},
|
||||
{id: '1', role: 'AccountAdmin', label: 'Account Admin', base_role_type: 'AccountMembership'}
|
||||
],
|
||||
...props
|
||||
})
|
||||
|
||||
it('renders the RoleList component', () => {
|
||||
const list = shallow(<RoleList {...defaultProps()} />, {disableLifecycleMethods: true})
|
||||
expect(list.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders description', () => {
|
||||
const list = render(<RoleList {...defaultProps()} />)
|
||||
expect(list.text()).toContain('My RoleList')
|
||||
})
|
||||
|
||||
it('renders roles with account admin first', () => {
|
||||
const list = render(<RoleList {...defaultProps()} />)
|
||||
expect(list.text()).toContain('Account AdminCustom Admin')
|
||||
})
|
||||
|
||||
it('does not renders description without roles', () => {
|
||||
const list = render(<RoleList {...defaultProps({roles: []})} />)
|
||||
expect(list.text()).not.toContain('My RoleList')
|
||||
})
|
|
@ -2035,4 +2035,11 @@ class Account < ActiveRecord::Base
|
|||
def self.ensure_dummy_root_account
|
||||
Account.find_or_create_by!(id: 0) if Rails.env.test?
|
||||
end
|
||||
|
||||
def roles_with_enabled_permission(permission)
|
||||
roles = available_roles
|
||||
roles.select do |role|
|
||||
RoleOverride.permission_for(self, permission, role, self, true)[:enabled]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,7 +38,7 @@ module Api::V1::Role
|
|||
perm = RoleOverride.permission_for(account, permission, role, account, true)
|
||||
next if permission == :manage_developer_keys && !account.root_account?
|
||||
json[:permissions][permission] = permission_json(perm, current_user, session) if perm[:account_allows]
|
||||
end
|
||||
end unless opts[:skip_permissions]
|
||||
|
||||
json
|
||||
end
|
||||
|
|
|
@ -82,6 +82,23 @@ describe OutcomesController do
|
|||
expect(permissions.key?(permission)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'account_level_mastery_scales feature flag enabled' do
|
||||
before(:once) do
|
||||
@account.root_account.enable_feature! :account_level_mastery_scales
|
||||
end
|
||||
|
||||
it 'includes proficiency roles' do
|
||||
user_session(@admin)
|
||||
get 'index', params: {:account_id => @account.id}
|
||||
|
||||
%i[PROFICIENCY_CALCULATION_METHOD_ENABLED_ROLES PROFICIENCY_SCALES_ENABLED_ROLES].each do |key|
|
||||
roles = controller.js_env[key]
|
||||
expect(roles.length).to eq 1
|
||||
expect(roles.dig(0, :role)).to eq 'AccountAdmin'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET 'show'" do
|
||||
|
|
|
@ -2050,4 +2050,24 @@ describe Account do
|
|||
expect(Account.find(sub_acc2.id).effective_brand_config).to eq config2
|
||||
end
|
||||
end
|
||||
|
||||
context '#roles_with_enabled_permission' do
|
||||
let(:account) { account_model }
|
||||
|
||||
it 'returns expected roles with the given permission' do
|
||||
role = account.roles.create :name => 'AssistantGrader'
|
||||
role.base_role_type = 'TaEnrollment'
|
||||
role.workflow_state = 'active'
|
||||
role.save!
|
||||
RoleOverride.create!(
|
||||
context: account,
|
||||
permission: 'change_course_state',
|
||||
role: role,
|
||||
enabled: true
|
||||
)
|
||||
expect(
|
||||
account.roles_with_enabled_permission(:change_course_state).map(&:name).sort
|
||||
).to eq %w[AccountAdmin AssistantGrader DesignerEnrollment TeacherEnrollment]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue