简体   繁体   中英

Updating extra attributes in a has_many, :through relationship using Rails

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!

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!

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!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM