So I have four database tables.
Users (:name etc..) Recipes (:name, :description, :user_id etc..), Scrapbooks (:name, :description, :user_id) and Scrapbook_Entry (:user_id, recipe_id, :scrapbook_id)
I am able to populate the Users, Recipes and Scrapbooks tables fine but what I now want to do is have an option to save a recipe into a scrapbook. By doing this I need to populate the Scrapbook_Entry table which I have made a model for.
Scrapbook_Entry Model:
has_one :recipe
has_one :scrapbook
Recipe Model:
has_many :scrapbooks, through: :scrapbook_entries
Scrapbook Model
has_many :recipes, through: :scrapbook_entries
User Model
has_many :recipes, dependent: :destroy
has_many :scrapbooks, dependent: :destroy
I want to create a form in the Recipe view to allow me to select a scrapbook to save the recipe into and for it then to submit and populate the Scrapbook_Entry table.
My question is: Will I need to create a new controller for my scrapbook_entries and have a create method in that or would I be able to use the recipes controller and if so, how so?
I am new to rails so still trying to figure it all out. Thank you!
You will not need a new controller for this. You should be able to do something along the lines of
#recipe.scrapbook_entries.build(scrapbook: #scrapbook)
assuming you have a #recipe variable with a Recipe object in it, and a #scrapbook variable with a Scrapbook object in it.
This sounds like a job for accepts_nested_attributes_for
The way it works is that it takes a "nested model" (in your case, ScrapBookEntry), and allows you to send data directly to it from the parent model (Recipe). It's got a learning curve, but comes in very useful especially when you start dealing with lots of modular data
Accepts Nested Attributes For
There's a great RailsCast on this subject here
It works by building an ActiveRecord object for your nested model through your parent model's controller, thus allowing Rails to populate both objects when the form is submitted. This means you can add as much data as you want to your nested model whilst keeping your code efficient
Your Code
Instead of creating a new controller, you should be able to handle all the processing in your Recipes controller, like this:
#app/models/recipe.rb
Class Recipe < ActiveRecord::Base
accepts_nested_attributes_for :scrapbook_entries
end
#app/controllers/recipes_controller.rb
def new
#recipe = Recipe.new
#recipe.scrapbook_entries.build #-> repeat for number of fields you want
end
def create
#recipe = Recipe.new(recipe_params)
#recipe.save
end
private
def recipe_params
params.require(:recipe).permit(:recipe, :params, scrapbook_entries_attributes: [:extra, :data, :you, :want, :to, :save])
end
#app/views/recipes/new.html.erb
<%= form_for #recipe do |f| %>
<%= f.text_field :example_field %>
<%= f.fields_for :scrapbook_entries do |s| %>
<%= f.text_field :extra_data %>
<% end %>
<% end %>
Related
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
I have a pretty basic Rails 4 app, and am using Cocoon's nested forms to manage the has_many... :through model association.
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
# ... etc
end
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
# ... etc
end
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluation, reject_if: :all_blank
# ... etc
end
When I use Cocoon in the View, I want to use the New Assessment view to pre-fill all the Student records in order to create a new Evaluation for each one. I don't want to have to do some hacky logic on the controller side to add some new records manually, so how would I structure the incoming request? With Cocoon I see that requests have some number in the space where the id would go (I've replaced these with ?? below).
{"utf8"=>"✓", "authenticity_token"=>"whatever", "assessment"=>{"description"=>"quiz 3", "date(3i)"=>"24", "date(2i)"=>"10", "date(1i)"=>"2015", "assessments_attributes"=>{"??"=>{"student_id"=>"2", "grade" => "A"}, "??"=>{"student_id"=>"1", "grade" => "B"}, "??"=>{"student_id"=>"3", "grade"=>"C"}}, }}, "commit"=>"Create Assessment"}
I see in the Coccoon source code that this is somehow generated but I can't figure out how it works with the Rails engine to make this into a new record without an ID.
What algorithm should I use (or rules should I follow) to fill in the id above to make a new record?
"??"
Never a good sign in your params.
With Cocoon I see that requests have some number in the space where the id would go
That ID is nothing more than the next ID in the fields_for array that Rails creates. It's not your record's id (more explained below).
From your setup, here's what I'd do:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
end
#app/models/evaluation.rb
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
end
#app/models/assessment.rb
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluations, reject_if: :all_blank
end
This will allow you to do the following:
#app/controllers/assessments_controller.rb
class AssessmentsController < ApplicationController
def new
#assessment = Assessment.new
#students = Student.all
#students.each do
#assessment.evaluations.build
end
end
end
Allowing you:
#app/views/assessments/new.html.erb
<%= form_for #assessment do |f| %>
<%= f.fields_for :evaluations, #students do |e| %>
<%= e.hidden_field :student_id %>
<%= e.text_field :grade %>
<% end %>
<%= f.submit %>
<% end %>
As far as I can tell, this will provide the functionality you need.
Remember that each evaluation can connect with existing students, meaning that if you pull #students = Student.all, it will populate the fields_for accordingly.
If you wanted to add new students through your form, it's a slightly different ballgame.
Cocoon
You should also be clear about the role of Cocoon.
You seem like an experienced dev so I'll cut to the chase - Cocoon is front-end, what you're asking is back-end.
Specifically, Cocoon is meant to give you the ability to add a number of fields_for associated fields to a form. This was discussed in this Railscast...
Technically, Cocoon is just a way to create new fields_for records for a form. It's only required if you want to dynamically "add" fields (the RailsCast will tell you more).
Thus, if you wanted to just have a "static" array of associative data fields (which is I think what you're asking), you'll be able to use fields_for as submitted in both Max and my answers.
Thanks to #rich-peck I was able to figure out exactly what I wanted to do. I'm leaving his answer as accepted because it was basically how I got to my own. :)
assessments/new.html.haml (just raw, no fancy formatting)
= form_for #assessment do |f|
= f.fields_for :evaluations do |ff|
.meaningless-div
= ff.object.student.name
= ff.hidden_field :student_id, value: ff.object.student_id
= ff.label :comment
= ff.text_field :comment
%br/
assessments_controller.rb
def new
#assessment = Assessment.new
#students = Student.all
#students.each do |student|
#assessment.evaluations.build(student: student)
end
end
What i want to do -
I've got 2 models Record and Author. when calling Record.create params i whant to pass params for associated Author model.
Record has column body and Author has column name
When i try to pass as follows
Record.create { body: "some text", author: { name: 'Some name'}}
i get error ActiveRecord::UnknownAttributeError: unknown attribute: author
How can i do what i need ?
UPDATE 1
association - Record has author
Nested Attributes
You'll probably be looking for accepts_nested_attributes_for, or inverse_of - both relying on an association between your two models:
#app/models/record.rb
Class Record < ActiveRecord::Base
has_one :author
accepts_nested_attributes_for :author
end
#app/models/author.rb
Class Author < ActiveRecord::Base
belongs_to :record
end
Essentially, you'll need to build the associative data, allowing you to send the associated attributes through to your other model. I'll explain this further down the page
This is what I would do if I were you:
#app/controllers/records_controller.rb
Class RecordsController < ApplicationController
def new
#record = Record.new
#record.author.build
end
def create
#record = Record.new record_params
#record.save
end
private
def record_params
params.require(:record).permit(:record, :attributes, author_attributes: [:name])
end
end
#app/views/records/new.html.erb
<%= form_for #record do |f| %>
<%= f.text_field :record %>
<%= f.fields_for :author do |a| %>
<%= a.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>
This will allow you to save the author params / attributes upon save
--
inverse
Inverse attributes are also another idea for you.
I'm not sure whether they'll work directly in this instance, but you could use the following:
#app/models/record.rb
Class Record < ActiveRecord::Base
has_one :author, inverse_of: :author
before_create :build_record
end
#app/models/author.rb
Class Author < ActiveRecord::Base
belongs_to :record, inverse_of: :record
before_create :set_options
private
def set_options
self.draft = true unless self.record.draft.present?
end
end
This means you should be able to access the nested attribute data (I'm not sure whether you have to use accepts_nested_attributes_for still in this instance) in your other model
ActiveRecord Objects
Finally, you need to consider the role of ActiveRecord objects in this setup
Please remember you're not just passing single items of data here - you're constructing & passing objects. This means you have to consider how they work & what they mean. I'll give you a brief explanation:
Rails, because its built on Ruby, is an object-orientated framework. This means that every piece of data you create / use in this is an object. Objects are much different than variables - they are deeper & have much more data contained within them, allowing them to be used in a variety of different ways:
Rails makes use of objects in many different ways; the main one being that a lot of the helpers & other methods build themselves around the objects. That's why you get the resources directive in your routes, and can do the following: <%= link_to #user.name, #user %>
The problem many people have is they don't understand the value of object-orientation in a Rails app, and consequently try and think about their logic from the perspective of a disjointed system. Conversely, and this will help you tremendously, you need to consider that every time you create a record, you're building an object, and consequently, you need to ensure you build your app around them.
As noted, you have to ensure you have an association between the objects you wish to create. If you do that, you'll be able to build them both at the same time
Try this hopefully will solve your problem:
class Record < ActiveRecord::Base
has_one :author
accepts_nested_attributes_for :author, :reject_if => lambda { |a| a[:name].blank? }, :allow_destroy => true
end
And for more details see accepts_nested_attributes_for
Basically I have a Shop, Category and a join model ShopCategory with additional attributes
class Shop
has_many :shop_categories
has_many :categories, through: :shop_categories
class Category
has_many :shop_categories
has_many :shops, through: :shop_categories
class ShopCategory
belongs_to :shop
belongs_to :category
I have a shop form which I'd like to create or update the shop through it.
My first thought is to create a virtual attribute called :categories and to have the model handle the setter and getter through it, something like this (pseudocode for simplicity):
def categories=(cats)
cats.each do |c|
check if a ShopCategory exists with this shop (self) and that category.
if doesn't exist, create one, if exists ignore
for all the categories in self that weren't touched, delete that ShopCategory
end
end
but I feel this would cause problems in the long run because of the connection of 3 models and not though a controller
However, I can't seem to think of a simple way to have a create and update methods in the shops_controller for handling this
def update
#shop = Shop.find params[:id]
cats = params[:shop].delete :categories
#shop.update_attributes(shop_params)
## should I have a category update method here? How would I handle errors? This gets complicated
end
It sounds like you want a nested model form, for editing both a Shop and its associated ShopCategories.
Basically, what it entails is on the form for your Shop, you can simply iterate over the associated ShopCategories and print out fields for them, to edit them all together. Rails will automatically handle it all, as long as the parameters are structured correctly.
https://github.com/nathanvda/cocoon is a gem for making nested model forms easier.
There is also a tutorial on Railscasts:
http://railscasts.com/episodes/196-nested-model-form-revised
Collections
I don't know how experienced you are with Ruby on Rails, but you may wish to look at some of the documentation pertaining to collections
What you're looking at is how to populate your collections - which is actually relatively simple:
#app/controllers/shops_controller.rb
Class ShopsController < ApplicationController
def create
#shop = Shop.new(shop_params)
#shop.save
end
private
def shop_params
params.require(:shop).permit(:your, :attributes, category_ids: [])
end
end
This will allow you to use the following form:
#app/views/shops/new.html.erb
<%= form_for #shop do |f| %>
<% Category.all.each do |category| %>
<%= f.check_box :category_ids, category.id %>
<% end %>
<% end %>
--
Modularity
In terms of validating your collections for uniqueness, you will be best using DB, or Association-level validation:
class Shop
has_many :categories, -> { uniq }, through: :shop_categories
This will essentially create only unique categories for your shop, which you can populate with the method described above.
I have a many-to-many relationship between Recipes and Ingredients. I am trying to build a form that allows me to add an ingredient to a recipe.
(Variants of this question have been asked repeatedly, I have spent hours on this, but am fundamentally confused by what accepts_nested_attributes_for does.)
Before you get scared by all the code below I hope you'll see it's really a basic question. Here are the non-scary details...
Errors
When I display a form to create a recipe, I am getting the error "uninitialized constant Recipe::IngredientsRecipe", pointing to a line in my form partial
18: <%= f.fields_for :ingredients do |i| %>
If I change this line to make "ingredients" singular
<%= f.fields_for :ingredient do |i| %>
then the form displays, but when I save I get a mass assignment error Can't mass-assign protected attributes: ingredient.
Models (in 3 files, named accordingly)
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredient_id
has_many :ingredients, :through => :ingredients_recipes
has_many :ingredients_recipes
accepts_nested_attributes_for :ingredients
accepts_nested_attributes_for :ingredients_recipes
end
class Ingredient < ActiveRecord::Base
attr_accessible :name, :recipe_id
has_many :ingredients_recipes
has_many :recipes, :through => :ingredients_recipes
accepts_nested_attributes_for :recipes
accepts_nested_attributes_for :ingredients_recipes
end
class IngredientsRecipes < ActiveRecord::Base
belongs_to :ingredient
belongs_to :recipe
attr_accessible :ingredient_id, :recipe_id
accepts_nested_attributes_for :recipes
accepts_nested_attributes_for :ingredients
end
Controllers
As RESTful resources generated by rails generate scaffold
And, because the plural of "recipe" is irregular, inflections.rb
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'recipe', 'recipes'
end
View (recipes/_form.html.erb)
<%= form_for(#recipe) do |f| %>
<div class="field">
<%= f.label :name, "Recipe" %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :ingredients do |i| %>
<div class="field">
<%= i.label :name, "Ingredient" %><br />
<%= i.collection_select :ingredient_id, Ingredient.all, :id, :name %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Environment
Rails 3.2.9
ruby 1.9.3
Some things tried
If I change the view f.fields_for :ingredient then the form loads (it finds Recipe::IngredientRecipe correctly, but then when I save, I get a mass-assignment error as noted above. Here's the log
Started POST "/recipes" for 127.0.0.1 at 2012-11-20 16:50:37 -0500
Processing by RecipesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"/fMS6ua0atk7qcXwGy7NHQtuOnJqDzoW5P3uN9oHWT4=", "recipe"=>{"name"=>"Stewed Tomatoes", "ingredient"=>{"ingredient_id"=>"1"}}, "commit"=>"Create Recipe"}
Completed 500 Internal Server Error in 2ms
ActiveModel::MassAssignmentSecurity::Error (Can't mass-assign protected attributes: ingredient):
app/controllers/recipes_controller.rb:43:in `new'
app/controllers/recipes_controller.rb:43:in `create'
and the failing lines in the controller is simply
#recipe = Recipe.new(params[:recipe])
So the params being passed, including the nested attributes, are incorrect in some way. But I have tried lots of variants that fix-one-break-another. What am I failing to understand?
Thanks to clues from all, I have found what was wrong with my approach. Here's how I solved it.
I had originally tried with a simple HABTM many-to-many relationship, where the join table was named following standard Rails convention: ingredients_recipes. Then I realized that in a way, accepts_nested_attributes_for is designed for a 1-to-many relationship. So I converted to using has_many_through, creating a model IngredientsRecipes.
That name was the core problem, because Rails needs to be able to convert from plural to singular when using build to create form elements. This caused it to look for the non-existant class Recipe::IngredientsRecipe. When I changed my form so it used fields_for :ingredient the form displayed, but still failed to save with a mass assignment error. It even failed when I added :ingredients_attributes to attr_accessible. It still failed when I added #recipe.ingredients.build to RecipesController#new.
Changing the model to a singular form was the final key to resolve the problem. IngredientsRecipe would have worked, but I chose RecipeIngredients, as it makes more sense.
So to summarize:
can't use accepts_nested_attributes_for with has_and_belongs_to_many; need has_many with through option. (Thanks #kien_thanh)
adding accepts_nested_attributes_for creates a accessor that must be added to attr_accessible in the form <plural-foreign-model>_attributes, e.g. in Recipe I added attr_accessible :name, :ingredients_attributes (Thanks #beerlington)
before displaying the form in the new method of the controller, must call build on the foreign model after creating a new instance, as in 3.times { #recipe.ingredients.build }. This results in HTML having names like recipe[ingredients_attributes][0][name] (Thanks #bravenewweb)
join model must be singular, as with all models. (All me :-).
If you inspect the form that is generated, you'll notice that the nested fields have a name like "ingredients_attributes". The reason you're getting the mass-assignment error is because you need to add these fields to the attr_accessible declaration.
Something like this should fix it (you'll need to doublecheck the field names):
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
#...
end
Update: There's a similar answer here
Leave the call as
<%= f.fields_for :ingredients do |i| %>
But before that do
<% #recipe.ingredients.build %>
Im guessing that will allow your form to be created the right way, but there are likely other errors with your models, I can look # it more in detail when I have more time if its still not working, but:
As far as what accepts_nested_attributes_for does, when you pass in a correctly formatted params hash to the Model.new or Model.create or Model.update, it allows those attributes on the related model to be saved if they are in the params hash. In addition though, you do need to make the attributes accessible if they are unaccessible in the parent model as stated by beerlington.
I think you just need set up a one-to-many association, one recipe has many ingredients and one ingredient belongs to one recipe, so your model look like:
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
has_many :ingredients
accepts_nested_attributes_for :ingredients
end
class Ingredient < ActiveRecord::Base
attr_accessible :name, :recipe_id
belongs_to :recipe
end
You are built right form, so I don't write it again here. Now in your new and create controller will be like this:
def new
#recipe = Recipe.new
# This is create just one select field on form
#recipe.ingredients.build
# Create two select field on form
2.times { #recipe.ingredients.build }
# If you keep code above for new method, now you create 3 select field
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
...
else
...
end
end
How does params[:recipe] look like? If you just have one select field, maybe like this:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 } ] } }
If you have 2 ingredient select field:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 }, { id: 2 } ] } }