Unique children in has_many validation in rails 4 - ruby-on-rails

I'm using the Google places API to collect any number of locations on a profile. Each location has the profile_id, 4 address fields and a lat and long. I want to make sure that all locations for each profile are unique.
At the moment I'm using the code below to validate the uniqueness of the 4 address fields which is working perfectly however this results in a validation error being returned to the view. I would rather save the profile and locations (with duplicates removed) without returning an error to the user.
What is the best way of going about this? Is there a rails approach or should I create my own before-validation function?
class Profile < ActiveRecord::Base
has_many :locations
accepts_nested_attributes_for :locations, allow_destroy: true
etc etc
end
class Location < ActiveRecord::Base
belongs_to :profile
# Only allow unique locations for each profile
validates_uniqueness_of :profile_id, scope: [:sublocality, :locality, :administrative_area_level_1, :country]
etc etc
end

You can check for error[:profile_id].present? in after_validation callback.
If error related to profile_id is present - remove duplicates and save again
You should be careful if you have other validations related to profile_id and check for presence of specific error message in that case.

Related

Rails has_many associations are not saved when only attributes are changed

I have two models, Profile and Skill, where a profile has_many skills.
I am using Rails as an API and passing an array of skill objects.
I'm doing #profile.skills = #skills in the controller, where #skills is my data from the frontend.
Whenever I delete or add a new skill, the above works as expected - also, #profile.skills.replace(#skills) works just the same.
The associated object gets deleted or created in the database as well. All as expected.
However, if I only change one or more attributes on an already existing skill, the changes are not saved to the database.
If I log #profile.skills after the above line of code, it seems like the changes expected are present.
But it does not get saved to the database and on the next request the changes are obviously not present.
What am I doing wrong?
Have you tried to pass like this:
class Profile < ApplicationRecord
has_many :skills
accepts_nested_attributes_for :skills
end
or
class Profile < ApplicationRecord
has_many :skills, autosave: true
end
after that, edit the same attribute of the same record and save
#profile.skills.same.same_attr = new_val
#profile.save
accepts_nested_attributes_for - it also adds autosave to asociations, and a lot of other things, you can see more in the documentation and source code
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
a little bit about how autosave works for associations
https://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html

Skip saving associated object if invalid on update

I have a person table and address table like this:
class Person < ApplicationRecord
has_many :addresses, dependent: :destroy
accepts_nested_attributes_for :addresses
end
class Address < ApplicationRecord
belongs_to :person
validates :address_line_1, presence: true
end
In my controller, I want to update the person and the associated addresses, but if the address is invalid and the person is valid, I'd still want to update the Person object and keep the invalid address the same as before without running into a ROLLBACK.
What's the best way to handle this? I realize I can do some logic to check if the address is invalid and remove the addresses_attributes from the parameters, then assign the parameters again and save it, but is there any better way?
has_many association has a validate option you can set to false and handle validations however you want https://guides.rubyonrails.org/association_basics.html#options-for-has-many-validate
I think you are using accepts_nested_attributes_for since you named the addresses_attributes param, personally I wouldn't combine no validation with that, you may end up with invalid addresses.
Personally, I would do two step (with the default validate: true config):
first update only the user's attributes
call save on the user (so addresses doesn't mess the update up)
set the addresses attributes
call save on the user (so everything gets validated again)
EDIT: if you want to use the validate: false option you may want to set autosave: false too so you don't save invalid addresses https://guides.rubyonrails.org/association_basics.html#options-for-has-many-autosave
Ultimately you'll either want to check that the address attributes are valid and remove them if not OR saved the records separately.
A common pattern is to opt for second option using a form object. This helps keep the logic out of the controller and makes it easier to expand on updating a Person in the future.

accepts_nested_attributes_for has_many :through Create and delete join model objects, depending on other model

Here are my models
class User < ApplicationRecord
has_many :user_roles, dependent: :destroy
has_many :projects, through: :user_roles
has_secure_password
end
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :project
accepts_nested_attributes_for :user
end
class Project < ApplicationRecord
has_many :user_roles, dependent: :destroy
has_many :users, through: :user_roles
accepts_nested_attributes_for :user_roles
end
Currently I'm trying to figure out a proper way for creating multiple UserRole entries in Project creation form.
UserRole stores user_id, project_id and role.
The actual problem: form input for UserRole is User's email, i.e. I should find User with such email and if such User exists retrieve his id and save it in UserRole.user_id.
It works just well in console, but not with forms.
I tried to use accepts_nested_attributes_for and nested fields in forms
<%= form_for #project do |f| %>
# project fields
<%= f.fields_for :user_roles do |role_f| %>
# user role fields
<%= role_f.fields_for :user do |user_f| %>
# user fields
<% end %>
<% end %>
<% end %>
With this form I can easily modify any entries. But can't add or delete properly. Also, this form allows to modify User and that's troublesome.
I'm searching for a way to only create and delete Project's UserRoles while keeping User safe.
I tried to make a virtual attribute - email for UserRole that would be a link to User's actual email attribute, but failed.
The whole situation looks as if I am using wrong approach to this problem. Please, advice.
First of all I don't think you can have accepts_nested_attributes for :user in UserRoles, since UserRole is what is nested into User and not the other way round. From what you are describing you don't seem to need that anyway though, except if there are other cases in your business logic, apart from this form.
I would highly suggest you watch this railscast on nested forms. It shows a way to dynamically add and remove nested fields (add or remove a UserRole set of fields)
http://railscasts.com/episodes/197-nested-model-form-part-2?autoplay=true
However the user email logic would probably have to be extracted into something else. If I were you I would create an ajax call in the email input field. That call would send the email to the server, the server would try to find any matching users and if it finds some it will return their id-email pairs. Then you display the returned data (email) as a select option in a dropdown. When the user clicks the valid email that was fetched, the form sets the id as the value for that field (user_id field inside user_roles). If no user was found, you return a string that says no user was found with that email or something like that.
If you don't want to fetch the ids, you would have to extract the emails from the params hash in the controller. For each email try to find a user and then you would have to find a way to manage the errors for each one separately in case the user was not found. The first option would give a better user experience for sure.

Factory Girl with polymorphic association for has_many and has_one

I am currently working on a project and I wanted to create tests using factory girl but I'm unable to make it work with polymorphic has_many association. I've tried many different possibilities mentioned in other articles but it still doesn't work. My model looks like this:
class Restaurant < ActiveRecord::Base
has_one :address, as: :addressable, dependent: :destroy
has_many :contacts, as: :contactable, dependent: :destroy
accepts_nested_attributes_for :contacts, allow_destroy: true
accepts_nested_attributes_for :address, allow_destroy: true
validates :name, presence: true
#validates :address, presence: true
#validates :contacts, presence: true
end
class Address < ActiveRecord::Base
belongs_to :addressable, polymorphic: true
# other unimportant validations, address is created valid, the problem is not here
end
class Contact < ActiveRecord::Base
belongs_to :contactable, polymorphic: true
# validations ommitted, contacts are created valid
end
So bassically I want to create factory for Restaurant with address and contacts (with validations on Restaurant for presence, but if it's not possible, even without them) but I'm unable to do so. Final syntax should be like:
let(:restaurant) { FactoryGirl.create(:restaurant) }
Which should also create associated address and contacts. I have read many articles but I always get some sort of error. Currently my factories (sequences are defined correctly) are like this:
factory :restaurant do
name
# address {FactoryGirl.create(:address, addressable: aaa)}
# contacts {FactoryGirl.create_list(:contact,4, contactable: aaa)}
# Validations are off, so this callback is possible
after(:create) do |rest|
# Rest has ID here
rest.address = create(:restaurant_address, addressable: rest)
rest.contacts = create_list(:contact,4, contactable: rest)
end
end
factory :restaurant_address, class: Address do
# other attributes filled from sequences...
association :addressable, factory: :restaurant
# addressable factory: restaurant
# association(:restaurant)
end
factory :contact do
contact_type
value
association :contactable, :factory => :restaurant
end
Is there a way to create restaurant with one command in test with addresses and contacts set? Do I really need to get rid off my validations because of after(:create) callback?
Current state is as fllowing:
Restaurant is created with name and id.
Than the address is being created - all is correcct, it has all the values including addressable_id and addressable_type
After that all contacts are being creaed, again everything is fine, cntacts has the right values.
After that, restaurant doesn't have any ids from associated objects, no association to adddress or contacts
After than, for some reason restaurant is build again (maybe to add those associations?) and it fails: I get ActiveRecord:RecordInvalid.
I'm using factory_girl_rails 4.3.0, ruby 1.9.3 and rails 4.0.1. I will be glad for any help.
UPDATE 1:
Just for clarification of my goal, I want to be able to create restaurant in my spec using one command and be able to access associated address and contacts, which should be created upon creation of restaurant. Let's ignore all validations (I had them commented out in my example from the beginning). When I use after(:build), first restaurant is created, than address is created with restaurant's ID as addressable_id and class name as addressable_type. Same goes to contacts, all are correct. The problem is, that restaurant doesn't know about them (it has no IDs of address or contacts), I can't access them from restaurant which I want to.
After really thorough search I have found an answer here - stackoverflow question.This answer also point to this gist. The main thing is to build associations in after(:build) callback and then save them in after(:create) callback. So it looks like this:
factory :restaurant do
name
trait :confirmed do
state 1
end
after(:build) do |restaurant|
restaurant.address = build(:restaurant_address, addressable: restaurant)
restaurant.contacts = build_list(:contact,4, contactable: restaurant)
end
after(:create) do |restaurant|
restaurant.contacts.each { |contact| contact.save! }
restaurant.address.save!
end
end
I had also a bug in my rspec, because I was using before(:each) callback instead of before(:all). I hope that this solution helps someone.
The Problem
Validating the length of a related list of rows is a difficult problem to frame in SQL, so it's a difficult problem to frame in ActiveRecord as well.
If you're storing a restaurant foreign key on the addresses table, you can't ever actually create a restaurant that has addresses by the time it's saved, because you need to save the restaurant to get its primary key. You can get around this problem in ActiveRecord by building up the associated objects in memory, validating against those, and then committing the entire object graph in one SQL transaction.
How to do what you're asking
You can generally get around this by moving things into an after(:build) hook instead of after(:create). ActiveRecord will save its dependent has_one and has_many associations once it saves itself.
You're getting errors now because you can't modify an object to satisfy validations in an after(:create) block, because validations have already run by the time the callback runs.
You can change your restaurant factory to look something like this:
factory :restaurant do
name
after(:build) do |restaurant|
restaurant.address = build(:restaurant_address, addressable: nil)
restaurant.contacts = build(:contact, 4, contactable: nil)
end
end
The nils there are to break the cyclic relationship between the factories. If you do it this way, you can't have a validation on the addressable_id or contactable_id keys, because they won't be available until the restaurant is saved.
Alternatives
Although you can get both ActiveRecord and FactoryGirl to do what you're asking, it sets up a precarious list of dependencies which are difficult to understand and are likely to result in leaky validations or unexpected errors like the ones you're seeing now.
If you're validating contacts this way from the restaurant model because of a form in which you create both a restaurant and its corresponding contacts, you can save yourself a lot of pain by creating a new ActiveModel object to represent that form. You can collect the attributes you need for each object there, move some of the validations (especially the ones which validate the length of the contacts list), and then create the object graph on that form in a way that's much clearer and less likely to break.
This has the added benefit of making it easy to create lightweight restaurant objects in other tests which don't need to worry about contacts or addresses. If you force your factories to create these dependent objects every time, you'll quickly run into two problems:
Your tests will be painfully slow. Creating five dependent records every time you want to work with a restaurant won't scale very far.
If you ever want to specify different contacts or addresses in your tests, you'll constantly be fighting with your factories.

Removing redundant queries using Active Record

The following code works, but runs far too many SQL statements for my liking;
User.includes([:profile,:roles]).select("id, email, created_at, confirmed_at, countries.name").find_in_batches do |users|
users.map{|u| # In here I access profile.country.name}
end
What is meant to do is grab the User information along with some profile information and put it into a csv format.
Inside the .map I am calling profile.country.name this is coming in fine but Rails is running an extra SQL call to find the name, what I want to do is capture this information in my initial lookup.
My issue is that because country is linked to profile and not user I can't add it to my .includes because it doesn't see it as a linkable table. Is there anything I can do to force the includes to join a table linked to profile?
The relevant model information is below;
User:
has_one :profile, dependent: :destroy
Profile:
belongs_to :user
belongs_to :country
Country:
has_many :profiles
You should be able to include country in the original query by nesting it under profile in the .includes call:
User.includes( {:profile => [:country], :roles} )

Resources