Support students in CreateCourseModal

Since this modal will be used everywhere in Canvas, support
students and users with no enrollments creating courses (when enabled
at the root account). Students will see all accounts where they have
enrollments; users with no enrollments will only see the manually-
created courses subaccount. Don't show homeroom sync options to
students/users with no enrollments. Expose the manually-created
courses subaccount ID/basic info through a new API.

flag = create_course_subaccount_picker
closes LS-2678

Test plan:
 - Enable root account FF: create_course_subaccount_picker
 - In root account settings, allow techers, students, and users with
   no enrollments to create courses
 - Login to k5 dashboard as an admin and click the + (New Subject)
   button
 - Expect to see a manageable_courses network request and a dropdown
   to select an account (along with the checkbox to sync homerooms)
 - Login and open the modal as a teacher
 - Expect to see an enrollments network request and a dropdown with
   all the accounts where the user has a teacher enrollment (and the
   homeroom sync checkbox)
 - Login and open the modal as a student
 - Expect another enrollments network request and a dropdown with all
   their accounts (if there's more than one)
 - Expect to not see the sync options
 - Login as a user with no enrollments
 - Expect a network request to the manually_created_courses_account api
 - Expect to not see a dropdown with accounts or the sync options
 - Disable 'X can create courses' at the root account and expect the
   associated users to no longer see the create course modal at all
 - With FF disabled, expect course creation to work as before

Change-Id: I0e7d49628ce6395fd366037a3134133084fe6275
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/274986
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Product-Review: Peyton Craighill <pcraighill@instructure.com>
Reviewed-by: Robin Kuss <rkuss@instructure.com>
QA-Review: Robin Kuss <rkuss@instructure.com>
This commit is contained in:
Jackson Howe 2021-10-01 10:56:26 -06:00
parent bbe7c5d357
commit 498aaa11db
21 changed files with 269 additions and 151 deletions

View File

@ -293,7 +293,7 @@ require 'csv'
# } # }
class AccountsController < ApplicationController class AccountsController < ApplicationController
before_action :require_user, :only => [:index, :help_links] before_action :require_user, :only => [:index, :help_links, :manually_created_courses_account]
before_action :reject_student_view_student before_action :reject_student_view_student
before_action :get_context before_action :get_context
before_action :rce_js_env, only: [:settings] before_action :rce_js_env, only: [:settings]
@ -516,6 +516,15 @@ class AccountsController < ApplicationController
render :json => links render :json => links
end end
# @API Get the manually-created courses sub-account for the domain root account
#
# @returns Account
def manually_created_courses_account
account = @domain_root_account.manually_created_courses_account
read_only = !account.grants_right?(@current_user, session, :read)
render :json => account_json(account, @current_user, session, [], read_only)
end
include Api::V1::Course include Api::V1::Course
# @API List active courses in an account # @API List active courses in an account

View File

@ -498,6 +498,7 @@ class CoursesController < ApplicationController
format.html { format.html {
css_bundle :context_list, :course_list css_bundle :context_list, :course_list
js_bundle :course_list js_bundle :course_list
js_env({ CREATE_COURSES_PERMISSION: @current_user.create_courses_right(@domain_root_account) })
set_k5_mode(require_k5_theme: true) set_k5_mode(require_k5_theme: true)
@ -887,7 +888,7 @@ class CoursesController < ApplicationController
Auditors::Course.record_published(@course, @current_user, source: :api) Auditors::Course.record_published(@course, @current_user, source: :api)
end end
# Sync homeroom enrollments and participation if enabled and the course isn't a SIS import # Sync homeroom enrollments and participation if enabled and the course isn't a SIS import
if @course.elementary_enabled? && params[:course][:sync_enrollments_from_homeroom] && params[:course][:homeroom_course_id] && @course.sis_batch_id.blank? if @course.elementary_enabled? && value_to_boolean(params[:course][:sync_enrollments_from_homeroom]) && params[:course][:homeroom_course_id] && @course.sis_batch_id.blank?
progress = Progress.new(context: @course, tag: :sync_homeroom_enrollments) progress = Progress.new(context: @course, tag: :sync_homeroom_enrollments)
progress.user = @current_user progress.user = @current_user
progress.reset! progress.reset!

View File

@ -513,7 +513,8 @@ class UsersController < ApplicationController
STUDENT_PLANNER_ENABLED: planner_enabled?, STUDENT_PLANNER_ENABLED: planner_enabled?,
STUDENT_PLANNER_COURSES: planner_enabled? && map_courses_for_menu(@current_user.courses_with_primary_enrollment), STUDENT_PLANNER_COURSES: planner_enabled? && map_courses_for_menu(@current_user.courses_with_primary_enrollment),
STUDENT_PLANNER_GROUPS: planner_enabled? && map_groups_for_planner(@current_user.current_groups), STUDENT_PLANNER_GROUPS: planner_enabled? && map_groups_for_planner(@current_user.current_groups),
CAN_ENABLE_K5_DASHBOARD: k5_disabled && k5_user CAN_ENABLE_K5_DASHBOARD: k5_disabled && k5_user,
CREATE_COURSES_PERMISSION: @current_user.create_courses_right(@domain_root_account)
}) })
if k5_user? if k5_user?
@ -527,10 +528,6 @@ class UsersController < ApplicationController
SELECTED_CONTEXT_CODES: @current_user.get_preference(:selected_calendar_contexts), SELECTED_CONTEXT_CODES: @current_user.get_preference(:selected_calendar_contexts),
SELECTED_CONTEXTS_LIMIT: @domain_root_account.settings[:calendar_contexts_limit] || 10, SELECTED_CONTEXTS_LIMIT: @domain_root_account.settings[:calendar_contexts_limit] || 10,
INITIAL_NUM_K5_CARDS: Rails.cache.read(['last_known_k5_cards_count', @current_user.global_id].cache_key) || 5, INITIAL_NUM_K5_CARDS: Rails.cache.read(['last_known_k5_cards_count', @current_user.global_id].cache_key) || 5,
PERMISSIONS: {
create_courses_as_admin: @current_user.roles(@domain_root_account).include?('admin'),
create_courses_as_teacher: @domain_root_account.grants_right?(@current_user, session, :create_courses)
},
CAN_ADD_OBSERVEE: @current_user CAN_ADD_OBSERVEE: @current_user
.profile .profile
.tabs_available(@current_user, :root_account => @domain_root_account) .tabs_available(@current_user, :root_account => @domain_root_account)

View File

@ -1316,32 +1316,7 @@ class Account < ActiveRecord::Base
given { |user| self.root_account? && self.cached_all_account_users_for(user).any? } given { |user| self.root_account? && self.cached_all_account_users_for(user).any? }
can :read_terms can :read_terms
#################### Begin legacy permission block ######################### given { |user| user&.create_courses_right(self).present? }
given do |user|
user && !root_account.feature_enabled?(:granular_permissions_manage_courses) &&
self.cached_account_users_for(user).any? do |au|
au.has_permission_to?(self, :manage_courses)
end
end
can :create_courses
##################### End legacy permission block ##########################
given do |user|
result = false
next false if user&.fake_student?
if user && !root_account.site_admin?
scope = root_account.enrollments.active.where(user_id: user)
result = root_account.teachers_can_create_courses? &&
scope.where(:type => ['TeacherEnrollment', 'DesignerEnrollment']).exists?
result ||= root_account.students_can_create_courses? &&
scope.where(:type => ['StudentEnrollment', 'ObserverEnrollment']).exists?
result ||= root_account.no_enrollments_can_create_courses? &&
!scope.exists?
end
result
end
can :create_courses can :create_courses
# allow teachers to view term dates # allow teachers to view term dates

View File

@ -3250,4 +3250,18 @@ class User < ActiveRecord::Base
def pronouns=(pronouns) def pronouns=(pronouns)
write_attribute(:pronouns, untranslate_pronouns(pronouns)) write_attribute(:pronouns, untranslate_pronouns(pronouns))
end end
def create_courses_right(account)
return :admin if account.cached_account_users_for(self).any? do |au|
au.has_permission_to?(account, :manage_courses) || au.has_permission_to?(account, :manage_courses_add)
end
return nil if fake_student? || account.root_account.site_admin?
scope = account.root_account.enrollments.active.where(user_id: self)
return :teacher if account.root_account.teachers_can_create_courses? && scope.exists?(type: %w[TeacherEnrollment DesignerEnrollment])
return :student if account.root_account.students_can_create_courses? && scope.exists?(type: %w[StudentEnrollment ObserverEnrollment])
return :no_enrollments if account.root_account.no_enrollments_can_create_courses? && !scope.exists?
nil
end
end end

View File

@ -274,7 +274,7 @@
<div class="tall-row"> <div class="tall-row">
<div class="nobr"> <div class="nobr">
<%= f.check_box :sync_enrollments_from_homeroom, class: 'sync_enrollments_from_homeroom_checkbox', :disabled => !can_manage || @context.homeroom_course %> <%= f.check_box :sync_enrollments_from_homeroom, class: 'sync_enrollments_from_homeroom_checkbox', :disabled => !can_manage || @context.homeroom_course %>
<%= f.label :sync_enrollments_from_homeroom, :en => "Sync enrollments and course start/end dates from homeroom" %><br/> <%= f.label :sync_enrollments_from_homeroom, :en => "Sync enrollments and subject start/end dates from homeroom" %><br/>
</div> </div>
<div class="sync_enrollments_from_homeroom_select"> <div class="sync_enrollments_from_homeroom_select">
<% format_options = options_from_collection_for_select(@homeroom_courses, 'id', 'name', @context.homeroom_course_id) %> <% format_options = options_from_collection_for_select(@homeroom_courses, 'id', 'name', @context.homeroom_course_id) %>

View File

@ -1518,6 +1518,7 @@ CanvasRails::Application.routes.draw do
get 'accounts/:account_id/courses/:id', controller: :courses, action: :show, as: 'account_course_show' get 'accounts/:account_id/courses/:id', controller: :courses, action: :show, as: 'account_course_show'
get 'accounts/:account_id/permissions', action: :permissions get 'accounts/:account_id/permissions', action: :permissions
get 'accounts/:account_id/settings', action: :show_settings get 'accounts/:account_id/settings', action: :show_settings
get 'manually_created_courses_account', action: :manually_created_courses_account
delete 'accounts/:account_id/users/:user_id', action: :remove_user delete 'accounts/:account_id/users/:user_id', action: :remove_user
put 'accounts/:account_id/users/:user_id/restore', action: :restore_user put 'accounts/:account_id/users/:user_id/restore', action: :restore_user
end end

View File

@ -1180,6 +1180,33 @@ describe AccountsController do
end end
end end
describe "manually_created_courses_account" do
it "returns unauthorized if there's no user" do
get 'manually_created_courses_account'
assert_unauthorized
end
it "returns the account with lots of detail if user has :read on the account" do
account_with_admin_logged_in(active_all: true)
get 'manually_created_courses_account'
expect(response).to be_successful
account = json_parse(response.body)
expect(account['name']).to eq('Manually-Created Courses')
expect(account['default_storage_quota_mb']).to be(500)
end
it "returns limited details about the account to students" do
course_with_student_logged_in(active_all: true)
get 'manually_created_courses_account'
expect(response).to be_successful
account = json_parse(response.body)
expect(account['name']).to eq('Manually-Created Courses')
expect(account['default_storage_quota_mb']).to be_nil
end
end
describe "#account_courses" do describe "#account_courses" do
before do before do
@account = Account.create! @account = Account.create!

View File

@ -52,6 +52,7 @@ describe CoursesController do
expect(assigns[:current_enrollments][0]).to eql(@enrollment) expect(assigns[:current_enrollments][0]).to eql(@enrollment)
expect(assigns[:past_enrollments]).not_to be_nil expect(assigns[:past_enrollments]).not_to be_nil
expect(assigns[:future_enrollments]).not_to be_nil expect(assigns[:future_enrollments]).not_to be_nil
expect(assigns[:js_env][:CREATE_COURSES_PERMISSION]).to be_nil
end end
it "does not duplicate enrollments in variables" do it "does not duplicate enrollments in variables" do

View File

@ -2685,37 +2685,6 @@ describe UsersController do
end end
end end
context "ENV.PERMISSIONS" do
it "sets :create_courses_as_admin to true if user is admin" do
account_admin_user
user_session @user
get 'user_dashboard'
expect(assigns[:js_env][:PERMISSIONS][:create_courses_as_admin]).to be_truthy
end
it "sets only :create_courses_as_teacher to true if user is a teacher and teachers can create courses" do
Account.default.settings[:teachers_can_create_courses] = true
course_with_teacher_logged_in
get 'user_dashboard'
expect(assigns[:js_env][:PERMISSIONS][:create_courses_as_admin]).to be_falsey
expect(assigns[:js_env][:PERMISSIONS][:create_courses_as_teacher]).to be_truthy
end
it "sets :create_courses_as_admin and :create_courses_as_teacher to false if user is a teacher but teachers can't create courses" do
course_with_teacher_logged_in
get 'user_dashboard'
expect(assigns[:js_env][:PERMISSIONS][:create_courses_as_admin]).to be_falsey
expect(assigns[:js_env][:PERMISSIONS][:create_courses_as_teacher]).to be_falsey
end
it "sets :create_courses_as_admin and :create_courses_as_teacher to false if user is a student" do
course_with_student_logged_in
get 'user_dashboard'
expect(assigns[:js_env][:PERMISSIONS][:create_courses_as_admin]).to be_falsey
expect(assigns[:js_env][:PERMISSIONS][:create_courses_as_teacher]).to be_falsey
end
end
context "@cards_prefetch_observer_param" do context "@cards_prefetch_observer_param" do
before :once do before :once do
Account.site_admin.enable_feature!(:k5_parent_support) Account.site_admin.enable_feature!(:k5_parent_support)
@ -2743,6 +2712,15 @@ describe UsersController do
end end
end end
end end
it "sets ENV.CREATE_COURSES_PERMISSION to teacher if user is a teacher and can create courses" do
Account.default.settings[:teachers_can_create_courses] = true
Account.default.save!
course_with_teacher_logged_in(active_all: true)
get 'user_dashboard'
expect(assigns[:js_env][:CREATE_COURSES_PERMISSION]).to be(:teacher)
end
end end
describe "#pandata_events_token" do describe "#pandata_events_token" do

View File

@ -3675,4 +3675,66 @@ describe User do
expect(@teacher.comment_bank_items).to eq [@c1] expect(@teacher.comment_bank_items).to eq [@c1]
end end
end end
describe "create_courses_right" do
before :once do
@user = user_factory(active_all: true)
@account = Account.default
end
it "returns :admin for AccountUsers with :manage_courses" do
account_admin_user(user: @user)
expect(@user.create_courses_right(@account)).to be(:admin)
end
it "returns nil for AccountUsers without :manage_courses" do
account_admin_user_with_role_changes(user: @user, role_changes: { manage_courses: false })
expect(@user.create_courses_right(@account)).to be_nil
end
it "returns nil if fake student" do
fake_student = course_factory(active_all: true).student_view_student
expect(fake_student.create_courses_right(@account)).to be_nil
end
it "returns :teacher if user has teacher enrollments iff teachers_can_create_courses?" do
course_with_teacher(user: @user, active_all: true)
expect(@user.create_courses_right(@account)).to be_nil
@account.settings[:teachers_can_create_courses] = true
@account.save!
expect(@user.create_courses_right(@account)).to be(:teacher)
end
it "returns :student if user has student enrollments iff students_can_create_courses?" do
course_with_student(user: @user, active_all: true)
expect(@user.create_courses_right(@account)).to be_nil
@account.settings[:students_can_create_courses] = true
@account.save!
expect(@user.create_courses_right(@account)).to be(:student)
end
it "returns :no_enrollments if user has teacher enrollments iff no_enrollments_can_create_courses?" do
expect(@user.create_courses_right(@account)).to be_nil
@account.settings[:no_enrollments_can_create_courses] = true
@account.save!
expect(@user.create_courses_right(@account)).to be(:no_enrollments)
end
it "does not count deleted teacher enrollments" do
enrollment = course_with_teacher(user: @user)
enrollment.workflow_state = 'deleted'
enrollment.save!
@account.settings[:teachers_can_create_courses] = true
@account.save!
expect(@user.create_courses_right(@account)).to be_nil
end
it "returns :student if user has teacher and student enrollments but teachers_can_create_courses is false" do
course_with_teacher(user: @user, active_all: true)
course_with_student(user: @user, active_all: true)
@account.settings[:students_can_create_courses] = true
@account.save!
expect(@user.create_courses_right(@account)).to be(:student)
end
end
end end

View File

@ -290,7 +290,7 @@ module K5DashboardPageObject
end end
def sync_enrollments_checkbox_selector def sync_enrollments_checkbox_selector
"input + label:contains('Sync enrollments and course start/end dates from homeroom')" "input + label:contains('Sync enrollments and subject start/end dates from homeroom')"
end end
def welcome_title_selector def welcome_title_selector

View File

@ -73,22 +73,13 @@ ready(() => {
const container = document.getElementById('create_subject_modal_container') const container = document.getElementById('create_subject_modal_container')
if (container) { if (container) {
startButton.addEventListener('click', () => { startButton.addEventListener('click', () => {
let role
if (ENV.current_user_roles.includes('admin')) {
role = 'admin'
} else if (ENV.current_user_roles.includes('teacher')) {
role = 'teacher'
} else {
// should never get here
return
}
ReactDOM.render( ReactDOM.render(
<CreateCourseModal <CreateCourseModal
isModalOpen isModalOpen
setModalOpen={isOpen => { setModalOpen={isOpen => {
if (!isOpen) ReactDOM.unmountComponentAtNode(container) if (!isOpen) ReactDOM.unmountComponentAtNode(container)
}} }}
permissions={role} permissions={ENV.CREATE_COURSES_PERMISSION}
isK5User={ENV.K5_USER} isK5User={ENV.K5_USER}
/>, />,
container container

View File

@ -270,18 +270,7 @@ function showTodoList() {
const startButton = document.getElementById('start_new_course') const startButton = document.getElementById('start_new_course')
const modalContainer = document.getElementById('create_course_modal_container') const modalContainer = document.getElementById('create_course_modal_container')
let role if (startButton && modalContainer && ENV.FEATURES?.create_course_subaccount_picker) {
if (ENV.current_user_roles.includes('admin')) {
role = 'admin'
} else if (ENV.current_user_roles.includes('teacher')) {
role = 'teacher'
}
if (
startButton &&
modalContainer &&
role &&
ENV.FEATURES?.create_course_subaccount_picker
) {
startButton.addEventListener('click', () => { startButton.addEventListener('click', () => {
ReactDOM.render( ReactDOM.render(
<CreateCourseModal <CreateCourseModal
@ -289,7 +278,7 @@ function showTodoList() {
setModalOpen={isOpen => { setModalOpen={isOpen => {
if (!isOpen) ReactDOM.unmountComponentAtNode(modalContainer) if (!isOpen) ReactDOM.unmountComponentAtNode(modalContainer)
}} }}
permissions={role} permissions={ENV.CREATE_COURSES_PERMISSION}
isK5User={false} // can't be k5 user if classic dashboard is showing isK5User={false} // can't be k5 user if classic dashboard is showing
/>, />,
modalContainer modalContainer

View File

@ -35,13 +35,7 @@ ready(() => {
plannerEnabled={ENV.STUDENT_PLANNER_ENABLED} plannerEnabled={ENV.STUDENT_PLANNER_ENABLED}
timeZone={ENV.TIMEZONE} timeZone={ENV.TIMEZONE}
hideGradesTabForStudents={ENV.HIDE_K5_DASHBOARD_GRADES_TAB} hideGradesTabForStudents={ENV.HIDE_K5_DASHBOARD_GRADES_TAB}
createPermissions={ createPermissions={ENV.CREATE_COURSES_PERMISSION}
ENV.PERMISSIONS?.create_courses_as_admin
? 'admin'
: ENV.PERMISSIONS?.create_courses_as_teacher
? 'teacher'
: 'none'
}
showImportantDates={!!ENV.FEATURES.important_dates} showImportantDates={!!ENV.FEATURES.important_dates}
selectedContextCodes={ENV.SELECTED_CONTEXT_CODES} selectedContextCodes={ENV.SELECTED_CONTEXT_CODES}
selectedContextsLimit={ENV.SELECTED_CONTEXTS_LIMIT} selectedContextsLimit={ENV.SELECTED_CONTEXTS_LIMIT}

View File

@ -81,8 +81,6 @@ const HomeroomPage = ({
</div> </div>
) )
const canCreateCourses = createPermissions === 'admin' || createPermissions === 'teacher'
return ( return (
<section <section
id="dashboard_page_homeroom" id="dashboard_page_homeroom"
@ -100,7 +98,7 @@ const HomeroomPage = ({
<Flex.Item> <Flex.Item>
<Heading level="h2">{I18n.t('My Subjects')}</Heading> <Heading level="h2">{I18n.t('My Subjects')}</Heading>
</Flex.Item> </Flex.Item>
{canCreateCourses && ( {createPermissions && (
<Flex.Item> <Flex.Item>
<Tooltip renderTip={I18n.t('Start a new subject')}> <Tooltip renderTip={I18n.t('Start a new subject')}>
<IconButton <IconButton
@ -141,7 +139,7 @@ const HomeroomPage = ({
HomeroomPage.propTypes = { HomeroomPage.propTypes = {
cards: PropTypes.array, cards: PropTypes.array,
createPermissions: PropTypes.oneOf(['admin', 'teacher', 'none']).isRequired, createPermissions: PropTypes.oneOf(['admin', 'teacher', 'student', 'no_enrollments']),
homeroomAnnouncements: PropTypes.array.isRequired, homeroomAnnouncements: PropTypes.array.isRequired,
loadingAnnouncements: PropTypes.bool.isRequired, loadingAnnouncements: PropTypes.bool.isRequired,
visible: PropTypes.bool.isRequired, visible: PropTypes.bool.isRequired,

View File

@ -389,7 +389,7 @@ K5Dashboard.propTypes = {
assignmentsDueToday: PropTypes.object.isRequired, assignmentsDueToday: PropTypes.object.isRequired,
assignmentsMissing: PropTypes.object.isRequired, assignmentsMissing: PropTypes.object.isRequired,
assignmentsCompletedForToday: PropTypes.object.isRequired, assignmentsCompletedForToday: PropTypes.object.isRequired,
createPermissions: PropTypes.oneOf(['admin', 'teacher', 'none']).isRequired, createPermissions: PropTypes.oneOf(['admin', 'teacher', 'student', 'no_enrollments']),
currentUser: PropTypes.shape({ currentUser: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
display_name: PropTypes.string, display_name: PropTypes.string,

View File

@ -87,8 +87,8 @@ describe('HomeroomPage', () => {
}) })
describe('start a new subject button', () => { describe('start a new subject button', () => {
it('is not present if createPermissions is set to none', () => { it('is not present if createPermissions is set to null', () => {
const {queryByText} = render(<HomeroomPage {...getProps({createPermissions: 'none'})} />) const {queryByText} = render(<HomeroomPage {...getProps({createPermissions: null})} />)
expect(queryByText('Open new subject modal')).not.toBeInTheDocument() expect(queryByText('Open new subject modal')).not.toBeInTheDocument()
}) })

View File

@ -182,7 +182,7 @@ const defaultProps = {
canDisableElementaryDashboard: false, canDisableElementaryDashboard: false,
currentUser, currentUser,
currentUserRoles: ['admin'], currentUserRoles: ['admin'],
createPermissions: 'none', createPermissions: null,
plannerEnabled: false, plannerEnabled: false,
loadAllOpportunities: () => {}, loadAllOpportunities: () => {},
timeZone: defaultEnv.TIMEZONE, timeZone: defaultEnv.TIMEZONE,

View File

@ -76,7 +76,7 @@ export const CreateCourseModal = ({isModalOpen, setModalOpen, permissions, isK5U
}) })
} }
const teacherFetchOpts = { const teacherStudentFetchOpts = {
path: '/api/v1/users/self/courses', path: '/api/v1/users/self/courses',
success: useCallback(enrollments => { success: useCallback(enrollments => {
const accounts = getAccountsFromEnrollments(enrollments) const accounts = getAccountsFromEnrollments(enrollments)
@ -89,7 +89,8 @@ export const CreateCourseModal = ({isModalOpen, setModalOpen, permissions, isK5U
params: { params: {
per_page: 100, per_page: 100,
include: ['account'], include: ['account'],
enrollment_type: 'teacher' // Show teachers only accounts where they have a teacher enrollment
...(permissions === 'teacher' && {enrollment_type: 'teacher'})
} }
} }
@ -105,11 +106,22 @@ export const CreateCourseModal = ({isModalOpen, setModalOpen, permissions, isK5U
} }
} }
const noEnrollmentsFetchOpts = {
path: '/api/v1/manually_created_courses_account',
success: useCallback(account => {
setAllAccounts(account)
setSelectedAccount(account[0])
setAccountSearchTerm(account[0].name)
}, [])
}
useFetchApi({ useFetchApi({
loading: setLoading, loading: setLoading,
error: useCallback(err => showFlashError(I18n.t('Unable to get accounts'))(err), []), error: useCallback(err => showFlashError(I18n.t('Unable to get accounts'))(err), []),
fetchAllPages: true, fetchAllPages: true,
...(permissions === 'teacher' ? teacherFetchOpts : adminFetchOpts) ...(['teacher', 'student'].includes(permissions) && teacherStudentFetchOpts),
...(permissions === 'admin' && adminFetchOpts),
...(permissions === 'no_enrollments' && noEnrollmentsFetchOpts)
}) })
const handleAccountSelected = id => { const handleAccountSelected = id => {
@ -159,7 +171,10 @@ export const CreateCourseModal = ({isModalOpen, setModalOpen, permissions, isK5U
}, },
error: useCallback(err => showFlashError(I18n.t('Unable to get homerooms'))(err), []), error: useCallback(err => showFlashError(I18n.t('Unable to get homerooms'))(err), []),
fetchAllPages: true, fetchAllPages: true,
...(permissions === 'teacher' ? teacherHomeroomFetchOpts : adminHomeroomFetchOpts) // don't let students/users with no enrollments sync homeroom data
forceResult: ['no_enrollments', 'student'].includes(permissions) ? [] : undefined,
...(permissions === 'teacher' && teacherHomeroomFetchOpts),
...(permissions === 'admin' && adminHomeroomFetchOpts)
}) })
const handleHomeroomSelected = id => { const handleHomeroomSelected = id => {
@ -180,8 +195,10 @@ export const CreateCourseModal = ({isModalOpen, setModalOpen, permissions, isK5U
)) ))
} }
// Don't show the account select for teachers with only one account to show // Don't show the account select for non-admins with only one account to show
const hideAccountSelect = permissions === 'teacher' && allAccounts?.length === 1 const hideAccountSelect = permissions !== 'admin' && allAccounts?.length === 1
// Don't show homeroom sync to non-k5 users or to students/users with no enrollments
const showHomeroomSyncOptions = isK5User && ['admin', 'teacher'].includes(permissions)
return ( return (
<Modal label={modalLabel} open={isModalOpen} size="small" onDismiss={clearModal}> <Modal label={modalLabel} open={isModalOpen} size="small" onDismiss={clearModal}>
@ -210,15 +227,15 @@ export const CreateCourseModal = ({isModalOpen, setModalOpen, permissions, isK5U
{accountOptions} {accountOptions}
</CanvasAsyncSelect> </CanvasAsyncSelect>
)} )}
{isK5User && ( {showHomeroomSyncOptions && (
<Checkbox <Checkbox
label={I18n.t('Sync enrollments and course start/end dates from homeroom')} label={I18n.t('Sync enrollments and subject start/end dates from homeroom')}
value="syncHomeroomEnrollments" value="syncHomeroomEnrollments"
checked={syncHomeroomEnrollments} checked={syncHomeroomEnrollments}
onChange={event => setSyncHomeroomEnrollments(event.target.checked)} onChange={event => setSyncHomeroomEnrollments(event.target.checked)}
/> />
)} )}
{isK5User && syncHomeroomEnrollments && ( {showHomeroomSyncOptions && syncHomeroomEnrollments && (
<SimpleSelect <SimpleSelect
data-testid="homeroom-select" data-testid="homeroom-select"
renderLabel={I18n.t('Select a homeroom')} renderLabel={I18n.t('Select a homeroom')}
@ -265,6 +282,6 @@ export const CreateCourseModal = ({isModalOpen, setModalOpen, permissions, isK5U
CreateCourseModal.propTypes = { CreateCourseModal.propTypes = {
isModalOpen: PropTypes.bool.isRequired, isModalOpen: PropTypes.bool.isRequired,
setModalOpen: PropTypes.func.isRequired, setModalOpen: PropTypes.func.isRequired,
permissions: PropTypes.oneOf(['admin', 'teacher']).isRequired, permissions: PropTypes.oneOf(['admin', 'teacher', 'student', 'no_enrollments']).isRequired,
isK5User: PropTypes.bool.isRequired isK5User: PropTypes.bool.isRequired
} }

View File

@ -65,10 +65,20 @@ const ENROLLMENTS = [
} }
] ]
const MCC_ACCOUNT = {
id: 3,
name: 'Manually-Created Courses',
workflow_state: 'active'
}
const MANAGEABLE_COURSES_URL = '/api/v1/manageable_accounts?per_page=100' const MANAGEABLE_COURSES_URL = '/api/v1/manageable_accounts?per_page=100'
const ENROLLMENTS_URL = encodeURI( const TEACHER_ENROLLMENTS_URL = encodeURI(
'/api/v1/users/self/courses?per_page=100&include[]=account&enrollment_type=teacher' '/api/v1/users/self/courses?per_page=100&include[]=account&enrollment_type=teacher'
) )
const STUDENT_ENROLLMENTS_URL = encodeURI(
'/api/v1/users/self/courses?per_page=100&include[]=account'
)
const MCC_ACCOUNT_URL = 'api/v1/manually_created_courses_account'
describe('CreateCourseModal', () => { describe('CreateCourseModal', () => {
const setModalOpen = jest.fn() const setModalOpen = jest.fn()
@ -101,7 +111,7 @@ describe('CreateCourseModal', () => {
).toBeInTheDocument() ).toBeInTheDocument()
expect(getByLabelText('Subject Name')).toBeInTheDocument() expect(getByLabelText('Subject Name')).toBeInTheDocument()
expect( expect(
getByLabelText('Sync enrollments and course start/end dates from homeroom') getByLabelText('Sync enrollments and subject start/end dates from homeroom')
).toBeInTheDocument() ).toBeInTheDocument()
}) })
}) })
@ -194,44 +204,98 @@ describe('CreateCourseModal', () => {
expect(getByRole('button', {name: 'Cancel'})).not.toBeDisabled() expect(getByRole('button', {name: 'Cancel'})).not.toBeDisabled()
}) })
it('fetches accounts from enrollments api if permission is set to teacher', async () => { describe('with teacher permission', () => {
fetchMock.get(ENROLLMENTS_URL, ENROLLMENTS) it('fetches accounts from enrollments api', async () => {
const {getByText, getByLabelText} = render( fetchMock.get(TEACHER_ENROLLMENTS_URL, ENROLLMENTS)
<CreateCourseModal {...getProps({permissions: 'teacher'})} /> const {getByText, getByLabelText} = render(
) <CreateCourseModal {...getProps({permissions: 'teacher'})} />
await waitFor(() => expect(getByLabelText('Subject Name')).toBeInTheDocument()) )
act(() => getByLabelText('Which account will this subject be associated with?').click()) await waitFor(() => expect(getByLabelText('Subject Name')).toBeInTheDocument())
expect(getByText('Orange Elementary')).toBeInTheDocument() act(() => getByLabelText('Which account will this subject be associated with?').click())
expect(getByText('Clark HS')).toBeInTheDocument() expect(getByText('Orange Elementary')).toBeInTheDocument()
expect(getByText('Clark HS')).toBeInTheDocument()
})
it('hides the account select if there is only one enrollment', async () => {
fetchMock.get(TEACHER_ENROLLMENTS_URL, [ENROLLMENTS[0]])
const {queryByText, getByLabelText} = render(
<CreateCourseModal {...getProps({permissions: 'teacher'})} />
)
await waitFor(() => expect(getByLabelText('Subject Name')).toBeInTheDocument())
expect(
queryByText('Which account will this subject be associated with?')
).not.toBeInTheDocument()
})
it("doesn't break if the user has restricted enrollments", async () => {
fetchMock.get(TEACHER_ENROLLMENTS_URL, [
...ENROLLMENTS,
{
id: 1033,
access_restricted_by_date: true
}
])
const {getByLabelText, queryByText, getByText} = render(
<CreateCourseModal {...getProps({permissions: 'teacher'})} />
)
await waitFor(() => expect(getByLabelText('Subject Name')).toBeInTheDocument())
expect(queryByText('Unable to get accounts')).not.toBeInTheDocument()
act(() => getByLabelText('Which account will this subject be associated with?').click())
expect(getByText('Orange Elementary')).toBeInTheDocument()
expect(getByText('Clark HS')).toBeInTheDocument()
})
}) })
it('hides the account select for teachers with only one enrollment', async () => { describe('with student permission', () => {
fetchMock.get(ENROLLMENTS_URL, [ENROLLMENTS[0]]) beforeEach(() => {
const {queryByText, getByLabelText} = render( fetchMock.get(STUDENT_ENROLLMENTS_URL, ENROLLMENTS)
<CreateCourseModal {...getProps({permissions: 'teacher'})} /> })
)
await waitFor(() => expect(getByLabelText('Subject Name')).toBeInTheDocument()) it('fetches accounts from enrollments api', async () => {
expect( const {findByLabelText, getByLabelText, getByText} = render(
queryByText('Which account will this subject be associated with?') <CreateCourseModal {...getProps({permissions: 'student'})} />
).not.toBeInTheDocument() )
expect(await findByLabelText('Subject Name')).toBeInTheDocument()
act(() => getByLabelText('Which account will this subject be associated with?').click())
expect(getByText('Orange Elementary')).toBeInTheDocument()
})
it("doesn't show the homeroom sync options", async () => {
const {findByLabelText, queryByText} = render(
<CreateCourseModal {...getProps({permissions: 'student'})} />
)
expect(await findByLabelText('Subject Name')).toBeInTheDocument()
expect(
queryByText('Sync enrollments and subject start/end dates from homeroom')
).not.toBeInTheDocument()
expect(queryByText('Select a homeroom')).not.toBeInTheDocument()
})
}) })
it("doesn't break if the user has restricted enrollments", async () => { describe('with no_enrollments permission', () => {
fetchMock.get(ENROLLMENTS_URL, [ beforeEach(() => {
...ENROLLMENTS, fetchMock.get(MCC_ACCOUNT_URL, MCC_ACCOUNT)
{ })
id: 1033,
access_restricted_by_date: true it('uses the manually_created_courses_account api to get the right account', async () => {
} const {findByLabelText} = render(
]) <CreateCourseModal {...getProps({permissions: 'no_enrollments'})} />
const {getByLabelText, queryByText, getByText} = render( )
<CreateCourseModal {...getProps({permissions: 'teacher'})} /> expect(await findByLabelText('Subject Name')).toBeInTheDocument()
) })
await waitFor(() => expect(getByLabelText('Subject Name')).toBeInTheDocument())
expect(queryByText('Unable to get accounts')).not.toBeInTheDocument() it("doesn't show the homeroom sync options or account dropdown", async () => {
act(() => getByLabelText('Which account will this subject be associated with?').click()) const {findByLabelText, queryByText} = render(
expect(getByText('Orange Elementary')).toBeInTheDocument() <CreateCourseModal {...getProps({permissions: 'no_enrollments'})} />
expect(getByText('Clark HS')).toBeInTheDocument() )
expect(await findByLabelText('Subject Name')).toBeInTheDocument()
expect(
queryByText('Sync enrollments and subject start/end dates from homeroom')
).not.toBeInTheDocument()
expect(
queryByText('Which account will this subject be associated with?')
).not.toBeInTheDocument()
})
}) })
describe('with isK5User set to false', () => { describe('with isK5User set to false', () => {
@ -245,7 +309,7 @@ describe('CreateCourseModal', () => {
) )
expect(await findByLabelText('Course Name')).toBeInTheDocument() expect(await findByLabelText('Course Name')).toBeInTheDocument()
expect( expect(
queryByText('Sync enrollments and course start/end dates from homeroom') queryByText('Sync enrollments and subject start/end dates from homeroom')
).not.toBeInTheDocument() ).not.toBeInTheDocument()
expect(queryByText('Select a homeroom')).not.toBeInTheDocument() expect(queryByText('Select a homeroom')).not.toBeInTheDocument()
}) })