Rails validation count limit on has_many :through

Posted by Jeremy on Stack Overflow See other posts from Stack Overflow or by Jeremy
Published on 2012-10-26T20:48:58Z Indexed on 2012/10/30 23:01 UTC
Read the original article Hit count: 260

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 %>

© Stack Overflow or respective owner

Related posts about ruby-on-rails-3

Related posts about count