Mike
2009-Oct-06 07:06 UTC
Problems Processing multiple form elements generated by javascript actions
Hi all, After many days of struggling, I have a multi-model form with ajax elements more or less working, but I''m hitting a wall with a few bugs that I can''t figure out. Guidance would be very much appreciated. I''m using the Ryan Bates technique from Advanced Rails recipes to dynamically add and remove elements on a multi-model form. http://media.pragprog.com/titles/fr_arr/multiple_models_one_form.pdf I have 3 models: users, schedules and markets. Users has_many :schedules has_many :markets, :through => :schedules Markets has_many :schedules has_many :users, :through => :schedules Schedules belongs_to :users belongs_to :markets Schedules has columns user_id and market_id, but also has additional columns: monday, tuesday, wednesday, thursday, friday, saturday, sunday. These are booleans. All the editing in my app happens from the user model. The list of available markets are prepopulated in the app, and the user cannot add new ones. On the user#edit view, I''m showing the user a dropdown for each of his existing markets and for each market, I''m rendering 7 checkboxes representing each day of the week. The goal is that each market selected and its associated selected dates should be saved as a "schedule" record in the schedules table. The row in that table should contain a user_id, a market_id, and each selected day of the week as true. When a user removes a market from the edit page, then that schedule row should be destroyed. As discussed in Ryan''s tutorial, there are issues regarding new vs. existing data. I''m running into one of these. I''m also having problems with deleting records. I''m going to explain my issues and then lay out my full set up below. New vs. existing data: The schema I explained above is mostly working. However, I''m having issues when the user selects a new market AND selects some checkboxes to indicate the days he attends. Using the technique described below, I have an "add new schedule" link that dynamically generates a market dropdown along with seven checkboxes for the dates. This works fine. I can select one of my markets and check off some days. However, when I click update, these inputs save as multiple records in the schedules table. The market_id, user_id and monday=false save as one row. Then, for each checkbox that I selected, the user_id and day save as a row. So if I select 3 days, then I get 4 new records: one for the market and one for each data checked. Here''s the part about new vs. existing data: When I go back to the edit page, I see the correct market in one dropdown with no checkboxes selected and then one additional empty dropdown with one checkbox selected for each checkbox that the user selected before hitting update. Now that the schedule is an "existing" record, when I check off some days associated with the dropdown where the market is selected, and I hit "update", these save perfectly and render correctly on the user#show page. So basically, the edit/update action is working perfectly and the creation of new schedules is not. The problem is really just the checkboxes because the market actually saves correctly. One solution I tried was to add something like the following to each check_box method, but this blew up the app: :index => (showing.new_record? ? '''' : nil). This is problem #1. Problem #2 is that when I click the "remove" link for a given schedule (i.e. a market and its 7 available checkboxes combination, and I hit update, the user_id and market_id are properly deleted from the schedules table. However, the record itself is not deleting and the boolean fields representing the days of the week remain as well. I''m able to hack around this and technically the app works because these half-empty rows aren''t associated with any of my models, but I''m quickly building up a database table filled with orphaned, useless data. Ideally, when I delete a "schedule" record, I''d like it to be destroyed. The following is my setup. You''ll see that I''m following Ryan''s tutorial very closely. One area where I depart from it is the checkboxes, which I implemented based on the Rails API fields_for method examples. User#Edit view: <%= error_messages_for :user %> <% form_for @user do |f| %> <%= add_schedule_link "+ Add another market" %> <div id="schedules"> <%= render :partial => ''schedule'', :collection => @user.schedules %> </div> <%= f.submit ''Update My Profile'' %> <% end %> User#_schedule.html.erb <div class="schedule"> <% new_or_existing = schedule.new_record? ? ''new'' : ''existing'' %> <% prefix = "user[#{new_or_existing}_schedule_attributes][]" %> <% fields_for prefix, schedule do |schedule_form| -%> <%= error_messages_for :schedule, :object => schedule %> <p><%= schedule_form.collection_select :market_id, Market.all, :id, :name, {:prompt => true} %></p> <p><%= link_to_function "- Remove Market", "$(this).up (''.schedule'').remove()" %></p> <%= schedule_form.check_box :monday %> <%= schedule_form.check_box :tuesday %> <%= schedule_form.check_box :wednesday %> <%= schedule_form.check_box :thursday %> <%= schedule_form.check_box :friday %> <%= schedule_form.check_box :saturday %> <%= schedule_form.check_box :sunday %> <% end -%> </div> User Model: validates_associated :schedules, :on => :update after_update :save_schedules accepts_nested_attributes_for :schedules, :allow_destroy => :true, :reject_if => :all_blank def new_schedule_attributes=(schedule_attributes) schedule_attributes.each do |attributes| schedules.build(attributes) end end def existing_schedule_attributes=(schedule_attributes) schedules.reject(&:new_record?).each do |schedule| attributes = schedule_attributes[schedule.id.to_s] if attributes schedule.attributes = attributes else schedules.delete(schedule) end end end def save_schedules schedules.each do |schedule| schedule.save(false) end end User Controller def new @user = User.new end def create cookies.delete :auth_token @user = User.new(params[:user]) @user.save! flash[:notice] = "Thanks for signing up! Please check your email to activate your account before logging in." redirect_to login_path rescue ActiveRecord::RecordInvalid flash[:error] = "There was a problem creating your account." render :action => ''new'' end def edit @user = current_user end def update params[:user][:existing_schedule_attributes] ||= {} params[:user][:existing_season_attributes] ||= {} @user = User.find(current_user) if @user.update_attributes(params[:user]) flash[:notice] = "Your information has been updated." redirect_to :action => ''show'', :id => current_user else render :action => ''edit'' end end def destroy @user = User.find(params[:id]) if @user.update_attribute(:enabled, false) flash[:notice] = "User disabled" else flash[:error] = "There was a problem disabling this user." end redirect_to :back #action => ''index'' end Helpers: users_helper.rb module UsersHelper def add_schedule_link(name) link_to_function name do |page| page.insert_html :bottom, :schedules, :partial => ''schedule'', :object => Schedule.new end end end
Mike
2009-Oct-06 08:14 UTC
Re: Problems Processing multiple form elements generated by javascript actions
As a quick follow up, it looks like the first problem I''m facing is because of the checkboxes. Ryan actually states at the end of his chapter in Rails Recipes that checkboxes won''t work because their value is not passed by the browser when the box is unchecked. So you cannot tell which market a given checkbox belongs to when a new schedule record is created. His solution is to use a select menu for boolean fields, where No =false and Yes = true. I tried that and it does fix my problem. However, I''d prefer to use checkboxes if there''s a way around this. My second problem regarding destroying schedule records still remains outstanding. Thanks, Mike On Oct 6, 3:06 am, Mike <mikeho...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:> Hi all, > > After many days of struggling, I have a multi-model form with ajax > elements more or less working, but I''m hitting a wall with a few bugs > that I can''t figure out. Guidance would be very much appreciated. > > I''m using the Ryan Bates technique from Advanced Rails recipes to > dynamically add and remove elements on a multi-model form.http://media.pragprog.com/titles/fr_arr/multiple_models_one_form.pdf > > I have 3 models: users, schedules and markets. > Users > has_many :schedules > has_many :markets, :through => :schedules > > Markets > has_many :schedules > has_many :users, :through => :schedules > > Schedules > belongs_to :users > belongs_to :markets > > Schedules has columns user_id and market_id, but also has additional > columns: monday, tuesday, wednesday, thursday, friday, saturday, > sunday. These are booleans. > > All the editing in my app happens from the user model. The list of > available markets are prepopulated in the app, and the user cannot add > new ones. On the user#edit view, I''m showing the user a dropdown for > each of his existing markets and for each market, I''m rendering 7 > checkboxes representing each day of the week. The goal is that each > market selected and its associated selected dates should be saved as a > "schedule" record in the schedules table. The row in that table should > contain a user_id, a market_id, and each selected day of the week as > true. When a user removes a market from the edit page, then that > schedule row should be destroyed. > > As discussed in Ryan''s tutorial, there are issues regarding new vs. > existing data. I''m running into one of these. I''m also having problems > with deleting records. I''m going to explain my issues and then lay out > my full set up below. > > New vs. existing data: The schema I explained above is mostly working. > However, I''m having issues when the user selects a new market AND > selects some checkboxes to indicate the days he attends. Using the > technique described below, I have an "add new schedule" link that > dynamically generates a market dropdown along with seven checkboxes > for the dates. This works fine. I can select one of my markets and > check off some days. However, when I click update, these inputs save > as multiple records in the schedules table. The market_id, user_id and > monday=false save as one row. Then, for each checkbox that I selected, > the user_id and day save as a row. So if I select 3 days, then I get 4 > new records: one for the market and one for each data checked. > > Here''s the part about new vs. existing data: When I go back to the > edit page, I see the correct market in one dropdown with no checkboxes > selected and then one additional empty dropdown with one checkbox > selected for each checkbox that the user selected before hitting > update. Now that the schedule is an "existing" record, when I check > off some days associated with the dropdown where the market is > selected, and I hit "update", these save perfectly and render > correctly on the user#show page. So basically, the edit/update action > is working perfectly and the creation of new schedules is not. The > problem is really just the checkboxes because the market actually > saves correctly. One solution I tried was to add something like the > following to each check_box method, but this blew up the app: :index > => (showing.new_record? ? '''' : nil). This is problem #1. > > Problem #2 is that when I click the "remove" link for a given schedule > (i.e. a market and its 7 available checkboxes combination, and I hit > update, the user_id and market_id are properly deleted from the > schedules table. However, the record itself is not deleting and the > boolean fields representing the days of the week remain as well. I''m > able to hack around this and technically the app works because these > half-empty rows aren''t associated with any of my models, but I''m > quickly building up a database table filled with orphaned, useless > data. Ideally, when I delete a "schedule" record, I''d like it to be > destroyed. > > The following is my setup. You''ll see that I''m following Ryan''s > tutorial very closely. One area where I depart from it is the > checkboxes, which I implemented based on the Rails API fields_for > method examples. > > User#Edit view: > > <%= error_messages_for :user %> > <% form_for @user do |f| %> > <%= add_schedule_link "+ Add another market" %> > <div id="schedules"> > <%= render :partial => ''schedule'', :collection => > @user.schedules %> > </div> > <%= f.submit ''Update My Profile'' %> > <% end %> > > User#_schedule.html.erb > > <div class="schedule"> > <% new_or_existing = schedule.new_record? ? ''new'' : ''existing'' %> > <% prefix = "user[#{new_or_existing}_schedule_attributes][]" %> > > <% fields_for prefix, schedule do |schedule_form| -%> > <%= error_messages_for :schedule, :object => schedule %> > <p><%= schedule_form.collection_select :market_id, > Market.all, :id, :name, {:prompt => true} %></p> > <p><%= link_to_function "- Remove Market", "$(this).up > (''.schedule'').remove()" %></p> > <%= schedule_form.check_box :monday %> > <%= schedule_form.check_box :tuesday %> > <%= schedule_form.check_box :wednesday %> > <%= schedule_form.check_box :thursday %> > <%= schedule_form.check_box :friday %> > <%= schedule_form.check_box :saturday %> > <%= schedule_form.check_box :sunday %> > <% end -%> > </div> > > User Model: > > validates_associated :schedules, :on => :update > after_update :save_schedules > > accepts_nested_attributes_for :schedules, :allow_destroy => :true, > :reject_if => :all_blank > > def new_schedule_attributes=(schedule_attributes) > schedule_attributes.each do |attributes| > schedules.build(attributes) > end > end > > def existing_schedule_attributes=(schedule_attributes) > schedules.reject(&:new_record?).each do |schedule| > attributes = schedule_attributes[schedule.id.to_s] > if attributes > schedule.attributes = attributes > else > schedules.delete(schedule) > end > end > end > > def save_schedules > schedules.each do |schedule| > schedule.save(false) > end > end > > User Controller > > def new > @user = User.new > end > > def create > cookies.delete :auth_token > @user = User.new(params[:user]) > @user.save! > flash[:notice] = "Thanks for signing up! Please check your email to > activate your account before logging in." > redirect_to login_path > rescue ActiveRecord::RecordInvalid > flash[:error] = "There was a problem creating your account." > render :action => ''new'' > end > > def edit > @user = current_user > end > > def update > params[:user][:existing_schedule_attributes] ||= {} > params[:user][:existing_season_attributes] ||= {} > > @user = User.find(current_user) > if @user.update_attributes(params[:user]) > flash[:notice] = "Your information has been updated." > redirect_to :action => ''show'', :id => current_user > else > render :action => ''edit'' > end > end > > def destroy > @user = User.find(params[:id]) > if @user.update_attribute(:enabled, false) > flash[:notice] = "User disabled" > else > flash[:error] = "There was a problem disabling this user." > end > redirect_to :back #action => ''index'' > end > > Helpers: users_helper.rb > > module UsersHelper > def add_schedule_link(name) > link_to_function name do |page| > page.insert_html :bottom, :schedules, :partial => > ''schedule'', :object => Schedule.new > end > end > > end