diff --git a/app/controllers/setup/course_details_controller.rb b/app/controllers/setup/course_details_controller.rb
index e46aa4c85..f710770d4 100644
--- a/app/controllers/setup/course_details_controller.rb
+++ b/app/controllers/setup/course_details_controller.rb
@@ -17,7 +17,7 @@ def new
end
def custom
- authorize! :create, :custom_course
+ authorize! :create_custom_course, @organization
print_setup_phases(2)
@course = Course.new
@@ -30,6 +30,7 @@ def create
authorize! :teach, @organization
custom = params[:course][:course_template_id].blank?
+ authorize! :create_custom_course, @organization if custom
create_params = custom ? course_params_for_create_custom : course_params_for_create_from_template
input_name = create_params[:name]
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 5342793c3..812fc015b 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -12,7 +12,6 @@ def initialize(user, auth_source: nil)
if user.administrator?
can :manage, :all
can :create, Course
- can :create, :custom_course
cannot :refresh, Course
can :refresh, Course, &:custom?
can :view, :participants_list
@@ -73,6 +72,10 @@ def initialize(user, auth_source: nil)
can? :teach, c.organization
end
+ can :create_custom_course, Organization do |o|
+ o.teachers_can_create_custom_courses? && can?(:teach, o)
+ end
+
can :refresh, Course do |c|
(c.taught_by?(user) || c.assistant?(user)) &&
c.custom? # user can only refresh his/her custom course.
diff --git a/app/views/course_templates/list_for_teachers.html.erb b/app/views/course_templates/list_for_teachers.html.erb
index 88f7988c6..49cf87bed 100644
--- a/app/views/course_templates/list_for_teachers.html.erb
+++ b/app/views/course_templates/list_for_teachers.html.erb
@@ -26,7 +26,7 @@
-<% if can? :create, :custom_course %>
+<% if can? :create_custom_course, @organization %>
Create custom course
<%= link_to 'Create New Custom Course', new_custom_setup_organization_course_path(@organization), class: "btn btn-sm btn-warning" %>
diff --git a/app/views/courses/index.html.erb b/app/views/courses/index.html.erb
index cabcee2b7..5f753f03a 100644
--- a/app/views/courses/index.html.erb
+++ b/app/views/courses/index.html.erb
@@ -32,7 +32,7 @@
<%= link_to 'Create New Course', setup_organization_course_chooser_index_path(@organization), class: "btn btn-primary" %>
<% end %>
-<% if can? :create, :custom_course %>
+<% if can? :create_custom_course, @organization %>
<%= link_to 'Create Custom Course', new_custom_setup_organization_course_path(@organization), class: "btn btn-sm btn-danger" %>
<% end %>
diff --git a/app/views/setup/course_chooser/index.html.erb b/app/views/setup/course_chooser/index.html.erb
index 0306cd8d8..ab678e2a0 100644
--- a/app/views/setup/course_chooser/index.html.erb
+++ b/app/views/setup/course_chooser/index.html.erb
@@ -33,7 +33,7 @@
- <% if can? :create, :custom_course %>
+ <% if can? :create_custom_course, @organization %>
Create custom course (advanced mode)
<%= link_to 'Create New Custom Course', new_custom_setup_organization_course_path(@organization), class: "btn btn-danger" %>
<% end %>
diff --git a/db/migrate/20261026120000_add_teachers_can_create_custom_courses_to_organizations.rb b/db/migrate/20261026120000_add_teachers_can_create_custom_courses_to_organizations.rb
new file mode 100644
index 000000000..1b8771653
--- /dev/null
+++ b/db/migrate/20261026120000_add_teachers_can_create_custom_courses_to_organizations.rb
@@ -0,0 +1,5 @@
+class AddTeachersCanCreateCustomCoursesToOrganizations < ActiveRecord::Migration[7.1]
+ def change
+ add_column :organizations, :teachers_can_create_custom_courses, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 9d62cab13..59701d81b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2026_09_17_120000) do
+ActiveRecord::Schema[7.1].define(version: 2026_10_26_120000) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -361,6 +361,7 @@
t.text "website"
t.boolean "pinned", default: false, null: false
t.string "whitelisted_ips", array: true
+ t.boolean "teachers_can_create_custom_courses", default: false, null: false
t.index ["creator_id"], name: "index_organizations_on_creator_id"
end
diff --git a/spec/controllers/setup/course_details_controller_spec.rb b/spec/controllers/setup/course_details_controller_spec.rb
index 4a89200cb..d71914005 100644
--- a/spec/controllers/setup/course_details_controller_spec.rb
+++ b/spec/controllers/setup/course_details_controller_spec.rb
@@ -78,7 +78,7 @@
describe 'with invalid parameters' do
it 'should not create a course' do
- post :create, params: { organization_id: @organization.slug, course: { name: 's p a c e s', title: 'New Course', source_url: @repo_path } }
+ post :create, params: { organization_id: @organization.slug, course: { name: 's p a c e s', title: 'New Course', course_template_id: @ct.id } }
expect(response).to render_template(:new)
expect(assigns(:course).name).to eq('s p a c e s')
end
@@ -191,6 +191,26 @@
end
end
+ describe 'when creating custom course' do
+ it 'denies access when the organization does not allow teachers to create custom courses' do
+ get :custom, params: { organization_id: @organization.slug }
+ expect(response.code.to_i).to eq(403)
+ expect do
+ post :create, params: { organization_id: @organization.slug, course: { name: 'NewCourse', title: 'New Course', source_url: @repo_path } }
+ end.not_to change { Course.count }
+ expect(response.code.to_i).to eq(403)
+ end
+
+ it 'creates the course when the organization allows teachers to create custom courses' do
+ @organization.update!(teachers_can_create_custom_courses: true)
+ get :custom, params: { organization_id: @organization.slug }
+ expect(response).to render_template(:new)
+ expect do
+ post :create, params: { organization_id: @organization.slug, course: { name: 'NewCourse', title: 'New Course', source_url: @repo_path } }
+ end.to change { Course.count }.by(1)
+ end
+ end
+
## TODO: Tests for the rest of the actions, when they are properly implemented
end
@@ -200,6 +220,13 @@
end
describe 'when creating custom course' do
+ it 'shows the custom course form even if the organization does not allow teachers to create custom courses' do
+ expect(@organization.teachers_can_create_custom_courses).to be false
+ get :custom, params: { organization_id: @organization.slug }
+ expect(response).to render_template(:new)
+ expect(assigns(:custom)).to be true
+ end
+
it 'should create the course' do
init_session
expect do
@@ -231,6 +258,12 @@
expect(response.code.to_i).to eq(403)
## TODO: Rest of the actions
end
+
+ it 'should not allow creating custom courses even if teachers are allowed to' do
+ @organization.update!(teachers_can_create_custom_courses: true)
+ get :custom, params: { organization_id: @organization.slug }
+ expect(response.code.to_i).to eq(403)
+ end
end
def init_session