I'm building a simple top-to-bottom Workout Routine app on ROR. I'm able to create a Workout Day (parent) and an Exercise (child) on the same form. But I can't seem to save the Weighted Set (grandchild) when I submit the form. The interesting thing is that since the Exercise is saved, I can go to that exercise edit page, add a Weighted Set, and the Weighted Set will show up in the Workout Day show page. I think it has to do with the Weighted Set not being associated with the Exercise at the time of creation. How cam I tie wll three models together? I know I'm close!
I have the whole app on github. I the link isn't working, try this URL https://github.com/j-acosta/routine/tree/association
Models
class WorkoutDay < ApplicationRecord
has_many :exercises, dependent: :destroy
has_many :weighted_sets, through: :exercises
accepts_nested_attributes_for :exercises
accepts_nested_attributes_for :weighted_sets
end
class Exercise < ApplicationRecord
belongs_to :workout_day, optional: true
has_many :weighted_sets, dependent: :destroy
accepts_nested_attributes_for :weighted_sets
end
class WeightedSet < ApplicationRecord
belongs_to :exercise, optional: true
end
Workout Day Controller
class WorkoutDaysController < ApplicationController
before_action :set_workout_day, only: [:show, :edit, :update, :destroy]
...
# GET /workout_days/new
def new
#workout_day = WorkoutDay.new
# has_many association .build method => #parent.child.build
#workout_day.exercises.build
# has_many :through association .build method => #parent.through_child.build
# #workout_day.weighted_sets.build
#workout_day.weighted_sets.build
end
...
# POST /workout_days
# POST /workout_days.json
def create
#workout_day = WorkoutDay.new(workout_day_params)
respond_to do |format|
if #workout_day.save
format.html { redirect_to #workout_day, notice: 'Workout day was successfully created.' }
format.json { render :show, status: :created, location: #workout_day }
else
format.html { render :new }
format.json { render json: #workout_day.errors, status: :unprocessable_entity }
end
end
end
...
private
# Use callbacks to share common setup or constraints between actions.
def set_workout_day
#workout_day = WorkoutDay.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def workout_day_params
params.require(:workout_day).permit(:title, exercises_attributes: [:title, :_destroy, weighted_sets_attributes: [:id, :weight, :repetition]])
end
end
New Workout Day form
<%= form_for #workout_day do |workout_day_form| %>
<% if workout_day.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(workout_day.errors.count, "error") %> prohibited this workout_day from being saved:</h2>
<ul>
<% workout_day.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= workout_day_form.label :title, 'Workout Day Name' %>
<%= workout_day_form.text_field :title %>
</div>
exercise_field will go here
<div>
<%= workout_day_form.fields_for :exercises do |exercise_field| %>
<%= exercise_field.label :title, 'Exercise' %>
<%= exercise_field.text_field :title %>
<% end %>
</div>
weighted_set_fields will go here
<div>
<%= workout_day_form.fields_for :weighted_sets do |set| %>
<%= render 'exercises/weighted_set_fields', f: set %>
<% end %>
</div>
<div>
<%= workout_day_form.submit %>
</div>
<% end %>
The culprit is the workout_day_params. In the form you have the fields of weighted_sets nested under the workout_day. But in the workout_day_params, you have weighted_sets_attributes under exercises_attributes which is the reason for your problem. Changing it to below should solve the issue.
def workout_day_params
params.require(:workout_day).permit(:title, exercises_attributes: [:title, :_destroy], weighted_sets_attributes: [:id, :weight, :repetition])
end
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection
This due to wrong associations. You should consider tweaking your associations like below
class WorkoutDay < ApplicationRecord
has_many :weighted_sets, dependent: :destroy
has_many :exercises, through: :weighted_sets
accepts_nested_attributes_for :exercises
accepts_nested_attributes_for :weighted_sets
end
class Exercise < ApplicationRecord
has_many :weighted_sets, dependent: :destroy
has_many :workout_days, through: :weighted_sets
end
class WeightedSet < ApplicationRecord
belongs_to :exercise, optional: true
belongs_to :workout_day, optional: true
end
Related
I have a little project management app.
In the app I have a Project, Item and Delivery Model.
class Project < ApplicationRecord
has_many :locations, dependent: :destroy
has_many :items, dependent: :destroy
has_many :deliveries, dependent: :destroy
end
class Item < ApplicationRecord
belongs_to :project
belongs_to :location, optional: true
has_many :delivery_items, dependent: :destroy
has_many :deliveries, through: :delivery_items
enum status: [:unscheduled, :scheduled, :delivered]
end
class Delivery < ApplicationRecord
belongs_to :project
has_many :delivery_items, dependent: :destroy
has_many :items, through: :delivery_items
enum status: [ :unapproved, :approved, :scheduled ]
end
I also have a delivery_item join table
class DeliveryItem < ApplicationRecord
belongs_to :delivery
belongs_to :item
end
I have added a new Model called location, which is a way of classifying the items into a group on the project.
class Location < ApplicationRecord
belongs_to :project
has_many :items
has_many :part_numbers, through: :items
def bulkadd(delivery)
self.items.each do |row|
batch << Product.new(row)
end
end
end
At the moment the user individually adds items to deliveries via a form on the page
<h6>Add to Delivery</h6>
<%= form_for #delivery_item, html: {class: 'form-inline'} do |form| %>
<div class="form-group">
<%= form.collection_select :delivery_id, #project.deliveries.all, :id, :date, placeholder: 'Add to Delivery', class: 'form-control' %>
</div>
<%= form.hidden_field :item_id, value: item.id %>
<div class="form-group">
<%= form.submit "Add",class: 'btn btn-primary' %>
</div>
<% end %>
I would like to simplify the process by adding a bulk add button to each location which would add all of the associated items to the delivery selected has many items.
I know that I will need the delivery_item(delivery, item).
I just cant seem to get the final part to work in my brain
When you create a has_many or has_and_belongs_to_many assocation the macro creates an others_ids setter/getter. In this case item_ids= which will automatically add/remove rows from the join table.
Its really easy to use this together with the form option helpers to create a select where the user can choose multiple records:
<%= form_for(#delivery) do |form| %>
<div class="field">
<%= f.label :item_ids, 'Select the items' %>
<%= f.collection_select :item_ids, #items, :id, :name, multiple: true %>
</div>
<% end %>
Or if you prefer checkboxes:
<%= form_for(#delivery) do |form| %>
<div class="field">
<%= f.label :item_ids, 'Select the items' %>
<%= f.collection_check_boxes :item_ids, #items, :id, :name %>
</div>
<% end %>
Replace :name with whatever attribute you want to use for the option text.
class DeliveriesController < ApplicationController
before_action :set_delivery, only: [:show, :edit, :update, :destroy]
# This avoids a database query in the view
before_action :set_items, only: [:new, :edit]
# POST /deliveries
def create
#delivery = Delivery.new(delivery_params)
if #delivery.save
redirect_to #delivery, notice: 'Delivery created'
else
set_items
render :new
end
end
# PUT|PATCH /deliveries/1
def update
if #delivery.update(delivery_params)
redirect_to #delivery, notice: 'Delivery updated'
else
set_items
render :edit
end
end
private
def set_delivery
#delivery = Delivery.find(params[:id])
end
def set_items
#items = Item.all
end
def delivery_item_params
# Passing the hash `item_ids: []` allows an array of permitted scalar types.
params.require(:delivery)
.permit(:foo, :bar, :baz, item_ids: [])
end
end
I have a form that has some prebuilt tags that the user can select on a post. These tags are set up with a has_many through: relationship. Everything seems to be working but when I save (the post does save) there is an Unpermitted parameter: :tags from the controller's save method.
Tag model:
class Tag < ApplicationRecord
has_many :post_tags
has_many :posts, through: :post_tags
end
PostTag model:
class PostTag < ApplicationRecord
belongs_to :tag
belongs_to :post
end
Post model:
class Post < ApplicationRecord
...
has_many :post_tags
has_many :tags, through: :post_tags
Post controller methods:
def update
# saves tags
save_tags(#post, params[:post][:tags])
# updates params (not sure if this is required but I thought that updating the tags might be causing problems for the save.
params[:post][:tags] = #post.tags
if #post.update(post_params)
...
end
end
...
private
def post_params
params.require(:post).permit(:name, :notes, tags: [])
end
def save_tags(post, tags)
tags.each do |tag|
# tag.to_i, is because the form loads tags as just the tag id.
post.post_tags.build(tag: Tag.find_by(id: tag.to_i))
end
end
View (tags are checkboxes displayed as buttons):
<%= form.label :tags %>
<div class="box">
<% #tags.each do |tag| %>
<div class="check-btn">
<label>
<%= check_box_tag('dinner[tags][]', tag.id) %><span> <%= tag.name %></span>
</label>
</div>
<% end %>
</div>
Again this saves, and works fine, but I'd like to get rid of the Unpermitted parameter that is thrown in the console.
Your whole solution is creative but extremely redundant. Instead use the collection helpers:
<%= form_with(model: #post) |f| %>
<%= f.collection_check_boxes :tag_ids, Tag.all, :id, :name %>
<% end %>
tags_ids= is a special setter created by has_many :tags, through: :post_tags (they are created for all has_many and HABTM assocations). It takes an array of ids and will automatically create/delete join rows for you.
All you have to do in your controller is whitelist post[tag_ids] as an array:
class PostsController < ApplicationController
# ...
def create
#post = Post.new(post_params)
if #post.save
redirect_to #post
else
render :new
end
end
def update
if #post.update(post_params)
redirect_to #post
else
render :edit
end
end
private
# ...
def post_params
params.require(:post)
.permit(:name, :notes, tag_ids: [])
end
end
any help would be most appreciated, I am rather new to Rails.
I have two models a Shopping List and a Product. I'd like to save/update multiple products to a shopping list at a time.
The suggested changes are not updating the models. I've been googling and is "attr_accessor" or find_or_create_by the answer(s)?
Attempt 1 - Existing code
Error
> unknown attribute 'products_attributes' for Product.
Request
Parameters:
{"_method"=>"patch",
"authenticity_token"=>"3BgTQth38d5ykd3EHiuV1hkUqBZaTmedaJai3p9AR1N2bPlHraVANaxxe5lQYaVcWNoydA3Hb3ooMZxx15YnOQ==",
"list"=>
{"products_attributes"=>
{"0"=>{"title"=>"ten", "id"=>"12"},
"1"=>{"title"=>"two", "id"=>"13"},
"2"=>{"title"=>"three", "id"=>"14"},
"3"=>{"title"=>"four", "id"=>"15"},
"4"=>{"title"=>"five", "id"=>"16"},
"5"=>{"title"=>""},
"6"=>{"title"=>""},
"7"=>{"title"=>""},
"8"=>{"title"=>""},
"9"=>{"title"=>""},
"10"=>{"title"=>""}}},
"commit"=>"Save Products",
"id"=>"7"}
Attempt 2 - no errors the page reloads and none of the expected fields are updated. In earnest, I am Googling around and copying and pasting code snippets in the vain hope of unlocking the right combo.
Added to Products mode
class Product < ApplicationRecord
attr_accessor :products_attributes
belongs_to :list, optional: true
end
<%= content_tag(:h1, 'Add Products To This List') %>
<%= form_for(#list) do |f| %>
<%= f.fields_for :products do |pf| %>
<%= pf.text_field :title %><br>
<% end %>
<p>
<%= submit_tag "Save Products" %>
</p>
<% end %>
<%= link_to "Back To List", lists_path %>
list controller
def update
#render plain: params[:list].inspect
#list = List.find(params[:id])
if #list.products.update(params.require(:list).permit(:id, products_attributes: [:id, :title]))
redirect_to list_path(#list)
else
render 'show'
end
list model
class List < ApplicationRecord
has_many :products
accepts_nested_attributes_for :products
end
original do nothing - product model
class Product < ApplicationRecord
belongs_to :list, optional: true
end
If you just want a user to be able to select products and place them on a list you want a many to many association:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
end
class ListItem < ApplicationRecord
belongs_to :list
belongs_to :product
end
class Product < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_products
end
This avoids creating vast numbers of duplicates on the products table and is known as normalization.
You can then select existing products by simply using a select:
<%= form_for(#list) do |f| %>
<%= f.label :product_ids %>
<%= f.collection_select(:product_ids, Product.all, :name, :id) %>
# ...
<% end %>
Note that this has nothing to with nested routes or nested attributes. Its just a select that uses the product_ids setter that's created by the association. This form will still submit to /lists or /lists/:id
You can whitelist an array of ids by:
def list_params
params.require(:list)
.permit(:foo, :bar, product_ids: [])
end
To add create/update/delete a bunch of nested records in one form you can use accepts_nested_attributes_for together with fields_for:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
accepts_nested_attributes_for :products
end
<%= form_for(#list) do |f| %>
<%= form.fields_for :products do |pf| %>
<%= pf.label :title %><br>
<%= pf.text_field :title %>
<% end %>
# ...
<% end %>
Of course fields_for won't show anything if you don't seed the association with records. That's where that loop that you completely misplaced comes in.
class ListsController < ApplicationController
# ...
def new
#list = List.new
5.times { #list.products.new } # seeds the form
end
def edit
#list = List.find(params[:id])
5.times { #list.products.new } # seeds the form
end
# ...
def update
#list = List.find(params[:id])
if #list.update(list_params)
redirect_to #list
else
render :new
end
end
private
def list_params
params.require(:list)
.permit(
:foo, :bar,
product_ids: [],
products_attrbutes: [ :title ]
)
end
end
Required reading:
Rails Guides: Nested forms
ActiveRecord::NestedAttributes
fields_for
I'm trying to create a view allowing me to create and edit values of my joining table directly. This model is called 'hires'. I need to be able to create multiple rows in my joining table for when a child hires up to 2 books. I'm having some trouble and I suspect it's down to my associations...
I have 3 models. Each Child can have 2 books:
class Child < ActiveRecord::Base
has_many :hires
has_many :books, through: :hires
end
class Hire < ActiveRecord::Base
belongs_to :book
belongs_to :child
accepts_nested_attributes_for :book
accepts_nested_attributes_for :child
end
class Book < ActiveRecord::Base
has_many :hires
has_many :children, through: :hires
belongs_to :genres
end
The controller looks like this:
class HiresController < ApplicationController
...
def new
#hire = Hire.new
2.times do
#hire.build_book
end
end
def create
#hire = Hire.new(hire_params)
respond_to do |format|
if #hire.save
format.html { redirect_to #hire, notice: 'Hire was successfully created.' }
format.json { render :show, status: :created, location: #hire }
else
format.html { render :new }
format.json { render json: #hire.errors, status: :unprocessable_entity }
end
end
end
...
private
# Use callbacks to share common setup or constraints between actions.
def set_hire
#hire = Hire.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def hire_params
params.require(:hire).permit(:child_id, book_attributes: [ :id, :book_id, :_destroy])
end
end
The view likes this:
<%= form_for(#hire) do |f| %>
<%= f.label :child_id %><br>
<%= f.select(:child_id, Child.all.collect {|a| [a.nickname, a.id]}) -%>
<%= f.fields_for :books do |books_form| %>
<%= books_form.label :book_id %><br>
<%= books_form.select(:book_id, Book.all.collect {|a| [a.Title, a.id]}) %>
<%# books_form.text_field :book_id #%>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The problem is, the hash is not submitting books_attributes as you'd expect, it's just submitting 'books':
Processing by HiresController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"xx", "hire"=>{"child_id"=>"1", "books"=>{"book_id"=>"1"}}, "commit"=>"Create Hire"}
Unpermitted parameter: books
I suspect this is because my associations for the Hire model are:
belongs_to :book
accepts_nested_attributes_for :book
which means I can't build the attributes correctly but I'm not sure how to solve this. Any help would be great, am I solving this problem badly?
Try changing books_attributes to book_attributes in strong paramters for hire_param.
def hire_params
params.require(:hire).permit(:child_id, book_attributes: [ :id, :book_id, :_destroy])
end
I'm building a Rails app (Rails 4.1.0) and I'm running into some trouble organizing the form logic. I think it is because I'm fundamentally missing some knowledge about Rails routing and how to handle it, so I apologize if this question is basic.
This is what my routes.rb looks like:
Rails.application.routes.draw do
resources :users do
resources :addresses
resources :appointments
resources :boxes do
resources :statuses
end
resources :statuses
end
end
The User signs up with their name, email, and password on the landing page. Afterwards, they are immediately presented with a form where they can create an Appointment and add an Address (using fields_for in my form - see below). This form is the same form that will be used for all future Appointment requests. Also, every time the User submits an Appointment request, a number of Boxes will be added to the database as well tied to the that User.
My question is, where do I handle all of the logic for this form submit? On the landing page, I have the form to create a new user. However, every time I add a new Appointment, would the I submit the form to the create method of the Users controller as well? That doesn't seem like the correct way to approach it since I'm creating an Appointment (and possibly creating/updating the Address).
I'm familiar with fields_for and I'm using it in my form. This is the form the user sees right after they sign up and are sent to their dashboard (which is right now, /users/show/:id). I have it rendered in a modal right now.
<%= form_for(#user) do |f| %>
<%= f.fields_for :appointments do |appt| %>
<%= appt.date_select :appointment_date %>
<%= appt.time_select :appointment_start_time %>
<%= appt.time_select :appointment_end_time %>
<%= appt.text_area :comments %>
<% end %>
<%= f.fields_for :addresses do |addr| %>
<%= addr.text_field :street_address %>
<%= addr.text_field :street_address_optional %>
<%= addr.text_field :city %>
<%= addr.text_field :state %>
<%= addr.number_field :zip_code %>
<%= addr.check_box :primary %><%= builder.label :primary %>
<% end %>
<%= f.submit "Sign Up" %>
<% end %>
This is what my UsersController looks like (haven't done anything with it yet except change the show method):
class UsersController < ApplicationController
# GET /users/1
# GET /users/1.json
def show
#user = current_user
end
# GET /users/new
def new
#user = User.new
end
# GET /users/1/edit
def edit
end
# POST /users
# POST /users.json
def create
#user = User.new(user_params)
respond_to do |format|
if #user.save
sign_in #user
format.html { redirect_to new_appointment_url, notice: 'User was successfully created.' }
format.json { render :new, status: :created, location: #appointment }
else
format.html { render :new }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
end
And here are my Models:
class User < ActiveRecord::Base
has_many :addresses, dependent: :destroy
has_many :boxes, dependent: :destroy
has_many :statuses, dependent: :destroy
has_many :appointments, dependent: :destroy
accepts_nested_attributes_for :addresses, :boxes, :statuses, :appointments
end
class Address < ActiveRecord::Base
belongs_to :user
end
class Appointment < ActiveRecord::Base
belongs_to :user
end
class Box < ActiveRecord::Base
belongs_to :user
has_many :statuses, dependent: :destroy
end
class Status < ActiveRecord::Base
belongs_to :box
belongs_to :user
end
Can you show us your def user_params in your controller? My suspicion is that it's missing an :addresses_attributes component. It should probably look something like this (I'm just making up these attributes):
def user_params
params.require(:user).permit(:first_name, :last_name, addresses_attributes: [:street, :city, :state]
end