In my Rails 5.2 app, I have a polymorphic model Vehicle of types Car, Bike, Jeep etc. which has belongs_to association vehicle_type. I would like to validate associated record attribute display_name. The following code snippet does the job but I would like to know a better way to do this.
class Car < Vehicle
validates :vehicle_type,
:inclusion => {
:in => [VehicleType.find_by(display_name: 'four wheeler')],
:message => "A Car can only be of vehicle_type 'four wheeler'",
}
}
You should put the validation on the id rather than the display name since you would have to refactor your code if you ever decide to change the display name.
class VehiculeType
FOUR_WHEELER = 1 (id of the four_wheeler type)
end
class Car < Vehicule
validate :validate_vehicule_type
private
def validate_vehicule_type
errors.add(:vehicule, "A Car can only be of vehicle_type 'four wheeler'") unless vehicule_type_id == VehiculeType::FOUR_WHEELER
end
end
I don't know what is the best way, but I'll share what i have done in one of my projects:
I decided to extend ActiveModel::Validator and create my own validation for my polymorphic associations
In you case
class CarValidator < ActiveModel::Validator
def validate_vehicle_type(record)
# where did you save the veicle type associatuon?
unless VehicleType.find_by(display_name: record.veicle_type).exists?
record.errors.add :veicle_type, "This veicle type does not exist"
end
end
then validates with CarValidator
I agree with Mathieu Larouche. One small thing I would add to this discussion is that this is not really a polymorphic association, as polymorphic associations are about how "a model can belong to more than one other model, on a single association". This is done via a combination of type and id fields (imageable_id and imageable_type, for example). See docs here.
It doesn't really affect the response your question, but I just wanted to mention it because polymorphic associations took me forever to wrap my head around, and I thought calling out the distinction could be helpful.
Related
So im relatively new to factory bot, and im pulling in some modals on some older php code into rails. And I seem to be running into a problem with one specific factory creation.
Right now here is my basic factories.rb file:
FactoryBot.define do
factory :"Core/User" do
username {"jDoe"}
end
factory :"Core/Sex" do
title {"Unspecified"}
abbreviation {"U"}
end
factory :"Core/Contact" do
first_name {"John"}
last_name {"Doe"}
display_phone_mobile {false}
internal {false}
archive {false}
"Core/Sex"
end
factory :"Core/Employee" do
"Core/User"
"Core/Contact"
username {"jDoe"}
end
end
Pretty basic right now, as the schema is sort of a tangled mess. Anyways, for whatever reason everything works until I get to trying to create an "Employee" (Sidenote: I had to add Core:: to everything and had to scour SO to find out how to add that to the symbols, since they are namespaced I guess? (I know that I need to use Core::<Model> to access the models in rails fwiw)
Anyways the models are relatively complex, but the important parts are here:
Contact.rb:
class Core::Contact < Core::BaseModel
self.primary_key = 'id'
has_one :employee
belongs_to :sex
User.rb:
class Core::User < Core::BaseModel
extend Core::ActiveDirectory
self.primary_key = 'id'
has_one :employee
Employee.rb:
class Core::Employee < Core::BaseModel
include ActionView::Helpers::DateHelper
self.primary_key = 'id'
belongs_to :contact
There are tons of other dependencies to tackle...but for whatever reason the associations don't seem to pull in the contact_id when making an employee. As in it specifically complains about TinyTds::Error: Cannot insert the value NULL into column 'contact_id'
Thing is, ALL the others work just fine. IE: if I make a Contact it pulls in the "Core/Sex" fine and I manually create the "Contact" factory and specifically pull in the ID like so:
#contact = create(:"Core/Contact")
puts #contact.attributes
#employee = create(:"Core/Employee", contact_id: #contact.id)
It works!, but I dont know why the other associations get pulled in just fine? Any ideas?
you don't need that; You just refer to the factories as a one-word thing than then the Factory constructor has a class: option on it
just move all of your factories over to standard names
factory :employee, class: "Core::Employee" do
user { create(:user) }
contact { create (:contact) }
username {"jDoe"}
end
when you refer to them as factories just use the short names with symbols and let the class: option do the rest.
I am developing a Rails 5 application in which I encountered the following difficulty.
I've got two models, let's say Kid and Toy, which are in one-to-one relationship like this:
class Kid < ActiveRecord::Base
has_one :toy
end
class Toy
belongs_to :kid, optional: true
end
So the toys can belong to zero or one kid, and from day to day it can change - it is always another kid's responsibility to look after a certain toy. Now, when I edit a toy, changing its kid is easy as can be: I just send kid_id in the strong params to update the record:
params.require(:toy).permit(:name, :type, :kid_id)
But recently, I was asked to implement the changing feature from the other way too, that is, when editing a kid, I should do something like this:
params.require(:kid).permit(:name, :age, :toy_id)
The problem is that - while belongs_to works with association_id and even has_many provides association_ids getter and setter - has_one relationship has nothing like this. What is more, has_one association gets saved the moment I call association = #record. So I simply cannot set it by sending the toy_id in the strong parameters.
I could do something like #kid.update(kid_params); #kid.toy = #toy on the controller level, but that would rather bring model logics to my controller, not to mention that I want to check if the newly assigned toy did not belong to another kid, which I imagine as some kind of validation.
The best I could come up with was to define some rails-like methods for Kid class like
def toy_id
#toy_id = toy.id unless defined?(#toy_id)
#toy_id
end
def toy_id_changed?
toy_id != toy.id
end
and set a validation and a before_commit callback
validate if: -> { toy_id.present? && toy_id_changed? } do
errors.add :toy_id, :other_has_it if new_toy.kid_id.present? && new_toy.kid_id != id
end
before_commit if: -> { toy_id_changed? } do
toy = new_toy
end
private
def new_toy
#new_toy ||= Toy.find(toy_id)
end
So far it works as expected, and now I can send toy_id in the strong params list to update a kid, and it updates the toy association if -
and only if - there is no validation error. I have even put it in a concern to be nice and separated.
My question is: isn't there a rails way to do this? haven't I reinvented the wheel?
Thanks in advance!
I'm looking for some best-practice advice for the following situation.
I have the following skeleton ActiveRecord models:
# user.rb
class User < ActiveRecord::Base
has_many :country_entries, dependent: destroy
end
# country_entry.rb
class CountryEntry < ActiveRecord::Base
belongs_to :user
validates :code, presence: true
end
Now suppose I need to get a comma-separated list of CountryEntry codes for a particular user. The question is, where do I put this method? There are two options:
# user.rb
#...
def country_codes
self.country_entries.map(&:code)
end
#...
-or-
# country_entry.rb
#...
def self.codes_for_user(user)
where(user_id: user.id).map(&:code)
end
#...
And so the APIs would be: #current_user.country_codes -or- CountryEntry.codes_for_user(#current_user)
Seems like placing the code in country_entry.rb decouples everything a little more, but it makes the API a little uglier. Any general or personal-experience best practices on this issue?
Instance method VS Class method: If the method is for an instance, of course it is better to be an instance method.
In user model VS in Coutry model: User model wins. Law of Demeter suggests one dot only in Ruby. If you have chance to do that, of course it's better to follow.
Conclusion: Your first method wins.
# user.rb
def country_codes
self.country_entries.map(&:code)
end
Add: Reference for Law of Demeter
http://en.wikipedia.org/wiki/Law_of_Demeter
http://rails-bestpractices.com/posts/15-the-law-of-demeter
http://devblog.avdi.org/2011/07/05/demeter-its-not-just-a-good-idea-its-the-law/
Now this is really an interesting question. And it has so many answers ;-)
From your initial question I would suggest you put the code in the association itself
class User < ActiveRecord::Base
has_many :country_entries do
def codes
proxy_association.owner.country_entries.map(&:code)
end
end
end
so you could do something like this
list_of_codes = a_user.country_entries.codes
Now obviously this is a violation of the Law of Demeter.
So you would best be advised to offer a method on the User object like this
class User < ActiveRecord::Base
has_many :country_entries do
def codes
proxy_association.owner.country_entries.map(&:code)
end
end
def country_codes
self.country_entries.codes
end
end
Obviously nobody in the Rails world cares about the Law of Demeter so take this with a grain of salt.
As for putting the code into the CountryEntry class I am not sure why you would do this. If you can look up country codes only with the user I dont see the need to create a class method. You are anyway only able to look that list up if you have a User at hand.
If however many different objects can have a country_entries association than it makes sense to put it as a class method into CountryEntry.
My favorite would be a combination of LOD and a class method for reuse purposes.
class User < ActiveRecord::Base
has_many :country_entries
def country_codes
CountryEntry.codes_for_user(self)
end
end
class CountryEntry < ActiveRecord::Base
belongs_to :user
validates :code, presence: true
def self.codes_for_user(some_id)
where(ref_id: some_id).map(&:code)
end
end
In terms of API developers get from the two proposals, adding to the user model seems pretty straightforward. Given the problem:
Now suppose I need to get a comma-separated list of CountryEntry codes for a particular user.
The context is made of a user, for which we want to get the code list. The natural "entry point" seems a user object.
Another way to see the problem is in terms of responsibilities (thus linking to #robkuz entry on Demeter's). A CountryEntry instance is responsible for providing its code (and maybe a few other things). A CountryEntry class is basically responsible for providing attributes and methods common to all its instances, and no more (well). Getting the list of comma-separated codes is a specialized usage of CountryEntry instances that only User objects care of apparently. In this case, the responsibility belongs to the current user object. Value in the eye of the beholder...
This is inline with most answers on the thread, although in the solutions so far, you do not get a comma-separated list of codes, but an array of codes.
In terms of performance, note there is probably a difference too because of lazy evaluation. Just a note---someone more deeply familiar with ActiveRecord could comment on that!
I think #current_user.country_codes is a better choice in this case because it will be easier to use in your code.
I have an ActiveRecord model called Books which has a has_one association on authors and a has_many association on publishers. So the following code is all good
books.publishers
Now I have another AR model, digital_publishers which is similar but which I would like to transparently use if the book's author responds to digital? - let me explain with some code
normal_book = Book.find(1)
normal_book.author.digital? #=> false
normal_book.publishers #=> [Publisher1, Publisher2, ...]
digital_book = Book.find(2)
digital_book.digital? #=> true
digital_book.publishers #=> I want to use the DigitalPublishers class here
So if the book's author is digital (the author is set through a has_one :author association so it's not as simple as having a has_many with a SQL condition on the books table), I still want to be able to call .publishers on it, but have that return a list of DigitalPublishers, so I want some condition on my has_many association that first checks if the book is digital, and if it is, use the DigitalPublishers class instead of the Publishers class.
I tried using an after_find callback using the following code:
after_find :alias_digital_publisher
def alias_digital_publisher
if self.author.digital?
def publishers
return self.digital_publishers
end
end
end
But this didn't seem to do the trick. Also, I'm using Rails 2.3.
I need more information about the project to really make a recommendation, but here are some thoughts to consider:
1. Publishers shouldn't belong to books
I'm guessing a publisher may be linked to more than one book, so it doesn't make a lot of sense that they belong_to books. I would consider
#Book.rb
has_many :publishers, :through=>:publications
2. Store digital publishers in the publishers table
Either use Single Table Inheritance (STI) for digital publishers with a type column of DigitalPublisher, or just add a boolean indicating whether a publisher is digital.
This way you can just call book.publishers, and you would get publishers that may or may not be digital, depending on which were assigned.
The trick is that you would need to ensure that only digital publishers are assigned to books with a digital author. This makes sense to me though.
3. (alternatively) Add a method for publishers
def book_publishers
author.digital? ? digital_publishers : publshers
end
I'm not really a fan of this option, I think you're better off having all the publishers in one table.
Have a look at this section from Rails Guides v-2.3.11. In particular note the following:
The after_initialize and after_find callbacks are a bit different from the others. They have no before_* counterparts, and the only way to register them is by defining them as regular methods. If you try to register after_initialize or after_find using macro-style class methods, they will just be ignored.
Basically, try defining your after_find as
def after_find
...
end
If that doesn't work, it might be because the book's fields haven't been initialized, so try after_initialize instead.
My solution is very simple.
class Book < ActiveRecord::Base
has_many :normal_publishers, :class_name => 'Publisher'
has_many :digital_publishers, :class_name => 'DigitalPublisher'
def publishers
if self.digital?
self.digital_publishers
else
self.normal_publishers
end
end
end
You can still chain some methods, like book.publishers.count, book.publishers.find(...).
If you need book.publisher_ids, book.publisher_ids=, book.publishers=, you can define these methods like book.publishers.
The above code works on Rails 2.3.12.
UPDATE: Sorry, I noticed klochner's alternate solution after I had posted this.
I have a controller/model hypothetically named Pets. Pets has the following declarations:
belongs_to :owner
has_many :dogs
has_many :cats
Not the best example, but again, it demonstrates what I'm trying to solve. Now when a request comes in as an HTTP POST to http://127.0.0.1/pets, I want to create an instance of Pets. The restriction here is, if the user doesn't submit at least one dog or one cat, it should fail validation. It can have both, but it can't be missing both.
How does one handle this in Ruby on Rails? Dogs don't care if cats exists and the inverse is also true. Can anyone show some example code of what the Pets model would look like to ensure that one or the other exists, or fail otherwise? Remember that dogs and cats are not attributes of the Pets model. I'm not sure how to avoid Pets from being created if its children resources are not available though.
errors.add also takes an attribute, in this case, there is no particular attribute that's failing. It's almost a 'virtual' combination that's missing. Parameters could come in the form of cat_name="bob" and dog_name="stew", based on the attribute, I should be able to create a new cat or dog, but I need to know at least one of them exists.
You're looking for errors.add_to_base. This should do the trick:
class Pet < ActiveRecord::Base
belongs_to :owner
has_many :dogs
has_many :cats
validate :has_cats_or_dogs
def has_cats_or_dogs
if dogs.empty? and cats.empty?
errors.add_to_base("At least one dog or cat required")
end
end
end
If you want to pass cat_name or dog_name to the controller action, it may look like this:
class PetsController < ApplicationController
# ...
def create
#pet = Pet.new(params[:pet])
#pet.cats.build(:name => params[:cat_name]) if params[:cat_name]
#pet.dogs.build(:name => params[:dog_name]) if params[:dog_name]
if #pet.save
# success
else
# (validation) failure
end
end
end
Alternatively, for some more flexibility you can use nested attributes to create new cats and dogs in your controller.