Rails best practice to use polymorphic association - ruby-on-rails

I'm actually creating a taxonomy system into my application. Here is my tables schema :
Taxonomies
class CreateTaxonomies < ActiveRecord::Migration
def change
create_table :taxonomies do |t|
t.references :taxonomable, polymorphic: true
t.integer :term_id
end
end
end
Terms
class CreateTerms < ActiveRecord::Migration
def change
create_table :terms do |t|
t.string :name
t.string :type
t.integer :parent_id, default: 0
end
end
end
and here are my associations :
class Taxonomy < ActiveRecord::Base
belongs_to :taxonomable, polymorphic: true
end
class Term < ActiveRecord::Base
self.inheritance_column = :_type_disabled // because I use type as table field
has_many :taxonomies, as: :taxonomable
end
To make it more generic, I create a concern :
module Taxonomable
extend ActiveSupport::Concern
included do
has_many :taxonomies, as: :taxonomable
end
def get_associate_terms
terms = self.taxonomies.map { |t| t.term_id }
taxonomies = Term.find terms
end
end
and I'm including it into my model :
class Post < ActiveRecord::Base
include Taxonomable
end
It works fine and I can retreive the associated data using get_associate_terms. My problem is located within my controller and my form. I'm actually saving it like that :
# posts/_form.html.haml
= f.select :taxonomy_ids, #categories.map { |c| [c.name, c.id] }, {}, class: 'form-control'
# PostsController
def create
#post = current_user.posts.new( post_params )
if #post.save
#post.taxonomies.create( term_id: params[:post][:taxonomy_ids] )
redirect_to posts_path
else
render :new
end
end
This create method is saving the data correctly but if you take a look at the params attribute, you will see that the object taxonomies is waiting for a term_id but I can only use taxonomy_ids in my form. It feels kind of dirty and I wanted to have your point of view to avoid hacking around like that.
Also, and I think this problem is linked with the one above, when editing my item, the select box is not checked the actual matching category associate to the current post.
Any thoughts are welcome.
Thanks a lot
EDIT :
Here is my form
= form_for #post, url: posts_path, html: { multipart: true } do |f|
= f.select :taxonomy_ids, #categories.map { |c| [c.name, c.id] }, {}, class: 'form-control'
EDIT 2 :
I added the accepts_nested_attributes_for to my concern as it :
module Taxonomable
include do
has_many :taxonomies, as: :taxonomable, dependent: :destroy
accepts_nested_attributes_for :taxonomies
end
end
and then used fields_for in my form :
= f.fields_for :taxonomies_attributes do |taxo|
= taxo.label :term_id, 'Categories'
= taxo.select :term_id, #categories.map { |c| [c.name, c.id] }, {}, class: 'form-control'
When submitting the form, I receive it :
"post"=>{"type"=>"blog", "taxonomies_attributes"=>{"term_id"=>"2"}, "reference"=>"TGKHLKJ-567TYGJGK", "name"=>"My super post"
However, when the post has been saved with success, the associations are not. Any ideas???
EDIT 3 :
In my controller, I added this :
def post_params
params.require(:post).permit(
taxonomies_attributes: [:term_id]
)
end
When submitting the form, I've got the following error :
no implicit conversion of Symbol into Integer
According to that post Rails 4 nested form - no implicit conversion of Symbol into Integer, it's because I'm using taxonomies_attributes in my fields_for as:
= f.fields_for :taxonomies_attributes do |taxo|
However, if I remove it to :
= f.fields_for :taxonomies do |taxo|
The block is displaying nothing except the div wrapping it :
<div class="form-group">
<!-- Should be display here but nothing -->
</div>
EDIT 4 :
Finally make it works. The fact that the select box was not showing came from the fact that when creating a new resource, my #post has no taxonomies. Doing the following fix that :
def new
#product = current_user.products.new
category = #product.taxonomies.build
end
thanks to everyone

I am not sure if I completely understand your question, but one thing I noticed is that you don't really need to save in two steps.
Just add the following line to your Taxonomable module (Rails 3)
attr_accessible :taxonomies_attributes
accepts_nested_attributes_for :taxonomies
If you are using Rails 4, add only the accepts_nested_attributes_for to your Taxonomable module and each of the Controllers, make sure to allow taxonomies attributes. For example, in your PostsController, add the following to your
def post_params
params.require(:post).permit(:name, taxonomies_attributes: [:name])
end
Your create will change to:
def create
#post = current_user.posts.new( post_params )
if #post.save
redirect_to posts_path
else
render :new
end
end
This is assuming that your form uses the proper nesting via fields_for - I am not sure about that since you didn't post the form code.

Related

Nested has_many model in form repeats on Edit or if form has validation errors

If you have the following two models and using the simple_form gem to create a form:
class Poll < ApplicationRecord
has_many :poll_options, dependent: :destroy
accepts_nested_attributes_for :poll_options
end
class PollOption < ApplicationRecord
belongs_to :poll
end
The controller:
class PollsController < ApplicationController
def new
#poll = Poll.new
#poll.poll_options.build
end
def edit
end
private
def poll_params
# params.fetch(:poll, {}).permit(:poll_options_attributes)
# params.require(:poll).permit!
params.require(:poll).permit(:title, poll_options_attributes: [ :id, :destroy, :poll_id, :label ])
end
end
The form:
= simple_form_for(#poll) do |f|
= f.input :title, required: true
= f.simple_fields_for :poll_options do |option_a|
= option_a.input :label
= f.simple_fields_for :poll_options do |option_b|
= option_b.input :label
If I submit this on purpose without a required field (label), the page reloads with 4 options, and I submit again, 6 options, etc. For some reason it keeps adding two more to the form.
Also, editing a poll loads 4 options to begin with instead of the 2 saved in the database (it shows all possible combinations of the options).
Any idea on why this would be happening?
Banging my head against the wall for 2 days. Any help would be greatly appreciated.
Ugh, finally figured it out. Here is the solution...
In the controller action, build it twice:
2.times do
#poll.poll_options.build
end
In the view, only loop once. As apparently you can't have two loops to get two instances of the nested form:
= f.simple_fields_for :poll_options do |options|
= options.input :label
Try this:
private
def poll_params
params.require(:poll).permit(poll_options_attributes: [:id, :destroy, ...other poll option params])
end
UPDATE
def new
#poll = Poll.new
#poll.poll_options.build unless #poll.poll_options.any?
end
UPDATE 2
Change simple_form helper to simple_nested_form (don't forget about js in your application.js)
I've no idea anymore, sorry.

How do I create multiple new records in a single controller action?

I have a student profile page. There's a form on that page which allows you to create new Note record with for that student. Works well.
I want to add a new field above the "Note" text field labeled "Who?" which allows you to enter additional students, thereby logging notes in bulk:
I'm not quite sure how to structure my form and controller action to achieve this bulk creation of new Note records associated with each student listed.
I'm thinking of taking this approach:
POST to the same action (/notes#create) and detect the presence of the array field student_ids in the params and then do something like this:
params[:student_ids].each do |s|
note_params[:event_attributes][:student_id] = s
note = Note.new(note_params)
note.save
end
But I'd have to modify note_params so that it contains the proper student_id reference on each iteration.
Currently, on the singular form, note_params looks like this:
=> {
"content" => "test",
"event_attributes" => {
"student_id" => "2",
"user_id" => "1",
"is_milestone" => "0",
"happened_at_string" => ""
}
}
Is there a better/cleaner/easier way to do this without having to loop through each id and manually modify the params?
You don't need to modify params that way.
params.fetch(:student_ids, []).each do |student_id|
student = Student.find(student_id)
note = student.notes.new(note_params)
note.save
end
I think you'd be better creating some sort of join table for this functionality.
This way, you can create one note, and then "copy" it by simply duplicating the join records. The downside is that one note may be accessible to multiple people, but that's okay:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :created_notes, class_name: "Note", foreign_key: :user_id
has_and_belongs_to_many :notes
end
#app/models/note.rb
class Note < ActiveRecord::Base
belongs_to :user #-> to know who made it
has_and_belongs_to :users
end
Then you can use your notes#create method as follows:
#app/controllers/notes_controller.rb
class NotesController < ApplicationController
def new
#note = current_user.created_notes.new
#users = User.where.not(id: current_user.id)
end
def create
#note = current_user.created_notes.new note_params
#note.save
end
private
def note_params
params.require(:note).permit(:body, :user_ids)
end
end
Thus, you'll be able to use the collection_select (or similar) functionality to define user_ids for your note:
#app/views/notes/new.html.erb
<%= form_for #note do |f| %>
<%= f.collection_select :user_ids, #users, :id, :name %>
<%= f.submit %>
<% end %>

Create parent model from child controller

I'm working on on a web app that takes in information about companies. Information can be taken for a PreferredOffering (of stock) or an Incorporation. So in other words, when I create a new entry to either one of those models, a new company is formed.
It works out that my database is cleaner if PreferredOffering and Incorporation are children of Company, even though I'm trying to go through the preferred_offerings_controller or theincorporations_controller to create a new Company. Here lies my question; I'm trying to figure out how to configure my view and controllers to create a parent model from a child controller. I've done some research and have seen two other S/O posts on how to accomplish this with Rails 3, however it would seem that the addition of strong params adds another layer of complexity to the endeavor.
So I have my models set up like this
class Company < ActiveRecord::Base
belongs_to :user
has_one :incorporation, dependent: :destroy
has_many :preferred_offerings, dependent: :destroy
accepts_nested_attributes_for :preferred_offerings, allow_destroy: true
accepts_nested_attributes_for :incorporation, allow_destroy: true
end
.
class Incorporation < ActiveRecord::Base
belongs_to :company
end
.
class PreferredOffering < ActiveRecord::Base
belongs_to :company
end
The controller and view are what I'm iffy on.
Let's just take a look at the incorporation view/controller. If I were to configure it so that Incorporation has_one :company, I would set it up as follows:
class IncorporationsController < ApplicationController
def index
end
def new
#user=current_user
#incorporation = #user.incorporations.build
#company = #incorporation.build_company
end
def create
#incorporation = current_user.incorporations.build(incorporation_params)
end
private
def incorporation_params
params.require(:incorporation).permit(:title, :trademark_search, :user_id, :employee_stock_options, :submit, :_destroy,
company_attributes: [:id, :name, :employee_stock_options, :options_pool, :state_corp, :street, :city, :state, :zip, :issued_common_stock, :outstanding_common_stock, :fiscal_year_end_month, :fiscal_year_end_day, :user_id, :_destroy]
)
end
end
And the view would be:
<%= simple_form_for #incorporation, html: {id:"incorporationform"}, remote: false, update: { success: "response", failure: "error"} do |f| %>
(incorporation-specific fields)
<%= f.simple_fields_for :company do |company| %>
(Company-specific fields)
<% end %>
<% end %>
So my question is:
How do I need to modify my controller and view to create a Company from the incorporations_controller IF Company has_one :incorporation
Any suggestions would be much appreciated.
While it isn't the "Rails Way", there is nothing really wrong with having #company being the parent in your form, even though it is in the incorporations#new action. Your view would change to this:
<%= simple_form_for #company, html: {id:"companyform"}, remote: false, update: { success: "response", failure: "error"} do |f| %>
(company-specific fields)
<%= f.simple_fields_for :incorporation do |incorporation| %>
(incorporation-specific fields)
<% end %>
<% end %>
And your strong params would change so that Company is the parent and Incorporation is the child.
Another option would be to simply go through the Company controller. You could create two new actions: new_preferred_offering and new_incorporation. You would then create the objects in those actions. Or you could pass in some kind of :type param so that the normal new action renders one of two forms based on which one you want.

using 'reform' gem with nested routing

i want to use 'reform' gem to create object with nested attribute. i have models:
class Dish < ActiveRecord::Base
belongs_to :lunch_set
end
class Side < ActiveRecord::Base
belongs_to :lunch_set
end
class LunchSet < ActiveRecord::Base
belongs_to :restaurant
belongs_to :day
has_one :dish
has_many :sides
end
lunchset controller 'new' method:
def new
#lunch_set = #restaurant.lunch_sets.build
#form = LunchSetForm.new(dish: Dish.new, side: Side.new)
respond_to do |format|
format.html # new.html.erb
format.json { render json: #lunch_set }
end
end
routes file:
namespace :admin do
resources :restaurants do
resources :lunch_sets
resources :days do
resources :lunch_sets
end
end
end
and LunchSetForm
class LunchSetForm < Reform:Form
include DSL
include Reform::Form::ActiveRecord
property :name, on: :dish
property :name, on: :side
end
my question is how to construct views/admin/lunch_sets/_form.html , especially considering those routes? when i tried
= simple_form_for #form do |f|
= f.input :name
= f.input :name
.actions
= f.submit "Save"
but it gives me error
undefined method `first' for nil:NilClass
and points into line
= simple_form_for #form do |f|
form_for (and in turn, simple_form_for) expect the form object to have ActiveModel methods like model_name to figure out how to name your form and its inputs and to resolve the form's action url. You're close to getting it right by including Reform::Form::ActiveRecord, but there are a couple more things you need to do:
require 'reform/rails'
class LunchSetForm < Reform:Form
include DSL
include Reform::Form::ActiveRecord
property :name, on: :dish
property :name, on: :side
model :dish
end
The model :dish line tells Reform that you want the form's 'main model' to be the Dish instance. This means that it will make your form respond to the methods that ActiveModel usually provides for normal Rails models using the 'main model' to provide the values for those methods. Your form input names will look like dish[name] etc and it will post to your dishes_url. You can set your model to anything you like, but an instance whatever you choose needs to be passed into the form constructor.

Rails 3.2 Updating Nested Attributes

Here's my problem. If I go to Projects#edit, I am unable to change the course it is assigned to. If I attempt to create a new course for it, or choose from an existing one, I get the following error:
Couldn't find Course with ID=23 for Project with ID=62
app/controllers/projects_controller.rb:49:in `update'
{...
"project"=>{"title"=>"Sup",
"due_date"=>"2012-01-23",
"course_id"=>"27", # THIS IS THE ID OF THE NEW COURSE I WANT
"course_attributes"=>{"name"=>"Calc I", # THIS IS THE OLD ONE, I don't need this. Why is it passing this?
"number"=>"MATH102",
"user_id"=>"3",
"id"=>"23"},
"description"=>"bla bla bla"},
"commit"=>"Update Project",
"user_id"=>"3",
"id"=>"62"}
So I can see that it's trying to pass in the course_attributes, but it's not actually setting the new course_id. I don't understand why course_attributes is being passed, the other form is blank, and the course_attributes being passed, are the attributes of the old course. I want to set the course_id to be the course_id being passed (27 in this case).
ProjectsController
def new
#project = #user.projects.new
#courses = #user.courses # Used to populate the collection_select
#project.build_course # I was informed I need to use this to get the form_for to work
end
def edit
#project = Project.find(params[:id])
#courses = #user.courses # Used to populate the collection_select
end
def update
#project = Project.find(params[:id])
#courses = #user.courses
if #project.update_attributes(params[:project])
flash[:notice] = 'Project was successfully updated.'
redirect_to user_projects_path(#user)
else
render :edit
end
end
Line 49 is the call to update_attributes.
Other Information
project.rb
belongs_to :user
belongs_to :course
attr_accessible :course_id, :course_attributes
accepts_nested_attributes_for :course
course.rb
belongs_to :user
has_many :projects
user.rb
has_many :projects
has_many :courses
So a project has a course_id in the database. I'm currently creating or choosing an existing course on the fly on the Projects#new page. Here's my form for that. I use a JavaScript toggle to alternate between the collection_select and the two text_fields.
projects/new.html.haml
= form_for [#user, #project] do |f|
# This is shown by default
= f.collection_select :course_id, #courses, :id, :name, { prompt: true }
.hidden # This is hidden by default and shown using a toggle
= f.fields_for :course do |builder|
= builder.text_field :name, class: 'large', placeholder: 'Ex: Calculus I'
= builder.label :number, 'Number'
= builder.text_field :number, class: 'new_project_course_number'
= builder.hidden_field :user_id, value: current_user.id
Now, if I am on the new project page, and I attach it to a course by choosing an existing course, it will work correctly. The Project will be created, and the course_id will be set correctly.
If I am on the new project page, and I create a course by using my JavaScript toggle, then filling out the Course Name and Course Number, and click Create, then it will also work. The Course will be created, and the Project will be created with the correct course_id.
Sorry for the lengthy post, but I wanted to provide all information I could. Thanks!
UPDATE 1
routes.rb
resources :users do
resources :projects do
collection do
get 'completed'
match 'course/:course_number' => 'projects#course', as: 'course'
end
end
resources :courses
end
Assuming that your Projects#edit form is similiar to your Projects#new
This is creating the course_attributes in params
.hidden # This is hidden by default and shown using a toggle
= f.fields_for :course do |builder|
= builder.text_field :name, class: 'large', placeholder: 'Ex: Calculus I'
= builder.label :number, 'Number'
= builder.text_field :number, class: 'new_project_course_number'
= builder.hidden_field :user_id, value: current_user.id
That's because if a user has a current course it will create fields for every course.
If you want to be able to build a new course on the fly in edit and new change this line to:
= f.fields_for :course, #project.build_course(:user => #user) do |builder|
This will build a new course regardless of whether you're in edit or new. (Also you can remove #project.build_course in your controller this way.)
You don't list your routes, but assuming that is setup correctly, then the ProjectsController update should be able to do something like:
#course = Course.find(params[:course_id])

Resources