Rails validation: Only N records can be true - ruby-on-rails

How would you write a validation that checks if a certain amount of records already have an attribute that is true?
class BlogPost
scope :onStartpage, -> { where(onStartpage: true) }
validate :number_on_startpage
def number_on_startpage
return unless onStartpage?
errors.add(:foobar, 'foo') if Blog::Post.where(onStartpage: true).count > 3
end
end
Like in the example above - I want to make sure that there are no more than three BlogPosts with the attribute onStartpage set to true.
But it's not working because when I want to save the record the count is still 2. And all later updates get rejected because the count is too high.

The problem is that your validation will be checked for previous blog posts, even if you just wanted to change the title for instance.
You could check your validation only if the attribute onStartPage has changed.
Also, you could change the condition to >=, as if you already have 3 articles with that attribute, 3 is not strictly superior to 3...so it will only return false after you have 4 articles with onStartpage set to true.
class BlogPost
scope :onStartpage, -> { where(onStartpage: true) }
validate :number_on_startpage
def number_on_startpage
return unless onStartpage_changed?
errors.add(:foobar, 'foo') if Blog::Post.where(onStartpage: true).count >= 3
end
end

Related

Toggling between status on model with Rails 4

The scenario
There is a table on my database called budgets. It has author_id, status, answered_at columns.
The goal
When status != 1, then author_id and answered_at should be nil/null.
The problem
I have the following method on my budgets_controller.rb:
def update
budget = Budget.find(params[:id])
budget.update_attributes(status: 1, author_id: current_user.id, answered_at: DateTime.now.to_date)
budget.save!
end
I want to know if is possible to reuse the same method (update) to change the author_id and the answered_at to null and the status itself to 0 if it is already 1. Some kind of toggle.
Knowledge
I saw Rails offers this method to toggling boolean values, but I can't see how it can suit my need since I'm working with two other columns which aren't booleans.
I think what you probably want is a model callback. In your instance, something like
# on models/budget.rb
before_update :nullify_author
def nullify_author
if status == 1
author = nil
answered_at = nil
status = 0
save
end
end
Also, you shouldn't use toggle here. Ruby's falsiness is WAY more restrictive than JavaScript's. In Ruby's case, only false and nil are falsy. Relevantly to you, 0 is not falsy but truthy. To prove it, try !0. The returned value is the boolean false not the FixNum 1

Using the Map function to remove a object from an activerecord::relation

I have a complicated scope where I'm grabbing a checklist. In this checklist there are a bunch of tasks under difference categories. I only want the completed checklists, but some of the checklists will be complete without every category being finished. So I need to be able to check if each column is needed before checking if the column has anything in it. Here's my example.
scope :complete, lambda {|check_lists| check_lists.map do |check_list|
not_complete = false
if check_list.event.booking.video_for_event?
if check_list.raw_footage_user_id.blank? && check_list.raw_footage_check.blank? then not_complete = true end
end
if check_list.event.booking.eblast_not_blank?
# more checking...
end
if check_list.event.booking.on_site_not_blank?
# more checking...
end
if not_complete then reject end
end } #If videos, verify video items. if eblasts, verify eblast items, etc...
So basically I need to know how to finish it off by removing non_complete objects from the array being mapped out.
If I understand clearly you want only the completed checklists
Basically you must return the checklist when it's true and nil when it's not, then eliminate the nils in resulting array with compact... which is the work of select
checklists.map do |checklist|
# ....
checklist unless not_completed
end.compact
or more concisely :
checklists.select do |checklist|
# ....
!not_completed
end

Rails unique column has duplicates

This is the stripped down version of my model .
model Paper
PAPER_STARTING_NUMBER = 1
validate_uniqueness_of :number, :allow_blank => true
before_create :alocate_paper_number
def alocate_paper_number
return true if self.number.present?
p_number = Paper.maximum('number') || Paper::PAPER_STARTING_NUMBER
self.number = p_number >= Paper::PAPER_STARTING_NUMBER ? p_number+1 : Paper::PAPER_STARTING_NUMBER
return true
end
end
the problem is I have duplicates in the number column .
Any ideas why and how I can fix this without changing the callback .
I know I could add a uniqueness validation on the database or make a sequence on that column , any other ideas ?
First you have to understand the order of callbacks :
(-) save
(-) valid
(1) before_validation
(-) validate
(2) after_validation
(3) before_save
(4) before_create
(-) create
(5) after_create
(6) after_save
(7) after_commit
So as you can see , it validates the uniquity of your number attribute, and then before_create can at its own disposal go against what your validation wants to accomplish.
In regards to a more cleaner architecture, I would put both of these ideas together in your custom model, as it doesn't seem that the number can be choosen by the User. It's just an incrementer, right?
def alocate_paper_number
p_number = Paper.maximum('number') || Paper::PAPER_STARTING_NUMBER
self.number = p_number + 1
end
That snippet alone, would prevent duplicates, as it always increments upwards ( unless, there's the possibility of the number going the other way that I'm not aware of ), and also there's no reason to return all those trues. Its true enough!
It is in de docs. validate_uniqueness_of TRIES to make it unique. But if two processes add one record at the same time, they both can contain the same number.
If you want to guarantee uniqueness, let the database do it. But because that is different for each DB, Rails does not support it by design.
It's explained here: http://guides.rubyonrails.org/active_record_validations_callbacks.html#uniqueness
With the solution: "To avoid that, you must create a unique index in your database."
How I fixed it ( bare in mind that I couldn't return a validation error )
I've added a uniquness index on the number column ( as mu and Hugo suggested )
and because I couldn't return a validation error in the controller
class PaperController < ApplicationController
def create
begin
#paper.save
rescue ActiveRecord::RecordNotUnique
#paper.number = nil
create
end
end
end

Rail3 'Return False Unless XYZ' Query Not Working

In my rails3.1 application, I'm trying to apply the following logic in one of my order model.
def digital?
line_items.map { |line_item| return false unless line_item.variant_id = '102586070' }
end
I've created a separate variant called prepaid_voucher which has id = 102586070. Despite this, the result is false...
Order has many line_items
LineItem belongs to order and variant
Variant has many line_items
Is this the best way to perform such a task and how can I fix?
First of all I think you want a double == here line_item.variant_id = '102586070', then I rather go for something like that (If I understand what you want)
def digital?
line_items.select{|line_item| line_item.variant_id == '102586070'}.any?
end
But it's hard to understand what you really want, what is the expected behavior if the id is not found?

Rails scope with HABTM relationship count

I have an Event class with a HABTM relationship with a User class. I'm trying to create a Event scope that includes only Events that have 2 Users associated with it.
I currently have a Event#status method that returns the following:
def status
self.users.length == 2 ? "matched" : "not matched"
end
So now basically I'm trying to find how to write a scope that includes all "matched" events. I tried scope :matched, self.users.length == 2, which didn't work at all, but is there a similar way that I'm missing?
EDIT: This class method does this correctly, but it'd still be nice if I could encapsulate it in a scope.
def self.pending
Event.all.map { |e| e if e.status == "matched" }
end
You've got a few problems here. Right now, your status method is returning literal strings, which is a bit surprising -- it would be more common to have this return a Boolean value. Also, the name status is not descriptive -- perhaps exactly_two_users? would be better. In addition, if you use users.count instead of users.length, then the DB will do the count more efficiently.
Your scope could simply be where(:users.count => 2), I believe.

Resources