Issue with nested forms and posting simultaneously to two associated tables - ruby-on-rails

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)

Related

Handling join table entries based on association attributes

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.

Using accepts_nested_attributes_for outside of proper restful routes

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?

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 %>

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

No foreign key in new scaffold

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.

Resources