Proper way to create instances of models with has_many :through relationship - ruby-on-rails

I have the following two classes and I'm struggling to find information about how to properly create an event and register users. Any help would be greatly appreciated - my code is below
class UserEvents < ActiveRecord::Base
belongs_to :user
belongs_to :event
end
class User < ActiveRecord::Base
has_many :user_events
has_many :events, :through => :user_events
end
class Event < ActiveRecord::Base
has_many :user_events
has_many :attendees, :class_name => "User", :through :user_events
end
Controller action for creating an event
def create
new_event = event.new(event_params)
current_user.events << new_event
new_event.attendees << current_user
if new_event.save and current_user.save
render json: new_event, status: :ok
else
render json: { errors: "Creation failed" }, status: :unprocessable_entity
end
Am I going about this correctly? Should I be using an array of ID's to keep track of the attendees instead? I've also seen it done in ways similar to:
current_user.events.create ( ... )
I'm just not sure which approach would be best to take

You can setup your event to accept nested attributes, then created all the necessary objects with a single save. Here's an example:
class Event < ActiveRecord::Base
has_many :user_events
has_many :attendees, :class_name => "User", through: :user_outings
accepts_nested_attributes_for :user_events, allow_destroy: true
end
Then modify event_params in your controller to accept attributes for user_events. Example:
def event_params
params.permit([
:..event attributes...,
user_events_attributes: [...user event attributes here...]
])
end
If it's only ever going to be the current user going to a new event, then this is overkill. But if you're adding other users to the event at event creation time, then this is how you'd do it.
If it's just the current user, then you can modify your create action to look like:
def create
new_event = event.new(event_params)
new_event.attendees << current_user
if new_event.save and current_user.save
render json: new_event, status: :ok
else
render json: { errors: "Creation failed" }, status: :unprocessable_entity
end
Edit: You can also do accepts_nested_attributes and use attendees but you may need to use the inverse_of option in your model. See https://robots.thoughtbot.com/accepts-nested-attributes-for-with-has-many-through for more details.

Related

how disable adding existing record in has_many through

I add unique index, but record dont save, validation error. I need update tags in my post,existing tags adding to tags with new id, but I need existing tags not to be added
class Tag < ApplicationRecord
has_many :tags_posts
has_many :tags, through: :tags_posts
accepts_nested_attributes_for :tags_posts, :allow_destroy => true, :update_only=>true
end
class TagsPost < ApplicationRecord
belongs_to :post
belongs_to :tag
accepts_nested_attributes_for :tag, :allow_destroy => true, :update_only=>true
end
controller code:
def update
#resource=resource_class.find(params[:id])
#resource.assign_attributes(resource_params)
if #resource.save
render json: #resource.as_json(as_json_resource)
else
render json: {errors:#resource.errors}, status: :unprocessable_entity
end
end
def resource_class
Post
end
def resource_params
params.require(:post).permit(:user_id,:title,:category_id, :content, :date_of_publication, tags_posts_attributes: [tag_attributes: [:name]] )
end
Add id to tag_attributes
params.require(:post).permit(:user_id,..., tags_posts_attributes: [tag_attributes: [:id, :name]] )
That will prevent it from adding again.

Rails 5 API - has_many through create action is returning 2 records although only 1 is persisted in the database

I have the following M2M through associations for these 3 models
Customer -> Residences <- Properties
Also Property model is related to Address:
class Address < ApplicationRecord
has_one :property
end
A customer will always exist before creating a Property.
A property is created by submitting an Address.
Here is the controller action, which works except on success the render always returns 2 properties (ie. basically 2 residence records).
However, only one is in the database. I understand it is related to stale objects, but cannot figure out how to solve it.
I tried adding #customer.reload and #customer.reload.residences and #customer.reload.properties but still get 2 records.
# POST /customers/:id/properties
def create
#customer = set_customer
Customer.transaction do
address = Address.find_by_place_id(address_params[:place_id])
if address.nil?
#property = #customer.properties.create
#property.address = Address.new(address_params)
if #property.save
#customer.reload
render json: #customer, status: :created
else
render json: #property.errors, status: :unprocessable_entity
end
else
# irrelevant code to the problem
end
end
end
def set_customer
Customer.find(params[:customer_id])
end
A comment on this question (from #Swaps) indicates using << instead of create can sometimes result in duplicates, but whichever way I do it I always get 2.
EDIT
I managed to force it work like this but this feels like a hack:
if #property.save
#customer = set_customer
render json: #customer, status: :created
else
** UPDATE - the models **
class Customer < ApplicationRecord
has_many :residences
has_many :properties, through: :residences
end
class Residence < ApplicationRecord
belongs_to :customer
belongs_to :property
end
class Property < ApplicationRecord
belongs_to :address
has_many :residences
has_many :customers, through: :residences
end
class Address < ApplicationRecord
has_one :property
has_one :location # ignore this, not relevant
end
You're trying to do manually what ActiveRecord can do automatically with accepts_nested_attributes_for. It even works with has_many through operations.
class Customer < ApplicationRecord
has_many: :residences, inverse_of :customer
has_many: :properties, through: :residences
accepts_nested_attributes_for :residences
end
class Residence < ApplicationRecord
belongs_to :customer
belongs_to :property
validates_presence_of :customer
validates_presence_of :property
accepts_nested_attributes_for :property
end
class Property < ApplicationRecord
has_one :address
has_many :residences
has_many :customers, through: :residences
accepts_nested_attributes_for :address
end
class Address < ApplicationRecord
belongs_to :property
end
class CustomersController < ApplicationController
def create
customer = Customer.new(customer_params)
if customer.save
redirect_to customer, notice: 'Customer saved!'
else
render :new
end
end
def customer_params
params.require(:customer).permit(
name:, ...,
residences_attributes: [
property_attributes: [
name, ...,
address_attributes: [
street, city, state, postal_code, ...
]
]
]
)
end
end
References:
https://www.pluralsight.com/guides/ruby-on-rails-nested-attributes
https://robots.thoughtbot.com/accepts-nested-attributes-for-with-has-many-through
Could you please try this?
def create
#customer = set_customer
Customer.transaction do
address = Address.find_by_place_id(address_params[:place_id])
if address.nil?
#customer.properties.new(address_params)
if #customer.save
render json: #customer, status: :created
else
render json: #customer.errors, status: :unprocessable_entity
end
else
# irrelevant code to the problem
end
end
end
I was thinking do you really need #property instance variable. Is it for your view files?
Update 1
Could you please add your Customer and Residence model as like this:
Customer model
class Customer < ApplicationRecord
has_many :residences
has_many :properties, through: :residences
end
Residence model
class Residence < ApplicationRecord
belongs_to :customer
belongs_to :property
end
The problem was stale objects in ActiveRecord versus what is in the database after saving.
The ".reload" did not work, I have to actually force ActiveRecord using my hack to force ActiveRecord to find the customer in the database again, and that forces a reload (I presume it invalidates the AR cache):
def create
#customer = set_customer
Customer.transaction do
address = Address.find_by_place_id(address_params[:place_id])
if address.nil?
#property = #customer.properties.create
#property.address = Address.new(address_params)
if #property.save!
#customer = set_customer # force reload from db
render json: #customer, status: :created
end
else
address.update!(address_params)
if #customer.properties.find_by_id(address.property.id).nil?
# although we updated the address, that is just a side effect of this action
# the intention is to create an actual residence record for this customer
#customer.properties << address.property
#customer = set_customer # force reload from db
render json: #customer, status: :created
else
#customer.errors.add(:customer, 'already has that property address')
render json: ErrorSerializer.serialize(#customer.errors), status: :unprocessable_entity
end
end
end
end
def set_customer
Customer.find(params[:customer_id])
end

Rails nested association is not saving

I’m trying to save a nested association, but for some reason it’s not working. Does anyone see anything wrong with this?
Order:
class Order < ApplicationRecord
has_many :replenishable_products, through: :order_items
has_many :order_items
accepts_nested_attributes_for :order_items
end
OrderItem
class OrderItem < ApplicationRecord
belongs_to :order
has_one :replenishable_product
accepts_nested_attributes_for :replenishable_product
end
ReplenishableProduct
class ReplenishableProduct < ActiveRecord::Base
belongs_to :order_item
end
Order Controller
def create
#order = #customer.orders.new(order_params)
authorize #order
if #order.save
render json: #order, serializer: ::V1::OrderSerializer, status: :created
else
render json: #order.errors, status: :unprocessable_entity
end
end
def order_params
params.require(:order).permit(order_items_attributes: [:quantity, :product_id, :selling_unit_of_measure_id,
replenishable_product_attributes: [:product_id, :salesperson_id,
:customer_id, :replenishable_date]])
end
Example request data:
```
{"order"=><ActionController::Parameters {"additional_tax_total"=>"8.27", "basket_id"=>"1", "custom_order_number"=>"pf000", "customer_attributes"=>{"contact_name"=>"Jason Test", "email"=>"new#test.com", "phone"=>"6095453234"}, "order_items_attributes"=>[{"product_id"=>"1", "quantity"=>"5", "replenishable_product_attributes"=>{"customer_id"=>"1", "product_id"=>"1", "replenishable_date"=>"2017-06-09T13:00:39-04:00", "salesperson_id"=>"1"}, "selling_unit_of_measure_id"=>"1"}, {"product_id"=>"2", "quantity"=>"5", "selling_unit_of_measure_id"=>"2"}], "payment_total"=>"103.31", "salesperson_id"=>"1", "shipping_address"=>"Test", "special_instructions"=>"test"} permitted: false>, "customer_id"=>"1", "format"=>"json", "controller"=>"api/v1/orders", "action"=>"create"}
```
So every other association is saving fine, but the replenishable_product is not being saved. I looked over it a few times and I don't see why it wouldn't be saving. Anyone have any ideas?

Rails accepts_nested_attributes_for associated models not created

I have two models (Company and User) that have a belongs_to/has_many relationship.
class Company < ActiveRecord::Base
attr_accessor :users_attributes
has_many :users
accepts_nested_attributes_for :users, allow_destroy: true
end
class User < ActiveRecord::Base
belongs_to :company
end
In my CompaniesController I want to create a new instance of Company along with a group of Users.
class Cms::CompaniesController < ApplicationController
def create
company = Company.new(company_params)
respond_to do |format|
if company.save
format.json { render json: company, status: :ok }
else
format.json { render json: company.errors.messages, status: :bad_request }
end
end
end
private
def company_params
params.require(:company).permit(
:id,
:name,
users_attributes: [
:id,
:_destroy,
:first_name,
:last_name,
:email
]
)
end
end
When I call company.save, I would expect a new instance of Company along with several new instances of User to be saved, depending on how many users I have in my params, however no users are persisted.
Here is a sample of what company_params looks like:
{"id"=>nil, "name"=>"ABC", "users_attributes"=>[{"first_name"=>"Foo", "last_name"=>"Bar", "email"=>"foo#bar.com"}]}
What am I missing here?
Remove attr_accessor:
class Company < ActiveRecord::Base
has_many :users
accepts_nested_attributes_for :users, allow_destroy: true
end
Everything else should work.
--
attr_accessor creates getter/setter methods in your class.
It's mostly used for virtual attributes (ones which aren't saved to the database). Your current setup is preventing you from being able to save the users_attributes param, thus your users are not saving.

Railscasts 258 Token Fields - Set additional attributes in :through table - the rails way to do it

I'm having a problem based on the excellent RailsCast #258 from Ryan Bates.
The situation is as follows:
class User < ActiveRecord::Base
has_many :capabilities,
:dependent => :destroy
has_many :skills, :through => :capabilities,
:uniq => true
has_many :raters,
:through => :capabilities,
:foreign_key => :rater_id,
:uniq => true
attr_accessible :name, :skill_tokens
attr_reader :skill_tokens
def skill_tokens=(tokens)
self.skill_ids = Skill.ids_from_tokens(tokens)
end
end
class Capability < ActiveRecord::Base
belongs_to :user
belongs_to :rater, class_name: "User"
belongs_to :skill
validates_uniqueness_of :rater_id, :scope => [:user_id, :skill_id]
end
class Skill < ActiveRecord::Base
has_many :capabilities
has_many :users, :through => :capabilities,
:uniq => true
has_many :raters, :through => :capabilities,
:foreign_key => :rater_id
end
The form contains a normal textfield for the skill tokens which are passed as ids:
.field
= f.label :skill_tokens, "Skills"
= f.text_field :skill_tokens, data: {load: #user.skills}
So a user can get many skills assigned through capabilities. While assigning the skill, the rater should also be tracked in the capability model.
Using Ryans example of jquery TokenInput I created an appropriate form to allow a user to assign (and create) skills using a tokenInput text field.
The Problem lies now in processing the data and setting the rater before the association is saved.
Through some ruby magic, self.skill_ids on the user model sets the ids used for the association model creation so the controller action is quite simple:
def update
#user = User.find(params[:id])
respond_to do |format|
if #user.update_attributes(params[:user])
format.html { redirect_to #user, notice: 'User was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
Obviously, if I want to set the additional rater attribute on the capability model it won't work so easily with update_attributes.
So how can I achieve this with "the rails way" to do it - writing beautiful, readable code?
ANY help would be greately appreciated!
How are you setting the rater_id?
If you plan accept a user input for the rater for each skill the user adds on the form,
I can't see how you'll be able to use input fields based on token inputs to achieve this. You're going to have to choose some other types of inputs.
If you plan to set the rater to the currently logged in user, or are setting the rater based on some other business logic, my approach would be overwriting the skill_ids= method in the User model to work how you want it, adding an attr_accessor to store the current_rater and passing the current_rate from the controller.
Something like:
#user.rb
attr_accessor :current_rater
def skill_ids=(ids)
return false if current_rater.nil? || User.find_by_id(current_rater).nil?
capabilities.where("skill_id not in (?)", ids).destroy_all
ids.each do |skill_id|
capabilities.create(:skill_id => skill_id, :rater_id => self.current_rater) if capabilities.find_by_id(skill_id).nil?
end
end
#users_controller.rb
def update
#user = User.find(params[:id])
#Replace 'current_user' with whatever method you are using to track the logged in user
params[:user].merge(:current_rater => current_user)
respond_to do |format|
...
end
end
Probably not as elegant as you were hoping, but it should do the job?

Resources