Triple Nested Attribute Forms in Rails - ruby-on-rails

I have seen a few things about triple nested routes but cannot figure it out on the form level. I have everything else working it seems like including views, just not the form. So what I am attempting to do is create a rails app where I can add Group>Classification>Item
I have setup the models for has_many and belongs_to. I followed: https://www.digitalocean.com/community/tutorials/how-to-create-nested-resources-for-a-ruby-on-rails-application
On how to setup a nested form. I got two levels working perfectly. Once I get to the third I can view the index but I cannot add. I have a feeling its either my controller or my form_with model. I have tried both:
<%= form_with(model: [#group, #classification, item], local: true) do |form| %>
<%= form_with(model: [#classification, item], local: true) do |form| %>
In the controller for the Item I have
before_action :get_group_classification
Private
def get_group_classification
#classification= Classification.find(params[:classification_id])
end
Routes are setup like
resources :groups do
resources :classifications do
resources :items
end
end
Any thoughts would be appreciated. I don't want to go any deeper than 3 tiers, but can't tell if that's bad protocol.

In parent model:
class Group < ActiveRecord::Base
belongs_to :add_on_slider
has_many :classifications, dependent: :destroy
accepts_nested_attributes_for :classifications, allow_destroy: true, :reject_if => :all_blank
end
In parent controller :
def grouo_params
params.require(:group).permit(:attribute name1, :attribute name2, :sub_title, classifications_attributes: [:id, :main_point, :_destroy, :important_id ,items_attributes: [:id, :attribute, :_destroy, :important_id]])
end
In the view page:
You can use gem 'cocon'. You can see any tutorial related to it.
https://www.youtube.com/watch?v=56xjUOAAZY8

You can certainly do it that way, the question is if you have any additional benefit of nesting routes so deeply. The main use case I've seen in the past where this was worth it was where authorization was handled by a parent attribute (in your case, if the authorization for an item was handled through the group).
A simpler way where you don't have to get so involved with routing so much is to pass group_id and classification_id as parameters in the form (e.g. as hidden_field_tag).
In the end, it's up to what you're trying to achieve and what other constraints you might have that are not evident from the bit of code that you've posted.

Related

Nested many to many form using cocoon Rails 6

I'm trying to make it possible to add multiple crops to a spray_program through program_crops in a nested form using cocoon. I'm able to add one but the link_to_add_association button just adds # to the url. Ill add the bits of code I think will be relevant let me know if there is something else...
nested form in spray_program form
<div id="programCrops">
<%= form.fields_for :program_crops do |program_crop_form| %>
<%= render 'program_crop_fields', f: program_crop_form%>
<% end %>
</div>
<div class="links">
<%= link_to_add_association "Add crop", form, :program_crops %>
</div>
_program_crop_fields.html.erb
<div class="nested-fields">
<div class="field">
<%= f.label :name %>
<%= f.collection_select :crop_id, Crop.all, :id, :name %>
</div>
<%= link_to_remove_association "Remove crop", f %>
</div>
I suspect building the model only once here may be the issue and this needs to be passed elsewhere
def new
#spray_program = SprayProgram.new
#spray_program.build_program_sprayer
#spray_program.program_crops.build
end
the model
class ProgramCrop < ApplicationRecord
belongs_to :crop
belongs_to :spray_program inverse_of: :program_crops
validates :spray_program, uniqueness: { scope: :crop }
end
No JavaScript errors being thrown in console. The dropdown renders correctly and creates one many to many association as expected. Sorry if I've missed something important it's getting late!
--Additional models
class Product < ApplicationRecord
belongs_to :farm
has_one :user, through: :farm, dependent: :destroy
has_many :program_doses, dependent: :destroy
end
```ruby
class SprayProgram < ApplicationRecord
has_one :program_sprayer, dependent: :destroy
has_many :program_crops, inverse_of: :spray_program, dependent: :destroy
has_one :sprayer, through: :program_sprayer
has_many :crops, through: :program_crops
accepts_nested_attributes_for :program_sprayer
accepts_nested_attributes_for :program_crops, reject_if: :all_blank, allow_destroy: true
# validates_associated :program_sprayer
end
params produced look like this
{"authenticity_token"=>"[FILTERED]",
"spray_program"=>
{"date(1i)"=>"2022",
"date(2i)"=>"8",
"date(3i)"=>"1",
"farm_id"=>"1",
"program_sprayer_attributes"=>{"sprayer_id"=>"1", "litres_per_min"=>"4", "speed"=>"7"},
"program_crops_attributes"=>{"0"=>{"crop_id"=>"8", "_destroy"=>"false"}}},
"commit"=>"Create Spray program"}
without the other model I can't answer for sure, but I suspect that you are missing either accepter_nested_attributes_for or inverse_of, check the note under Basic Usage in here, https://www.rubydoc.info/gems/cocoon/1.2.12
Rails 5 Note: since rails 5 a belongs_to relation is by default required. While this absolutely makes sense, this also means associations have to be declared more explicitly. When saving nested items, theoretically the parent is not yet saved on validation, so rails needs help to know the link between relations. There are two ways: either declare the belongs_to as optional: false, but the cleanest way is to specify the inverse_of: on the has_many. That is why we write : has_many :tasks, inverse_of: :project
This basically allows to have a reference to an object on the new action, without having that objects id(because it hasn't been created yet), by building the object you are giving the data to the form, but the form doesn't know how to link them, so you end up sending back to the backend two objects that don't reference each other
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Please review this, you need to tell the modal that you are accepting nested attributes, cocoon works this way, if you still not finding the solution, please share other modal too
I got it working eventually, I tried testing whether jquery was being loaded and it wasn't, cocoon didn't. I think something was wrong in my webpacker settings, after running rake assets:clobber I hit a webpack missing entry error so followed this guide after which my local server worked again.
At this point I noticed there was an error locating the cocoon module in application js after webpack compiled for my local server (I messed up the install or import somehow).
In the end i used this vanilla js version of coccoon following the instructions and it's playing nice now.

Rails 4+ Edit multiple objects in one form BEST PRACTICE

I have a User, Drinks, Gyms, Foods Model.
class User < ActiveRecord::Base
has_many :drinks
has_many :foods
has_many :gyms
end
I track the number of drinks a user had during the day and save it in the database.I do the same with Foods and Gyms.
I have a User and a Session (for login) controller. So far I haven't needed a controller for my "passiv" Models (Drink, Food, Gym).
Now I have one page with a form on which the User can change the entries of all tables(Drink, Food, Gym) of the previous day.
I think I need to use fields_for in the form to edit objects of multiple Models in one form.
However I don't know how many controllers I need and where I should put in all the business logic... I don't want to do anything quick and dirty, but rather follow certain Best Practices.
My approach so far:
A lot of forms on one page
<%= form_for :running, url: data_update_user_path do |f| %>
<%= form_for :drinks, url: data_update_user_path do |f| %>
<%= form_for :food, url: data_update_user_path do |f|
One DataController who handles all the different updates (It's basically a big if elsif)
class DataController
def update
if params[:drinks]
#update drinks
elsif params[:foods]
#update foods
elsif params[:gyms]
#update gyms
end
end
end
So my question: What is the best practice in such a situation?
Use nested form with only one controller; the users controller, the update action for the user will update it's related models as well when you use accepts_nested_attributes_for so basically your user model will be
class User < ActiveRecord::Base
has_many :foods
has_many :drinks
has_many :gyms
accepts_nested_attributes_for :foods, :drinks, :gyms
end
And the for for user will contain fields_for foods, drinks and gyms
Don't forget in your users controller if you are using strong_parameters to permit the attributes of the nested models
def user_params
params.require(:user).permit(:id, .... , foods_attributes: [:id, :destroy, ....], gyms_attributes: [:id, :_destroy, ....], drinks_attributes: [:id, :_destroy, ....])
end

Access current id for a has_one relationship in rails

I have two models in my app. One is called meetings and the other is outcome. I wanted to create the outcome of each meeting using: #outcome=current_meeting.outcome.build(params[:outcome]). Further, each meeting would have only one outcome so it is clearly a has_one relationship. I am really confused about getting the current_meeting. I have the meetings and outcomes models as:
Meeting Model
class Meeting < ActiveRecord::Base
attr_accessible :info, :meeting_date, :name, :venue
has_many :participants, :dependent => :destroy
has_one :outcome, :dependent => :destroy
validates_presence_of :name, :info, :meeting_date, :venue
end
Outcome Model
class Outcome < ActiveRecord::Base
attr_accessible :result
belongs_to :meeting
validates :meeting_id, presence: true
end
I want the new outcome to be present inside the show of the meeting, if there are none present and if there is one present creating new outcome should be made impossible
meetings/show.html.erb
<% if #meeting.outcomes.any? %>
<%= render #outcomes %>
<% else %>
<%= link_to "Add the outcome of meeting", new_outcome_path %>
<% end %>
My controllers are:
Meetings controller
def show
#meeting = Meeting.find(params[:id])
#outcomes = #meeting.outcomes.all
end
Outcomes controller
def new
#outcome = current_meeting.microposts.new
end
def create
#outcome = current_meeting.outcomes.build(params[:outcome])
if #outcome.save
flash[:success] = "Outcome created!"
redirect_to root_url
else
render 'static_pages/home'
end
end
I don't know how to find out the current_meeting. Please help.
First of all, the question is very confusing as to the plurality of outcome vs outcomes. If a Meeting has_one Outcome then you you would use the singular form when referring to the reference. Basically, given has_one :outcome, ":outcome" is the method name to be used. So you'd say meeting.outcome instead of meeting.outcomes. And the build method for has_one would be like meeting.build_outcome instead of meeting.outcomes.build. The latter is the api for a has_many relationship.
With that out of the way, if you want to get the current Meeting from the Outcomes controller, the best way to do this is with nested resources. So in the routes file you'd have, e.g.:
resources :meetings do
resources :outcomes
end
After you do that, run rake routes to see the routes available to you. In there you'll see an expected url format of POST /meetings/:id/outcomes which you would use here. So in this case, the create method would get the Meeting object from params[:id], from which the outcome relationship can be created.
At first glance it does not seem like you are defining current_meeting anywhere. You probably already know this and if so the case would be that you are unsure of how/where to define it. You will probably need to do that somewhere in the code. This could mean saying something like this meeting is current because it is during the current time and/or today. This is based on how your app works to determine this logic.
In your controller or in a helper you will need to write a method that gives you the current meeting if one exists. From there the current_meeting variable in your controller will be set correctly and should call your other methods right.
If I have misunderstood the issue I apologize and please provide any other details you can and I can try to help.

nested resources with shallow routes in rails 3

I have been working with rails for a while but I have yet to overcome the problem of using 3 level deep nested resources. When I am on the notes page I would like to link course name to the class and course but rails keeps giving me an error.
I have 3 models class, course, and notes. A class has many courses and courses belong to a class. Course has many notes and note belong to a course. I will explain below.
class.rb
has_many :courses
course.rb
belongs_to :class
has_many :schedules
has_many :notes, :through => :schedules
note.rb
has_many :schedules
has_many :courses, :through => :schedules
schedule.rb
belongs_to :course
belongs_to :note
routes.rb
resources :classes, :shallow => true do
resources :courses do
resources :notes
end
end
index.html.erb
<% #notes.each do |note| %>
<% note.courses.each do |course| %>
<%= note_class(course) %>
<% end %>
<% end %>
notes_helper.rb
def note_class(course)
link_to course.course_name, class_course_path(class, course)
end
Shallow routes works great except when rails give me an error 'undefined local variable or method `class' for'. I think my code above is right but I am not sure why it is not working correctly. Any suggestions on how I can get the course to link to a url like so mysite.com/classes/1/course/3?
I realize this question was asked a while back but I figured since it hasn't been 'answered', I'll give it a go.
Couple of things. First, the error you're getting inside the helper method is a result of not having a reference to the class instance. You're only passing the course object as a parameter. This would work given your associations above
notes_helper.rb
def note_class(course)
link_to course.course_name, class_course_path(course.class, course)
end
Second, as mentioned in one of the comments, class is a reserved keyword in Ruby so it would serve you best to avoid using it for your models and associations. Cheers.

Multiple parameters for form_for()

I'm reading Beginning Rails 3. It creates a blog with Users who can post Articles and also post Comments to these Articles. They look like this:
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
attr_accessor :password
has_many :articles, :order => 'published_at DESC, title ASC',
:dependent => :nullify
has_many :replies, :through => :articles, :source => :comments
class Article < ActiveRecord::Base
attr_accessible :body, :excerpt, :location, :published_at, :title, :category_ids
belongs_to :user
has_many :comments
class Comment < ActiveRecord::Base
attr_accessible :article_id, :body, :email, :name
belongs_to :article
in app/views/comments/new.html.erb there's a form which begins like this:
<%= form_for([#article, #article.comments.new]) do |f| %>
My confusion lies in why form_for() has two parameters. What do they resolve to and why are they necessary?
thanks,
mike
Actually, in your example, you are calling form_forwith one parameter (which is Array). If you check the documentation you will see parameters it expects: form_for(record, options = {}, &proc).
In this case a record can be ActiveRecord object, or an Array (it can be also String, Symbol, or object that quacks like ActiveRecord). And when do you need to pass it an Array?
The simplest answer is, when you have a nested resource. Like in your example, you have defined Article has many Comments association. When you call rake routes, and have correctly defined routes, you will see that Rails has defined for you different routes for your nested resource, like: article_comments POST /article/:id/comments.
This is important, because you have to create valid URI for your form tag (well not you, Rails does it for you). For example:
form_for([#article, #comments])
What you are saying to Rails is: "Hey Rails, I am giving you Array of objects as a first parameter, because you need to know the URI for this nested resource. I want to create new comment in this form, so I will give you just initial instance of #comment = Comment.new. And please create this comment for this very article: #article = Article.find(:id)."
This is roughly similar to writing:
form_for(#comments, {:url => article_comments_path(#aticle.id)})
Of course, there is more to the story, but it should be enough, to grasp the idea.
This is a form for commenting on an article. So you, you need the Article you're commenting on (#article) and a new Comment instance (#article.comments.new). The form action for this form will be something like:
/articles/1/comments
It contains the id of the article you're commenting on, which you can use in your controller.
If you omit the #article like this: form_for #article.comments.new, the form action will looks like this:
/comments
In the controller you would have no way of knowing to which article the comment belongs.
Note that for this to work, you need to define a nested resource in your routes file.

Resources