I have a model structure as follows:
class Client
belongs_to :lead
accepts_nested_attributes_for :lead
end
class Lead
has_one :client
has_many :defense_practices, through: :some_join_model, source: :practice, source_type: "DefensePractice"
accepts_nested_attributes_for :defense_practices
end
I have a form structure has follows:
<%= form_for #client do |f| %>
<%= f.fields_for :leads do |lead_builder|
<%= f.fields_for defense_practices do |practice_builder|
<%= practice_builder.text_field :some_field %>
<% end %>
<% end %>
<% end %>
The data is submitted correctly in the params hash:
Parameters: {
"client"=>{
"lead_attributes"=>
{"id"=>"45",
"defense_practices_attributes"=>
{"0"=>{
"some_field"=>"some_value",
"id"=>"52"
},
"4"=>{
"some_field"=>"some_value"
}
}
}
}
}
The second practice was added through javascript, using the same technique used in the railscasts nested model form.
When the models are initialized in the create action, the defense practice which was added through javascript (which is unsaved as shown by the lack of an id attribute in the hash above) is not initialized:
def create
#client = Client.new client_profile_params
puts #client.lead.defense_practices.size # => 1
end
def client_profile_params
params[:client].permit!
end
One special note: In Client model, I had to override lead_attributes= because I have an unsaved client model with a saved lead model and if I did not override, then rails complains: Couldn't find Lead with ID=46 for Client with ID=.
def lead_attributes=(params)
if params[:id].present?
lead = Lead.find(params[:id])
self.lead = lead
else
super
end
end
It should initialized two defense practices. But it only initialized the defense practice which was already associated with that lead. It did not add the new defense practice to the association. What am I doing wrong?
This is what I call a ripple effect. One limitation in Rails leads to using a hack which unleashes a can of worms. Because the Lead was created before the Client, I couldn't save the Client with an existing Lead. So I had to override the Rails core lead_attributes= method in my model. Well, by doing that it circumvented a lot of the Rails internals. I was able to grab it back by using update_attributes and passing in the params hash:
def lead_attributes=(params)
if params[:id].present?
lead = Lead.find(params[:id])
lead.update_attributes params
self.lead = lead
else
super
end
end
Unfortunately, this is giving rise to new problems, but it does resolve the question that was asked.
Related
So I have tried now to do this for multiple hours pulling from different sources. I'm sure I can probably hack this together in a very ugly way with multiple creates, but im sure there is a simple way to do this.
im trying to create a simple time and task tracker. I have a time tracker table that records the start and end times of a task and the task id.
There is an associated table that contains the title and details of the task.
When I try to .save! it
it abandons the save with ActiveRecord::RecordInvalid (Validation failed: Task must exist):
the logic seems to be that it should first try to create the task details (title and details) which creates the ID, and then it should create the timerecord(timetracker) record and include the task id in the task_id column of the time record. But based on the error, guess this is not happening.
I appreciate any help getting this working or point me in the right direction of what im doing incorrectly.
##########
Edit After posting, I realized that a task can have multiple time records, since a user can stop and restart on the same task that creates a new record with the same task ID. I've changed the task model from has_one to has_many.
###########
Here are the two models
class Timerecord < ApplicationRecord
belongs_to :task
end
class Task < ApplicationRecord
#has_one :timerecord #this has been changed to
has_many :timerecord
accepts_nested_attributes_for :timerecord
end
in my Timetracker controller #new & #create & private
def new
#timetracker = Timerecord.new
#timetracker.build_task
end
def create
#timetracker = Timerecord.new(timetracker_params)
if #timetracker.save
flash[:notice] = "Time Tracker Started"
redirect_to timetracker_index_path
end
end
private #at the tail of permit you will see including the task attribute title
def timetracker_params
params.require(:timerecord).permit(:user_id, :agency_id, :client_id, :task_id, :is_billable, :time_start, :time_end, :manual_input_hours, :timerecordgroup_id, :is_paused, :service_type_id, task_attributes: [:title])
end
and the form
<%= form_with(model: #timetracker, url: timetracker_index_path, local: true) do |f| %>
... a bunch of fields for #timetracker
<%#= fields_for :task, #timetracker.task do |t| %>
<%#= t.text_field :title, class: "form-control shadow-sm", placeholder: "Title" %>
<%# end %>
... more fields for timetracker
<% end %>
First of all you have to know which model is nested within the other and which one you're going to call the save method on.
Since your models are:
class Timerecord < ApplicationRecord
belongs_to :task
end
class Task < ApplicationRecord
#has_one :timerecord #this has been changed to
has_many :timerecord
accepts_nested_attributes_for :timerecord
end
You should call save on the Task model (which will also save timerecord) and not on the TimeRecord model because your Task model is the one accepting the nested attributes for TimeRecord model and not the other way around.
Your controller should look something like this:
def create
#timetracker = Task.new(task_params)
if #timetracker.save
flash[:notice] = "Time Tracker Started"
redirect_to timetracker_index_path
end
end
private
def task_params
params.require(:task).permit(#your permitted params for task, timetracker_attributes: [:title])
end
if you notice, your Task model accepts nested attributes for timetracker, hence you should permit timetracker_attributes and not task_attributes.
It's easy to know which nested params you should permit, just look at your models and see which model the accept_nested_attributes_for is refering to and then just add _attributes at the end.
Check out the accept_nested_attributes_for documentation, it should help
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Not an answer as such, just a few suggestions to point you in the right direction as there are quite a few things wrong with your implementation.
you don't need accepts_nested_attributes_for :timerecord
in task, as you aren't creating timerecords at the time you are creating/editing the task.
you don't need to require the nested attributes for task in the TimerecordController
the form should have a hidden_field for the :task_id - this is primarily why you are getting the error.
but you don't have an associated task for the timerecord so in addition you need to assign it at the point you generate a new timerecord. Your TimerecordController new action should be something like:
def new
#timetracker = Timerecord.new
#timetracker.task_id = params[:task_id]
end
which means you also need to ensure you are sending task_id to the new action, for example new_timerecord_path(task_id: my_task_instance.id)
I have a polymorphic association (belongs_to :resource, polymorphic: true) where resource can be a variety of different models. To simplify the question assume it can be either a Order or a Customer.
If it is a Order I'd like to preload the order, and preload the Address. If it is a customer I'd like to preload the Customer and preload the Location.
The code using these associations does something like:
<%- #issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.resource.name %> <%= issue.resource.location.name %>
<%- when Order -%>
<%= issue.resource.number %> <%= issue.resource.address.details %>
<%- end -%>
Currently my preload uses:
#issues.preload(:resource)
However I still see n-plus-one issues for loading the conditional associations:
SELECT "addresses".* WHERE "addresses"."order_id" = ...
SELECT "locations".* WHERE "locations"."customer_id" = ...
...
What's a good way to fix this? Is it possible to manually preload an association?
You can do that with the help of ActiveRecord::Associations::Preloader class. Here is the code:
#issues = Issue.all # Or whatever query
ActiveRecord::Associations::Preloader.new.preload(#issues.select { |i| i.resource_type == "Order" }, { resource: :address })
ActiveRecord::Associations::Preloader.new.preload(#issues.select { |i| i.resource_type == "Customer" }, { resource: :location })
You can use different approach when filtering the collection. For example, in my project I am using group_by
groups = sale_items.group_by(&:item_type)
groups.each do |type, items|
conditions = case type
when "Product" then :item
when "Service" then { item: { service: [:group] } }
end
ActiveRecord::Associations::Preloader.new.preload(items, conditions)
You can easily wrap this code in some helper class and use it in different parts of your app.
This is now working in Rails v6.0.0.rc1: https://github.com/rails/rails/pull/32655
You can do .includes(resource: [:address, :location])
You can break out your polymorphic association into individual associations. I have followed this and been extremely pleased at how it has simplified my applications.
class Issue
belongs_to :order
belongs_to :customer
# You should validate that one and only one of order and customer is present.
def resource
order || customer
end
end
Issue.preload(order: :address, customer: :location)
I have actually written a gem which wraps up this pattern so that the syntax becomes
class Issue
has_owner :order, :customer, as: :resource
end
and sets up the associations and validations appropriately. Unfortunately that implementation is not open or public. However, it is not difficult to do yourself.
You need to define associations in models like this:
class Issue < ActiveRecord::Base
belongs_to :resource, polymorphic: true
belongs_to :order, -> { includes(:issues).where(issues: { resource_type: 'Order' }) }, foreign_key: :resource_id
belongs_to :customer, -> { includes(:issues).where(issues: { resource_type: 'Customer' }) }, foreign_key: :resource_id
end
class Order < ActiveRecord::Base
belongs_to :address
has_many :issues, as: :resource
end
class Customer < ActiveRecord::Base
belongs_to :location
has_many :issues, as: :resource
end
Now you may do required preload:
Issue.includes(order: :address, customer: :location).all
In views you should use explicit relation name:
<%- #issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.customer.name %> <%= issue.customer.location.name %>
<%- when Order -%>
<%= issue.order.number %> <%= issue.order.address.details %>
<%- end -%>
That's all, no more n-plus-one queries.
I would like to share one of my query that i have used for conditional eager loading but not sure if this might help you, which i am not sure but its worth a try.
i have an address model, which is polymorphic to user and property.
So i just check the addressable_type manually and then call the appropriate query as shown below:-
after getting either user or property,i get the address to with eager loading required models
###record can be user or property instance
if #record.class.to_s == "Property"
Address.includes(:addressable=>[:dealers,:property_groups,:details]).where(:addressable_type=>"Property").joins(:property).where(properties:{:status=>"active"})
else if #record.class.to_s == "User"
Address.includes(:addressable=>[:pictures,:friends,:ratings,:interests]).where(:addressable_type=>"User").joins(:user).where(users:{is_guest:=>true})
end
The above query is a small snippet of actual query, but you can get an idea about how to use it for eager loading using joins because its a polymorphic table.
Hope it helps.
If you instantiate the associated object as the object in question, e.g. call it the variable #object or some such. Then the render should handle the determination of the correct view via the object's class. This is a Rails convention, i.e. rails' magic.
I personally hate it because it's so hard to debug the current scope of a bug without something like byebug or pry but I can attest that it does work, as we use it here at my employer to solve a similar problem.
Instead of faster via preloading, I think the speed issue is better solved through this method and rails caching.
I've come up with a viable solution for myself when I was stuck in this problem. What I followed was to iterate through each type of implementations and concatenate it into an array.
To start with it, we will first note down what attributes will be loaded for a particular type.
ATTRIBS = {
'Order' => [:address],
'Customer' => [:location]
}.freeze
AVAILABLE_TYPES = %w(Order Customer).freeze
The above lists out the associations to load eagerly for the available implementation types.
Now in our code, we will simply iterate through AVAILABLE_TYPES and then load the required associations.
issues = []
AVAILABLE_TYPES.each do |type|
issues += #issues.where(resource_type: type).includes(resource: ATTRIBS[type])
end
Through this, we have a managed way to preload the associations based on the type. If you've another type, just add it to the AVAILABLE_TYPES, and the attributes to ATTRIBS, and you'll be done.
I have a couple models shown below and I'm using the search class method in Thing to filter records
class Category << ActiveRecord::Base
has_many :thing
end
class Thing << ActiveRecord::Base
belongs_to :category
:scope approved -> { where("approved = true") }
def self.search(query)
search_condition = "%" + query + "%"
approved.where('name LIKE ?', search_condition)
end
end
It works fine in my Things controller. The index route looks like so:
def index
if params[:search].present?
#things = Thing.search(params[:seach])
else
#thing = Thing.all
end
end
On the categories show route I display the Things for this category. I also have the search form to search within the category.
def show
#category = Categories.find(params[:id])
if params[:search].present?
#category.things = #category.things.search()
end
end
So the problem is that the category_id attribute of all the filtered things are getting set to nil when I use the search class method in the categories#show route. Why does it save it to database? I thought I would have to call #category.save or update_attribute for that. I'm still new to rails so I'm sure its something easy I'm overlooking or misread.
My current solution is to move the if statement to the view. But now I'm trying to add pages with kaminiri to it and its getting uglier.
<% if params[:search].present? %>
<% #category.things.search(params[:search]) do |thing| %>
... Show the filtered things!
<% end %>
<% else %>
<% #category.things do |thing| %>
... Show all the things!
<% end %>
<% end %>
The other solution I thought of was using an #things = #categories.things.search(params[:search]) but that means I'm duplicated things passed to the view.
Take a look at Rails guide. A has_many association creates a number of methods on the model to which collection=(objects) also belongs. According to the guide:
The collection= method makes the collection contain only the supplied
objects, by adding and deleting as appropriate.
In your example you are actually assigning all the things found using #category.things.search() to the Category which has previously been queried using Categories.find(params[:id]).
Like Yan said, "In your example you are actually assigning all the things found using #category.things.search() to the Category which has previously been queried using Categories.find(params[:id])". Validations will solve this problem.
Records are being saved as nil because you have no validations on your model. Read about active record validations.
Here's the example they provide. You want to validate presence as well because records are being created without values.
class Person < ActiveRecord::Base
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
Situation:
One form that allows users to select multiple quantities of items they'd like to request
This form POSTs to two models, one parent: Request, and child: Items.
Upon submit, one Request is created, but up to several Items are created, depending on the quantity indicated
To handle this, I have two sets of params, one for the Items, one for Requests
Desired end state:
I do not want an Item to be created without a Request nor a Request to be created without the Item
All the errors present in the form (whether it's not selecting at least one item, or errors in the attributes of the Request object) are shown together to the user once the page is re-rendered; i.e., I would like to do all the error checking together
Current hacky solution & complication:
Currently, I'm checking in stages, 1) are there quantities in the Items? If not, then regardless of what the user may have put for Request attributes, the page is re-rendered (i.e., all attributes for Request are lost, as are any validation errors that would be shown). 2) Once the first stage is passed, then the model validations kicks in, and if it fails, the new page is re-rendered again
I've spent waaaaay too long thinking about this, and nothing elegant comes to mind. Happy with the hacky solution, but would love insights from much smarter people!
Controller code (fat for now, will fix later)
def create
request_params
#requestrecord = #signup_parent.requests.build
if #itemparams.blank?
#requestrecord.errors[:base] = "Please select at least one item"
render 'new'
else
#requestrecord = #signup_parent.requests.create(#requestparams)
if #requestrecord.save
items_to_be_saved = []
#itemparams.each do |item, quantity|
quantity = quantity.to_i
quantity.times do
items_to_be_saved << ({:request_id => 0, :name => item })
end
end
Item.create items_to_be_saved
flash[:success] = "Thanks!"
redirect_to action: 'success'
else
render 'new'
end
end
end
def request_params
#requestparams = params.require(:request).permit(:detail, :startdate, :enddate)
#itemparams = params["item"]
#itemparams = #transactionparams.first.reject { |k, v| (v == "0") || (v == "")}
end
And in case it's helpful, the snippet of the view code that generates the params["item"]
<% itemlist.each do |thing| %>
<%= number_field_tag "item[][#{thing}]", :quantity, min: 0, placeholder: 0 %>
<%= label_tag thing %>
</br>
<% end %>
<!-- itemlist is a variable in the controller that is populated with a list of items -->
Validations
When you mention you want all the errors to be returned at the same time, it basically means you need to use the Rails' validations functionality.
This populates the #model.errors object, which you can then use on your form like this:
<% if #model.errors.any? %>
<ul>
<% #model.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
I think your problem is you're trying to use the validations in the controller. This is both against MVC principles & generally bad for programming modularity. The functionality you require is available with the validations features:
You may benefit from using inverse_of to create some conditional validations; or using reject_if
reject_if
#app/models/request.rb
Class Request < ActiveRecord::Base
accepts_nested_attributes_for :items, reject_if: proc { |attributes| attributes['an_item_param'].blank? #-> attributes are the "item" attributes }
end
This will only be triggered if a request is created. I.E if your request fails for some reason (validation issue), the accepts_nested_attributes_for method will not run, returning your object with the appended errors
This is mainly used to validate the nested resources (I.E you can't save an item unless its title attribute is populated etc)
--
inverse_of
#app/models/request.rb
Class Request < ActiveRecord::Base
has_many :items, inverse_of: :request
accepts_nested_attributes_for :items
end
#app/models/item.rb
Class Item < ActiveRecord::Base
belongs_to :request, inverse_of: :items
validates :title, presence: true, unless: :draft?
private
def draft?
self.request.draft #-> an example we've used before :)
end
end
This is more for model-specific validations; allowing you to determine specific conditions. We use this if we want to save a draft etc
Good day... I have huge trouble with this and it drives me insane.
My user creation should have some additional data, like this:
<div class="field"><%= label_tag 'species', %>
<%= f.collection_select :species_id, Species.all, :id, :name %></div>
It displays the list of species correctly, yes, but when I do a submit, it is utterly ignored. Not even the number appears in the table of the database, it just disappears. I have put the correct associations:
class Species < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
# ... Other stuff
belongs_to :species
# ... Other stuff
end
I have also tried manipulating the controller:
class UsersController < ApplicationController
def create
logout_keeping_session!
#user = User.new(params[:user])
#user.species = Species.find(params[:species_id])
# Other stuff
end
end
But that only gives me 'Cannot find Species without id' even though the params array contains an element 'species_id' with a value.
I am at the end of my wits. Quite new to this, but is this RESTful? Not to find out how to get things done that seem easy? I love Rails and would like to continue.
Thanks for listening
your find fails because the params is probably: params[:user][:species_id] but if it is there like it is supposed, it should be set already, too.