I'm building a rails app for a competition leaderboard. The data model is
class Tournament
has_many :events
end
class User
has_many :entries
has_many :events, through: :entries
end
class Events
has_many :entries
belongs_to :tournament
end
class Entry
belongs_to :user
belongs_to :event
end
Each tournament has 2 events and a user can enter up to 4 entries per event, so a total of 8 entries per tournament. I'd like to allow the user to enter all 8 entries in one form, with one submit button.
My routes
resources :entries, except: [:index, :show] do
collection do
match 'create_collection', via: [:create]
end
end
I also have a create_collection method in the Entries controller to process the entries, though I haven't got that far yet. I'm not sure how to get the form working properly.
_form.html.haml (which is a partial rendered by views/tournaments/index.html.haml and passes next_events which is the 2 event instances for the Tournament.
.entry-form
= form_tag create_collection_entries_path do |form|
- next_events.each do |event|
= event.name
= fields_for "events[]", event do |f|
- 4.times do
= f.fields_for :entries do |f|
= f.label_tag 'player'
= f.text_field 'player'
= submit_tag "Submit", class: "btn btn-success"
The form displays as I expect but when I click the submit button only the last 4 entries are submitted in the params and the event id is not being passed
"events"=>[{"entries_attributes"=>
{"0"=>{"player"=>"player5","id"=>"33"},
"1"=>{"player"=>"player6", "id"=>"34"},
"2"=>{"player"=>"player7", "id"=>"35"},
"3"=>{"player"=>"player8", "id"=>"36"}}}],
"commit"=>"Submit"}
How do I get all the entry parameters for both events to be submitted with the correct event-id?
My research indicates that rails doesn't provide a way to submit multiple instances of a model in one form with one submit. I looked at this and this which didn't quite work for me. So I rolled my own.
The form to submits all the values in the params hash in a fairly manageable way. It's not the most elegant solution and requires a fair bit of processing in the back end. But I can't find a way to do this in a more railsy way.
form.html.haml
.entry-form.on
= form_tag create_collection_entries_path(@user.id) do
- Tournament.next.events.each do |event|
%div= event.name
%div
- @num_of_entry_forms.times do |n|
%div
= label_tag 'player'
= text_field_tag "event[#{event.id}][entries][player_#{n}]"
= submit_tag "Submit", class: "btn btn-success"
routes.rb
resources :entries, except: [:index, :show] do
collection do
get '/new_collection/:user_id', to: 'entries#new_collection', as: 'new_collection'
post '/create_collection/:user_id', to: 'entries#create_collection', as: 'create_collection'
end
end
entries_controller.rb
def new_collection
@num_of_entry_forms = EventEntryCollectionCreator::MAX_ENTRIES_PER_EVENT
@user = User.find(params[:user_id])
end
def create_collection
@eec = EventEntryCollection.new(params)
if @eec.save
redirect_to root_path
else
render :new_collection
end
end
I'm still working on the EventEntryCollectionCreator which is a service model, but here's the core of it.
MAX_ENTRIES_PER_EVENT = 4
def save
params["event"].each do |event|
event.last["entries"]["player"].each do |entry|
entry = Entry.new(event_id: event.first, user_id: user_id, player: entry.last)
@entries_collection << entry
end
end
save_collection
end
private
def save_collection
@entries_collection.each do |entry|
if valid_entry?
entry.save
@valid_models << entry
else
@invalid_models << entry
end
end
end
As I said this isn't the most elegant solution but it solves the problem. The good points are that the messy backend processing is abstracted out of the controller into its own service model. The negative aspects are there are a few code smells, a lot of forcing values into hashes and objects having to know too much about the hash structure.
I'm open to other solutions or improvements to this work in progress.