Rails validation count limit on has_many :through
- by Jeremy
I've got the following models: Team, Member, Assignment, Role
The Team model has_many Members. Each Member has_many roles through assignments. Role assignments are Captain and Runner. I have also installed devise and CanCan using the Member model.
What I need to do is limit each Team to have a max of 1 captain and 5 runners.
I found this example, and it seemed to work after some customization, but on update ('teams/1/members/4/edit'). It doesn't work on create ('teams/1/members/new'). But my other validation (validates :role_ids, :presence = true
) does work on both update and create. Any help would be appreciated.
Update: I've found this example that would seem to be similar to my problem but I can't seem to make it work for my app.
It seems that the root of the problem lies with how the count (or size) is performed before and during validation.
For Example:
When updating a record...
It checks to see how many runners there are on a team and returns a count. (i.e. 5) Then when I select a role(s) to add to the member it takes the known count from the database (i.e. 5) and adds the proposed changes (i.e. 1), and then runs the validation check. (Team.find(self.team_id).members.runner.count 5) This works fine because it returns a value of 6 and 6 5 so the proposed update fails without saving and an error is given.
But when I try to create a new member on the team...
It checks to see how many runners there are on a team and returns a count. (i.e. 5) Then when I select a role(s) to add to the member it takes the known count from the database (i.e. 5) and then runs the validation check WITHOUT factoring in the proposed changes. This doesn't work because it returns a value of 5 known runner and 5 = 5 so the proposed update passes and the new member and role is saved to the database with no error.
Member Model:
class Member < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :password, :password_confirmation, :remember_me
attr_accessible :age, :email, :first_name, :last_name, :sex, :shirt_size, :team_id, :assignments_attributes, :role_ids
belongs_to :team
has_many :assignments, :dependent => :destroy
has_many :roles, through: :assignments
accepts_nested_attributes_for :assignments
scope :runner, joins(:roles).where('roles.title = ?', "Runner")
scope :captain, joins(:roles).where('roles.title = ?', "Captain")
validate :validate_runner_count
validate :validate_captain_count
validates :role_ids, :presence => true
def validate_runner_count
if Team.find(self.team_id).members.runner.count > 5
errors.add(:role_id, 'Error - Max runner limit reached')
end
end
def validate_captain_count
if Team.find(self.team_id).members.captain.count > 1
errors.add(:role_id, 'Error - Max captain limit reached')
end
end
def has_role?(role_sym)
roles.any? { |r| r.title.underscore.to_sym == role_sym }
end
end
Member Controller:
class MembersController < ApplicationController
load_and_authorize_resource :team
load_and_authorize_resource :member, :through => :team
before_filter :get_team
before_filter :initialize_check_boxes, :only => [:create, :update]
def get_team
@team = Team.find(params[:team_id])
end
def index
respond_to do |format|
format.html # index.html.erb
format.json { render json: @members }
end
end
def show
respond_to do |format|
format.html # show.html.erb
format.json { render json: @member }
end
end
def new
respond_to do |format|
format.html # new.html.erb
format.json { render json: @member }
end
end
def edit
end
def create
respond_to do |format|
if @member.save
format.html { redirect_to [@team, @member], notice: 'Member was successfully created.' }
format.json { render json: [@team, @member], status: :created, location: [@team, @member] }
else
format.html { render action: "new" }
format.json { render json: @member.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @member.update_attributes(params[:member])
format.html { redirect_to [@team, @member], notice: 'Member was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: @member.errors, status: :unprocessable_entity }
end
end
end
def destroy
@member.destroy
respond_to do |format|
format.html { redirect_to team_members_url }
format.json { head :no_content }
end
end
# Allow empty checkboxes
# http://railscasts.com/episodes/17-habtm-checkboxes
def initialize_check_boxes
params[:member][:role_ids] ||= []
end
end
_Form Partial
<%= form_for [@team, @member], :html => { :class => 'form-horizontal' } do |f| %>
#...
# testing the count...
<ul>
<li>Captain - <%= Team.find(@member.team_id).members.captain.size %></li>
<li>Runner - <%= Team.find(@member.team_id).members.runner.size %></li>
<li>Driver - <%= Team.find(@member.team_id).members.driver.size %></li>
</ul>
<div class="control-group">
<div class="controls">
<%= f.fields_for :roles do %>
<%= hidden_field_tag "member[role_ids][]", nil %>
<% Role.all.each do |role| %>
<%= check_box_tag "member[role_ids][]", role.id, @member.role_ids.include?(role.id), id: dom_id(role) %>
<%= label_tag dom_id(role), role.title %>
<% end %>
<% end %>
</div>
</div>
#...
<% end %>