Using accepts_nested_attributes_for outside of proper restful routes - ruby-on-rails

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?

Related

Issue with nested forms and posting simultaneously to two associated tables

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)

Validate Associated Object Presence Before Create

I've been following the Getting Started rails tutorial and am now trying some custom functionality.
I have 2 models, Person and Hangout. A Person can have many Hangouts. When creating a Hangout, a Person has to be selected and associated with the new Hangout. I'm running into issues however when I call my create action. This fires before the validate_presence_of for person.
Am I going about this the wrong way? Seems like I shouldn't have to create a custom before_create validation to make sure that a Hangout was created with a Person.
#hangout_controller
def create
#person = Person.find(params[:hangout][:person_id])
#hangout = #person.hangouts.create(hangout_params)
#hangout.save
redirect_to hangouts_path(#hangout)
end
#hangout.rb
class Hangout < ActiveRecord::Base
belongs_to :person
validates_presence_of :person
end
#person.rb
class Person < ActiveRecord::Base
has_many :hangouts
validates :first_name, presence: true
validates :met_location, presence: true
validates :last_contacted, presence: true
def full_name
"#{first_name} #{last_name}"
end
end
Create action fires before the validate_presence_of for person
I think you are confused about rails MVC. Your form contains a url and when you submit your form your form params are send to your controller action according to the routes you have defined in routes.rb Your controller action, in this case create action, interacts with model this is very it checks for your validations and if all the validations are passed your object is saved in databse so even though in your app the control is first passed to your controller but your object is saved only once if all the validations are passed.
Now lets comeback to your code. There are couple of things you are doing wrong
a. You don't need to associate your person separately:
In your create action you have this line:
#person = Person.find(params[:hangout][:person_id])
You don't need to do this because your person_id is already coming from your form and it'll automatically associate your hangout with person.
b. You are calling create method instead of build:
When you call .association.create method it does two things for you it first initialize your object, in your case your hangout and if all the validations are passed it saves it. If all the validations are not passed it simply rollback your query.
If you'll use .association.build it'll only initialize your object with the params coming from your form
c. Validation errors won't show:
As explained above, since you are calling create method instead of build your validation error won't show up.
Fix
Your create method should look like this:
def create
#hangout = Hangout.new(hangout_params) # since your person_id is coming from form it'll automatically associate your new hangout with person
if #hangout.save
redirect_to hangouts_path(#hangout)
else
render "new" # this will show up validation errors in your form if your hangout is not saved in database
end
end
private
def hangout_params
params.require(:hangout).permit(:person_id, :other_attributes)
end
You are confused with the controller and model responsibilities.
Let me try to explain what I think is confusing you:
First try this in your rails console:
Hangout.create
It shouldn't let you because you are not passing a Person object to the create method. So, we confirm that the validation is working fine. That validation means that before creating a Hangout, make sure that there is a person attribute. All this is at the model level, nothing about controllers yet!
Let's go to the controllers part. When the create action of the controller 'is fired', that controller doesn't know what you are trying to do at all. It doesn't run any validations. It is just an action, that if you want, can call the Hangout model to create one of those.
I believe that when you say 'it fires' you are saying that the create action of the HangoutController is called first than the create method on the Hangout model. And that is completely fine. The validations run at the model level.
Nested Attributes
I think you'll be better using accepts_nested_attributes_for - we've achieved functionality you're seeking before by using validation on the nested model (although you'll be able to get away with using reject_if: :all_blank):
#app/models/person.rb
Class Person < ActiveRecord::Base
has_many :hangouts
accepts_nested_attributes_for :hangouts, reject_if: :all_blank
end
#app/models/hangout.rb
Class Hangout < ActiveRecord::Base
belongs_to :person
end
This will give you the ability to call the reject_if: :all_blank method -
Passing :all_blank instead of a Proc will create a proc that will
reject a record where all the attributes are blank excluding any value
for _destroy.
--
This means you'll be able to create the following:
#config/routes.rb
resources :people do
resources :hangouts # -> domain.com/people/:people_id/hangouts/new
end
#app/controllers/hangouts_controller.rb
Class HangoutsController < ApplicationController
def new
#person = Person.find params[:people_id]
#hangout = #person.hangouts.build
end
def create
#person = Person.find params[:people_id]
#person.update(hangout_attributes)
end
private
def hangout_attributes
params.require(:person).permit(hangouts_attributes: [:hangout, :attributes])
end
end
Although I've not tested the above, I believe this is the way you should handle it. This will basically save the Hangout associated object for a particular Person - allowing you to reject if the Hangout associated object is blank
The views would be as follows:
#app/views/hangouts/new.html.erb
<%= form_for [#person, #hangout] do |f| %>
<%= f.fields_for :hangouts do |h| %>
<%= h.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>

Refactor code to avoid circular logic in controller

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

Rails - Allow a model to associate with existing objects or create a new one

Let's say I have a two models, Event and Person. An event has a coordinator that is a person:
class Event < ActiveRecord::Base
belongs_to :coordinator, :class_name => 'Person', :foreign_key => 'coordinator_id'
accepts_nested_attributes_for :coordinator
end
class Person < ActiveRecord::Base
validates :name, :length => 20
end
In my form, I would like to let the user pick from existing People objects (let's say a list of radio buttons), but also have a text box to create a new Person (with the name entered in the text box).
How would I elegantly implement this? I can figure it out, but it involves a lot of ugly code in the Controller, and is a pain to validate.
I've done something similar in which the radio buttons set person[id] and then I just checked for the id.
So in my controller#create method:
if params[:person][:id]
#person = Person.find(params[:person][:id])
else
#person = Person.new(params[:person])
#Handle saving #person here.
end
You may have to delete the id param in the elseblock if the form sends it even if nothing is selected.
Edit to answer validation question:
In the #Handle saving #person here. is where you'd do what you normally do for creating an object. Like:
if #person.save
flash[:notice] = "User created successfully"
else
render :action => 'new' # (or whatever the action is)
return
end
The validation code on person, will be executed everytime you save a person.
To save bypassing the validator, write #person.save(false).
Hope it integrate the pcg79's answer

Rails 3: How to enforce model's validation to fail when the validation of the associated model fails?

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])

Resources