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
Related
TL;DR
What is the best way to create join table entries based on a form with the attributes of a association, like a bar code or a plate number?
Detailed explanation
In this system that records movements of items between storage places, there is a has_many_and_belongs_to_many relationship between storage_movements and storage_items because items can be moved multiple times and multiple items can be moved at once.
These items are previously created and are identified by a plate number that is physically attached to the item and recorded on its creation on the application.
The problem is that I need to create storage_movements with a form where the user inputs only the plate number of the storage_item that is being moved but I cant figure it out a way to easily do this.
I have been hitting my head against this wall for some time and the only solution that I can think of is creating nested fields on the new storage_movements form for the storage_items and use specific code on the model to create, update and delete these storage_movements by explicitly querying these plate numbers and manipulating the join table entries for these actions.
Is this the correct way of handling the problem? The main issue with this solution is that I can't seem to display validation errors on the specific plates number that are wrong (I'm using simple_forms) because I don't have storage_item objects to add errors.
Below there is a snipped of the code for the form that I'm currently using. Any help is welcome :D
# views/storage_movements/_form.html.erb
<%= simple_form_for #storage_movement do |movement_form| %>
#Other form inputs
<%= movement_form.simple_fields_for :storage_items do |item_form| %>
<%= item_form.input :plate, label: "Plate number" %>
<% end %>
<% end %>
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many_and_belongs_to_many :storage_items, inverse_of: :storage_movements, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
... several callbacks and validations ...
end
# models/storage_item.rb
class StorageItem < ActiveRecord::Base
has_many_and_belongs_to_many :storage_movements, inverse_of: :storage_items
... more callbacks and validations ...
end
The controllers were the default generated ones.
This was my solution, it really "feels" wrong and the validations also are not shown like I want it to... But it was what I could come up with... Hopefully it helps someone.
I created the create_from_plates and update_from_plates methods on the model to handle the create and update and updated the actions of the controller to use them.
Note: had to switch to a has_many through association due to callback necessities.
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many :movements_items, dependent: :destroy, inverse_of: :storage_movement
has_many :storage_items, through: :movements_items, inverse_of: :allocations, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
validate :check_plates
def StorageMovement::create_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
unless items_attributes.nil?
item_plates = items_attributes.collect {|k, h| h["plate"]}
items = StorageItem.where, plate: item_plates
end
if not items_attributes.nil? and item_plates.length == items.count
new_allocation = Allocation.new attributes
movements_items.each {|i| new_allocation.items << i}
return new_allocation
else
Allocation.new mov_attributes
end
end
def update_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
if items_attributes.nil?
self.update mov_attributes
else
transaction do
unless items_attributes.nil?
items_attributes.each do |k, item_attributes|
item = StorageItem.find_by_plate(item_attributes["plate"])
if item.nil?
self.errors.add :base, "The plate #{item_attributes["plate"]} was not found"
raise ActiveRecord::Rollback
elsif item_attributes["_destroy"] == "1" or item_attributes["_destroy"] == "true"
self.movements_items.destroy item
elsif not self.items.include? item
self.movements_items << item
end
end
end
self.update attributes
end
end
end
def check_plates
movements_items.each do |i|
i.errors.add :plate, "Plate not found" if StorageItem.find_by_plate(i.plate).nil?
end
end
... other validations and callbacks ...
end
With this, the create works as I wanted, because, in case of a error, the validation adds the error to the specific item attribute. But the update does not because it has to add the error to the base of the movement, since there is no item.
I am building a setup wizard for a model called Project. This model has a lot of associated information, which includes a number of nested models.
After some research and a fair bit of trial and error, I decided to manage the setup process in a SetupController, using the :id parameter to track which step I'm on, resulting in a path pattern like so: projects/:project_id/setup/:id/edit (based on this blog)
Here are the relevant bits:
Project Model
class Project < ActiveRecord::Base
has_many :ratings
accepts_nested_attributes_for :ratings, allow_destroy: true, reject_if: -> x { x[:value].blank? }
end
Rating Model
class Rating < ActiveRecord::Base
# has a null: false constraint on value
belongs_to :project
end
Setup Controller
ProjectSetupController < ApplicationController
STEPS = %w(step_1 step_2 step_3)
layout 'setups'
def edit
#project.ratings.build
render step
end
def update
if #project.update_attributes(project_params)
if next_step && params[:button].downcase.include?('continue')
redirect_to edit_project_setup_path(#project, next_step), flash: {success: "Updated project"}
else
redirect_to project_path(#project)
end
else
flash.now[:error] = "Please complete all required fields"
render step
end
end
private
def step
STEPS.find {|s| s == params[:id].to_s.downcase}
end
def current_step_index
STEPS.index(step)
end
def next_step
STEPS[current_step_index+1]
end
def project_params
params.require(:project).permit(:name, ratings_attributes: [:id, :value, :_destroy])
end
end
And this is all well and good, except when it comes to nested attributes. A Project accepts_nested_attributes_for Ratings, but rejects any ratings with blank values. I want the user to be able to submit a form with blank values because multiple rating fields can be added dynamically to the project form and there will always be an empty new field, I just don't want any record without a value to be saved. However, something gets muddled when using the :id parameter as something other than the id of the parent model, and these records are not discarded when the form is submitted. Instead, they hit the Rating database validation for presence of value and an error is thrown.
Form
= simple_form_for #project, url: project_setup_path(#project, params[:id]), as: :project, html: {id: 'customization-form'} do |f|
- #project.ratings.each do |rating|
.rating-wrapper{class: rating.new_record? && "new"}
= f.fields_for :ratings, rating do |ff|
= ff.input_field :value, placeholder: "Enter New Rating"
= button_tag(type: 'submit') do
Update
If I mock a submission with the params[:id] as the id of the project I'm submitting the form for, then everything works as expected (of course this results in a redirect error as the project id is not a valid step), so I feel like there must be some way to point the attributes to the correct id, alas, this magic is beyond me.
Current possible workarounds:
I can submit the form to the regular project controller action with a
button parameter that will redirect the user back into the setup
process
I could remove the empty value fields from the DOM via javascript on
submission
If I remove the Rating validations, I can submit the form as
is, and all blank ratings will be saved, but I could delete them in a
callback
Currently I'm employing the first workaround, but is there a more Rails-y solution that allows me to keep this process within the setup controller, without removing database validations or using javascript? The blog article I modeled my wizard after suggested sub-models for handling interim validations - I don't think that's exactly what I'm looking for here, but maybe there's a way I could leverage something like that?
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
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.
I have the following two models:
class Product < ActiveRecord::Base
belongs_to :shop
validates_numericality_of :price, :greater_than_or_equal_to => 0
end
class Shop < ActiveRecord::Base
has_many :products
validates_presence_of :name
end
Here is the create method of my ProductsController:
def create
if params[:product][:shop_id] == "new_shop"
#shop = Shop.find_by_name(params[:new_shop]) || Shop.create(:name => params[:new_shop]) # Is there a simpler method to do this ?
params[:product][:shop_id] = #shop.id
end
#product = Product.new(params[:product])
if #product.save
redirect_to(:action => 'index')
else
render('new')
end
end
When user adds a new product he has a select box to choose the shop. The last option in this select box lets user to add a new shop (an additional input text field appears). The value of this last option is new_shop.
If the validation of the new entered shop fails, I would like the validation of the product to fail and display an appropriate error (currently an error displayed only if the validation of the product itself fails).
What would be the most "Rails 3 method" to achieve this ?
I think it would be simpler if you use accepts_nested_attributes_for. So to your Product model add:
accepts_nested_attributes_for :shop
And then in view depending on your select list value you can modify form (in js), so there will be either shop_id field or a whole set of fileds for a shop:
<% f.fields_for :shop do |sf| %>
...
<% end %>
Then if user selects existing shop, it will only pass shop_id, but if users selects new shop, then form will pass also new associated object.
If you want shop name to be unique, then just add validates_uniqueness_of to Shop model.
If validation of a shop fails, then product won't be saved. Basicaly, your controller stays as simple as it could be (just creating new product object from params - you don't care about shop there).
I agree with #klew, you should probably be using accepts_nested_attributes_for.
But, the simple and direct answer to your question is to use validates_associated.
Also, the nicer way of doing:
#shop = Shop.find_by_name(params[:new_shop]) || Shop.create(:name => params[:new_shop])
would be:
#shop = Shop.find_or_create_by_name(params[:new_shop])