Rails validation callback just once - ruby-on-rails

I'm trying to figure out how can I validate a model just once.
Here is the situation:
It's about a library management
class Loan < ActiveRecord::Base
attr_accessible :book_id, :check_in, :check_out, :member_id
belongs_to :book
belongs_to :member
validates :member_id, :presence => true
validates :book_id, :presence => true
validate :returned_book, :on => :update
def returned_book
self.book.update_attributes! quantity: self.book.quantity + 1
self.book.update_attributes! borrowed_count: self.book.borrowed_count - 1
end
end
What i would like to do, is when i put a check_out date (i'm returning a book):
Increment the quantity of the book
Decrement the borrowed_count of the book
But just once, not every time I update the record or change the check_out date
Im my code here, every time I save (update) the record the validation runs...

Validations should be used to validate things, not modify state. A callback would be more appropriate in this case. Maybe something like:
attr_accessor :quantities_adjusted
before_save :update_quantities
def update_quantities
if changed_attributes.keys.include?('check_out') && !quantities_adjusted
self.book.quantity += 1
self.book.borrowed_count -= 1
self.quantities_adjusted = true
end
end

Related

Add new record to a new record

I'm using rails 4.1.6
I have a problem creating new records and then saving them. Here are the models:
class Function < ActiveRecord::Base
belongs_to :theater
has_many :showtimes, dependent: :destroy
belongs_to :parsed_show
validates :theater, presence: :true
validates :date, presence: :true
end
class Theater < ActiveRecord::Base
has_many :functions, :dependent => :destroy
validates :name, :presence => :true
accepts_nested_attributes_for :functions
end
class Showtime < ActiveRecord::Base
belongs_to :function
validates :time, presence: true
validates :function, presence: true
end
showtime = Showtime.new time: Time.current
theater = Theater.first # read a Theater from the database
function = Function.new theater: theater, date: Date.current
function.showtimes << showtime
function.showtimes.count # => 0
why is the showtime not being added to the function's showtimes? I need to save the function later with the showtimes with it.
Your Function object hasn't been persisted. You need to make sure that it's persisted (which of course also requires that it be valid) before you can add things to its list of showtimes.
Try saving the function beorehand, and if it succeeds (that is if function.persisted? is true), it should allow you to << directly into function.showtimes, as you like. Alternatively, you can use the Function#create class method instead of the Function#new class method, since the former automatically persists the record.
You can use Function#create
theater = Theater.first # read a Theater from the database
function = Function.new theater: theater, date: Date.current
function.showtimes.create(time: Time.current)
I forgot to check if saving the function also saved the theaters, and even though function.showtimes.count returns 0, the showtimes are saved to the database anyway.
function.showtimes.count returns:
=> 0
but
function.showtimes returns:
=> #<ActiveRecord::Associations::CollectionProxy [#<Showtime id: nil, time: "2015-01-09 04:46:50", function_id: nil>]>

How to ensure record is not saved by two users at the same time?

I have this class:
class Payment < ActiveRecord::Base
attr_accessible :amount, :invoice_id
belongs_to :invoice
validates :amount, :numericality => { :greater_than => 0, :less_than_or_equal_to => :maximum_amount }, :if => "invoice.present?"
private
def maximum_amount
invoice.total if invoice.present?
end
end
The code above works. But how can I make sure that no two users can ever save a new payment record at the same time, thereby exceeding the invoice total?
Is it possible to do this at the database level somehow?
Thanks for any help.
You could modify your maximum_amount function to be something like the following:
def maximum_amount
if invoice.present?
amt_paid = 0
invoice.payments.each do |payment|
amt_paid += payment.amount
end
invoice.total - amt_paid
end
end
This isn't at the database level per say, but if a second user attempts to make a payment more than the existing balance it will not validate.

Rails order Post by def in model

I want to order posts based on the total votes it has. This is what I have in the Post Model:
class Post < ActiveRecord::Base
attr_accessible :title, :url
validates :title, presence: true
validates :url, presence: true
has_many :votes
def vote_number
votes.where(direction: "up").count - votes.where(direction: "down").count
end
end
And this is what I attempted to do in the Post Controller:
def index
#posts = Post.last(10).order('vote_number')
end
Nevertheless I get this error from the index:
undefined method `order' for #<Array:0x3787158>
The other questions in Stack Overflow resolved this problem by making the calculation in the Post Controller but I can not do it because votes are arrays and not integers.
Found a way to solve it. Instead of using order I used sort_by.
Instead of having this in the Post Controller:
def index
#posts = Post.last(10).order('vote_number')
end
I used sort_by:
def index
#posts = Post.all.sort_by{|post|-post.vote_number}
end
You should try counter cache.
You can read more about it from the following links -
How to sort authors by their book count with ActiveRecord?
http://hiteshrawal.blogspot.com/2011/12/rails-counter-cache.html
http://railscasts.com/episodes/23-counter-cache-column
Counter cache only works inside rails. If you updated from outside application you might have to do some work around.
first, last and all execute query. Insert order always before those three.
class Post < ActiveRecord::Base
attr_accessible :title, :url
attr_reader :vote_difference # getter
attr_writer :vote_difference # setter
validates :title, presence: true
validates :url, presence: true
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :post, :counter_cache => true
#more class methods here
def after_save
self.update_counter_cache
end
def after_destroy
self.update_counter_cache
end
def update_counter_cache
post.vote_difference = post.comments.where(direction: 'up').count - post.comments.where(direction: 'down').count
post.save
end
end
Now you can sort by vote_difference when you query up.
for example -
posts = Post.order(:vote_difference, :desc)
I haven't check the correctness of my code yes. If you find any issues please let me know. I am sure it can be adapted to make it works.
If you follow this pattern to use counter_cache you might will to run a migration to add a vote_difference column, and another migration to update the vote_difference column for previous created post.

Sort by latest created comment

What i have created is a "active" field in my topics table which i can use to display the active topics, which will contain at first the time the topic was created and when someone comments it will use the comment.created_at time and put it in the active field in the topics table, like any other forum system.
I found i similar question here
How to order by the date of the last comment and sort by last created otherwise?
But it wont work for me, im not sure why it wouldn't. And i also don't understand if i need to use counter_cache in this case or not. Im using a polymorphic association for my comments, so therefore im not sure how i would use counter_cache. It works fine in my topic table to copy the created_at time to the active field. But it wont work when i create a comment.
Error:
NoMethodError in CommentsController#create
undefined method `topic' for
Topic.rb
class Topic < ActiveRecord::Base
attr_accessible :body, :forum_id, :title
before_create :init_sort_column
belongs_to :user
belongs_to :forum
validates :forum_id, :body, :title, presence: true
has_many :comments, :as => :commentable
default_scope order: 'topics.created_at DESC'
private
def init_sort_column
self.active = self.created_at || Time.now
end
end
Comment.rb
class Comment < ActiveRecord::Base
attr_accessible :body, :commentable_id, :commentable_type, :user_id
belongs_to :user
belongs_to :commentable, :polymorphic => true
before_create :update_parent_sort_column
private
def update_parent_sort_column
self.topic.active = self.created_at if self.topic
end
end
Didn't realise you were using a polymorphic association. Use the following:
def update_parent_sort_column
commentable.active = created_at if commentable.is_a?(Topic)
commentable.save!
end
Should do the trick.

Rails 3: validates_presence_of validation errors on default value and in associated model

I have a basic invoice setup with models: Invoice, Item, LineItems.
# invoice.rb
class Invoice < ActiveRecord::Base
has_many :line_items, :dependent => :destroy
validates_presence_of :status
before_save :default_values
def default_values
self.status = 'sent' unless self.status
end
end
# item.rb
class Item < ActiveRecord::Base
has_many :line_items
validates_presence_of :name, :price
end
# line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :item
belongs_to :invoice
before_save :default_values
validates_presence_of :invoice_id
validates :item_id, :presence => true
end
There is more in the model but I only presented the above for simplicity.
I get the following errors:
2 errors prohibited this invoice from being saved:
Line items invoice can't be blank
Status can't be blank
So two problems:
If I remove validates :invoice_id, :presence => true I don't get the Line items invoice can't be blank error message anymore, but why? I do want to validate the invoice_id on line_items, ALL line_items are supposed to have an invoice_id. How can I validate the invoice_id on line_items without getting an error?
Why am I getting the Status can't be blank error if I set it as a default value? I can probably set it up on the invoices_controller but I think the default value should be set in the model, right? How can I validate the presence of status and still have a default value in the model for it?
Both of these validation errors are occurring because validations get called before save (and before the before_save callback).
I'm assuming that you're using a nested_form to create the invoice and it's line items at the same time. If this is the case, you don't want to validates :invoice_id, :presence => true on the line items - the invoice and the line items are coming in at the same time, and the invoice hasn't been saved yet, so it doesn't have an id. If you leave the validation in, you'll need to create and save an empty invoice first, and then create the line items later so the invoice_id is available. If you only want to make sure invoice_id is still set after any edits, you can enforce this via validates :invoice_id, :presence => true, :on => :update this will skip the validation when the line item is being created (and the invoice_id isn't available yet).
You're running into problems with validates :status, :presence => true for similar reasons - the values coming in via the request are being validated against, and the "status" value isn't there. The before_save callback runs after validation. You can set the default value in the before_validation or after_initialization callback and the values will be there when validations are run.
Check out the Callbacks documentation for Rails for more info.
I'll start with 2:
before save is being executed only before save, meaning, after the object passed validation and is about to be saved. If the validation fails - it won't be executed.
as for 1:
Can you give an example of how you're trying to create an invoice?
Problem 1
Try validates_associated which checks that the associated models are all valid
Problem 2
Like most of the answers say before_save gets called after validations. The magic you're looking for is after_initialize which gets run after an object's initialize method is called.
class Invoice < ActiveRecord::Base
after_initialize :default_values
validates :status, presence: true
private
def default_values
self.status ||= 'sent'
end
end

Resources