How to validate person_ids in Rails model? - ruby-on-rails

In my Rails app, I have a has_many through relationship between two models and therefore I am creating new objects like this:
Project.new(:name => 'Test', :person_ids => [1, 2, 3])
What is a good way to validate those person_ids in the model?
This is what I have so far:
class Project < ActiveRecord::Base
has_many :people_projects
has_many :people, :through => :people_projects
validates :person_ids, inclusion => { :in => lambda { |x| x.valid_people } }
def valid_people
user.people.map(&:id)
end
end
However, this doesn't work because the person_ids get posted in an array.
Can anybody help?

Use a custom validation method like
class Project < ActiveRecord::Base
validate :valid_people
def valid_people
people = user.people.pluck(:id)
if person_ids.blank? || (person_ids - people).any?
errors.add(:person_ids, "Please add real people")
end
end
end

Related

accepts_nested_attributes_for with polymorphic association

Is there any way to halt saving of child before parent.
I am using accepts_nested_attributes_for with polymorphic association.
I used multiple options validates_presence_of :parent_id , validates_assoicated :parent but none are working.
For example, I do have a class
Class Person
include HasPhoneNumbers
..
end
module HasPhoneNumbers
def self.included(kclass)
kclass.has_many :phone_numbers, :as => :callable, :dependent => kclass == Person ? :destroy : :nullify
end
klass.accepts_nested_attributes_for :phone_numbers, :reject_if => lambda {|pn| pn.keys.any?{|k| k.to_sym != :id && pn[k].blank?} }
end
class PhoneNumber
belongs_to :callable, :polymorphic => true
end
So while saving person due to validation in person object, it was not saving. However, child(phone_number) was saving. So I need to restrict it to not save child(phone_number) before parent(person) saves.
I did try multiple options using validates_presence_of and validates_associated, but none are working for me.
#person = Person.new(params[:person])
ActiveRecord::Base.transaction do
person.save!
end
Wrapping your saves within a transaction should roll back the phone number save if the person fails validation.
Reference: ActiveRecord Transactions

Update counter cache when doing collection<<(object, …)

Hi I'm having trouble with a simple counter cache in rails. I have the following models:
class CarImage < ActiveRecord::Base
belongs_to :car, :counter_cache => :images_count
end
class Car < ActiveRecord::Base
has_many :images, :class_name => "CarImage", :dependent => :destroy, :limit => 4
end
The problem occurs when doing this:
car1.images << car2.images
None of the images_count on the two cars are being updated accordingly.
I found this on the topic: How to update counter_cache when updating a model?
And I my case the solution would look something like this:
class Car < ActiveRecord::Base
has_many :images, :class_name => "CarImage", :dependent => :destroy, :limit => 4
after_save :update_counter, :if => :car_id_changed?
private
def update_counter
new_car = Car.find(car_id)
Car.increment_counter(:images_count, new_car.id)
if car_id_was.present?
old_car = Car.find(car_id_was)
Car.decrement_counter(:images_count, old_car.id)
end
end
end
But - I'm asking myself - Why is this not build into rails at first hand? I can't find anything in the documentation about the issue, and to me using the build-in counter_cache in rails is almost unusable, if its really true, that it only supports create and destroy - not update! Can anyone give a good explanation as to why this is? Is it really necessary for me to build a callback and keep an eye on the relations my self?
BTW. I'm using 3.1

ActiveRecord (Rails 2.3.8) - Update existing, add new record when updating nested attributes

I have a "user" model that "has_one" "membership" (active at a time). For auditing and data integrity reasons, I'd like it so that if the membership changes for a user, the old/current record (if existing) has an inactive/active flag swapped, and a new row is added for the new changed record. If there are no changes to the membership, I'd like to just ignore the update. I've tried implementing this with a "before_save" call-back on my user model, but have failed many times. Any help is greatly appreciated.
models:
class User < ActiveRecord::Base
has_one :membership, :dependent => :destroy
accepts_nested_attributes_for :membership, :allow_destroy => true
end
class Membership < ActiveRecord::Base
default_scope :conditions => {:active => 1}
belongs_to :user
end
I have what I think is a pretty elegant solution. Here's your user model:
class User < ActiveRecord::Base
has_one :membership, :dependent => :destroy
accepts_nested_attributes_for :membership
def update_membership_with_history attributes
self.membership.attributes = attributes
return true unless self.membership.changed?
self.membership.update_attribute(:active, false)
self.build_membership attributes
self.membership.save
end
end
This update_membership_with_history method allows us to handle changed or unchanged records. Next the membership model:
class Membership < ActiveRecord::Base
default_scope :conditions => {:active => true}
belongs_to :user
end
I changed this slightly, since active should be a boolean, not 1's and 0's. Update your migration to match. Now the update action, which is the only part of your scaffold that needs to change:
def update
#user = User.find(params[:id], :include => :membership)
membership_attributes = params[:user].delete(:membership_attributes)
if #user.update_attributes(params[:user]) && #user.update_membership_with_history(membership_attributes)
redirect_to users_path
else
render :action => :edit
end
end
We're simply parsing out the membership attributes (so you can still use fields_for in your view) and updating them separately, and only if needed.
Did you look at acts_as_versioned? In the before_save of the Membership you could create a new version of the User, which would be acts_as_versioned.
Got it working. While it's probably not the best implementation, all my tests are passing. Thanks for the input guys.
before_save :soft_delete_changed_membership
def soft_delete_changed_membership
if !membership.nil? then
if !membership.new_record? && membership.trial_expire_at_changed? then
Membership.update_all( "active = 0", [ "id = ?", self.membership.id ] )
trial_expire_at = self.membership.trial_expire_at
self.membership = nil
Membership.create!(
:user_id => self.id,
:trial_expire_at => trial_expire_at,
:active => true
)
self.reload
end
end
end
Why don't you just assume that the latest membership is the active one. This would save you a lot of headache.
class User < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
end
class Membership < ActiveRecord::Base
nested_scope :active, :order => "created_at DESC", :limit => 1
belongs_to :user
def update(attributes)
self.class.create attributes if changed?
end
end
then you can use
#user.memberships.active
to get the active membership, and you can just update any membership to get a new membership, which will become the active membership because it is the latest.

validate uniqueness amongst multiple subclasses with Single Table Inheritance

I have a Card model that has many CardSets and a CardSet model that has many Cards through a Membership model:
class Card < ActiveRecord::Base
has_many :memberships
has_many :card_sets, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :card
belongs_to :card_set
validates_uniqueness_of :card_id, :scope => :card_set_id
end
class CardSet < ActiveRecord::Base
has_many :memberships
has_many :cards, :through => :memberships
validates_presence_of :cards
end
I also have some sub-classes of the above using Single Table Inheritance:
class FooCard < Card
end
class BarCard < Card
end
and
class Expansion < CardSet
end
class GameSet < CardSet
validates_size_of :cards, :is => 10
end
All of the above is working as I intend. What I'm trying to figure out is how to validate that a Card can only belong to a single Expansion. I want the following to be invalid:
some_cards = FooCard.all( :limit => 25 )
first_expansion = Expansion.new
second_expansion = Expansion.new
first_expansion.cards = some_cards
second_expansion.cards = some_cards
first_expansion.save # Valid
second_expansion.save # **Should be invalid**
However, GameSets should allow this behavior:
other_cards = FooCard.all( :limit => 10 )
first_set = GameSet.new
second_set = GameSet.new
first_set.cards = other_cards # Valid
second_set.cards = other_cards # Also valid
I'm guessing that a validates_uniqueness_of call is needed somewhere, but I'm not sure where to put it. Any suggestions?
UPDATE 1
I modified the Expansion class as sugested:
class Expansion < CardSet
validate :validates_uniqueness_of_cards
def validates_uniqueness_of_cards
membership = Membership.find(
:first,
:include => :card_set,
:conditions => [
"card_id IN (?) AND card_sets.type = ?",
self.cards.map(&:id), "Expansion"
]
)
errors.add_to_base("a Card can only belong to a single Expansion") unless membership.nil?
end
end
This works! Thanks J.!
Update 2
I spoke a little too soon. The above solution was working great until I went to update an Expansion with a new card. It was incorrectly identifying subsequent #valid? checks as false because it was finding itself in the database. I fixed this by adding a check for #new_record? in the validation method:
class Expansion < CardSet
validate :validates_uniqueness_of_cards
def validates_uniqueness_of_cards
sql_string = "card_id IN (?) AND card_sets.type = ?"
sql_params = [self.cards.map(&:id), "Expansion"]
unless new_record?
sql_string << " AND card_set_id <> ?"
sql_params << self.id
end
membership = Membership.find(
:first,
:include => :card_set,
:conditions => [sql_string, *sql_params]
)
errors.add_to_base("a Card can only belong to a single Expansion") unless membership.nil?
end
I'm really not sure about that, I'm just trying because I'm not able to test it here... but maybe something like the following works for you. Let me know if it does :]
class Expansion < Set
validate :validates_uniqueness_of_cards
def validates_uniqueness_of_cards
membership = Membership.find(:first, :include => :set,
:conditions => ["card_id IN (?) AND set.type = ?",
self.cards.map(&:id), "Expansion"])
errors.add_to_base("Error message") unless membership.nil?
end
end
Very late to the party here, but assuming you've set up STI, then you can now validate uniqueness of an attribute scoped to the sti type,
e.g
validates_uniqueness_of :your_attribute_id, scope: :type

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

Resources