4

While working on Rails 3 app, I came to the problem of nested forms.

One collection is a set of predefined objects (created with db:seed). The other collection should show a form to allow to choose a few elements.

An example is better than a long description, so here it is.

Suppose you have 2 models: User and Group.

Suppose there are a few groups: Member, Admins, Guests, ...

You want your users to have multiple groups, and so you need a intermediate table: memberships.

The model code is obvious:

class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, :through => :memberships

  accepts_nested_attributes_for :memberships, :allow_destroy => true
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end

class Group < ActiveRecord::Base
  has_many :memberships
  has_many :users, :through => :memberships
end

The controller should not need to be changed. The view, however is more complicated.

I want to show a list of checkboxes to choose a few groups of the predefined ones.

I am using here the special _destroy field, with reversed value, to destroy when it is actually unchecked (and so add the user to the group when it is checked)

%p
  = f.label :name
  %br
  = f.text_field :name
%ul  
  = f.fields_for :memberships, @groups do |g|
    %li
      - group = g.object
      = g.hidden_field :group_id, :value => group.id
      = g.check_box :_destroy, {:checked => @user.groups.include?(group)}, 0, 1
      = g.label :_destroy, group.name

However, this do not work as expected, because the form g will always create an input with an arbitrary id after each group (and even break the layout by including it after the </li>):

<input id="user_memberships_attributes_0_id" name="user[memberships_attributes][0][id]" type="hidden" value="1" />
<input id="user_memberships_attributes_1_id" name="user[memberships_attributes][1][id]" type="hidden" value="2" />
# ...

Knowing the syntax of nested attributes is the following:

{:group_id => group.id, :_destroy => 0} # Create
{:group_id => group.id, :_destroy => 0, :id => membership.id} # Update
{:group_id => group.id, :_destroy => 1, :id => membership.id} # Destroy
{:group_id => group.id, :_destroy => 1} # Do nothing

Sending every time the id will not work, because it will try to update a record which does not exist instead of creating it, and try to destroy when the record does not exist.

The current solution I found is to remove all the ids, which are wrong anyway (they should be the ids of the memberships, instead of simple indexes), and add the real id when the user already has the group. (this is called in the controller before create and update)

def clean_memberships_attributes
  if membership_params = params[:user][:memberships_attributes]
    memberships = Membership.find_all_by_user_id params[:id]
    membership_params.each_value { |membership_param|
      membership_param.delete :id
      if m = memberships.find { |m| m[:group_id].to_s == membership_param[:group_id] }
        membership_param[:id] = m.id
      end
    }
  end
end

This seems so wrong, and it adds a lot of logic in the controller, just to control the bad behavior of the view fields_for helper.

Another solution is to create all the form html yourself, trying to mimic the Rails conventions, and avoid the id problem, but that is really noisy in the code and I believe there is a better way.

  • Is it a way to make fields_for work better?
  • Is there any helper more appropriate ?
  • Am I reasoning wrong somewhere in this question?
  • How would you do to achieve this?

Thanks

4

1 に答える 1

1

私はあなたを正しく理解していることを願っていますか?

グループは事前定義されており、ユーザーをグループに追加できるようにしたいと考えています。ユーザーの編集画面で、事前定義されたグループのすべてまたは一部を表示します。チェックボックスをオンにしてレコードを保存することで、ユーザーを追加します。チェックボックスのチェックを外すと、チェックを外したグループのこのユーザーのメンバーシップはゼロになります。

企業やプロジェクトでこれを行う方法は次のとおりです。

Class Company
  has_many :datasets
  has_many :projects, :through => :datasets

Class Project
  has_many :datasets
  has_many :companies, :through => :datasets


<% for company in Company.all %>
  <tr>
    <td>
      <%= check_box_tag 'project[company_ids][]', company.id, @project.companies.include?(company) %>
    </td>
      </tr>
<% end %>

すべての会社を一覧表示し、プロジェクトに含めたい会社にチェックを入れます。

これがすでにあなたを助けているかどうか教えてください。あなたの例ではhamlを使用していると思います。私はその表記にあまり慣れていません。

サブセットを使用したい場合は、スコープを使用できます。

scope :recent, Company.where(:created_at => (Time.now.midnight - 1.day)..Time.now.midnight)

次に、このスコープを .all メソッドのように使用できます。

Company.recent

これは役に立ちますか?

于 2011-02-03T10:02:43.450 に答える