Skip to content
Merged
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
33 changes: 33 additions & 0 deletions app/controllers/classroom_modules_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class ClassroomModulesController < AdminController
before_action :set_classroom_module

def update
if @classroom_module.update(classroom_module_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to schedule_classroom_url(@classroom_module.classroom_program.classroom) }
end
else
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
dom_id(@classroom_module),
partial: "classroom_modules/row",
locals: { classroom_module: @classroom_module }
), status: :unprocessable_entity
end
format.html { redirect_to schedule_classroom_url(@classroom_module.classroom_program.classroom) }
end
end
end

private

def set_classroom_module
@classroom_module = ClassroomModule.find(params[:id])
end

def classroom_module_params
params.expect(classroom_module: [ :publish_on ])
end
end
27 changes: 21 additions & 6 deletions app/controllers/classrooms_controller.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
class ClassroomsController < AdminController
before_action :set_classroom, only: %i[ edit update ]
before_action :set_classroom, only: %i[edit update schedule]

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

def schedule
@classroom_programs = @classroom.classroom_programs
.includes(:program, classroom_modules: :content_module)
.order("programs.name")
@active_enrollment = @classroom_programs.find { |cp| cp.id.to_s == params[:classroom_program_id] } || @classroom_programs.first
end

def update
respond_to do |format|
if @classroom.update(classroom_params)
@classroom.classroom_programs.reload.each(&:generate_modules!)
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
54 changes: 54 additions & 0 deletions app/controllers/content_modules_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
class ContentModulesController < AdminController
before_action :set_content_module, only: %i[edit update destroy]

def index
@programs = Program.order(:name)
@active_program = @programs.find { |p| p.id.to_s == params[:program_id] } || @programs.first
@modules_by_level = @active_program
.content_modules
.includes(:links)
.group_by(&:level)
end

def new
@content_module = ContentModule.new
end

def create
@content_module = ContentModule.new(content_module_params)

if @content_module.save
redirect_to content_modules_path, notice: "Module was successfully created."
else
render :new, status: :unprocessable_entity
end
end

def edit
end

def update
if @content_module.update(content_module_params)
redirect_to content_modules_path, notice: "Module was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end

def destroy
if @content_module.destroy
redirect_to content_modules_path, notice: "Module was successfully deleted.", status: :see_other
else
redirect_to content_modules_path, alert: "Cannot delete a module that has been assigned to classrooms."
end
end

private
def set_content_module
@content_module = ContentModule.find(params.expect(:id))
end

def content_module_params
params.expect(content_module: [ :name, :program_id, :level, :position ])
end
end
47 changes: 47 additions & 0 deletions app/controllers/links_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class LinksController < AdminController
before_action :set_content_module, only: %i[new create]
before_action :set_link, only: %i[edit update destroy]

def new
@link = @content_module.links.build
end

def create
@link = @content_module.links.build(link_params)

if @link.save
redirect_to edit_content_module_path(@content_module), notice: "Link was successfully created."
else
render :new, status: :unprocessable_entity
end
end

def edit
end

def update
if @link.update(link_params)
redirect_to edit_content_module_path(@link.content_module), notice: "Link was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end

def destroy
@link.destroy!
redirect_to edit_content_module_path(@link.content_module), notice: "Link was successfully deleted.", status: :see_other
end

private
def set_content_module
@content_module = ContentModule.find(params.expect(:content_module_id))
end

def set_link
@link = Link.find(params.expect(:id))
end

def link_params
params.expect(link: [ :title, :url, :link_type, :position ])
end
end
10 changes: 10 additions & 0 deletions app/controllers/student_homes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,15 @@ class StudentHomesController < ApplicationController

def index
@student = Current.student
classroom = @student.classroom
@classroom_programs = classroom.classroom_programs
.includes(:program)
.order("programs.name")
@active_program = @classroom_programs.find { |cp| cp.id.to_s == params[:classroom_program_id] } || @classroom_programs.first
@published_modules = if @active_program
@active_program.classroom_modules.published.includes(content_module: :links)
else
[]
end
end
end
6 changes: 6 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
module ApplicationHelper
def safe_external_url(url)
uri = URI.parse(url.to_s)
uri.scheme.in?(%w[http https]) ? url : "#"
rescue URI::InvalidURIError
"#"
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"
}
}
10 changes: 10 additions & 0 deletions app/javascript/controllers/publish_now_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["date"]

setToday() {
this.dateTarget.value = new Date().toISOString().split("T")[0]
this.element.requestSubmit()
}
}
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
6 changes: 6 additions & 0 deletions app/models/classroom_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ClassroomModule < ApplicationRecord
belongs_to :classroom_program
belongs_to :content_module

scope :published, -> { where.not(publish_on: nil).where("publish_on <= ?", Date.current) }
end
27 changes: 27 additions & 0 deletions app/models/classroom_program.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class ClassroomProgram < ApplicationRecord
belongs_to :classroom
belongs_to :program
has_many :classroom_modules, dependent: :destroy

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

validates :level, presence: true
validate :level_unchanged_if_modules_scheduled, on: :update

def generate_modules!
current_cm_ids = program.content_modules.where(level: level).pluck(:id)
classroom_modules.where.not(content_module_id: current_cm_ids).destroy_all
current_cm_ids.each do |cm_id|
classroom_modules.find_or_create_by!(content_module_id: cm_id)
end
end

private

def level_unchanged_if_modules_scheduled
return unless level_changed?
if classroom_modules.where.not(publish_on: nil).exists?
errors.add(:level, "cannot be changed when modules have publish dates set")
end
end
end
11 changes: 11 additions & 0 deletions app/models/content_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class ContentModule < ApplicationRecord
belongs_to :program
has_many :links, dependent: :destroy
has_many :classroom_modules, dependent: :restrict_with_error

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

validates :name, :level, presence: true

default_scope { order(:position) }
end
10 changes: 10 additions & 0 deletions app/models/link.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class Link < ApplicationRecord
belongs_to :content_module

enum :link_type, { survey: "survey", game: "game" }, validate: true

validates :title, :url, :link_type, presence: true
validates :url, format: { with: /\Ahttps?:\/\/.+\z/i, message: "must start with http:// or https://" }, allow_blank: true

default_scope { order(:position) }
end
5 changes: 5 additions & 0 deletions app/models/program.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Program < ApplicationRecord
has_many :classroom_programs, dependent: :destroy
has_many :classrooms, through: :classroom_programs
has_many :content_modules, dependent: :destroy
end
24 changes: 24 additions & 0 deletions app/views/classroom_modules/_row.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<tr id="<%= dom_id(classroom_module) %>">
<td><%= classroom_module.content_module.name %></td>
<td>
<%= form_with model: classroom_module, url: classroom_module_path(classroom_module),
data: { controller: "publish-now" } do |form| %>
<div class="flex items-center gap-2">
<%= form.date_field :publish_on, value: classroom_module.publish_on, class: "input input-sm",
data: { publish_now_target: "date" } %>
<%= form.submit "Save", class: "btn btn-xs btn-primary" %>
<button type="button" class="btn btn-xs btn-ghost"
data-action="publish-now#setToday">Publish Now</button>
</div>
<% end %>
</td>
<td>
<% if classroom_module.publish_on.nil? %>
<span class="badge badge-ghost">Unscheduled</span>
<% elsif classroom_module.publish_on > Date.current %>
<span class="badge badge-warning">Scheduled</span>
<% else %>
<span class="badge badge-success">Published</span>
<% end %>
</td>
</tr>
3 changes: 3 additions & 0 deletions app/views/classroom_modules/update.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= turbo_stream.replace dom_id(@classroom_module) do %>
<%= render "row", classroom_module: @classroom_module %>
<% 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
Loading