I have the following mongoid model, with a scoped validation to prevent multiple votes on one bill. Each vote belongs to a user and a group:
class Vote
include Mongoid::Document
field :value, :type => Symbol # can be :aye, :nay, :abstain
field :type, :type => Symbol # TODO can delete?
belongs_to :user
belongs_to :polco_group
embedded_in :bill
validates_uniqueness_of :value, :scope => [:polco_group_id, :user_id, :type]
end
The user has the following method to add a vote to a bill:
def vote_on(bill, value)
if my_groups = self.polco_groups # test to make sure the user is a member of a group
my_groups.each do |g|
# TODO see if already voted
bill.votes.create(:value => value, :user_id => self.id, :polco_group_id => g.id, :type => g.type)
end
else
raise "no polco_groups for this user" # #{self.full_name}"
end
end
and a Bill class which embeds many :votes. This is designed to allow a user to associate their vote with different groups ("Ruby Coders", "Women", etc.) and is working well, except the database currently allows a user to vote multiple times on one bill. How can I get the following to work?
u = User.last
b = Bill.last
u.vote_on(b,:nay)
u.vote_on(b,:nay) -> should return a validation error
Most probably validators on Vote are not getting fired. You can confirm that by adding a validates function and outputting something or raising an exception in it.
class Vote
validate :dummy_validator_to_confirmation
def dummy_validator_to_confirmation
raise "What the hell, it is being called, then why my validations are not working?"
end
end
If after creating above validations User#vote_on doesn't raises exception, it confirms that callbacks are not fired for Vote via vote_on method. You need to change your code to fire callbacks on Vote. Probably changing it to resemble following would help:
def vote_on(bill, value)
if my_groups = self.polco_groups # test to make sure the user is a member of a group
my_groups.each do |g|
# TODO see if already voted
vote = bill.votes.new(:value => value, :user_id => self.id, :polco_group_id => g.id, :type => g.type)
vote.save
end
else
raise "no polco_groups for this user" # #{self.full_name}"
end
end
There is an open issue on mongoid github issue tracker to allow cascade callbacks to embedded documents. Right now callbacks are only fired on document on which persistence actions are taking place on.
Related
I have a group of input, for which I expect a large amount data (list of objects), so I want this input on create/update action to be wrapped inside of ActiveRecord transaction.
There is model Student, which has_one Account.
ACCOUNT_FIELDS=[:name, :surname, :email, :phone, :activated]
has_one :account, :as => :account_holder, autosave: true, :dependent => :destroy
validates_associated_extended :account
ACCOUNT_FIELDS.each do |action|
define_method action do
get_or_build_account.send(action)
end
setter_action = action.to_s + "="
define_method setter_action do |arg|
get_or_build_account.send(setter_action, arg)
end
end
here I made a reader/writer methods, so #student.name will return related data from account, also I can assign it through #student thanks to autosave.
Issue: as I said, I want it to be wrapped inside of transaction, so in my controller I don't save anything. Each student is assigned like this
student.attributes = #...data
Where student later on passed to transaction block.
But! For this specific model I want to student.attributes also return fields from ACCOUNT_FIELDS.
Normally it works with student.account_attributes but as I said, later student is processed in transaction, and it is made with module, which I want to be reusable for some other models (which doesn't need this logic).
So rather than modifying my module code with some conditions, I want instance of this model to return needed account fields when just called self.attributes
#student.attributes #=> :school_id => 1, :name => "John"...
where name is self.name from self.account.name
Try this:
def attributes
new_attributes={}
ACCOUNT_FIELDS.each do |field|
new_attributes[field]=self.send(field)
end
super.merge(new_attributes)
end
I'm working in a large Rails 2.3 application and I have data on a model that would like to move to another model. I need to do this is phases as there are places in the Rails code base that are reading and writing this model data and outside applications reading the table data directly via SQL. I need to allow a period of time where the attribute is synchronized on both models and their associated tables before I drop one model and table altogether.
My models have a has_one and belongs_to relationship like this:
class User < ActiveRecord::Base
has_one :user_email, :inverse_of => :user
accepts_nested_attributes_for :user_email
validates_presence_of :email
def email=( value )
write_attribute(:email, value)
user_email.write_attribute(:email, value)
end
end
class UserEmail < ActiveRecord::Base
belongs_to :user, :inverse_of => :user_email
validates_presence_of :email
def email=( value )
write_attribute(:email, value)
user.write_attribute(:email, value)
end
end
I'd like to do away with UserEmail and its associated table altogether, but for a time I need to keep email up-to-date on both models so if it's set on one model, it's changed on the other. Overriding email= on each model is straightforward, but coming up with a commit strategy is where I'm hitting a wall.
I have places in the code base that are doing things like:
user.user_email.save!
and I'm hoping to find a way to continue to allow this kind of code for the time being.
I can't figure out a way to ensure that saving an instance of User ensures the corresponding UserEmail data is committed and saving an instance of UserEmail ensures the corresponding User instance data is also committed without creating an infinite save loop in the call backs.
This is the flow I would like to be able to support for the time being:
params = { user: { email: 'foo#bar.com', user_email: { email: 'foo#bar.com' } } }
user = User.create( params )
user.email = "moo#bar.com"
user.save
puts user.user_email # puts "moo#bar.com"
user.user_email.email = "foo#bar.com"
user.user_email.save
user.reload
puts user.email # puts "foo#bar.com"
Is there a way to achieve this sort of synchronization between the User and UserEmail models so they are kept in sync?
If it helps, I can probably do away with accepts_nested_attributes_for :user_email on User.
Using ActiveModel::Dirty
In User model
after_save :sync_email, :if => :email_changed?
def sync_email
user_email.update_column(:email, email) if user_email.email != email
end
In UserEmail model
after_save :sync_email, :if => :email_changed?
def sync_email
user.update_column(:email, email) if user.email != email
end
Let's assume, for sanity's sake, that the models are "User" and "Cart", and the shared field is "email". I would do this:
#in User
after_save :update_cart_email
def update_cart_email
if self.changes["email"]
cart = self.cart
if cart.email != self.email
cart.update_attributes(:email => self.email)
end
end
end
#in Cart
after_save :update_user_email
def update_user_email
if self.changes["email"]
user = self.user
if user.email != self.email
user.update_attributes(:email => user.email)
end
end
end
Because we check if the other model's email has already been set, it shouldn't get stuck in a loop.
This works if you drop accepts_nested_attributes_for :user_email -- otherwise you'll get a save loop that never ends.
I have two different objects which can belong to one parent object. These child objects can both also belong to each other (many to many). What's the best way to ensure that child objects which belong to each other also belong to the same parent object.
As an example of what I'm trying to do I have a Kingdom which has both many People and Land. The People model would have a custom validate which checks each related Land and error.adds if one has a mismatched kingdom_id. The Land model would have a similar validate.
This seems to work, but when updating it allows the record to save the 'THIS IS AN ERROR' error is in people.errors, however the Land which raised the error has been added to the People collection.
kingdom = Kingdom.create
people = People.create(:kingdom => kingdom)
land = Land.create(:kingdom_id => 999)
people.lands << land
people.save
puts people.errors.inspect # #messages={:base=>["THIS IS AN ERROR"]
puts people.lands.inspect # [#<Land id: 1...
Ideally I'd want the error to cancel the record update. Is there another way I should be going about this, or am I going in the wrong direction entirely?
# Models:
class Kingdom < ActiveRecord::Base
has_many :people
has_many :lands
end
class People < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :lands
validates :kingdom_id, :presence => true
validates :kingdom, :associated => true
validate :same_kingdom?
private
def same_kingdom?
if self.lands.any?
errors.add(:base, 'THIS IS AN ERROR') unless kingdom_match
end
end
def kingdom_match
self.lands.each do |l|
if l.kingdom_id != self.kingdom_id
return false
end
end
end
end
class Land < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :people
end
Firstly, the validation won't prevent the record from being added to the model's unpersisted collection. It will prevent the revised collection from being persisted to the database. So the model will be in an invalid state, and flagged as such with the appropriate errors. To see this, you can simply reload the people object.
You also have an error in your logic - the kingdom_match method will never return true even if no invalid kingdom_id's are found. You should add a line to fix this:
def kingdom_match
self.lands.each do |l|
return false if l.kingdom_id != self.kingdom_id
end
true
end
And you can make this validation a bit more concise and skip the kingdom_match method entirely:
def same_kingdom?
if self.lands.any?{|l| l.kingdom_id != self.kingdom_id }
errors.add(:base, 'THIS IS AN ERROR')
end
end
I have an object with multiple validations.
gist of the Approval model: https://gist.github.com/1579150 (side note, I know the Email Domain Validor doesn't work...)
The point is, if these validations fail, I want the object to save, but then set a value on approval.issue = true. Approval.issue is a boolean field that defaults to false, but then if the object fails validations I want the system admin to be able to see it and then handle it appropriately.
To make it more idiot proof, it would be nice to have some validations that can force the user to make changes, but then some would be exempt and would simply trigger the .issue field to true.
For instance, if the email is of the right domain but the email doesn't exist in the system, it would save it but then set issue => true. I could then set up a simple view for Approvals where :issue => :true. then the admin could modify or delete bad Approvals.
Ideas?
Code from gist:
class Approval < ActiveRecord::Base
class ApproverEmailValidator < ActiveModel::EachValidator
def validate_each(approval, attribute, value)
approval.errors[attribute] << "must be a valid e-mail address in our system" unless is_valid_email?(value)
end
protected
def is_valid_email?(address)
User.find_by_email(address)
end
end # End Approver Validator
class EmailDomainValidator < ActiveModel::EachValidator
def email_domain_is?(domain)
unless /ravennainteractive.com$/ =~ email(domain)
errors.add(:email, "You must Use an Eddie Bauer email address")
end
end
end #End Email Domain Validator
belongs_to :recommendation
attr_accessible :approval, :email, :user_id
validates :email, :email_domain
validates :next_approver_email, :approver_email => { :if => :recently_approved? }
before_save :create_next_approval
after_create :approval_notification
attr_accessor :next_approver_email
def recently_approved?
self.approved_changed? && self.approved?
end
def create_next_approval
next_approval = self.recommendation.approvals.build(:email => self.next_approver_email, :user_id => User.find_by_email(next_approver_email))
next_approval.save if next_approver_email.present? && recently_approved?
end
def email_domain_is?
unless /ravennainteractive.com$/ =~ email
errors.add(:email, "You must Use an Eddie Bauer email address")
end
end
private
def approval_notification
ApprovalMailer.needs_approval(self).deliver
end
end
You can implement observer for Approval that will analyze you objects before saving and set issue to "true", if there is some suspicious input.
UPDATE: Here is short guide how to implement observer:
rails generate observer - after this step you`ll see _observer.rb file.
Implement needed methods. Here is simple example extracted from one of my projects (It seems like you should use "before_save" method):
class HomeworkObserver < ActiveRecord::Observer
def after_create(homework)
TeacherMailer.send_later(:student_submitted_homework, homework)
end
def after_save(homework)
if (homework.checked)
StudentMailer.send_later(:teacher_checked_homework, homework)
end
end
end
Also you need to enable observer by adding it to your config/application.rb, e.g:
config.active_record.observers = :homework_observer
Official docs: http://api.rubyonrails.org/classes/ActiveRecord/Observer.html
I have a model named Tickets that being saved to the database even when
invalid. This is stopping me from using validations to help prevent
duplicate data being saved to the DB. In script/console
>> Ticket.last.valid?
=> False
>> Ticket.first.valid?
=> False
If I try to see what errors are associated with this invalid object
>> Ticket.last.errors.each{|attr,msg| puts "#{attr} - #{msg}\n" }
=> {}
So does anyone know how it's possible to save an invalid object to the
database, and how can I find what is making the object invalid?
Ticket.rb (model)
class Ticket < ActiveRecord::Base
belongs_to :whymail
belongs_to :forms
attr_accessible :to_email, :to_email, :from_email, :subject, :body
validates_uniqueness_of :to_email, :scope => [:body, :from_email]
validates_presence_of :to_email
validates_presence_of :from_email
validates_presence_of :subject
validates_presence_of :body
def after_create
if self.valid?
whymail = Whymail.find(:first, :include => :user, :conditions => ['(email = ?)', self.to_email.upcase ] )
if !whymail.nil?
self.whymail_id = whymail.id
self.save
MyMailer.deliver_forward(whymail.user.email, self.from_email, self.to_email, self.subject, self.body)
end
end
end
end
One part of this question was answered, second was not. Can anyone see problems with this model that may allow it to save even though it is invalid??
It is possible to skip validations. How are you saving it? Is it part of a nested form?
In any case, you should look at the errors like this:
>>t = Ticket.last
>>t.valid?
>>t.errors.each{|attr,msg| puts "#{attr} - #{msg}\n" }
The way you have it above, you are getting a new object with the second Ticket.last call and validation hasn't been run on that one, so you can't see what the errors are.
Try something like:
t = Ticket.last
t.save
puts t.errors.full_messages.inspect
The errors object won't be populated until you try to save the activerecord object.