The central problem: How do you merge attribute collections by a key during mass assignment from a nested form.
The details: I am using the following models:
class Location < ActiveRecord::Base
has_many :containers,
:dependent => :destroy,
:order => "container_type ASC"
validates_associated :containers
accepts_nested_attributes_for :containers,
:allow_destroy => true,
:reject_if => proc {|attributes| attributes["container_count"].blank? }
end
class Container < ActiveRecord::Base
belongs_to :location, :touch => true
validates_presence_of :container_type
validates_uniqueness_of :container_type, :scope => :location_id
validates_numericality_of :container_count,
:greater_than => 0,
:only_integer => true
end
So there is a constraint of having only one container type per location. The following views render the location and associated containers:
admin/containers/_index.html.erb
<% remote_form_for [:admin, setup_containers(#location)] do |f| -%>
<% f.fields_for :containers do |container_form| -%>
<%= render "admin/containers/form", :object => container_form %>
<% end -%>
<%= f.submit "Speichern" %>
<% end -%>
admin/containers/_form.html.erb
<% div_for form.object do -%>
<span class="label">
<%- if form.object.new_record? -%>
<%= form.select :container_type, { "Type1" => 1, "Type2" => 2, ... } %>
<%- else -%>
<%= form.label :container_count, "#{form.object.name}-Container" %>
<%= form.hidden_field :container_type %>
<%- end -%>
</span>
<span class="count"><%= form.text_field :container_count %></span>
<%- unless form.object.new_record? -%>
<span class="option"><%= form.check_box :_destroy %> Löschen?</span>
<%- end -%>
<% end -%>
module Admin::ContainersHelper
def setup_containers(location)
return location if location.containers.any? {|l| l.new_record? }
returning location do |l|
all_container_types = [1, 2, ...]
used_container_types = l.containers.try(:collect, &:container_type) || []
next_container_type = (all_container_types - used_container_types).first
l.containers.build :container_type => next_container_type if next_container_type
end
end
Essentially, the helper adds an new container to the collections except all types have already been associated or there is already a new container in the collection. This container is preinitialized to the first not-yet-defined container type. This works out pretty well so far. Adding containers works. Deleting containers works.
The problem is: I want to achieve that choosing and adding a container type which is already in the collection should sum up their counts (instead it would violate the unique constraint). I'm not sure what would be the best way without implementing/reinventing the complete accepts_nested_attributes_for magic - actually I wanted to reduce - not increase - code and complexity by using that.
Related
I have a resource called a ClinicalCase which has_many questions (nested resource). The premise is to show a case all the time, and be able to see one question at a time. My issue is that, if a case has 5 questions, the app is only showing 4. It will show the same question for the first array index and last array index, and everything in between will be the same.
For example, if case 1 has questions "foo", "bar" and "baz", "foo" and "bar" will only show up, with "foo" at the start and the end. I have no idea why this is happening.
I'm using will_paginate to assist with the pagination, as each user needs to see one case at a time, and one question at a time that belongs to the case.
Here's my code (working fine, except that the first and last question of a case show up as the exact same)
controller
def tagged
#cases = ClinicalCase.with_tag(params[:tag]).paginate(:page => params[:case], :per_page => 1)
end
model
class ClinicalCase < ApplicationRecord
belongs_to :user
has_many :questions, dependent: :destroy, inverse_of: :clinical_case
has_many :answers
accepts_nested_attributes_for :questions, allow_destroy: true, reject_if: :all_blank
acts_as_taggable
scope :with_tag, -> (tag) { tagged_with(tag) if tag.present? }
def paginated_questions(page, per_page = 1)
questions.paginate(page: page, per_page: per_page)
end
view part 1: tagged.html.erb
<% #cases.each do |clin_case| %>
<%= render "clinical_cases/question", clin_case: clin_case, questions: clin_case.questions %>
<%= will_paginate #cases, :param_name => 'case', :previous_label => 'Previous Case', :next_label => 'Next Case', :page_links => false %><br>
<% end %>
view part 2: _question.html.erb
<% questions = clin_case.paginated_questions(params[:page]) %>
<% questions.each do |question| %>
<%= question.title %>
<% question.answers.shuffle.each do |x| %>
<% if x.correct? %>
<%= x.choice %>
<% else %>
<%= x.choice %>
<% end %>
<% end %>
<%= will_paginate questions, :previous_label => 'Prev. Ques.', :next_label => 'Next Question', :page_links => false %>
<% end %>
I figured it out, and I'm not sure why it started working but it did.
<% #questions = clin_case.questions.order("created_at ASC").paginate(:page => params[:question], :per_page => 1) %>
<%= render #questions %>
I used the local variable to get the association and added an order method in ASC, and that caused it to work.
I have a form, that is saving the main attribute, but not its nested attributes. I have dug into a lot of documents, and seem to be doing things correctly, but still get an error that my nested attributes "must exist".
My interview attributes are saving correctly to the database, but my logs show "Unpermitted parameters: student, parents"
My code is modified for brevity, but I will still try to be thorough enough to get some direction as to what might be going wrong ...
Models (which I include accepts_nested_attributes for :student, :parents)
:student is singular since it has a has_one relationship
:parents is plural since it has a has_many relationship
class Interview < ApplicationRecord
has_one :student
has_many :parents
accepts_nested_attributes_for :student, :parents
end
class Student < ApplicationRecord
belongs_to :interview
end
class Parent < ApplicationRecord
belongs_to :interview
end
Controller
class InterviewsController < ApplicationController
def index
#interviews = Interview.all
end
def show
#interview = Interview.find(params[:id])
end
def new
#interview = Interview.new
#interview.build_student
2.times { #interview.parents.build }
end
def create
#interview = Interview.new(interview_params)
if #interview.save
redirect_to #interview
else
render :action => 'new'
end
end
private
def interview_params
params.require(:interview).permit(:date_today, :date_contact, :purpose_of_call, :problems_start_date, :cause, :violence, :running_away, :police_contact, :suicide, :self_harm, :other_info, :testing, :hospitalization, :medications, :school_problems, :teacher_relationships, :parent_goals, :notes,
student_attributes: [:id, :name, :age, :height, :weight, :dob, :interview_id],
parents_attributes: [:id, :name, :relationship, :parentage, :address, :phone_home, :phone_work, :phone_mobile, :phone_mobile, :email, :employer, :notes, :interview_id] )
end
end
Form (important bits)
<%= form_for(#interview) do |f| %>
<p class="inline">
<%= f.label :date_today, 'Today\'s Date' %>
<%= f.date_select(:date_today, :order => [:month, :day, :year], :start_year => 2000, :end_year => Date.today.year) %>
</p>
<p class="inline float_right">
<%= f.label :date_contact, 'Initial Contact' %>
<%= f.date_select(:date_contact, :order => [:month, :day, :year], :start_year => 2000, :end_year => Date.today.year) %>
</p>
<%= f.fields_for :student do |student_form| %>
<p><%= student_form.text_field :name, placeholder: 'Name' %></p>
<p class="inline">
<%= student_form.label :age %>
<%= student_form.text_field :age %>
</p>
<p class="inline">
<%= student_form.label :height %>
<%= student_form.text_field :height %>
</p>
<p class="inline">
<%= student_form.label :weight %>
<%= student_form.text_field :weight %>
</p>
///// removed for brevity /////
<% end %>
<%= f.fields_for :parents do |parent_form| %>
<%= render 'parents', :f => parent_form %>
<% end %>
Parent Partial
<p>
<%= f.label :name, 'Name' %>
<%= f.text_field :name %>
</p>
//// and more of the same /////
Routes
resources :interviews do
resources :student
resources :parents
end
The website form (at it's current state) can be found here: www.compassconsultingwi.com/interviews/new
and the link to the github can be found here: https://github.com/plantoteachme/compassconsultingwi
Params returns this ..
Parameters: {"utf8"=>"✓", "authenticity_token"=>"nU4WM2RO5GJd36eaSLHMxhRQCOnY8EPjDhUdFBHlYGkcw6H7/Oc5y7kFx0HMU9nm5cc47ZZZBDW6oQ2QNF5yhA==", "interview"=>{"date_today(2i)"=>"11", "date_today(3i)"=>"16", "date_today(1i)"=>"2016", "date_contact(2i)"=>"10", "date_contact(3i)"=>"23", "date_contact(1i)"=>"2016", "student"=>{"name"=>"John", "age"=>"12", "height"=>"5 feet", "weight"=>"123 lbs", "dob(2i)"=>"3", "dob(3i)"=>"13", "dob(1i)"=>"2004", "strengths"=>"Great with his siblings", "weaknesses"=>"Lazy", "likes"=>"Food", "dislikes"=>"Chores", "medical_prolems"=>"ADD", "religous_training"=>"Catholic", "ethnic_issues"=>"none", "grade_level"=>"6"}, "parents"=>{"name"=>"Jamie", "relationship"=>"Mom", "parentage"=>"Strict", "address"=>"Miwaukee Wi", "phone_home"=>"555-1000", "phone_work"=>"555-1001", "phone_mobile"=>"555-1002", "email"=>"jj#jj.com", "employer"=>"Googleer", "notes"=>"PhD in Computer Science"}, "purpose_of_call"=>"Depression causing suicidal tendencies", "problems_start_date"=>"When we moved from Nigeria last year", "cause"=>"Relocating", "violence"=>"none", "running_away"=>"no", "police_contact"=>"no", "suicide"=>"Hasn't acted on it, but talks about it", "self_harm"=>"Minor bruising from \"sports\"", "other_info"=>"", "testing"=>"Yes, for ADD", "hospitalization"=>"no", "medications"=>"Regeline", "school_problems"=>"Getting bullied", "teacher_relationships"=>"Strained", "parent_goals"=>"Improve self awareness", "notes"=>"Our family was in Nigeria for mission work"}, "button"=>""}
Unpermitted parameters: student, parents
Try to use cocoon gem.
You can build a model object using link_to_add_association method of cocoon gem.
Also, you can remove object using link_to_remove_association
A fully working example here: https://github.com/nathanvda/cocoon/wiki/ERB-examples
So I'm having an issue setting a primary image for my Dress object via a form.
The form allows the user to edit the dress details and then add/remove images to the form (using nested_form) and for each of them set a label and assign a primary image.
Everything works so far except for setting the primary image via radio buttons.
Dress Model:
class Dress < ActiveRecord::Base
has_many :dress_images
has_one :primary_dress_image, :class_name => "DressImage", :conditions => { :is_primary => true }
accepts_nested_attributes_for :dress_images, :allow_destroy => true
validates :name, :presence => true, :length => { :maximum => 99 }
end
DressImage Model
class DressImage < ActiveRecord::Base
belongs_to :dress
# Same as:
# def self.primary
# where(:is_primary => true)
# end
scope :primary, where(:is_primary => true)
# clear old primary if:
# this is a new record
# this is existing and is_primary has been set to true
before_save :clear_primary,
:if => Proc.new{ |r| (r.new_record? && r.is_primary) || (r.is_primary_changed? && r.is_primary) }
validates :label, :presence => true, :length => { :maximum => 60 }
validates :caption, :length => { :maximum => 200 }
mount_uploader :image, ImageUploader
def clear_primary
DressImage.update_all( {:is_primary => false}, :dress_id => self.dress_id )
end
end
Dress edit form
<h1>Dress</h1>
<% #dress.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %> <%#= f.label :name %>
<%= nested_form_for #dress, :as => :dress, :url => { :action => :update }, :html=>{ :multipart => true } do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :description %>
<%= f.text_area :description %>
<%= f.fields_for :dress_images do |dress_image_form| %>
<div class="dress-image">
<%= image_tag dress_image_form.object.image_url(:thumb) %>
<%= dress_image_form.text_field :label %>
<%= dress_image_form.file_field :image %>
<div class="primary-image-radio">
<%= dress_image_form.label :is_primary, "Main Image" %>
<%= f.radio_button :primary_dress_image_id, dress_image_form.object.id %>
</div>
<p>
<%= dress_image_form.link_to_remove "Remove this attachment" %>
</p>
</div>
<% end %>
<%= f.link_to_add "Add Photo", :dress_images %>
<%= f.submit "Save Dress" %>
<% end %>
With this radio button, the primary_dress_image_id attribute is set on the Dress object, but #dress.primary_dress_image gives a different result to the ID.
If I change the radio button to <%= dress_image_form.radio_button :is_primary, true %> it works better but because the name of each radio button is different, they are not treated as the same group.
I'm new to rails so I might be missing something completely obvious or doing it all wrong.
Here's one solution.
Add a hidden input to each of your nested field groups. Use a radio_button_tag instead of radio_button to make sure they are in the same group:
<div class="dress-image">
<%= dress_image_form.hidden_field :is_primary, :class => 'primary-image' %>
...
<%= radio_button_tag "select_primary_image", true, dress_image_form.object.is_primary? %>
...
</div>
Then add some javascript to update the hidden field according to the radio button selection:
$("body").on "change", ".primary-image-radio input:radio" ->
$(#).closest(".dress-image").find(".primary-image").val( $(#).is(":checked") )
You might need to modify the code a little because it's just an untested quick example, but it should give you an idea.
So I ended up using this method: <%= dress_image_form.radio_button :is_primary, true %> and using jquery to deselect all the other radio buttons when one is clicked.
This seems like a bit of a hacky method to me - there must be a purely Rails way of doing this without having to resort to JS? Until I find a better one, I'm going to stick with this solution.
I have tried for sometime and i think i got it wrong.
The form that i use is a nested form with fields_for and all i wanted is to save each of the array values in the rails select function into new rows in the db.
I have serialized :newpages in my blackwhite.rb model.
<% forms_for #prints do |f| %>
...
...
<%= f.fields_for :blackwhites_attributes do |blackwhite| %>
<%= blackwhite.select :newpages , options_for_select((1..(#print.number_of_images_entry.to_i)).to_a), :multiple => true, :size => #print.number_of_images_entry.to_i %>
<% end %>
<% end %>
Edit 1:
It has "multiple" as i wanted to have multiple selections for the pages.
blackwhite.rb model:
class Blackwhite < ActiveRecord::Base
attr_accessible :print_id
serialize :newpages
belongs_to :print
end
print.rb model:
class Print < ActiveRecord::Base
has_many :blackwhites
belongs_to :user
accepts_nested_attributes_for :blackwhites, :allow_destroy => true
...
...
end
Update 2:
I have watched railscasts and had modified my nested forms as below:
<%= f.fields_for :blackwhites do |blackwhite| %>
<% render 'blackwhites', f: blackwhite %>
<% end %>
in partial _blackwhites.html.erb:
<%= f.select :newpages , (1..(#print.number_of_images_entry)), { :prompt => "0" }, :multiple => true, :size => #print.number_of_images_entry ) %>
and my select fields is no longer appearing.
Your render is not printed because you forgot the equal sign.
<%= render 'blackwhites', f: blackwhite %>
Somehow, I was under impression that accepts_nested_attributes_for will let me populate child object through parent:
person.update_attributes person_hash
but in practice I ended up doing this:
person.address.update_attributes person_hash[:address]
person_hash.delete :address
person.update_attributes person_hash
Now, http://guides.rubyonrails.org mentions accepts_nested_attributes_for only indirectly and API documentation for this method is also quite cryptic.
1) Could someone show basic use-case for accepts_nested_attributes_for? I mean, I understand how it's declared, I'm confused about the difference it makes.
2) Is the way I populate nested object the 'right' one in rails, or there's something more 'elegant'?
Thanks!
update
Model, for clarity
class Person < ActiveRecord::Base
has_one :address
accepts_nested_attributes_for :address
end
update2, for j.
Form declaration goes like this
<% fields_for "people", person, :index => person.id do |person_form| %>
...
<% person_form.fields_for person.address do |address_form| %>
<%= address_form.text_field :street %>
<% end %>
...
<% end %>
But it gives me html names like people[66][address][street] and not people[66][address_attributes][street]
With accepts_nested_attributes_for you can do the following:
# example from railsapi.com
# view
<% form_for #person, :url => { :action => "create" } do |person_form| %>
Name: <%= person_form.text_field :name %>
...
<% person_form.fields_for :address do |address_fields| %>
Street : <%= address_fields.text_field :street %>
Zip code: <%= address_fields.text_field :zip_code %>
<% end %>
<% end %>
With this, your parameters will look like
params = {
:person => {
:name => 'Jack',
:address_attributes => { :street => 'Street', :zip_code => '11111' }
}
}
and you're able to create a person and the related address just using
#person = Person.create(params)
For more information, railsapi.com. I hope it helps.
Edit
I have no idea why your params look like this :/
But using
#person.address.update_attributes person_hash.delete(:address) OR
person.address.update_attributes person_hash[:address]
person_hash.delete :address
person.update_attributes person_hash
is not wrong. Just could be done with more elegant code, as you said.