Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions app/controllers/classrooms_controller.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
class ClassroomsController < AdminController
before_action :set_classroom, only: %i[ edit update ]

# GET /classrooms/1/edit
def edit
build_missing_program_enrollments
end
# PATCH/PUT /classrooms/1 or /classrooms/1.json

def update
respond_to do |format|
if @classroom.update(classroom_params)
format.html { redirect_to school_students_url(@classroom.school), notice: "Classroom was successfully updated.", status: :see_other }
format.json { render :show, status: :ok, location: @classroom }
else
build_missing_program_enrollments
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @classroom.errors, status: :unprocessable_entity }
end
end
end

private
# Use callbacks to share common setup or constraints between actions.
def set_classroom
@classroom = Classroom.find(params.expect(:id))
end

# Only allow a list of trusted parameters through.
def build_missing_program_enrollments
@classroom.classroom_programs.load unless @classroom.classroom_programs.loaded?
enrolled_ids = @classroom.classroom_programs.target.map(&:program_id).to_set
Program.order(:name).each do |program|
@classroom.classroom_programs.build(program: program) unless enrolled_ids.include?(program.id)
end
end

def classroom_params
params.expect(classroom: [ :name, :teacher ])
params.expect(classroom: [ :name, :teacher, classroom_programs_attributes: [ [ :id, :program_id, :level, :_destroy ] ] ])
end
end
11 changes: 11 additions & 0 deletions app/javascript/controllers/program_enrollment_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["levelSection", "destroy"]

toggle(event) {
const enrolled = event.target.checked
this.levelSectionTarget.style.display = enrolled ? "" : "none"
this.destroyTarget.value = enrolled ? "0" : "1"
}
}
15 changes: 15 additions & 0 deletions app/models/classroom.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
class Classroom < ApplicationRecord
belongs_to :school
has_many :students
has_many :classroom_programs, dependent: :destroy
has_many :programs, through: :classroom_programs

accepts_nested_attributes_for :classroom_programs,
allow_destroy: true,
reject_if: proc { |attrs| attrs["id"].blank? && attrs["_destroy"] == "1" }

validate :at_least_one_active_program, on: :update

private

def at_least_one_active_program
active = classroom_programs.reject(&:marked_for_destruction?)
errors.add(:base, "must have at least one program enrolled") if active.empty?
end
end
8 changes: 8 additions & 0 deletions app/models/classroom_program.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ClassroomProgram < ApplicationRecord
belongs_to :classroom
belongs_to :program

enum :level, { basic: "basic", moderate: "moderate", advanced: "advanced" }, validate: true

validates :level, presence: true
end
4 changes: 4 additions & 0 deletions app/models/program.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Program < ApplicationRecord
has_many :classroom_programs, dependent: :destroy
has_many :classrooms, through: :classroom_programs
end
24 changes: 24 additions & 0 deletions app/views/classrooms/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@
<%= form.label :teacher, class: "label" %>
<%= form.text_field :teacher, class: "input w-full" %>

<div class="flex flex-col gap-1">
<span class="label">Programs</span>
<%= form.fields_for :classroom_programs do |cp_form| %>
<% enrolled = cp_form.object.persisted? %>
<div class="flex items-center gap-3" data-controller="program-enrollment">
<%= cp_form.hidden_field :program_id %>
<%= cp_form.hidden_field :_destroy, value: enrolled ? "0" : "1", data: { program_enrollment_target: "destroy" } %>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" class="checkbox checkbox-primary"
<%= "checked" if enrolled %>
autocomplete="off"
data-action="change->program-enrollment#toggle">
<span class="label"><%= cp_form.object.program.name %></span>
</label>
<div data-program-enrollment-target="levelSection" <%= "style='display:none'" unless enrolled %>>
<%= cp_form.select :level,
ClassroomProgram.levels.keys.map { |l| [l.humanize, l] },
{ include_blank: "Select level", selected: cp_form.object.level },
class: "select select-bordered select-sm" %>
</div>
</div>
<% end %>
</div>

<%= form.label :Link, class: "label" %>
<%= link_to classroom_roster_url(classroom.uuid), classroom_roster_path(classroom.uuid), class: 'link' %>

Expand Down
19 changes: 19 additions & 0 deletions db/migrate/20260516200133_create_programs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class CreatePrograms < ActiveRecord::Migration[8.1]
def change
create_table :programs do |t|
t.string :name

t.timestamps
end

create_table :classroom_programs do |t|
t.references :classroom, null: false, foreign_key: true
t.references :program, null: false, foreign_key: true
t.string :level, null: false

t.timestamps
end

add_index :classroom_programs, [ :classroom_id, :program_id ], unique: true
end
end
21 changes: 20 additions & 1 deletion db/schema.rb

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

9 changes: 8 additions & 1 deletion db/seeds.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
return unless Rails.env.development?

programs = Program.create! [ { name: "Know Your Health" }, { name: "3D Wellness" } ]

User.find_or_create_by!(email_address: "admin@example.com") do |user|
user.name = "Admin"
user.password = "password"
Expand All @@ -26,7 +28,12 @@ def build_student_attrs(overrides = {})
grades = (1..12).to_a.sample(3)
grades.each do |grade|
classrooms = 2.times.map do |i|
school.classrooms.create!(name: "Classroom #{ i + 1 }", teacher: maybe { Faker::Name.name }, uuid: SecureRandom.urlsafe_base64(32))
classroom = school.classrooms.create!(name: "Classroom #{ i + 1 }", teacher: maybe { Faker::Name.name }, uuid: SecureRandom.urlsafe_base64(32))
assigned = Faker::Boolean.boolean ? [ programs.sample ] : programs
assigned.each do |program|
classroom.classroom_programs.create!(program: program, level: %w[basic moderate advanced].sample)
end
classroom
end
students = 10.times.map { |i| build_student_attrs(grade_level: grade, classroom_id: classrooms[i % 2].id) }
school.students.create!(students)
Expand Down
83 changes: 80 additions & 3 deletions test/controllers/classrooms_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,86 @@ class ClassroomsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end

test "should update classroom" do
sign_in_as users(:admin)
patch classroom_url(@classroom), params: { classroom: { name: @classroom.name, teacher: @classroom.teacher } }
test "should update classroom name and teacher" do
patch classroom_url(@classroom), params: { classroom: { name: "Updated Name", teacher: "New Teacher" } }
assert_redirected_to school_students_url(@classroom.school)
assert_equal "Updated Name", @classroom.reload.name
end

test "should add a program enrollment" do
program = programs(:"3dw")

assert_difference "ClassroomProgram.count" do
patch classroom_url(@classroom), params: {
classroom: {
name: @classroom.name,
classroom_programs_attributes: [ { program_id: program.id, level: "moderate" } ]
}
}
end

assert_redirected_to school_students_url(@classroom.school)
assert @classroom.classroom_programs.exists?(program: program, level: "moderate")
end

test "should update an existing enrollment level" do
cp = classroom_programs(:one)

patch classroom_url(@classroom), params: {
classroom: {
name: @classroom.name,
classroom_programs_attributes: [ { id: cp.id, program_id: cp.program_id, level: "advanced" } ]
}
}

assert_redirected_to school_students_url(@classroom.school)
assert_equal "advanced", cp.reload.level
end

test "should remove an enrollment" do
cp = classroom_programs(:one)
# Add a second enrollment so the classroom still has one after removal
@classroom.classroom_programs.create!(program: programs(:"3dw"), level: "basic")

assert_difference "ClassroomProgram.count", -1 do
patch classroom_url(@classroom), params: {
classroom: {
name: @classroom.name,
classroom_programs_attributes: [ { id: cp.id, _destroy: "1" } ]
}
}
end

assert_redirected_to school_students_url(@classroom.school)
end

test "is invalid when removing all programs" do
cp = classroom_programs(:one)

assert_no_difference "ClassroomProgram.count" do
patch classroom_url(@classroom), params: {
classroom: {
name: @classroom.name,
classroom_programs_attributes: [ { id: cp.id, _destroy: "1" } ]
}
}
end

assert_response :unprocessable_entity
end

test "is invalid when a program is selected without a level" do
program = programs(:"3dw")

assert_no_difference "ClassroomProgram.count" do
patch classroom_url(@classroom), params: {
classroom: {
name: @classroom.name,
classroom_programs_attributes: [ { program_id: program.id, level: "" } ]
}
}
end

assert_response :unprocessable_entity
end
end
3 changes: 1 addition & 2 deletions test/controllers/students_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ class StudentsControllerTest < ActionDispatch::IntegrationTest
end

test "can update a student's classroom" do
classroom = @student.classroom.dup
classroom.update! uuid: SecureRandom.urlsafe_base64(32), name: "New Classroom"
classroom = @school.classrooms.create!(name: "New Classroom", uuid: SecureRandom.urlsafe_base64(32))
assert_changes -> { @student.reload.classroom_id } do
patch student_url(@student), params: { student: { classroom_id: classroom.id } }
assert_redirected_to student_url(@student)
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/classroom_programs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
one:
classroom: one
program: kyh
level: basic

two:
classroom: two
program: 3dw
level: moderate
7 changes: 7 additions & 0 deletions test/fixtures/programs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

kyh:
name: Know Your Health

3dw:
name: 3D Wellness
Loading