diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 99028a5..27cd6b0 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -336,8 +336,15 @@ def import_details def update_coursecode authorize @course, :update? - @course.generate_coursecode! - flash.now[:notice] = 'Course join code successfully generated' + if params[:generate] == 'true' + @course.generate_coursecode! + flash.now[:notice] = 'Course join code successfully generated' + end + + if params[:course]&.key?(:coursecode_enabled) + @course.update!(coursecode_enabled: params[:course][:coursecode_enabled]) + flash.now[:notice] ||= 'Course join code settings updated' + end rescue StandardError => e flash.now[:alert] = e.message ensure @@ -348,19 +355,14 @@ def update_coursecode end def enroll_via_coursecode - code = params[:coursecode] - @course = Course.by_coursecode(code).first - - if @course - Enrolment.find_or_create_by!( - user: current_user, - course: @course, - role: :student - ) - redirect_back_or_to '/', notice: 'Successfully joined the course.' + new_enrolment = Enrolment.enroll_via_coursecode(current_user, params[:coursecode]) + if new_enrolment + redirect_back_or_to '/', notice: 'Successfully joined the course' else - redirect_back_or_to '/', alert: 'Invalid course code.' + redirect_back_or_to '/', notice: 'You already joined the course' end + rescue StandardError => e + redirect_back_or_to '/', alert: e.message end private diff --git a/app/javascript/controllers/coursecode_form_handler_controller.js b/app/javascript/controllers/coursecode_form_handler_controller.js new file mode 100644 index 0000000..ee73ba9 --- /dev/null +++ b/app/javascript/controllers/coursecode_form_handler_controller.js @@ -0,0 +1,16 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="coursecode-form-handler" +export default class extends Controller { + static targets = ["generateFlag"]; + + submitForm() { + this.element.requestSubmit(); + } + + generateCode(event) { + event.preventDefault(); + this.generateFlagTarget.value = "true"; + this.element.requestSubmit(); + } +} diff --git a/app/models/enrolment.rb b/app/models/enrolment.rb index 0206bdf..b4c8b3f 100644 --- a/app/models/enrolment.rb +++ b/app/models/enrolment.rb @@ -4,4 +4,22 @@ class Enrolment < ApplicationRecord has_many :projects, dependent: :destroy enum :role, { lecturer: 0, coordinator: 1, student: 2 } + + # Enrolls a user in a course using a course code. + # Validates that the course exists and that joining via course code is enabled. + # Returns true if the user was newly enrolled, or false if they were already enrolled. + def self.enroll_via_coursecode(user, code) + course = Course.by_coursecode(code).first + + raise 'Invalid course code' unless course + raise 'Joining via course code is disabled for this course' unless course.coursecode_enabled + + enrolment = find_or_create_by!( + user: user, + course: course, + role: :student + ) + + enrolment.previously_new_record? + end end diff --git a/app/views/courses/_course_code_form.html.erb b/app/views/courses/_course_code_form.html.erb index 32d7d9c..06f5116 100644 --- a/app/views/courses/_course_code_form.html.erb +++ b/app/views/courses/_course_code_form.html.erb @@ -1,6 +1,6 @@ <%= turbo_frame_tag "course_code_form" do %>
- <%= form_with url: update_coursecode_course_path(@course), model: @course, method: :post do |form| %> + <%= form_with url: update_coursecode_course_path(@course), model: @course, method: :post, data: { controller: "coursecode-form-handler" } do |form| %> <%= form.label :coursecode, 'Course Join Code & Invite Link', class: "block text-sm font-medium text-gray-700" %> @@ -10,11 +10,16 @@ readonly: true, disabled: true, class: "appearance-none block w-[10em] font-mono px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" %> -
+ + <% end %> <% end %> diff --git a/app/views/project_templates/edit.html.erb b/app/views/project_templates/edit.html.erb index bef1426..0ea01dc 100644 --- a/app/views/project_templates/edit.html.erb +++ b/app/views/project_templates/edit.html.erb @@ -72,7 +72,7 @@ Req. - Editable
Post-Approval + Editable
Post-Approval diff --git a/db/migrate/20260406172051_add_coursecode_enabled_to_courses.rb b/db/migrate/20260406172051_add_coursecode_enabled_to_courses.rb new file mode 100644 index 0000000..48fd4e6 --- /dev/null +++ b/db/migrate/20260406172051_add_coursecode_enabled_to_courses.rb @@ -0,0 +1,5 @@ +class AddCoursecodeEnabledToCourses < ActiveRecord::Migration[8.0] + def change + add_column :courses, :coursecode_enabled, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 3066bcb..329c71f 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[8.0].define(version: 2026_04_05_053252) do +ActiveRecord::Schema[8.0].define(version: 2026_04_06_172051) do create_table "comments", force: :cascade do |t| t.integer "user_id", null: false t.string "text", null: false @@ -38,6 +38,7 @@ t.string "course_description" t.string "file_link" t.string "coursecode" + t.boolean "coursecode_enabled", default: false, null: false t.index ["coursecode"], name: "index_courses_on_coursecode", unique: true end diff --git a/test/integration/enroll_via_coursecode_test.rb b/test/integration/enroll_via_coursecode_test.rb index c634c2d..8a6b5bf 100644 --- a/test/integration/enroll_via_coursecode_test.rb +++ b/test/integration/enroll_via_coursecode_test.rb @@ -9,6 +9,8 @@ class EnrollViaCoursecodeTest < ActionDispatch::IntegrationTest end test 'should successfully enroll in course with valid coursecode' do + @course.update!(coursecode_enabled: true) + # Sign in the student post session_path, params: { email_address: @student.email_address, password: 'password' } assert_redirected_to root_path @@ -21,12 +23,28 @@ class EnrollViaCoursecodeTest < ActionDispatch::IntegrationTest # The student is redirected to '/' and sees a success message assert_redirected_to '/' - assert_equal 'Successfully joined the course.', flash[:notice] + assert_equal 'Successfully joined the course', flash[:notice] # Verify that an enrolment was created assert @course.students.include?(@student) end + test 'should notify students that they are already enrolled in the course' do + @course.update!(coursecode_enabled: true) + + # Sign in the student + post session_path, params: { email_address: @student.email_address, password: 'password' } + assert_redirected_to root_path + + # Submit valid coursecode (and again) + post invite_path, params: { coursecode: @course.coursecode } + post invite_path, params: { coursecode: @course.coursecode } + + # The student is redirected to '/' and sees a success message + assert_redirected_to '/' + assert_equal 'You already joined the course', flash[:notice] + end + test 'should display error for invalid coursecode' do # Sign in the student post session_path, params: { email_address: @student.email_address, password: 'password' } @@ -37,7 +55,7 @@ class EnrollViaCoursecodeTest < ActionDispatch::IntegrationTest # Student cannot join, is redirected with an error alert assert_redirected_to '/' - assert_equal 'Invalid course code.', flash[:alert] + assert_equal 'Invalid course code', flash[:alert] # Check that they did not join the course assert_not @course.students.include?(@student) diff --git a/test/integration/update_coursecode_test.rb b/test/integration/update_coursecode_test.rb index 93fef1c..5fc5246 100644 --- a/test/integration/update_coursecode_test.rb +++ b/test/integration/update_coursecode_test.rb @@ -12,11 +12,9 @@ class UpdateCoursecodeTest < ActionDispatch::IntegrationTest post session_path, params: { email_address: @lecturer.email_address, password: 'password' } assert_redirected_to root_path - # Initial state assert_nil @course.coursecode - # Call the API explicitly - post update_coursecode_course_path(@course), headers: { 'Accept' => 'text/vnd.turbo-stream.html' } + post update_coursecode_course_path(@course), params: { generate: true }, headers: { 'Accept' => 'text/vnd.turbo-stream.html' } # The request should succeed and return turbo stream content assert_response :success @@ -30,4 +28,23 @@ class UpdateCoursecodeTest < ActionDispatch::IntegrationTest # It replaces the 'course_code_form' which contains the new course code string. assert_match @course.coursecode, response.body end + + test 'should toggle the coursecode_enabled field in courses' do + # Sign in the lecturer (coordinator) + post session_path, params: { email_address: @lecturer.email_address, password: 'password' } + assert_redirected_to root_path + + assert_nil @course.coursecode + assert_equal @course.coursecode_enabled, false + + post update_coursecode_course_path(@course), params: { course: { coursecode_enabled: true } }, headers: { 'Accept' => 'text/vnd.turbo-stream.html' } + + # The request should succeed and return turbo stream content + assert_response :success + assert_equal 'text/vnd.turbo-stream.html', response.media_type + + # The coursecode_enabled must be set to true + @course.reload + assert_equal @course.coursecode_enabled, true + end end