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.
Related
So in my rails project, I have a Patient class, which has one Treatment class. This treatment class then has many DrNotes inside of it. I am still fairly new to rails, and I am aware that nesting this deeply is not recommended in Rails, but I am proceeding with this method.
My problem is with the editing of DrNotes. Since there are many doctor notes within treatment, I am trying to only edit one specific note. I am using Form_for to pass parameters to the doctor's note. When I submit the form, it redirects me to the page that should be shown only when the update function has succeeded. However, none of the notes are actually updated, and no errors are thrown when I try to perform the update.
Here are the models in question:
patient.rb
class Patient < ApplicationRecord
has_one :treatment, dependent: :destroy
accepts_nested_attributes_for :treatment, update_only: true
end
treatment.rb
class Treatment < ApplicationRecord
belongs_to :patient
has_many :dr_notes, class_name: "DrNote",
foreign_key: "treatment_id", dependent: :destroy
accepts_nested_attributes_for :dr_notes
end
dr_note.rb
class DrNote < ApplicationRecord
belongs_to :treatment
end
In my controller I have:
Doctor Note Edit Function
def edit_dr_note
#patient = Patient.find(params[:patient_id])
#dr_note = #patient.treatment.dr_notes.find(params[:dr_id])
#dr_note.update if #dr_note.nil?
end
Doctor Note Update Function
def update_dr_note
#patient = Patient.find(params[:patient_id])
#dr_note = #patient.treatment.dr_notes.find(params[:dr_id])
if #dr_note.update(dr_note_params)
redirect_to page_path(#patient)
else
flash.now[:error] = "Cannot update Doctor's notes"
render 'edit_dr_note'
end
end
Doctor Note Params
def dr_note_params
params.require(:dr_note).permit(:id, :name, :message)
end
I have :id in the params.permit because from researching, I heard that you need to include it when updating models, but i'm not sure if it is needed here.
I have the following code in the routes.rb
get '/pages/:patient_id/treatment/edit/edit_dr_note/:dr_id', to: 'pages#edit_dr_note', as: :edit_dr_note
match "pages/:patient_id/treatment/update/update_dr_note/:dr_id" => "pages#update_dr_note", as: :update_dr_note, via: [:patch, :post]
And in the edit_dr_note.html.erb
<%= form_for #patient.treatment.dr_notes.find(params[:dr_id]), url: update_dr_note_path do |patient_form| %>
<% #patient.treatment.dr_notes.each do |doctor| %>
<% if doctor.id == #dr_note.id %> #Only displays the fields for the desired note
<%= patient_form.fields_for :dr_note, doctor do |doctor_fields| %>
Name: <%= doctor_fields.text_field :name %>
Message: <%= doctor_fields.text_field :message %>
<% end %>
<p>
<%= patient_form.submit %>
</p>
<% end %>
<% end %>
<% end %>
Any help would be greatly appreciated. Thanks!
You are mixing two approaches(the nested resources and the nested attributes). Use one to serve your purpose.
With the nested resources:
<%= form_for [:pages, #patient, #treatment, #dr_note], url: update_dr_note_path do |dr_note| %>
Name: <%= dr_note.text_field :name %>
Message: <%= dr_note.text_field :message %>
<p>
<%= dr_note.submit %>
</p>
<% end %>
The routes would be
get '/pages/:patient_id/treatment/:treatment_id/edit_dr_note/:dr_id', to: 'pages#edit_dr_note', as: :edit_dr_note
match "pages/:patient_id/treatment/:treatment_id/update_dr_note/:dr_id" => "pages#update_dr_note", as: :update_dr_note, via: [:patch, :post]
Edit the edit_dr_note to define #treatment
def edit_dr_note
#patient = Patient.find(params[:patient_id])
#treatment = #patient.treatment
#dr_note = #patient.treatment.dr_notes.find(params[:dr_id])
#dr_note.update if #dr_note.nil?
end
And finally remove accepts_nested_attribute_for from the models, you don't need it in this approach.
With the nested attributes:
Keep the accepts_nested_attributes_for in the models. And change the routes and form like below
get '/edit_dr_note/:dr_id', to: 'pages#edit_dr_note', as: :edit_dr_note
match "/update_dr_note/:dr_id" => "pages#update_dr_note", as: :update_dr_note, via: [:patch, :post]
And the form_for
<%= form_for #patient, url: update_dr_note_path do |patient| %>
<%= patient.fields_for :treatment do |t| %>
<%= t.fields_for :dr_notes, #dr_note do |dr_note| %>
Name: <%= dr_note.text_field :name %>
Message: <%= dr_notetext_field :message %>
<% end %>
<% end %>
<p>
<%= patient.submit %>
</p>
<% end %>
And change the dr_note_params method as below
def dr_note_params
params.require(:patient).permit(:id, treatment_attributes: [:id, dr_notes_attributes: [:id, :name, :message])
end
When you write the following line, you're trying to find a DrNote using the dr_id:
#dr_note = #patient.treatment.dr_notes.find(params[:dr_id])
Whereas the dr_notes relation on Treatment does not seem to define any particular behavior, and this is your problem.
You'll need to find_by doctor's id (or dr_id in your code) and thus first define the relation on DrNote.
I'm kinda new to ruby on rails, I've been reading documentation on assosiations and I've been having an easy time (and usually a quick google search solves most of my doubts) however recently I'm having problems with a seemingly easy thing to do.
What I'm trying to do is to create an Event, linked to an existing Category.
Event model
class Event < ApplicationRecord
has_many :categorizations
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations
.
.
.
end
Category model
class Category < ApplicationRecord
has_many :categorizations
has_many :events, through: :categorizations
end
Categorization model
class Categorization < ApplicationRecord
belongs_to :event
belongs_to :category
end
Event controller
class EventsController < ApplicationController
def new
#event = Event.new
end
def create
#user = User.find(current_user.id)
#event = #user.events.create(event_params)
if #event.save
redirect_to root_path
else
redirect_to root_path
end
end
private
def event_params
params.require(:event).permit(:name, category_ids:[])
end
Here is the form, which is where I think the problem lies:
<%= form_for #event, :html => {:multipart => true} do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :categorizations do |categories_fields|%>
<% categories = [] %>
<% Category.all.each do |category| %>
<% categories << category.name %>
<% end %>
<%= categories_fields.label :category_id, "Category" %>
<%= categories_fields.select ( :category_id, categories) %>
<% end %>
.
.
.
<%= f.submit "Create"%>
<% end %>
I previously populate the Category db with some categories, so what's left to do is to while creating an event, also create a categorization that is linked both to the new event and the chosen Categorization. but the things I've tried don't seem to be working.
Other things seem to be working ok, whenever I try to submit the event all things are populated as expected except the categorization.
As you mentioned that you are new to rails, you'll find this cocoon gem very interesting. You can achieve what you wanted. And the code will cleaner.
I don't have the points to comment, that's why I am giving this as an answer.
I have an enrollment form where a user can enroll to some sort of event.
However, I want to give the posibility for teams to enroll also and I was thinking about a wizard like form.
Basically create 5 records at a time.
The problem is, I'll have a new enrollment creation on each step, so I thought the wicked gem would not do it for this scenario.
Can you give me a few guidelines on how should I approach this?
Maybe just render new after creation if a i.e. team attr is sent from the form?
Maybe use self join?
That's off the top of my head but I know there has to be a clever way to do this.
I'm not sure how your models are structured, but if you have something like:
class Attendee
has_many :enrolments
has_many :events, through: :enrolments
end
class Enrolment
has_many :attendees
belongs_to :event
end
class Event
has_many :enrolments
has_many :attendees, through: :enrolments
accepts_nested_attributes_for :enrolments
end
Then you can do something like:
# controllers/enrolments_controller.rb
class EnrolmentController < ApplicationController
def new
#event = Event.find(params[:event_id])
pax = params[:persons].to_i
pax.times do
#event.enrolments.build
end
end
def create
#event = Event.find(params[:event_id])
#event.enrolments.build(enrolment_params)
#event.save
end
protected
def enrolment_params
# require specific parameters here
params.require(:event).permit(:attendee_attributes => [])
end
end
# views/enrolments/new.html.erb
<%= form_for #event, url: event_enrolments_path(#event) do |f| %>
<%= f.hidden_field :event_id %>
<%= f.fields_for :enrolments do |af| %>
<%= af.select :attendee_id, Attendee.all.collect {|p| [ p.name, p.id ] } %>
<% end %>
<%= f.submit %>
<% end %>
# routes.rb
resources :events do
resources :enrolments
end
That's off the top of my head, but the general idea is that you build the nested fields by running event.enrolments.build based on the number of people passed in the params.
This uses fields_for and accepts_nested_attributes_for. This also makes it really convenient to reuse existing forms by passing in the form context in the partial:
<%= f.fields_for :enrolments do |af| %>
<%= render "enrolments/form", f: af %>
<% end %>
I want a User to be able to answer all questions that are assigned to them, in an Answer model. Now I'm trying to create a form that allows me to loop through the questions a User have assigned to them, and answer them in an Answer model.
In the answer model I save the reply, and the question id. However this requires multiple saves in one form, which I'm unable to do.
Model associations look like this:
User
has_many :answers
has_many :questions, through: :question_participants
Answer
belongs_to :user
belongs_to :question
now I'm trying to create an Answer#new form like this:
<%= form_for #answer do |f| %>
<% #questions.each do |question| %>
<h3><%= question.name %></h3>
<%= f.hidden_field :question_id, value: question.id %>
<%= f.text_field :reply, class: 'form-control' %>
<% end %>
<%= f.submit 'Send inn', class: 'btn btn-success' %>
<% end %>
and thus hoping it will allow me to save multiple columns in one, but that doesn't work. It only saves the last column, no matter what.
My answers controller:
class AnswersController < ApplicationController
def new
#questions = current_user.questions
#answer = current_user.answers.new
end
def create
#questions = current_user.questions
#answer = current_user.answers.new(answer_params)
if #answer.save
redirect_to answers_path
else
render 'new'
end
end
private
def answer_params
params.require(:answer).permit(:reply, :question_id)
end
end
What you're looking for is accepts_nested_attributes_for:
This should work:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :answers
has_many :questions, through: :answers
accepts_nested_attributes_for :answers
#this will have to be populated on user create
before_create :build_answers
private
def build_answers
questions = Question.find [1,3,4,6]
questions.each do |question|
user.build_answer(question: question)
end
end
end
#app/models/answer.rb
class Answer < ActiveRecord::Base
#columns id | user_id | question_id | response | created_at | updated_at
belongs_to :user
belongs_to :question
end
#app/models/question.rb
class Question < ActiveRecord::Base
has_many :answers
has_many :users, through: :answers
end
This will give you the ability to do the following:
#config/routes.rb
resources :answers, only: [:edit, :update]
#app/controllers/answers_controller.rb
class AnswersController < ApplicationController
def edit
#questions = current_user.questions
end
def update
#answers = current_user.answers.update answer_params
end
private
def answer_params
params.require(:answer).permit(:response) #-> question_id and user_id set on create, don't need to be changed
end
end
This will allow you to use the following form:
#app/views/answers/edit.html.erb
<%= form_tag answers_update_path, method: :patch do |f| %>
<% #questions.each do |question| %>
<%= f.fields_for "answers[]", question do |qf| %>
<%= qf.label question.title %>
<%= qf.text_field :response %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
--
Typically, you'd use the accepts_nested_attributes_for with the nested model. However, since you just want multiple answer responses, you can use the above.
The bugs in this would likely be in the strong params, or in the form declaration (IE current_user.questions). If you reply with information, I'll write some upates
Ref: Multiple objects in a Rails form
I want a user to create an event, on this event they need to be able to create a dynamic form form for the user to 'register' for the event (creating fields on the fly). Data which is then submitted to this form will get stored in the database. I've used most of this tutorial to get me to where I am now.
My structure looks like this so far:
class Admin::Event < ActiveRecord::Base
attr_accessible :body, :date, :title, :end_date, :status, :event_location_id, :payment, :fields_attributes, :answers
belongs_to :event_location
has_many :fields, class_name: "Admin::EventField"
has_many :event_surveys
accepts_nested_attributes_for :fields, allow_destroy: true
end
I have been able to successfully dynamically create fields against an event which get stored in admin_event_fields table. Done exactly how the Rails Cast tutorial does it.
class Admin::EventField < ActiveRecord::Base
belongs_to :event
attr_accessible :field_type, :name, :required
end
I then have this model, my idea is to store the answers in a hash; so when one user submits a 'survey' it gets saved against the admin_event_id then the answers get stored in the answers hash. This should repeat when another user submits an answer... My Issue is, saving the answers to Admin::EventSurvey... I can't figure out a logical way to do this.
class Admin::EventSurvey < ActiveRecord::Base
attr_accessible :admin_event_id, :answers
has_one :admin_event, :class_name => "Admin::Event"
accepts_nested_attributes_for :admin_event, :allow_destroy => true
serialize :answers, Hash
end
Because the form is going to be submitable from the show action, i've put a form in show.html.erb.
Admin::EventsController:
# Admin::EventsController
# GET /events/1
# GET /events/1.json
def show
#event = Admin::Event.find(params[:id])
#event_survey_answer = Admin::EventSurvey.new(admin_event_id: params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #event }
end
end
Show.html.erb:
<%= form_for #event_survey_answer do |f| %>
<% if #event_survey_answer.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#event_survey_answer.errors.count, "error") %> prohibited this event_survey_answer from being saved:</h2>
<ul>
<% #event_survey_answer.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% f.fields_for :admin_event do %>
<%= #event.fields.each do |field| %>
<%= render "admin/events/fields/#{field.field_type}", locals: { field: field, f: builder } %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
At the moment with the above error I'm getting this error because of the loop on #event.fields:
undefined local variable or method `builder'