Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create and update with nested models using strong parameters Rails

Here are my 3 models.

User
    has_many :memberships
    has_many :teams, through: :memberships, dependent: :destroy
    accepts_nested_attributes_for :memberships

Team
    has_many :memberships
    has_many :users, through: :memberships, dependent: :destroy
    accepts_nested_attributes_for :memberships

Membership
    belongs_to :team
    belongs_to :user

Here are some portions of my Team controller. My objective here is to add/update members to a certain team. Note that the source for adding members already exists as a group of users.

TeamsController
    def create
        @team = Team.new(team_params)
        @team.users << User.find(member_ids) #add leader and members to team
        if @team.save 
            #redirect to created team
        else
            #show errors
        end
    end

    def update
        #TO DO: update team roster here
        if @team.update(team_params)
            #redirect to updated team
        else
            #show errors
        end
    end

Strong parameters for Team controller

#parameters for team details
def team_params
    params.require(:team).permit(:name, :department)
end

#parameters for members (includes leader)
def members_params
    params.require(:team).permit(:leader, members:[])
end

#get id values from members_params and store in an array
def member_ids
    members_params.values.flatten
end

For the form, I only have:

  1. Name (text field)
  2. Department (combo box)
  3. Leader (combo box, with options generated depending on the selected department, submits as a selected user's user id)
  4. Members (combo box, multiple, with options generated depending on the selected department, submits as an array of selected users' user ids)

I can successfully create a team, together with the passing of validations (both team and membership model), on my create. However, I have no idea on how to update the team, because if I use @team.users.clear and then simply do the same thing from create (I know, it's a bit stupid to do this), it will validate, but it will save it regardless if there's an error or not.

FORM CODE

<%= form_for(@team, remote: true) do |f| %>

    <%= f.label :name, "Name" %>
    <%= f.text_field :name %>

    <%= f.label :department, "Department" %>
    <%= f.select :department, options_for_select(["Architectural", "Interior Design"], department), include_blank: true %>

    <%= f.label :leader, "Leader" %>
    <%= f.select :leader, select_leaders(department, @team.id), {include_blank: true, selected: pre_select(:leader)} %>

    <%= f.label :members, "Members" %>
    <%= f.select :members, select_members(department, @team.id), {include_blank: true}, {id: "team_members", multiple: :multiple,  data: {member_ids: pre_select(:members)}}%>

<% end %>

Note for the form:

  1. This form works for both blank and populated forms.
  2. The :members field is select2 enabled.

So my questions here are:

  1. How can I update members of team? Is it possible to update based from what my strong parameters currently have, or do they need to be revised?
  2. Should my create method be revised too?

SOME OPTIONS LEADING TO SOLUTION

Option #1 (best solution so far)

I only did a first-aid solution for this, so I think there's a better approach than what I did below. What I did here is to create users params with the users found from the member_ids as values.

TeamsController
    def create
        team = Team.new(team_params.merge({users: User.find(member_ids)}))
        ...
    end

    def update
        ...
        if @team.update(team_params.merge({users: User.find(member_ids)}))
        ..
    end

Option #2

Independent from solution 1, I only had team_params as strong parameter.

TeamsController
    ...
    private
        def team_params
            params.require(:team).permit(:name, :department, :leader, members:[])
        end   

I created setter methods for both leader and members. But it seems that members overwrites the leader setter because I used the update method for both setters, and the update uses the same resource which is users. A workaround seems to be possible with this option.

Team
    ...
    def leader=(leader_id)
        #self.update(users: User.find(leader_id))
    end

    def members=(members_ids)
        #self.update(users: User.find(members_id))
    end
like image 285
oLraX Avatar asked Aug 30 '14 11:08

oLraX


1 Answers

Since, leader and members are not so different in your scenario. You can change your models and view form to something like this:

class Team < ActiveRecord::Base
  has_many :memberships
  has_many :users, through: :memberships, dependent: :destroy
  accepts_nested_attributes_for :memberships
end

class User < ActiveRecord::Base
  has_many :memberships
  has_many :teams, through: :memberships, dependent: :destroy
end

class Membership < ActiveRecord::Base
  belongs_to :team
  belongs_to :user
  accepts_nested_attributes_for :user  
end

and form view code:

<%= form_for(@team) do |f| %>
  <% if @team.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@team.errors.count, "error") %> prohibited this team from being saved:</h2>

      <ul>
      <% @team.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <%= f.fields_for :memberships do |m| %>
    <div class="field">
      <%= m.label :memberships_name %><br>
      <%= m.text_field :name %>
    </div>

    <%= m.fields_for :user do |u| %>
      <div class="field">
        <%= u.label :user_name %><br>
        <%= u.text_field :name %>
      </div>    
    <% end %>
  <% end %>


  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Also, please make sure you change this in your controller:

# GET /teams/new
def new
  @team = Team.new
  3.times do # number of members you need to generate!
    @team.memberships.build{ |m| m.build_user } 
  end
end

# GET /teams/1/edit
def edit
end

# POST /teams
# POST /teams.json
def create
  @team = Team.new(team_params)

  respond_to do |format|
    if @team.save
      format.html { redirect_to @team, notice: 'Team was successfully created.' }
      format.json { render action: 'show', status: :created, location: @team }
    else
      format.html { render action: 'new' }
      format.json { render json: @team.errors, status: :unprocessable_entity }
    end
  end
end

private
  # Use callbacks to share common setup or constraints between actions.
  def set_team
    @team = Team.find(params[:id])
  end

  # Never trust parameters from the scary internet, only allow the white list through.
  def team_params
    params.require(:team).permit(:name, memberships_attributes: [:id, :name, user_attributes: [:id, :name]])
  end

Although you can do this in Rails console to do a quick code validation:

team_params = {"name"=>"Team", "memberships_attributes"=>{"0"=>{"name"=>"Membership 1", "user_attributes"=>{"name"=>"User 1"}}, "1"=>{"name"=>"Membership 2", "user_attributes"=>{"name"=>"User 2"}}, "2"=>{"name"=>"Membership 3", "user_attributes"=>{"name"=>"User 3"}}}}

team = Team.new(team_params)
team.save
team.users
#=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "User 1", email: nil, created_at: "2014-09-04 11:25:48", updated_at: "2014-09-04 11:25:48">, #<User id: 2, name: "User 2", email: nil, created_at: "2014-09-04 11:25:48", updated_at: "2014-09-04 11:25:48">, #<User id: 3, name: "User 3", email: nil, created_at: "2014-09-04 11:25:48", updated_at: "2014-09-04 11:25:48">]>

I hope it helps.

like image 101
Surya Avatar answered Nov 14 '22 21:11

Surya