Rails validate model which receive values using accepts_nested_attributes_for - ruby-on-rails

I have 2 models: Dealer & Location.
class Dealer < AR::Base
has_many :locations
accepts_nested_attributes_for :locations
validate :should_has_one_default_location
private
def should_has_one_default_location
if locations.where(default: true).count != 0
errors.add(:base, "Should has exactly one default location")
end
end
end
class Location < AR::Base
# boolean attribute :default
belongs_to :dealer
end
As you understood, should_has_one_location adds error everytime, because .where(default: true) makes an sql query. How can I avoid this behaviour?
The very dirty solution is to use combination of inverse_of and select instead of where, but it seems very dirty. Any ideas?

I actually got an answer to a similar question of my own. For whatever it's worth, If you wanted to do a validation like you have above (but without the db query), you would do the following:
errors.add(:base, ""Should have exactly one default location") unless locations.any?{|location| location.default == 'true'}

Related

Rails: querying associations during validation

During validation, I want to query associations but neither solution seems to be good because ActiveRecord’s style of validation. Here is an example:
class User < ApplicationRecord
has_many :borrowed_books
validate :should_only_borrow_good_books
def should_only_borrow_good_books
# What I want but it does not work:
#
# unless borrowed_books.where(condition: "bad").empty?
# errors.add(:borrowed_books, "only good books can be borrowed")
# end
#
# ^ this query always returns an empty array
# This approach works but it's not ideal:
unless borrowed_books.all? { |b| b.condition == "good" }
errors.add(:borrowed_books, "only good books can be borrowed")
end
end
end
class BorrowedBook < ApplicationRecord
belongs_to :user
# attr: condition - ["bad", "good"]
end
One more option is to move the validation to BorrowedBook with something like validates :condition, inclusion: { in: %w(good) }, if: -> { user_id.present? } and perhaps validate association in User like validates_associated :borrowed_books. But I don't like this approach because it complicates things by moving the logic belonging to User to BorrowedBook. A few validations like this and your app might become really messy.
This validation should definitely stay in the user model. I do disagree that it will look messy if there are multiple conditions. If it makes messy, it often indicate that you should split the model or refactor the code there. It's the models job to enable you to access and validate the data from db. A way to improve the current code is to convert the condition column to enum, ref: https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Enum.html you can write it like this
class User < ApplicationRecord
has_many :borrowed_books
validate :should_only_borrow_good_books
private
def should_only_borrow_good_books
return unless books.not_good.any?
errors.add(:borrowed_books, "only good books can be borrowed")
end
end
end
class BorrowedBook < ApplicationRecord
belongs_to :user
enum status: [ :good, :bad ]
end

Custom belongs_to attribute writer

An application I'm working on, is trying to use the concept of polymorphism without using polymorphism.
class User
has_many :notes
end
class Customer
has_many :notes
end
class Note
belongs_to :user
belongs_to :customer
end
Inherently we have two columns on notes: user_id and customer_id, now the bad thing here is it's possible for a note to now have a customer_id and a user_id at the same time, which I don't want.
I know a simple/better approach out of this is to make the notes table polymorphic, but there are some restrictions, preventing me from doing that right now.
I'd like to know if there are some custom ways of overriding these associations to ensure that when one is assigned, the other is unassigned.
Here are the ones I've tried:
def user_id=(id)
super
write_attribute('customer_id', nil)
end
def customer_id=(id)
super
write_attribute('user_id', nil)
end
This doesn't work when using:
note.customer=customer or
note.update(customer: customer)
but works when using:
note.update(customer_id: 12)
I basically need one that would work for both cases, without having to write 4 methods:
def user_id=(id)
end
def customer_id=(id)
end
def customer=(id)
end
def user=(id)
end
I would rather use ActiveRecord callbacks to achieve such results.
class Note
belongs_to :user
belongs_to :customer
before_save :correct_assignment
# ... your code ...
private
def correct_assignment
if user_changed?
self.customer = nil
elsif customer_changed?
self.user = nil
end
end
end

Rails finding model in other model's method

I would like to implement a server side validation for the following:
Subscriptions for a certain posting can only be created as long as the number of subscriptions doesn't exceed the number of spots for this posting.
class Cposting < ActiveRecord::Base
belongs_to :user
has_many :subscriptions,
foreign_key: "post_id",
dependent: :destroy
...
def spots_left #returns the number of places left in this class
self.spots.to_i - Subscription.where(post_id: self.id).count.to_i
end
...
end
In the Subscription model I tried to call the spots_left method to determine whether there are any spots left for the Cposting a new subscription belongs to.
class Subscription < ActiveRecord::Base
belongs_to :subscriber, class_name: "User"
belongs_to :post, class_name: "Cposting"
...
validate :class_not_full
def class_not_full
Cposting.find_by(id: self.post_id).spots_left > 0
end
end
Running tests on the Subscription model returned a nil error
NoMethodError: undefined method `spots_left' for nil:NilClass
It seems I can not use find_by, find or where methods to point to this Cposting.
I would like to know how to refer to the Cposting that belongs to the Subscription being validated, or an alternative way to implement this validation.
Thanks
EDIT adding tests
require 'test_helper'
class SubscriptionTest < ActiveSupport::TestCase
def setup
#cposting = cpostings(:one) #has one spot
#customer = users(:customer)
#customer2 = users(:customer2)
#subscription = Subscription.new(post_id: #cposting.id, subscriber_id: #customer.id)
end
...
test "subscriptions cannot exceed spots" do
#subscription.save
assert #cposting.subscriptions.count == #cposting.spots
#subscription2 = Subscription.new(post_id: #cposting.id, subscriber_id: #customer2.id)
assert_not #subscription2.valid?
end
end
Running rake test TEST=test/models/subscription_test.rb gives
1) Failure:
SubscriptionTest#test_subscriptions_cannot_exceed_spots [/~/test/models/subscription_test.rb:37]:
Expected true to be nil or false
5 runs, 7 assertions, 1 failures, 0 errors, 0 skips
EDIT 2 adding create method
class SubscriptionsController < ApplicationController
def create
#posting = Cposting.find(params[:post_id])
current_user.subscriptions.create(post_id: #posting.id)
flash[:success] = "Subscribed!"
redirect_to subscriptions_path
end
end
Make use of the rails relations. You dont need to query everything again.
Try the following:
class Cposting < ActiveRecord::Base
belongs_to :user
has_many :subscriptions,
foreign_key: "post_id",
dependent: :destroy
def spots_left
self.spots - self.subscriptions.count # i assume that spots is an integer db field
end
end
and
class Subscription < ActiveRecord::Base
belongs_to :subscriber, class_name: "User"
belongs_to :post, class_name: "Cposting"
validate :class_not_full
def class_not_full
post.spots_left > 0
end
end
And the creation of a subscription:
#cposting.build_subscription(subscriber: #customer2)
Rails offers you a bunch of methods to choose from. You don't even need to work with the ids. Just use the relations. In general I discovered Rails to work so much smoother when you stick to the AR methods (it would even be a good idea to stick to the rails conventions when naming your tables)
Please read this carefully, it'll help you a lot.
The nil error was fixed by adding a nil check and an error if the test didn't pass.
validate :class_not_full
def class_not_full #checks if there are spots left for the posting
errors.add(:post, "posting is already full") unless !post.nil? && post.spots > post.subscriptions.count
end

rails - How to know if dependents exist

class Country < ActiveRecord::Base
with_options dependent: :restrict_with_error do
has_many :airports
has_many :owners
has_many :users
end
def deletable?
[Airport, Owner, User].none? { |m| m.exists?(country_id: self.id) }
end
end
I need a deletable field so that the UI knows whether or not to display a delete button.
Is there a way I can use the callback made with the dependent: :restirct_with_error? Or at least get a list of the associations with the dependency restriction?
The deletable? method seems redundant since the information about dependents is already expressed above. Also, I'm doing this for multiple models so it would be nice if the code can be shared.
I'm thinking of a hack right now that involves calling destroy then rolling back.
Honestly, I think your code is just fine.
If you still want more generalized approach, you can extend ActiveRecord (modified example from here):
module DestroyableRecord
def can_destroy?
self.class.reflect_on_all_associations.all? do |assoc|
assoc.options[:dependent] != :restrict ||
(assoc.macro == :has_one && self.send(assoc.name).nil?) ||
(assoc.macro == :has_many && self.send(assoc.name).empty?)
end
end
end
class Country < ActiveRecord::Base
include DestroyrableRecord
# ...
end
country = Country.firist
country.can_destroy?

validate the presence of at least one association object in rails 3.2

I've one small problem, I can't get solved. I want to validate that there is at least one associated model. Like in the following
class User < ActiveRecord::Base
has_many :things
validates_presence_of :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
This works fine when I update my model via #update_attributes, but when I simply set #user.things = [], I am able to get invalid data in the database. My workaroud to solve this is to overwrite the setter method
def things=(val)
begin
if val.blank?
errors.add(:things, "not valid")
raise SomeError
end
super
rescue SomeError
false
end
end
But somehow this doesn't feel right. Isn't there a way to archive the same result via validations and/or callbacks, preferably so that #things= return false (and not val) and so that #user.things is not changed (I mean the cached #user.things, #user.things(true) should work fine anyway).
You can create a custom validator that will check the presence of things.
Instead of
validates_presence_of :things
You could do
validate :user_has_things
def user_has_things
if self.things.size == 0
errors.add("user has no thingies")
end
end

Resources