Skip to content

Commit

Permalink
Add separate room assignment model
Browse files Browse the repository at this point in the history
Resolves #690

This commit adds a join table between users and rooms in preparation for
using a `DrawMembership` model to allow users to belong to multiple
draws over multiple years. This will allow us to maintain historical
draw data.
  • Loading branch information
orenyk committed Jun 14, 2018
1 parent d9d39a7 commit a4c8d70
Show file tree
Hide file tree
Showing 39 changed files with 557 additions and 381 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
* Remove db hit from DrawSuitesUpdate#find_suites_to_remove ([#206](https://gitlab.com/yale-sdmp/vesta/issues/206)).
* Prevent admins from assigning special groups to suites that are in a draw in suite selection([#548](https://gitlab.com/yale-sdmp/vesta/issues/548)).
* Add LotteryAssignments and Clips to the superuser dashboard ([#768](https://gitlab.com/yale-sdmp/vesta/issues/768)).
* Add separate RoomAssignments model ([#690](https://gitlab.com/yale-sdmp/vesta/issues/690)).

### Fixed
* Intent and locking deadline cannot be in the past ([#600](https://gitlab.com/yale-sdmp/vesta/issues/600))
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/results_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ def suites
end

def students
@students = User.includes(room: :suite).where(role: %w(student rep))
.where.not(room_id: nil).order(:last_name)
@students = User.includes(room: :suite)
.where(role: %w(student rep))
.where.not(room_assignments: { room_id: nil })
.order(:last_name)
end

def export
Expand Down
23 changes: 14 additions & 9 deletions app/controllers/room_assignments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,54 @@

# Controller for room assignments
class RoomAssignmentsController < ApplicationController
prepend_before_action :set_room_assignment
prepend_before_action :set_room_assignment_form
prepend_before_action :set_group
before_action :set_rooms

def new; end

def create
result = @room_assignment.assign(room_assignment_params)
result = @room_assignment_form.assign(room_assignment_params)
handle_action(**result)
end

def confirm
result = @room_assignment.prepare(room_assignment_params)
result = @room_assignment_form.prepare(room_assignment_params)
handle_action(action: 'new', **result) unless result.empty?
end

def edit
@room_assignment.build_from_group!
@room_assignment_form.build_from_group!
end

def update
result = @room_assignment_form.update(room_assignment_params)
handle_action(action: 'edit', **result)
end

private

def authorize!
authorize @room_assignment
authorize RoomAssignment.from_group(@group)
end

def set_group
@group = Group.find(params[:group_id])
end

def set_room_assignment
@room_assignment = RoomAssignment.new(group: @group)
def set_room_assignment_form
@room_assignment_form = RoomAssignmentForm.new(group: @group)
end

def set_rooms
@rooms = @group&.suite&.rooms&.where('beds > 0')
end

def room_assignment_params
params.require(:room_assignment).permit(*valid_ids)
params.require(:room_assignment_form).permit(*valid_ids)
end

def valid_ids
@room_assignment.valid_field_ids
@room_assignment_form.valid_field_ids
end
end
183 changes: 183 additions & 0 deletions app/forms/room_assignment_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# frozen_string_literal: true

# Form object for room assignment. Ensures that all students in a group
# are assigned rooms, that all rooms belong to the group's suite, and that no
# room is assigned more students than beds.

# rubocop:disable ClassLength
class RoomAssignmentForm
include ActiveModel::Model

attr_reader :group

validate :all_members_have_rooms
validate :all_rooms_exist
validate :no_overassigned_rooms

# Initialize a new RoomAssignmentForm
#
# @param group [Group] the group in question
def initialize(group:)
@group = group
@members = group.members
@rooms = group.suite.rooms
end

# Prepare the RoomAssignmentForm for confirmation / execution, handles params
# processing and validations
#
# @param p [#to_h] the parameters from the controller
# @return [Hash] the results hash, empty if valid
def prepare(p)
process_params(p)
@prep ||= valid? ? {} : error(self)
end

# Execute / save the room assignment
#
# @param p [#to_h] the parameters from the controller
# @return [Hash{Symbol=>Group,Array,Hash}] the results hash
def assign(p)
return prepare(p) unless prepare(p).empty?
create_room_assignment
success
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid => e
error(e)
end

# Update existing room assignments
#
# @param p [#to_h] the parameters from the controller
# @return [Hash{Symbol=>Group,Array,Hash}] the results hash
def update(p)
return prepare(p) unless prepare(p).empty?
update_room_assignment
success
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid => e
error(e)
end

# Returns the valid form field ids for a given group
#
# @return [Array<Symbol>] the list of form fields
def valid_field_ids
return [] unless members
@valid_field_ids ||= members.map { |m| param_for(m) }
end

# Return the display hash for the confirmation view, sets the rooms as keys
# with an array of the assigned students as the values
#
# @return [Hash{Room=>Array<User>}] the assignment hash
def assignment_hash
members_per_room.transform_keys { |r_id| room(r_id) }
.transform_values { |ms| ms.map { |m_id| member(m_id) } }
end

# Generate assignment param hash and call prepare based on existing
# assignments
#
# @return [RoomAssignment] returns self
def build_from_group!
params_hash = members.each_with_object({}) do |m, hash|
hash[param_for(m).to_s] = m.room.id.to_s
hash
end
prepare(params_hash)
self
end

private

attr_reader :members, :rooms, :params

def process_params(p)
assign_current_attrs(p)
@params = p.to_h.reject { |_k, v| v.empty? }
.transform_keys { |k| member_id_from_field(k) }
.transform_values(&:to_i)
end

def assign_current_attrs(p)
p.each { |k, v| instance_variable_set("@#{k}".to_sym, v) }
end

def all_members_have_rooms
return if (members.map(&:id) - params.keys).empty?
errors.add(:base, 'All members must have a room assigned')
end

def all_rooms_exist
return true if (params.values.uniq - rooms.map(&:id)).empty?
errors.add(:base, 'All rooms must exist')
false
end

def no_overassigned_rooms
return unless all_rooms_exist
return unless members_per_room.any? do |r_id, ms|
room(r_id).beds < ms.count
end
errors.add(:base, 'All rooms must have no more members than beds')
end

def members_per_room
params.each_with_object({}) do |(k, v), room_hash|
room_hash[v] ? room_hash[v] << k : room_hash[v] = [k]
room_hash
end
end

def room(id)
rooms.find { |r| r.id == id }
end

def member(id)
members.find { |m| m.id == id }
end

def success
obj = group.draw ? [group.draw, group] : group
{ redirect_object: obj, msg: { notice: 'Successfully assigned rooms' } }
end

def error(e)
msg = ErrorHandler.format(error_object: e)
{ redirect_object: nil, msg: { error: msg } }
end

# creates dynamic attr_readers for user fields
def method_missing(method_name, *args, &block)
super unless valid_field_ids.include? method_name
instance_variable_get("@#{method_name}")
end

def param_for(member)
"room_id_for_#{member.id}".to_sym
end

def member_id_from_field(field)
/room_id_for_(\d+)$/.match(field)[1].to_i
end

def respond_to_missing?(method_name, include_all = false)
valid_field_ids.include?(method_name) || super
end

def create_room_assignment
ActiveRecord::Base.transaction do
params.each do |student_id, room_id|
RoomAssignment.create!(user_id: student_id, room_id: room_id)
end
end
end

def update_room_assignment
ActiveRecord::Base.transaction do
params.each do |student_id, room_id|
r_a = RoomAssignment.find_by(user_id: student_id)
r_a.update!(room_id: room_id)
end
end
end
end
6 changes: 5 additions & 1 deletion app/models/group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,11 @@ def restore_member_draws
end

def remove_member_rooms
ActiveRecord::Base.transaction { members.update(room_id: nil) }
ActiveRecord::Base.transaction do
members.each do |member|
member.room_assignment.destroy! if member.room_assignment.present?
end
end
rescue
throw(:abort)
end
Expand Down
3 changes: 2 additions & 1 deletion app/models/room.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
# was in. Is an empty string unless the room belongs to a merged suite.
class Room < ApplicationRecord
belongs_to :suite
has_many :users, dependent: :nullify
has_many :room_assignments, dependent: :destroy
has_many :users, through: :room_assignments

validates :suite, presence: true
validates :number, presence: true, allow_blank: false,
Expand Down
Loading

0 comments on commit a4c8d70

Please sign in to comment.