views:

63

answers:

2

I've managed to set up a many-to-many relationship between the following models

  • Characters
  • Skills
  • PlayerSkills

PlayerSkills, right now, has an attribute that Skills don't normally have: a level.

The models look something like this (edited for conciseness):

class PlayerSkill < ActiveRecord::Base
  belongs_to :character
  belongs_to :skill
end

class Skill < ActiveRecord::Base
  has_many :player_skills
  has_many :characters, :through => :player_skills

  attr_accessible :name, :description
end

class Character < ActiveRecord::Base
  belongs_to :user

  has_many :player_skills
  has_many :skills, :through => :player_skills
end

So nothing too fancy in the models... The controller is also very basic at this point... it's pretty much a stock update action.

The form I'm looking to modify is characters#edit. Right now it renders a series of checkboxes which add/remove skills from the characters. This is great, but the whole point of using has_many :through was to track a "level" as well.

Here is what I have so far:

- form_for @character do |f|
  = f.error_messages
  %p
    = f.label :name
    %br
    = f.text_field :name
  %p
    = f.label :race
    %br
    = f.text_field :race
  %p
    = f.label :char_class
    %br
    = f.text_field :char_class
  %p
    - @skills.each do |skill|
      = check_box_tag "character[skill_ids][]", skill.id, @character.skills.include?(skill)
      =h skill.name
      %br
  %p
    = f.submit

After it renders "skill.name", I need it to print a text_field that updates player_skill.

The problem, of course, is that player_skill may or may not exist! (Depending on if the box was already ticked when you loaded the form!)

From everything I've read, has_many :through is great because it allows you to treat the relationship itself as an entity... but I'm completely at a loss as to how to handle the entity in this form.

As always, thanks in advance for any and all help you can give me!

A: 

Hi, I am not sure about the answer but here is what I think :

For the controller :

@character = Character.find(params[:id])

In the view :

<% if @character.skills!=0 %>
    <% for skill in @character.skills %>
        <%=h skill.name %>
        <%= check_box_tag(skill.name, value = "1", checked = false, options = {...}) %>
    <% end %>
<% end %>

Hope it will help!

Michaël
A: 

I, so far, have fixed the problem I was having...

It was actually relatively straight forward once I learned about nested attributes!

Here is the new characters model!

class Character < ActiveRecord::Base
  belongs_to :user

  has_many :player_skills
  has_many :skills, :through => :player_skills
  accepts_nested_attributes_for :player_skills

  def skills_pre_update(params)
    skills = Skill.find(:all, :order => 'id')
    skills = skills.map do |skill|
      skill.id
    end

    self.skill_ids = []
    self.skill_ids = skills

    self.skill_ids.each_with_index do |skill_id, index|
      self.player_skills[index].level = params[:character][:player_skills_attributes][index][:level]
    end

    self.skill_ids = params[:character][:skill_ids]
  end
end

And the update action for the character controller was mildly changed:

@character.skills_pre_update(params)
params[:character].delete(:player_skills_attributes)
params[:character].delete(:skill_ids)

The reason being, those two portions are already handled by the pre_update action, therefore they don't need to be handled again by update_attributes, which gets called later.

The view was relatively straight forward. the Many-to-Many checkboxes are still the same, I did however add the new textboxes!

- @skills.each_with_index do |skill,index|
  = check_box_tag "character[skill_ids][]", skill.id, @character.skills.include?(skill)
  =h skill.name
  -ps = skill.player_skills.find_by_character_id(@character) || skill.player_skills.build
  -fields_for "character[player_skills_attributes][]", ps do |psf|
    =psf.text_field(:level, :index => nil)
    =psf.hidden_field(:id, :index => nil)

In essence, the reason I have to blank out skill_ids (skill_ids = []) in the Characters model is because otherwise it improperly sets the order.

In essence, I add all the skills.
Update the levels, using the text-boxes.
Then reset the skills to what the user actually checked (which will delete any unused skills.)

I don't feel this is the greatest solution - in fact it feels rather hackish to me. So if anybody else wants to chime in with a better, possibly faster/more elegant solution, feel free!

Otherwise, I do hope this helps someone else... because modifying the extra attributes on the join table (without giving the join table its own controller/views) was a real pain!

Robbie
I would like to note that you have to modify the pre_update method for every extra attribute you want on the join model. This is obviously not ideal.
Robbie
You can use `fields_for` on collections. It saves a lot of messing with implementation details.
hurikhan77
...and order problems.
hurikhan77