Skip to content
28 changes: 15 additions & 13 deletions app/controllers/courses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions app/javascript/controllers/coursecode_form_handler_controller.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
18 changes: 18 additions & 0 deletions app/models/enrolment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 8 additions & 3 deletions app/views/courses/_course_code_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<%= turbo_frame_tag "course_code_form" do %>
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10 max-w-3xl mx-auto border border-gray-100 mb-6">
<%= 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" %>
Expand All @@ -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" %>
<button
class="px-4 py-2 bg-blue-200 hover:bg-blue-300 text-gray-800 text-sm font-medium rounded shadow-sm">
<button id="regenerate-code-btn" type="button" data-action="click->coursecode-form-handler#generateCode" class="px-4 py-2 bg-blue-200 hover:bg-blue-300 text-gray-800 text-sm font-medium rounded shadow-sm">
<%= @course.coursecode.present? ? 'Re-Generate Join Code' : 'Generate Join code' %>
</button>
</div>
<input type="hidden" name="generate" value="false" data-coursecode-form-handler-target="generateFlag">
<label class="inline-flex items-center cursor-pointer mt-4">
<%= form.check_box :coursecode_enabled, class: "sr-only peer", role: "switch", data: { action: "change->coursecode-form-handler#submitForm" } %>
<div class="relative w-11 h-6 bg-gray-400 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[10%] after:left-[5%] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-[80%] after:w-[45%] after:transition-all peer-checked:bg-green-500"></div>
<span class="ml-3 text-sm font-medium text-gray-700">Allow joining via course code</span>
</label>
<% end %>
</div>
<% end %>
2 changes: 1 addition & 1 deletion app/views/project_templates/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
Req.
</th>
<th scope="col" class="px-2 py-5 text-center text-[10px] font-bold text-gray-500 uppercase tracking-normal leading-4 w-[8%]">
Editable <br/> Post-Approval
Editable <br> Post-Approval
</th>
</tr>
</thead>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddCoursecodeEnabledToCourses < ActiveRecord::Migration[8.0]
def change
add_column :courses, :coursecode_enabled, :boolean, null: false, default: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions test/integration/enroll_via_coursecode_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' }
Expand All @@ -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)
Expand Down
23 changes: 20 additions & 3 deletions test/integration/update_coursecode_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Loading