How to add has_many :through details to a form RoR - ruby-on-rails

I'm doing what appears to be a common learning app for Ruby on Rails, the recipe app. Specifically, working on recipes and ingredients as a has_many :through relationship. Through looking at a million examples and questions, I've got my many-to-many relationship setup and my multi-model form working, but I'd like to add an additional field and can't get it working. Feels like I'm close to understanding how this stuff works. Here are the quick details:
Models:
class Ingredient < ActiveRecord::Base
has_many :recipe_ingredients
has_many :recipes, :through => :recipe_ingredients
end
class RecipeIngredient < ActiveRecord::Base
belongs_to :recipe
belongs_to :ingredient
end
class Recipe < ActiveRecord::Base
has_many :recipe_ingredients
has_many :ingredients, :through => :recipe_ingredients
accepts_nested_attributes_for :ingredients, :recipe_ingredients
def new_recipe_ingredient_attributes=(recipe_ingredient_attributes)
recipe_ingredient_attributes.each do |attributes|
recipe_ingredients.build(attributes)
end
end
def existing_recipe_ingredient_attributes=(recipe_ingredient_attributes)
recipe_ingredients.reject(&:new_record?).each do |recipe_ingredient|
attributes = recipe_ingredient_attributes[recipe_ingredient.id.to_s]
if attributes
recipe_ingredient.attributes = attributes
else
recipe_ingredient.delete(recipe_ingredient)
end
end
end
def save_recipe_ingredients
recipe_ingredients.each do |recipe_ingredient|
recipe_ingredient.save(false)
end
end
end
Controller:
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
redirect_to :action => 'show', :id => #recipe
flash[:notice] = "Your record has been saved."
else
render :action => 'new'
end
end
def update
params[:recipe][:existing_recipe_ingredient_attributes] ||= {}
#recipe = Recipe.find(params[:id])
if #recipe.update_attributes(params[:recipe])
redirect_to :action => 'show', :id => #recipe
flash[:notice] = "Your changes have been saved."
else
render :action => 'edit'
end
end
View:
<% form_for(#recipe) do |f| %>
<%= f.label :name %><br />
<%= f.text_field :name %>
etc.....
Ingredients:
<div id="recipe_ingredients">
<div class="recipe_ingredient">
<% new_or_existing = recipe_ingredient.new_record? ? 'new' : 'existing' %>
<% prefix = "recipe[#{new_or_existing}_recipe_ingredient_attributes][]" %>
<% fields_for prefix, recipe_ingredient do |ri_form| %>
<p>
<%= ri_form.collection_select(:id, Ingredient.find(:all), :id, :name, :include_blank => true) %>
<%= ri_form.text_field :amount %>
</p>
<% end %>
</div>
</div>
</p>
<p>
<%= f.submit 'Create' %>
</p>
<% end %>
Sorry for the wall of code, hopefully it makes sense. The thing I can't understand is why the "amount" text field doesn't work. I've tried a million different ways, but can't get it working. In this case, the error I get is "undefined method `amount' for #"
What key connection am I missing here? Thanks.

At first glance it appears you should simply replace:
<% fields_for prefix, recipe_ingredient do |ri_form| %>
with:
<%= fields_for prefix, recipe_ingredient do |ri_form| %>

Related

Simple_form_for many to many with validation

Setup
I have a simple many to many relationship between a Submit and an Answer through SubmitAnswer.
Answers are grouped by a Question (in my case each question has three answers) - think of it as a multiple choice quiz.
I have been trying to use SimpleFormFor to make a form which renders a predetermined set of questions, where each question has a predetermined set of answers.
Something like this:
#form
<%= simple_form_for Submit.new, url: "/questionnaire" do |f| %>
<% #questions.each do |question| %>
<%= f.association :answers, collection: question.answers %>
<% end %>
<%= f.submit :done %>
<% end %>
#controller
def create
#submit = Submit.new(submit_params)
#submit.user = current_user
if #submit.save
redirect_to root_path
else
render :new
end
end
def submit_params
params.require(:submit).permit(answer_ids: [])
end
When I submit the form, Rails creates the join table, SubmitAnswers, automatically.
So here is the crux of the matter: Whats the easiest way to re-render the form, errors and all, if not all questions have been answered, ie if #submit.answers.length != #question.length ?
I can add a custom error with errors.add(:answers, 'error here'), but when I re-render, the correctly selected answers arent repopulated, which is suboptimal.
For completions sacke, here are my models:
class Submit < ApplicationRecord
belongs_to :user
has_many :submit_answers
has_many :answers, through: :submit_answers
end
class SubmitAnswer < ApplicationRecord
belongs_to :submit
belongs_to :answer
end
class Answer < ApplicationRecord
has_many :submit_answers
has_many :submits, through: :submit_answers
end
Alright, after some digging we did find the answer to make the form work, albeit with more pain that we anticipated a simple many-to-many should take.
#model
class Submit < ApplicationRecord
belongs_to :user
has_many :submit_answers
has_many :answers, through: :submit_answers
accepts_nested_attributes_for :submit_answers
end
#controller
def new
#submit = Submit.new
#questions.count.times { #submit.submit_answers.build }
end
def create
#submit = Submit.new(submit_params)
#submit.user = current_user
if #submit.save
redirect_to root_path
else
render :home
end
end
def submit_params
params.require(:submit).permit(submit_answers_attributes:[:answer_id])
end
#form
<%= simple_form_for #submit do |f| %>
<%= f.simple_fields_for :submit_answers do |sa| %>
<%= sa.input :answer_id, collection: #answers[sa.options[:child_index]], input_html: { class: "#{'is-invalid' if sa.object.errors.any?}"}, label: #questions[sa.options[:child_index]].name %>
<div class="invalid-feedback d-block">
<ul>
<% sa.object.errors.full_messages.each do |msg| %>
<li> <%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.submit :done %>
<% end %>
The solution is to use simple_fields_for/fields_for. Note that <%= sa.input :answer_id %> must be :answer_id, not :answer, which is something I had tried before.
Also one must allow accepts_nested_attributes_for :submit_answers, where :submit_answers is the join_table.
I prebuild my SubmitAnswers like so: #questions.count.times { #submit.submit_answers.build } which generates an input field for each question, all of which get saved on the form submit, a la build.
For the strong_params one needs to permit the incoming ids:
params.require(:submit).permit(submit_answers_attributes:[:answer_id]), so in this case submit_answers_attributes:[:answer_id].
For anyone wondering what the params look like:
{"authenticity_token"=>"[FILTERED]",
"submit"=>
{"submit_answers_attributes"=>
{"0"=>{"answer_id"=>""}, "1"=>{"answer_id"=>""}, "2"=>{"answer_id"=>""}, "3"=>{"answer_id"=>""}, "4"=>{"answer_id"=>""}, "5"=>{"answer_id"=>""}, "6"=>{"answer_id"=>""}}},
"commit"=>"done"}
As for the errors, im sure there might be a better way, but for now I have just manually added them with input_html: { class: "#{'is-invalid' if sa.object.errors.any?}"}.
On a final note, the sa.object # => SubmitAnswer allows me to retrieve the Model, the errors of that Model or whatever else one might want.

Rails nested form attributes not getting saved

I've already looked through every other stackoverflow for this issue, but none of the solutions have fixed this. My elements in a nested_form are not being saved in the database. I've also made sure that all model associations are correct. I've been trying to fix this for nearly 8 hours now, and would really appreciate some help, especially considering every other solution hasn't worked.
Basically, I have a Playlist model that contains multiple Song models. I'm trying to use a nested_form to add the Song models to the Playlist. However, none of the Songs are ever being saved. I apologize if my methods are misguides, as I'm still fairly new to Rails.
GitHub Repo:https://github.com/nsalesky/Ultra-Music
playlists_controller.rb
def index
#user = current_user
#playlists = #user.playlists
end
def show
#user = current_user
#playlist = #user.playlists.find(params[:id])
end
def new
#playlist = Playlist.new
#I was told to do this
#playlist.songs.build
end
def create
#user = current_user
#playlist = #user.playlists.create(playlist_params)
if #playlist.save
redirect_to #playlist
else
render :action => 'new'
end
end
def edit
#playlist = current_user.playlists.find(params[:id])
end
def update
#user = current_user
#playlist = #user.playlists.find(params[:id])
if #playlist.update_attributes(playlist_params)
redirect_to #playlist
else
render :action => 'edit'
end
end
def destroy
#user = current_user
#playlist = #user.playlists.find(params[:id])
#playlist.destroy
redirect_to playlists_path(#user.playlists)
end
private
def playlist_params
params.require(:playlist).permit(:name, :description, songs_attributes: [:id, :name, :link, :_destroy])
end
playlist.rb
belongs_to :user
has_many :songs, dependent: :destroy
accepts_nested_attributes_for :songs, :allow_destroy => true, :reject_if => lambda { |a| a[:content].blank? }
validates :name, presence: true
validates_associated :songs, presence: true
_form.html.erb
<%= nested_form_for #playlist do |f| %>
<div>
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<div>
<%= f.label :description %>
<%= f.text_field :description %>
</div>
<!--<div>
<button type="button" id="addsong">Add Song</button><br>
<button type="button" id="removesong">Remove Song</button><br>
</div> !-->
<div>
<%= f.fields_for :songs do |song_form| %>
<%= song_form.text_field :name %>
<%= song_form.text_field :link %>
<%= song_form.link_to_remove "Remove Song" %>
<% end %>
<p><%= f.link_to_add "Add Song", :songs %></p>
</div>
<div>
<%= f.submit %>
</div>
<% end %>
In your playlist.rb, you wrote:
:reject_if => lambda { |a| a[:content].blank? }
Here the block parameter |a| stands for attributes of a specific song. So a[:attribute] relates to a single attribute. The problem is your Song doesn't have a :content attribute. So this a[:content].blank? will always be true, means you would be rejected building a song.
Just change a[:content] to a valid attribute such as a[:name]

acts as commentable: comment body formatting

I think I have a working version of acts_as_commenting_with_threading in my rails app, but it seems like the body of every comment is saved with weird formatting. How do I remove the formatting in my view so it only displays the text (and not the formatting)? For example, if I type the text "test comment," the body of the comment is saved as "---\nbody: test comment\n". I tried html_safe, but it didn't work.
step.rb
class Step < ActiveRecord::Base
extend FriendlyId
acts_as_commentable
friendly_id :position
has_ancestry :orphan_strategy => :adopt
attr_accessible :description, :name, :position, :project_id, :images_attributes, :parent_id, :ancestry, :published_on
belongs_to :project
has_many :images, :dependent => :destroy
accepts_nested_attributes_for :images, :allow_destroy => :true
validates :name, :presence => true
end
comments_controller.rb
class CommentsController < ApplicationController
def create
#project = Project.find(params[:project_id])
#commentText = params[:comment]
#user = current_user
#comment = Comment.build_from(#project.steps.find(params[:step_id]), #user.id, #commentText)
respond_to do |format|
if #comment.save
format.html {redirect_to :back}
else
format.html { render :action => 'new' }
end
end
end
end
show.html.erb:
<div class="stepComments">
<% if step.comment_threads.count >0 %>
<% step.comment_threads.each do |stepComment| %>
<% if stepComment.body.length>0 %>
<%= render :partial => 'comments', :locals => {:comment=> stepComment} %>
<% end %>
<br>
<% end %>
<% end %>
</div>
_comments.html.erb
<div class="comment">
<div class="userIcon">
<%= User.find(comment.user_id).username %>
<%= image_tag(User.where(:id=>comment.user_id).first.avatar_url(:thumb), :class=>"commentAvatar img-polaroid")%>
</div>
<div class="field">
<%= comment.body %>
</div>
</div>
This prints: "---\nbody: test comment\n"
The rails helper simple_format will print using the formatting rules so you will get just the text.
For example, <% simple_format(comment.body) %>
I couldn't figure out a way to do it besides just edited the string manually. This is what I ended up using:
<%= comment.body.slice((comment.body.index(' ')+1..comment.body.length)) %>
It seems very odd that there isn't some built in function for doing this...
It ended up being a quite simple solution; I had been calling the parameter incorrectly. It should have been:
#commentText = params[:comment][:body]

Rails model - how to differentiate different types

I am currently working on a nested model form.
I have a subject model.
This subject model has lessons of 3 different types - tutorial, lecture and laboratory.
I am able to get the nested form working with https://github.com/ryanb/nested_form.
But I want to fix it such that in the form only 3 forms for the child(lesson model) will be produced and that their first field (lesson_type field) will be automatically filled in and fixed.
I am not too sure on how to model such a situation on Rails.
These are the codes I have so far.
Any advice on what I could try out or point out the mistakes I have made would be appreciated.
This is the form.
Right now I could get the form to show up three times on my controller but I am not sure how I could generate different values for the fields. They are all showing lecture as of now.
<%= nested_form_for(#subject, :remote=>true) do |f| %>
<div class="field">
<%= f.label :subject_code %><br />
<%= f.text_field :subject_code %>
</div>
<%= f.fields_for :lessons do |lesson_form| %>
<%= lesson_form.label :lesson_type %><br/>
<%= lesson_form.text_field :lesson_type, :value=> "lecture"%><br/>
<%= lesson_form.label :name %><br/>
<%= lesson_form.text_field :name %><br/>
<%= lesson_form.fields_for :lesson_groups do |lesson_group_form| %>
<%= lesson_group_form.label :group_index %><br/>
<%= lesson_group_form.text_field :group_index %>
<%= lesson_group_form.link_to_remove "Remove this task" %>
<% end %>
<p><%= lesson_form.link_to_add "Add a lesson_group",:lesson_groups,:id=>"open-lesson"%></p>
<% end %>
<% end %>
This is the controller. The creation will happen on the index page.
def index
#subjects = Subject.all
#subject = Subject.new
lecture = #subject.lessons.build
lecture.lesson_groups.build
lecture.destroy
tutorial = #subject.lessons.build
tutorial.lesson_groups.build
tutorial.destroy
laboratory = #subject.lessons.build
laboratory.lesson_groups.build
laboratory.destroy
respond_to do |format|
format.html # index.html.erb
format.json { render json: #subjects }
format.js
end
end
The subject model
class Subject < ActiveRecord::Base
attr_accessible :subject_code, :lessons_attributes
has_many :lessons, :dependent => :destroy
accepts_nested_attributes_for :lessons, :allow_destroy => :true, :reject_if => lambda { |a| a[:lesson_type].blank? }
end
And the lesson model
class Lesson < ActiveRecord::Base
belongs_to :subject
attr_accessible :lesson_type, :name, :subject, :lesson_groups_attributes
has_many :lesson_groups, :dependent => :destroy
accepts_nested_attributes_for :lesson_groups, :allow_destroy => true
end
Okay, I am not sure if this is to the Rails convention but I got it working according to what I want. Added the following lines in the subject model: Basically assigning the lesson type field in the model.
lecture = #subject.lessons.build
lecture.lesson_type = "lecture"
lecture.lesson_groups.build
lecture.destroy
tutorial = #subject.lessons.build
tutorial.lesson_type = "tutorial"
tutorial.lesson_groups.build
tutorial.destroy
laboratory = #subject.lessons.build
laboratory.lesson_type = "laboratory"
laboratory.lesson_groups.build
laboratory.destroy
And to make it such that they can't change the lesson type I made it read only
<%= lesson_form.text_field :lesson_type, :readonly=>true%><br/>

nil object in view when building objects on two different associations

I'm relatively new to Ruby on Rails so please don't mind my newbie level!
I have following models:
class Paintingdescription < ActiveRecord::Base
belongs_to :paintings
belongs_to :languages
end
class Paintingtitle < ActiveRecord::Base
belongs_to :paintings
belongs_to :languages
end
class Painting < ActiveRecord::Base
has_many :paintingtitles, :dependent => :destroy
has_many :paintingdescriptions, :dependent => :destroy
has_many :languages, :through => :paintingdescriptions
has_many :languages, :through => :paintingtitles
end
class Language < ActiveRecord::Base
has_many :paintingtitles, :dependent => :nullify
has_many :paintingdescriptions, :dependent => :nullify
has_many :paintings, :through => :paintingtitles
has_many :paintings, :through => :paintingdescriptions
end
In my painting new/edit view, I would like to show the painting details, together with its title and description in each of the languages, so I can store the translation of those field.
In order to build the languagetitle and languagedescription records for my painting and each of the languages, I wrote following code in the new method of my Paintings_controller.rb:
#temp_languages = #languages
#languages.size.times{#painting.paintingtitles.build}
#painting.paintingtitles.each do |paintingtitle|
paintingtitle.language_id = #temp_languages[0].id
#temp_languages.slice!(0)
end
#temp_languages = #languages
#languages.size.times{#painting.paintingdescriptions.build}
#painting.paintingdescriptions.each do |paintingdescription|
paintingdescription.language_id = #temp_languages[0].id
#temp_languages.slice!(0)
end
In form partial which I call in the new/edit view, I have
<% form_for #painting, :html => { :multipart => true} do |f| %>
...
<% languages.each do |language| %>
<p>
<%= label language, language.name %>
<% paintingtitle = #painting.paintingtitles[counter] %>
<% new_or_existing = paintingtitle.new_record? ? 'new' : 'new' %>
<% prefix = "painting[#{new_or_existing}_title_attributes][]" %>
<% fields_for prefix, paintingtitle do |paintingtitle_form| %>
<%= paintingtitle_form.hidden_field :language_id%>
<%= f.label :title %><br />
<%= paintingtitle_form.text_field :title%>
<% end %>
<% paintingdescription = #painting.paintingdescriptions[counter] %>
<% new_or_existing = paintingdescription.new_record? ? 'new' : 'new' %>
<% prefix = "painting[#{new_or_existing}_title_attributes][]" %>
<% fields_for prefix, paintingdescription do |paintingdescription_form| %>
<%= paintingdescription_form.hidden_field :language_id%>
<%= f.label :description %><br />
<%= paintingdescription_form.text_field :description %>
<% end %>
</p>
<% counter += 1 %>
<% end %>
...
<% end %>
But, when running the code, ruby encounters a nil object when evaluating paintingdescription.new_record?:
You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.new_record?
However, if I change the order in which I
a) build the paintingtitles and painting descriptions in the paintings_controller new method and
b) show the paintingtitles and painting descriptions in the form partial
then I get the nil on the paintingtitles.new_record? call.
I always get the nil for the objects I build in second place. The ones I build first aren't nil in my view.
Is it possible that I cannot build objects for 2 different associations at the same time? Or am I missing something else?
Thanks in advance!
Actually I found a pretty simple solution. I provide a hash with values for the language ids when building the records.
#languages = Language.all
#languages.each do |language|
#painting.paintingtitles.build( {:language_id => language.id} )
#painting.paintingdescriptions.build( {:language_id => language.id} )
end

Resources