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 B. technique from Advanced Rails recipes to
dynamically add and remove elements on a multi-model form.
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” %>
<%= render :partial => ‘schedule’, :collection =>
@user.schedules %>
User#_schedule.html.erb
<% fields_for prefix, schedule do |schedule_form| -%>
<%= error_messages_for :schedule, :object => schedule %>
<%= schedule_form.collection_select :market_id,
Market.all, :id, :name, {:prompt => true} %>
<%= link_to_function “- Remove Market”, “$(this).up
(‘.schedule’).remove()” %>
<%= 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 -%>
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