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
Related
In my app I have User and Language models.
User can have multiple languages and a language can have multiple users.
class Language < ApplicationRecord
has_and_belongs_to_many :users
end
class User < ApplicationRecord
has_and_belongs_to_many :languages
end
I want to create a form that will allow user to add a new language to the profile.
Since both User and Language models already exist, I'm wondering how to create a form that will not create any new model, but just create a relation between existing models.
Both the has_many and HABTM macros create _ids and _ids= setters and getters that make it trivial to associate different records:
<%= form_with(model: #user) do |form| %>
<div class="field">
<%= form.label :language_ids, "Languages" %>
<%= form.collection_select(:language_ids, Language.all, :id, :name, multiple: true) %>
# or if you prefer checkboxes
<%= form.collection_checkboxes(:language_ids, Language.all, :id, :name) %>
</div>
# ...
<% end %>
The form collection helpers are smart enough to iterate accross the collection and will select/check depending on if an assocation already exists.
You whitelist an array parameter by passing a hash key to permit with an empty array as its value:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
# ...
end
private
def user_params
params.require(:user)
.permit(
:foo, :bar, :baz,
langauge_ids: []
)
end
end
I would also seriously consider if you want to use has_and_belongs_to_many in the first place. Since there is no model you can't access any additional columns on the join table like for example how proficient a user is or if its their primary language. There is also no straight forward way to query the join table directly. has_many through: is a actually better solution in most cases.
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.
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 %>