Ruby on Rails Association build and assign 2 related associations - ruby-on-rails

So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.

From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id

In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.

My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly

Related

Best way to update (create & delete) multiple associations (has_many :through)

I have what i feel could be a simple question, and i have this working, but my solution doesn't feel like the "Rails" way of doing this. I'm hoping for some insight on if there is a more acceptable way to achieve these results, rather than the way i would currently approach this, which feels kind of ugly.
So, lets say i have a simple has_many :through setup.
# Catalog.rb
class Catalog < ActiveRecord::Base
has_many :catalog_products
has_many :products, through: :catalog_products
end
# Products.rb
class Product < ActiveRecord::Base
has_many :catalog_products
has_many :catalogs, through: :catalog_products
end
# CatalogProduct.rb
class CatalogProduct < ActiveRecord::Base
belongs_to :catalog
belongs_to :product
end
The data of Catalog and the data of Product should be considered independent of each other except for the fact that they are being associated to each other.
Now, let's say that for Catalog, i have a form with a list of all Products, in say a multi-check form on the front end, and i need to be able to check/uncheck which products are associated with a particular catalog. On the form field end, i would return a param that is an array of all of the checked products.
The question is: what is the most accepted way to now create/delete the catalog_product records so that unchecked products get deleted, newly checked products get created, and unchanged products get left alone?
My current solution would be something like this:
#Catalog.rb
...
def update_linked_products(updated_product_ids)
current_product_ids = catalog_products.collect{|p| p.product_id}
removed_products = (current_product_ids - updated_product_ids)
added_products = (updated_product_ids - current_product_ids)
catalog_products.where(catalog_id: self.id, product_id: removed_products).destroy_all
added_products.each do |prod|
catalog_products.create(product_id: prod)
end
end
...
This, of course, does a comparison between the current associations, figures out which records need to be deleted, and which need to be created, and then performs the deletions and creations.
It works fine, but if i need to do something similar for a different set of models/associations, i feel like this gets even uglier and less DRY every time it's implemented.
Now, i hope this is not the best way to do this (ignoring the quality of the code in my example, but simply what it is trying to achieve), and i feel that there must be a better "Rails" way of achieving this same result.
Take a look at this https://guides.rubyonrails.org/association_basics.html#methods-added-by-has-many-collection-objects
You don't have to remove and create manually each object.
If you have already the product_ids array, I think this should work:
#Catalog.rb
...
def update_linked_products(updated_product_ids)
selected_products = Product.where(id: updated_product_ids)
products = selected_products
end
...
First,
has_many :products, through: :catalog_products
generate some methods for you like product_ids, check this under auto-generated methods to know more about the other generated methods.
so we don't need this line:
current_product_ids = catalog_products.collect{|p| p.product_id}
# exist in both arrays are going to be removed
will_be_removed_ids = updated_product_ids & product_ids
# what's in updated an not in original, will be appended
will_be_added_ids = updated_product_ids - product_ids
Then, using <<, and destroy methods which are also generated from the association (it gives you the ability to deal with Relations as if they are arrays), we are going to destroy the will_be_removed_ids, and append the will_be_added_ids, and the unchanged will not be affected.
Final version:
def update_linked_products(updated_product_ids)
products.destroy(updated_product_ids & product_ids)
products << updated_product_ids - product_ids
end

Error in custom uniqueness validation for rspec

I am trying to create a rspec test for custom validation in a spree extension(like a gem)
I need to validate uniqueness of a variants
option values for a product (all Spree models)
Here is the basic structure of models(Although they are part of spree, a rails based e-commerce building):
class Product
has_many :variants
has_many :option_values, through: :variants #defined in the spree extension, not in actual spree core
has_many :product_option_types
has_many :option_types, through: :product_option_types
end
class Variant
belongs_to :product, touch: true
has_many :option_values_variants
has_many :option_values, through: option_values
end
class OptionType
has_many :option_values
has_many :product_option_types
has_many :products, through: :product_option_types
end
class OptionValue
belongs_to :option_type
has_many :option_value_variants
has_many :variants, through: :option_value_variants
end
So I have created a custom validation to check the uniqueness of a variants option values for a certain product. That is a product(lets say product1) can have many variants. And a variant with option values lets say (Red(Option_type: Color) and Circle(Option_type: Shape)) have to unique for this product
Anyway this is the custom validator
validate :uniqueness_of_option_values
def uniqueness_of_option_values
#The problem is in product.variants, When I use it the product.variants collection is returning be empty. And I don't get why.
product.variants.each do |v|
#This part inside the each block doesn't matter though for here.
variant_option_values = v.option_values.ids
this_option_values = option_values.collect(&:id)
matches_with_another_variant = (variant_option_values.length == this_option_values.length) && (variant_option_values - this_option_values).empty?
if !option_values.empty? && !(persisted? && v.id == id) && matches_with_another_variant
errors.add(:base, :already_created)
end
end
end
And finally here are the specs
require 'spec_helper'
describe Spree::Variant do
let(:product) { FactoryBot.create(:product) }
let(:variant1) { FactoryBot.create(:variant, product: product) }
describe "#option_values" do
context "on create" do
before do
#variant2 = FactoryBot.create(:variant, product: product, option_values: variant1.option_values)
end
it "should validate that option values are unique for every variant" do
#This is the main test. This should return false according to my uniqueness validation. But its not since in the custom uniqueness validation method product.variants returns empty and hence its not going inside the each block.
puts #variant2.valid?
expect(true).to be true #just so that the test will pass. Not actually what I want to put here
end
end
end
end
Anybody know whats wrong here. Thanks in advance
I have a guess at what's happening. I think a fix would be to change your validation with the following line:
product.variants.reload.each do |v|
What I think is happing is that when you call variant1 in your test, it is running the validation for variant1, which calls variants on the product object. This queries the database for related variants, and gets an empty result. However, since variant2 has the same actual product object, that product object will not re-query the database, and remembers (incorrectly) that its variants is an empty result.
Another change which might make your test run is to change your test as follows:
before do
#variant2 = FactoryBot.create(:variant, product_id: product.id, option_values: variant1.option_values)
end
It is subtle and I'd like to know if it works. This sets the product_id field on variant2, but does not set the product object for the association to be the actual same product object that variant1 has. (In practice this is more likely to happen in your actual code, that the product object is not shared between variant objects.)
Another thing for your correct solution (if all this is right) is to do the reload but put all your save code (and your update code) in a transaction. That way there won't be a race condition of two variants which would conflict, because in a transaction the first must complete the validation and save before the second one does its validation, so it will be sure to detect the other one which just saved.
Some suggested debugging techniques:
If possible, watch the log to see when queries are made. You might have caught that the second validation did not query for variants.
Check the object_id. You might have caught that the product objects were in fact the same object.
Also check new_record? to make sure that variant1 saved before you tested variant2. I think it does save, but it would have be nice to know you checked that.

Rails 5 - using a scope in an edit action to find relevant children of a specific instance

I am trying to learn how to use scopes in my Rails 5 app.
I have asked a background question here.
have models in my Rails 5 app for User, Proposal and Potential.
Users create Proposals which they themselves and others can then create comments on.
The associations between models are:
User
has_many :proposals, dependent: :destroy
has_many :potentials
Proposal
belongs_to :user
has_many :potentials, inverse_of: :proposal
accepts_nested_attributes_for :potentials, reject_if: :all_blank, allow_destroy: true
Potential
belongs_to :proposal, inverse_of: :potentials
belongs_to :user
In my routes file, I have two resources for potentials. I'm not sure if I've gone off piste with this bit- I cant find an example of how to do this otherwise. I have both:
resources :potentials
as well as:
resources :proposals do
resources :potentials
Objective:
When the user who made the proposal tries to edit it, I only want that user to be able to edit the potentials that they created themselves.
The reason I have two routes set up for potentials is that the nested resource has a nested form fields inside the proposal form, so that the proposal creator can make a potential in that way. Any other user that sees the proposal and makes a potential does it via a separate form.
Any user (including the proposal creator, can edit the potential via that separate form), and the proposal creator can also edit any of its own proposals by the nested form in the proposal form.
At the moment, whenever I edit the proposal form (even when I don't edit the potential nested fields), all of the potentials are updated to insert the proposal creator's user id overriding the actual potential creator's user id.
Solution
I am trying to limit the edit action in the proposals controller, so that it only allows the proposal /potentials to be edited if they have the user_id == the proposal.user_id.
For this purpose, I have written scopes in my proposal.rb
scope :owner_potentials, ->{ where(user_id: potential.user_id ) }
scope :third_party_potentials, ->{ where(user_id: != potential.user_id) }
The solution in the post i liked above was to try using a scope. Since scopes are meant to work on the class, rather than an instance, I'm stuck in trying to figure out how to adapt them so that I can use the scope to search for all the compliant potentials (i.e. potentials where potential.user_id == proposal.user_id). That means Im not searching the Proposal class, Im searching the specific proposal.
This post suggested defining Event.all inside the relevant controller action, but then how would I limit that so it only applied to the specific potentials edit line? I have other lines in my edit action which should not be tested on the Proposal table, but just the instance. If this were able to work, I imagine I would then need to rewrite my scope to try to exclude all the other proposals.
Is there a way to use an edit action in a controller with a scope, on a specific instance?
I would suggest scopes like this:
scope :owner_potentials, -> (user_id) { where(user_id: user_id) }
scope :third_party_potentials, -> (user_id) { where.not(user_id: user_id) }
When calling these scopes you just need to pass current user's id.
Scopes define queries for the AR class they are defined in. You say you have written owner_potentials and third_party_potentials scopes in proposal.rb. But if these scopes are meant to return a collection of potentials, then these should be defined in the Potential class. If you need to access these scopes from a proposal record, you can chain scopes to associations, e.g.
class Potential
scope :owner_potentials, -> (user) { where(user: user) }
scope :third_party_potentials, -> (user) { where.not(user: user) }
end
...
class ProposalsController # Proposals::PotentialsController..? imo Proposals::PotentialsController#edit sounds like an endpoint for editing exactly one potential record and nothing else, which doesn't sound like what you want. Your call on how to structure the controller/routes though.
def edit
#proposal = ... # Whatever your logic is to find the proposal
#proposal.potentials.owner_potentials(current_user) # do something with the user's potentials
#proposal.potentials.third_party_potentials(current_user) # do something with the potentials the user doesn't own
end
end
You can see here how you chain an association (.potentials) to a scope (.owner_potentials).
Also, if you have an association, you can treat that association as a field in a where method, a la where(user: user) instead of where(user_id: user.id).
Last thing to note is that you probably want to change the name of the scopes with this refactor.
potentials.owner_potentials(user) is a bit redundant. Maybe something like potentials.owned_by(user) ?

Including an association if it exists in a rails query

Update: This may be something that just isn't doable. See this
TLDR: How do you conditionally load an association (say, only load the association for the current user) while also including records that don't have that association at all?
Rails 3.1, here's roughly the model I'm working with.
class User
has_many :subscriptions
has_many :collections, :through => :subscriptions
end
class Collection
has_many :things
end
class Thing
has_many :user_thing_states, :dependent => :destroy
belongs_to :collection
end
class Subscription
belongs_to :user
belongs_to :collection
end
class UserThingState
belongs_to :user
belongs_to :thing
end
There exist many collections which have many things. Users subscribe to many collections and thereby they subscribe to many things. Users have a state with respect to things, but not necessarily, and are still subscribed to things even if they don't happen to have a state for them. When a user subscribes to a collection and its associated things, a state is not generated for every single thing (which could be in the hundreds). Instead, states are generated when a user first interacts with a given thing. Now, the problem: I want to select all of the user's subscribed things while loading the user's state for each thing where the state exists.
Conceptually this isn't that hard. For reference, the SQL that would get me the data needed for this is:
SELECT things.*, user_thing_states.* FROM things
# Next line gets me all things subscribed to
INNER JOIN subscriptions as subs ON things.collection_id = subs.collection_id AND subs.user_id = :user_id
# Next line pulls in the state data for the user
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I just don't know how to piece it together in rails. What happens in the Thing class? Thing.includes(:user_thing_states) would load all states for all users and that looks like the only tool. I need something like this but am not sure how (or if it's possible):
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
def state
# There should be only one user_thing_state if the include is correct, state method to access it.
self.user_thing_states.first
end
end
I need something like:
Thing.includes(:user_question_states, **where 'user_question_state.user_id => :user_id**).by_collections(user.collections)
Then I can do
things = User.things_subscribed_to
things.first.some_state_property # the property of the state loaded for the current user.
You don't need to do anything.
class User
has_many :user_thing_states
has_many :things, :through => :user_thing_states
end
# All Users w/ Things eager loaded through States association
User.all.includes(:things)
# Lookup specific user, Load all States w/ Things (if they exist for that user)
user = User.find_by_login 'bob'
user.user_thing_states.all(:include => :things)
Using includes() for this already loads up the associated object if they exist.
There's no need to do any filtering or add extra behavior for the Users who don't have an associated object.
Just ran into this issue ourselves, and my coworker pointed out that Rails 6 seems to include support for this now: https://github.com/rails/rails/pull/32655
*Nope, didn't solve it :( Here's a treatment of the specific issue I seem to have hit.
Think I've got it, easier than expected:
class Thing
has_many :user_thing_states
delegates :some_state_property, :to => :state, :allow_nil => true
scope :with_user_state, lambda { |user|
includes(:user_thing_states).where('user_thing_states.user_id = :user_id
OR user_thing_states.user_id IS NULL',
{:user_id => user.id}) }
def state
self.user_thing_states.first
end
end
So:
Thing.with_user_state(current_user).all
Will load all Things and each thing will have only one user_question_state accessible via state, and won't exclude Things with no state.
Answering my own question twice... bit awkward but anyway.
Rails doesn't seem to let you specify additional conditions for an includes() statement. If it did, my previous answer would work - you could put an additional condition on the includes() statement that would let the where conditions work correctly. To solve this we'd need to get includes() to use something like the following SQL (Getting the 'AND' condition is the problem):
LEFT JOIN user_thing_states as uts ON things.id = uts.thing_id AND uqs.user_id = :user_id
I'm resorting to this for now which is a bit awful.
class User
...
def subscribed_things
self.subscribed_things_with_state + self.subscribed_things_with_no_state
end
def subscribed_things_with_state
self.things.includes(:user_thing_states).by_subscribed_collections(self).all
end
def subscribed_things_with_no_state
Thing.with_no_state().by_subscribed_collections(self).all
end
end

How to handle this type of model validation in Ruby on Rails

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.

Resources