From 35cff74d6910c8c3e79f0b566d2e3b8344c819ed Mon Sep 17 00:00:00 2001 From: k3mystra Date: Tue, 7 Apr 2026 12:35:45 +0800 Subject: [PATCH 01/11] feat: Add coursecode_enabled column in Course --- .../20260406172051_add_coursecode_enabled_to_courses.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20260406172051_add_coursecode_enabled_to_courses.rb 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..3026672 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" t.index ["coursecode"], name: "index_courses_on_coursecode", unique: true end From bf0e6f437de5524488dfe3051cb768fc92b092ea Mon Sep 17 00:00:00 2001 From: k3mystra Date: Tue, 7 Apr 2026 12:59:27 +0800 Subject: [PATCH 02/11] fix: Previous migration was not applied properly --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 3026672..329c71f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -38,7 +38,7 @@ t.string "course_description" t.string "file_link" t.string "coursecode" - t.boolean "coursecode_enabled" + t.boolean "coursecode_enabled", default: false, null: false t.index ["coursecode"], name: "index_courses_on_coursecode", unique: true end From 6215f23889ead416d6fdb458920bd9aadaf217bd Mon Sep 17 00:00:00 2001 From: k3mystra Date: Tue, 7 Apr 2026 12:59:41 +0800 Subject: [PATCH 03/11] linter: Linter auto fix --- app/views/project_templates/edit.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8bcb2d7a3aeb2943383bb6b6296dc433bcc3c195 Mon Sep 17 00:00:00 2001 From: k3mystra Date: Tue, 7 Apr 2026 13:00:23 +0800 Subject: [PATCH 04/11] feat: Refactor coursecode form and add toggle --- app/controllers/courses_controller.rb | 11 +++++++++-- .../coursecode_form_handler_controller.js | 16 ++++++++++++++++ app/views/courses/_course_code_form.html.erb | 11 ++++++++--- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 app/javascript/controllers/coursecode_form_handler_controller.js diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 159763e..914f0f9 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 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..421e24c --- /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/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 %> From 851b0891793adea1a4a6f4e7283f2df6d6032730 Mon Sep 17 00:00:00 2001 From: k3mystra Date: Tue, 7 Apr 2026 13:01:04 +0800 Subject: [PATCH 05/11] formatting: Autoformat --- app/controllers/courses_controller.rb | 16 ++++------------ .../coursecode_form_handler_controller.js | 12 ++++++------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 914f0f9..8506176 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -829,13 +829,9 @@ def filtered_group_list group_list = group_list.select { |g| ids.include?(g.id) } end - if params[:status_filter].present? && params[:status_filter] != 'all' - group_list = @course.groups_with_status(params[:status_filter], group_list) - end + group_list = @course.groups_with_status(params[:status_filter], group_list) if params[:status_filter].present? && params[:status_filter] != 'all' - if params[:search_query].present? - group_list = search_groups(group_list, params[:search_query]) - end + group_list = search_groups(group_list, params[:search_query]) if params[:search_query].present? sorted_list = group_list.sort_by { |group| sort_value_for_group(group) } sort_descending? ? sorted_list.reverse : sorted_list @@ -848,13 +844,9 @@ def filtered_student_list student_list = student_list.select { |s| ids.include?(s.id) } end - if params[:status_filter].present? && params[:status_filter] != 'all' - student_list = @course.students_with_status(params[:status_filter], student_list) - end + student_list = @course.students_with_status(params[:status_filter], student_list) if params[:status_filter].present? && params[:status_filter] != 'all' - if params[:search_query].present? - student_list = search_students(student_list, params[:search_query]) - end + student_list = search_students(student_list, params[:search_query]) if params[:search_query].present? sorted_list = student_list.sort_by { |student| sort_value_for_student(student) } sort_descending? ? sorted_list.reverse : sorted_list diff --git a/app/javascript/controllers/coursecode_form_handler_controller.js b/app/javascript/controllers/coursecode_form_handler_controller.js index 421e24c..ee73ba9 100644 --- a/app/javascript/controllers/coursecode_form_handler_controller.js +++ b/app/javascript/controllers/coursecode_form_handler_controller.js @@ -1,16 +1,16 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="coursecode-form-handler" export default class extends Controller { - static targets = ["generateFlag"] + static targets = ["generateFlag"]; submitForm() { - this.element.requestSubmit() + this.element.requestSubmit(); } generateCode(event) { - event.preventDefault() - this.generateFlagTarget.value = "true" - this.element.requestSubmit() + event.preventDefault(); + this.generateFlagTarget.value = "true"; + this.element.requestSubmit(); } } From 7cebad50947d9616d1be52ff83072309110d05fe Mon Sep 17 00:00:00 2001 From: k3mystra Date: Tue, 7 Apr 2026 13:01:25 +0800 Subject: [PATCH 06/11] test: Update update_coursecode_test --- test/integration/update_coursecode_test.rb | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/test/integration/update_coursecode_test.rb b/test/integration/update_coursecode_test.rb index 93fef1c..6114b69 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 From 0fbdeaeb2366dc34b01cd0a77bc319e573169f3c Mon Sep 17 00:00:00 2001 From: k3mystra Date: Tue, 7 Apr 2026 13:01:35 +0800 Subject: [PATCH 07/11] formatting: Autoformatting --- test/integration/update_coursecode_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/update_coursecode_test.rb b/test/integration/update_coursecode_test.rb index 6114b69..5fc5246 100644 --- a/test/integration/update_coursecode_test.rb +++ b/test/integration/update_coursecode_test.rb @@ -37,7 +37,7 @@ class UpdateCoursecodeTest < ActionDispatch::IntegrationTest 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' } + 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 From 6429ac430849fb9acd05c54b66407ffd2e91fa17 Mon Sep 17 00:00:00 2001 From: k3mystra Date: Sun, 12 Apr 2026 12:17:40 +0800 Subject: [PATCH 08/11] feat: Add check if coursecode_enabled --- app/controllers/courses_controller.rb | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 8506176..9ed9f03 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -358,15 +358,26 @@ 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.' + unless @course + redirect_back_or_to '/', alert: 'Invalid course code' + return + end + + unless @course.coursecode_enabled + redirect_back_or_to '/', alert: 'Joining via course code is disabled for this course' + return + end + + enrolment = Enrolment.find_or_create_by!( + user: current_user, + course: @course, + role: :student + ) + + if enrolment.previously_new_record? + 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 end From 2257a866e8e1e8f0616ae942d94006e77962e5bf Mon Sep 17 00:00:00 2001 From: k3mystra Date: Sun, 12 Apr 2026 12:17:53 +0800 Subject: [PATCH 09/11] feat: Update enroll_via_coursecode test --- .../integration/enroll_via_coursecode_test.rb | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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) From 18a7f67354eccbab36ff345e0bebfd03854a2b8e Mon Sep 17 00:00:00 2001 From: k3mystra Date: Sun, 12 Apr 2026 12:27:54 +0800 Subject: [PATCH 10/11] refactor: Move logic into Enrolment model --- app/controllers/courses_controller.rb | 31 +++++++-------------------- app/models/enrolment.rb | 18 ++++++++++++++++ 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 37a306b..a1437f7 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -355,29 +355,14 @@ def update_coursecode end def enroll_via_coursecode - code = params[:coursecode] - @course = Course.by_coursecode(code).first - - unless @course - redirect_back_or_to '/', alert: 'Invalid course code' - return - end - - unless @course.coursecode_enabled - redirect_back_or_to '/', alert: 'Joining via course code is disabled for this course' - return - end - - enrolment = Enrolment.find_or_create_by!( - user: current_user, - course: @course, - role: :student - ) - - if enrolment.previously_new_record? - redirect_back_or_to '/', notice: 'Successfully joined the course' - else - redirect_back_or_to '/', notice: 'You already 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 '/', notice: 'You already joined the course' + end + rescue StandardError => e + redirect_back_or_to '/', alert: e.message end end 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 From 1a00d1aba071f01a6c57babd8485575ca5990d55 Mon Sep 17 00:00:00 2001 From: k3mystra Date: Sun, 12 Apr 2026 12:36:59 +0800 Subject: [PATCH 11/11] fix: extra end lol --- app/controllers/courses_controller.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index a1437f7..27cd6b0 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -355,15 +355,14 @@ def update_coursecode end def enroll_via_coursecode - 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 '/', notice: 'You already joined the course' - end - rescue StandardError => e - redirect_back_or_to '/', alert: e.message + 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 '/', notice: 'You already joined the course' end + rescue StandardError => e + redirect_back_or_to '/', alert: e.message end private