How to validate presence of at least one nested object? - ruby-on-rails

Code
class Survey < ApplicationRecord
has_many :questions, inverse_of: :survey, :dependent => :destroy
accepts_nested_attributes_for :questions
validates_associated :questions
end
class Question < ApplicationRecord
belongs_to :survey, inverse_of: :questions
validates_presence_of :survey
end
My Surveys Controller
def new
#survey = Survey.new
2.times {#survey.questions.build}
end
Form
<%= form_for #survey do |f|%>
<p>
<%= f.label :name%>
<%= f.text_field :name%>
</p>
<%= f.fields_for :questions do |builder|%>
<p>
<%= builder.text_area :content, rows: 3%>
</p>
<% end %>
<p><%= f.submit %></p>
<% end %>
As you can see when user creates a survey the form provides two questions, i want user to supply at least one question when creating the survey. How can it be achieve???

You could just test for the length of the array and simply do:
validates :questions, length: {minimum: 1, message: 'should have at least 1 question defined.'}

One of the options is to use custom validation:
validate :questions_count
private
# or something more explicit, like `at_least_one_question` (credits to #MrYoshiji)
def questions_count
errors.add(
:base,
'You can not save a survey without questions. Add at least one question'
) if questions.none?
end
Basically, the validation will be fired every time you create or "touch" (update) the survey object, and it will fail, if survey will not have at least one question associated.

Related

Rails has_many :through nested forms with simple form

I am trying to make a player character generator. I have a form that hopefully will allow me to attach skills with their values to a character sheet model. I made models like this:
class CharacterSheet < ApplicationRecord
has_many :character_sheet_skills, dependent: :destroy
has_many :skills, through: :character_sheet_skills
belongs_to :user
accepts_nested_attributes_for :skills
end
class Skill < ApplicationRecord
has_many :character_sheet_skills, dependent: :destroy
has_many :character_sheets, through: :character_sheet_skills
attr_reader :value
end
class CharacterSheetSkill < ApplicationRecord
belongs_to :skill
belongs_to :character_sheet
end
Character sheet model holds data about player character and skill model has all skills available in game. In CharacterSheetSkill I'd like to store the skills that the player chooses for his character together with an integer field setting the skill value.
When opening form, I already have a full list of skills in database. All I want to do in form is create a character sheet that has all of these skills with added value. I tried using "fields_for" in form, but I couldn't really get that to work. Right now it looks like this:
<%= simple_form_for [#user, #sheet] do |f| %>
<%= f.input :name %>
<%= f.input :experience, readonly: true, input_html: {'data-target': 'new-character-sheet.exp', class: 'bg-transparent'} %>
...
<%= f.simple_fields_for :skills do |s| %>
<%= s.input :name %>
<%= s.input :value %>
<% end %>
<% end %>
How can I make that form so it saves character sheet together with CharacterSheetSkills?
A better idea here is to use skills as a normalization table where you store the "master" definition of a skill such as the name and the description.
class CharacterSheetSkill < ApplicationRecord
belongs_to :skill
belongs_to :character_sheet
delegate :name, to: :skill
end
You then use fields_for :character_sheet_skills to create rows on the join table explicitly:
<%= f.fields_for :character_sheet_skills do |cs| %>
<fieldset>
<legend><%= cs.name %></legend>
<div class="field">
<%= cs.label :value %>
<%= cs.number_field :value %>
</div>
<%= cs.hidden_field :skill_id %>
</fieldset>
<% end %>
Instead of a hidden fields you could use a select if you want let the user select the skills.
Of course nothing will show up unless you "seed" the inputs:
class CharacterSheetController < ApplicationController
def new
#character_sheet = CharacterSheet.new do |cs|
# this seeds the association so that the fields appear
Skill.all.each do |skill|
cs.character_sheet_skills.new(skill: skill)
end
end
end
def create
#character_sheet = CharacterSheet.new(character_sheet_params)
if #character_sheet.save
redirect_to #character_sheet
else
render :new
end
end
private
def character_sheet_params
params.require(:character_sheet)
.permit(
:foo, :bar, :baz,
character_sheet_skill_attributes: [:skill_id, :value]
)
end
end

Rails: Passing nested attribute parameters

So im working through the Odin Project's "Flight Booker" project. https://www.theodinproject.com/courses/ruby-on-rails/lessons/building-advanced-forms. Which essentially is what it sounds like and im running into a problem with passing nested attributes.
First and foremost the Relevant Models:
class Booking < ApplicationRecord
belongs_to :passenger
belongs_to :flight
accepts_nested_attributes_for :passenger
end
class Flight < ApplicationRecord
has_many :bookings, dependent: :destroy
has_many :passengers, through: :bookings
belongs_to :to_airport, class_name: 'Airport', foreign_key: 'origin_id'
belongs_to :from_airport, class_name: 'Airport', foreign_key: 'destination_id'
end
class Passenger < ApplicationRecord
has_many :bookings, dependent: :destroy
has_many :flights, through: :bookings
end
The passenger schema just contains an email and name for right now. But the problem is when I pass the information to the "booking" controller. Here is my "New" form for booking.
<%= form_for #booking do |f| %>
<%= f.hidden_field :flight_id, value: params[:booking][:flight_num] %>
<%= f.hidden_field :passengers_num, value: params[:booking][:passengers_num] %>
<% params[:booking][:passengers_num].to_i.times do |passenger| %>
<%= fields_for :passenger do |passenger| %>
<%= passenger.label :name, 'Name', class: "Label" %>
<%= passenger.text_field :name %>
<%= passenger.label :email, 'email', class: "Label" %>
<%= passenger.email_field :email %>
<% end %>
<% end %>
<%= f.submit "Book Flight" %>
<% end %>
(Ignore the hidden fields for now, they are passed from the "Flights" search page and Im getting those just fine.)
So I am getting the multiple forms (name and email fields) but when I "Submit" I am only getting parameters for the last field sets. (So if there are 3 sets of name/email fields, I only get parameters for the last one).
It's possible im not understanding the fields_for however as I can't find a ton of good examples.
Thanks!
There could be many issues with your implementation...I'll layout a few...
Move <% params[:booking][:passengers_num].to_i.times do |passenger| %> logic into the new action of your bookings controller...ie
def new
#booking = Booking.new
3.times { #booking.passengers.new } # or whatever your logic is to display x amount of passenger fields
end
Make sure that in your bookings controller you are permitting the nested attributes like this...
params.require(:booking).permit(passengers_attributes: [:name, :email])
As far as the form, you'll need to treat it like a form within a form (makes sense...nested attributes created from a nested form!) and use the block variable...like this
<ul>
<%= f.fields_for :passengers do |passenger_form| %>
<li>
<%= passenger_form.label :name
<%= passenger_form.text_field :name %>
</li>
<!-- other permitted fields -->
<% end %>
</ul>

Check box that creates nested objects in rails

I'm building an app that creates exams. For the part where a user selects the answers on the exam, I want to use a checkbox (or a radio button) to allow them to pick the answer.
I want all the user-selected answers to be a table in itself called "responses". I can't figure out how to use a radio button to create records.
All the response record needs to do is take the ID's of the Exam, User, and Score. Score is a table that tracks the user's scores and the number of correct answers.
Here's my examination model (rails wouldn't let me use the word "exam"). I have it set for nested attributes.
class Examination < ApplicationRecord
belongs_to :user
has_many :questions, dependent: :destroy
has_many :scores
has_many :responses
has_secure_password
accepts_nested_attributes_for :responses, allow_destroy: true
end
The response model is pretty basic:
class Response < ApplicationRecord
belongs_to :user
belongs_to :score
belongs_to :examination
end
Here's the "take an exam" page:
<%= link_to "Back to all exams", examinations_path %>
<h2><%= #exam.name %></h2>
<h3><%= #exam.intro %></h3>
<%= form_for #exam do |f| %>
<%= f.hidden_field :name, value: #exam.name %>
<%= fields_for :responses do |res_f| %>
<% #exam.questions.each_with_index do |question, i| %>
<% index = i + 1 %>
<h2>Question #<%=index%></h2><span style="font-size: 24px; font-weight: normal">(<%= question.points %> Points)</span>
<hr>
<h3><%= question.body %></h3>
<% question.answers.each do |ans| %>
<table>
<tr>
<td><%= res_f.check_box :answer_id , ans.id, :examination_id , #exam.id, :user_id %></td>
<td><%= ans.body %></td>
</tr>
</table>
<% end %>
<% end %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
This code doesn't run because Rails expects the responses records to exist in order to use the form. It throws this error:
undefined method `merge' for 484:Integer
If I tweak that checkbox code to this:
<%= res_f.check_box :answer_id %>
The code will run and it will give me the following params on submit:
Started PATCH "/examinations/34" for 127.0.0.1 at 2018-02-24 16:22:41 -0800
Processing by ExaminationsController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"y4vcPByUKnDdM6NsWDhwxh8MxJLZU4TQo+/fUrmKYEfb3qLn5FVieJAYirNRaSl0w5hJax20w5Ycs/wz1bMEKw==", "examination"=>{"name"=>"Samuel Smith’s Oatmeal Stout"}, "responses"=>{"answer_id"=>"1"}, "commit"=>"Submit", "id"=>"34"}
I know it's not right but I was hoping it would create a record at least. All the checkbox has to do it create a response record. It should be able to grab the answer_id, exam_id and user_id. That's it.
Does anyone know how to do this?
Edit in response to Pablo 7:
Here are the other models (they're pretty basic right now)
class Score < ApplicationRecord
belongs_to :user
belongs_to :examination
has_many :responses, dependent: :destroy
end
class User < ApplicationRecord
has_many :examinations, dependent: :destroy
has_many :scores, dependent: :destroy
has_many :responses, dependent: :destroy
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
class Question < ApplicationRecord
belongs_to :examination
has_many :answers, dependent: :destroy
accepts_nested_attributes_for :answers, allow_destroy: true
validates_presence_of :body
validates_presence_of :question_type
end
#exam and Examination are the same. There is a "take" action in the Examination controller that allows a user to take an exam:
def take
#exam = Examination.find(params[:id])
#score = #exam.scores.build
#score.user_id = current_user.id
#score.save
end
So an exam belongs to the user that created it. The same user or a different one can take an exam using the take action. They would then have a score that belongs to them.
I think you must do some changes to your models:
A Response should belong to a Question (it's the question the user is responding).
A Response should belong to an Answer (it's the correct Answer for the question; the one that the user checks). If you want to allow multiple correct answers, this should be changed.
A Response should not belong to an Examination and should not belong to a User. In fact, a Response belongs to a Score and that's enough because the Score already belongs to an Examination and to a User.
An Examination should not have many responses. In fact, an Examination has many scores and scores have many responses. If you want, you can use has_many :responses, through: :scores
A User should not have many Responses. They have many Scores and Scores have many Responses. If you want, you can use has_many :responses, through: :scores
When you create a new score (in take), you should create empty responses for each question in the examination:
def take
#exam = Examination.find(params[:id])
#score = #exam.scores.build(user_id: current_user.id)
#exam.questions.each { |question| #score.responses.build(question_id: question.id) }
#I don't think you should save here.
#This method is like the new method
#You should save when the score is submitted
##score.save
end
In your form:
I would change the form to the score model (not examination). If you are using nested routes it could be [#exam, #score]
This may have many errors, as I cannot test it right now. I hope the idea is clear:
<%= form_for #score do |f| %>
<%= f.hidden_field :name, value: #score.examination.name %>
<% #score.responses.each_with_index do |response, i| %>
<%= f.fields_for response do |res_f| %>
<% index = i + 1 %>
<h2>Question #<%= index %></h2>
<span style="font-size: 24px; font-weight: normal">
(<%= response.question.points %> Points)
</span>
<hr>
<h3><%= response.question.body %></h3>
<%= res_f.collection_radio_buttons :answer_id, response.question.answers, :id, :body %>
<% end %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
The submit should call a method in Score model to create a Score (ScoresController.create)
Thanks Pablo, I finally got it working. Your code didn't quite work but it put me on the right path. I changed the model associations around as you suggested. That does make more sense.
Here are my models:
class Answer < ApplicationRecord
belongs_to :question
has_many :responses, dependent: :destroy
end
class Examination < ApplicationRecord
belongs_to :user
has_many :questions, dependent: :destroy
has_many :answers, :through => :questions
has_many :scores
has_secure_password
end
class Question < ApplicationRecord
belongs_to :examination
has_many :answers, dependent: :destroy
has_many :responses
accepts_nested_attributes_for :answers, allow_destroy: true, :reject_if => :all_blank
validates_presence_of :body
validates_presence_of :question_type
end
class Response < ApplicationRecord
belongs_to :score
belongs_to :answer
belongs_to :question
end
class Score < ApplicationRecord
belongs_to :user
belongs_to :examination
has_many :responses, dependent: :destroy
accepts_nested_attributes_for :responses, allow_destroy: true, reject_if: :no_answer_id?
private
def no_answer_id?(att)
att['answer_id'].blank?
end
end
I had to add that special method to the Score model to account for unchecked responses. Otherwise, it would throw an error.
I moved the "take a test" logic and view to the Score controller. With your code, I was getting a double loop (questions listed multiple times). I learned that you can actually access the responses through the "form_for" form loop using "res_f.object". That's pretty cool.
I also had to add a hidden field on the radio button collection form to get the question id.
Here it is:
<%= link_to "Back to all exams", examinations_path %><br/>
<h2><%= #exam.name %></h2>
<h3><%= #exam.intro %></h3>
<%= form_for [#exam, #score] do |f| %>
<%= f.hidden_field :user_id, value: current_user.id %>
<%= f.fields_for :responses do |res_f| %>
<h2>Question # <%= res_f.object.question.position %></h2>
<span style="font-size: 24px; font-weight: normal">
(<%= res_f.object.question.points %> Points)
</span>
<hr>
<h3><%= res_f.object.question.body %></h3>
<p><%= res_f.collection_radio_buttons :answer_id, res_f.object.question.answers, :id, :body do |b| %></p>
<div>
<%= b.radio_button %>
<%= b.label %>
<%= res_f.hidden_field :question_id, value: res_f.object.question.id %>
</div>
<% end %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
And the Scores controller:
class ScoresController < ApplicationController
def new
#exam = Examination.find(params[:examination_id])
#score = #exam.scores.build(user_id: current_user.id)
#exam.questions.each do |question|
res = #score.responses.build(question_id: question.id)
logger.info question.id
logger.info res
end
end
def create
#exam = Examination.find(params[:examination_id])
#score = #exam.scores.build(score_params)
if #score.save
redirect_to examination_path(#exam)
else
logger.info #score.errors.full_messages
redirect_to root_path
end
end
protected
def score_params
params.require(:score).permit(:examination_id, :user_id,
responses_attributes: [:id, :answer_id, :question_id, :selected])
end
end
This all works fine if there is only one correct answer. I'll have to modify it later to account for multiple answers. At least it works! I'll give you the credit Pablo.
Cheers

Rails nested form with unknown columns

I'm creating an admin interface where the admin (of a company) can add custom fields to their employees.
Example:
Models:
Employee: Basic info like name, contact info, etc (has_many employee_field_values)
EmployeeFields: These are the dynamic ones the admin can add (every company has different needs, it could be anything), lets say favorite_food
EmployeeFieldValues: The actual values based on the fields above, say pizza (belongs_to both models above)
What's a smart way of adding the EmployeeFieldValues fields while editing an employee?
I'm trying something simple like this, but not sure if I like it
# Controller
#custom_fields = EmployeeFields.all
# View
<%= form_for(#employee) do |f| %>
<%= f.text_field :first_name %>
<% #custom_fields.each do |custom_field| %>
<%= custom_field.name %>
<%= text_field_tag "employee_field_values[#{custom_field.name}]" %>
<% end %>
<%= f.submit :save %>
<% end %>
And then when updating, params[:employee_field_values] gives this:
<ActionController::Parameters {"favorite_food"=>"pizza"}>
So, not sure if this is a good direction, also I'm not sure how to handle future edits to an employee's custom_fields if they change.
I think it will be better to use EmployeeField as nested model and EmployeeFieldValue for select field.
For example:
Models
class Employee < ActiveRecord::Base
validates :name, presence: true
has_many :employee_field_values
accepts_nested_attributes_for :employee_field_values, reject_if: ->(x) { x[:value].blank? }
end
class EmployeeFieldValue < ActiveRecord::Base
belongs_to :employee
belongs_to :employee_field
end
class EmployeeField < ActiveRecord::Base
has_many :employee_field_values, inverse_of: :employee_field, dependent: :destroy
validates :title, presence: true, uniqueness: true
end
Controller
class EmployeesController < ApplicationController
def new
#employee = Employee.new
#employee.employee_field_values.build
end
end
View
= simple_form_for #employee, url: '/' do |f|
= f.input :name
= f.simple_fields_for :employee_field_values do |ff|
= ff.input :value
= ff.input :employee_field_id, collection: EmployeeField.all.map{|x| [x.title, x.id]}
Also you need to make buttons for adding/removing :employee_field_value, and you can do it with gem cocoon for example
OR you can build all objects in controller(for each EmployeeField) and do without select box

foreign key not updating

I'm developing an app for college where a user can log on & upload details of a hiking trail.
So far everything is working & I have also implemented a nested form for photos in each hiking trail. A user can log-on & create a hike.
I would like to display all the hikes which the user created in there show/profile page but when I've set up the relationships in my database & the has_many & belongs_to options in my model. I've also tried to do this with nested accepts_nested_attributes_for :hikingtrails it does none of this works.
I've checked my database when a hikingtrail is created by a user it is not updating the user_id field in the table.
I'm not sure if I'm approaching this entirely the wrong way, should I be looking at polymorphic associations?
class User < ActiveRecord::Base
attr_accessible :user_name, :email, :password, :password_confirmation, :photos_attributes, :hikingtrails_attributes
has_many :hikingtrails
accepts_nested_attributes_for :hikingtrails, :allow_destroy => :true, :reject_if => :all_blank
class Hikingtrail < ActiveRecord::Base
attr_accessible :description, :name, :looped, :photos_attributes,:directions_attributes, :user_id
has_many :photos
has_many :trails
has_many :directions
belongs_to :user
users/show.html.erb
<div class="page-header">
<h1>Your Profile</h1>
</div>
<p>
<b>username:</b>
<%= #user.user_name %>
</p>
<p>
<b>email:</b>
<%= #user.email %>
</p>
<h4>Small Photos</h4>
<% #user.photos.each do |photo| %>
<%= image_tag photo.image_url(:thumb).to_s %>
<% end %>
<h4>Hiking Trails</h4>
<% #user.hikingtrails.each do |hk| %>
<%= hk.name %>
<% end %>
<%= link_to "Edit your Profile", edit_user_path(current_user), :class => 'btn btn-mini' %>
You didn't add :user_id to your accessible attributes in the Hikingtrail model. Try the following:
attr_accessible :description,
:duration_hours,
:duration_mins,
:name,
:looped,
:addr_1,
:addr_2,
:addr_3,
:country,
:latitude,
:longitude,
:photos_attributes,
:trails_attributes,
:directions_attributes,
:user_id
UPDATE:
After seeing the form code, I think it's probably not necessary to do the above and could potentially also be unsafe. Instead, don't set the user_id through mass assignment, but handle user assignment in your controller like so:
class HikingtrailsController < ApplicationController
# ...
def create
#hikingtrail = Hikingtrail.new(params[:hikingtrail])
#hikingtrail.user = current_user
if #hikingtrail.save
# ...
else
# ...
end
end
end
Hope this helps :)

Resources