Create and update with nested models using strong parameters Rails - ruby-on-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:
Name (text field)
Department (combo box)
Leader (combo box, with options generated depending on the selected department, submits as a selected user's user id)
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:
This form works for both blank and populated forms.
The :members field is select2 enabled.
So my questions here are:
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?
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

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.

Related

Adding a student to a team when creating a team in a has_and_belongs_to_many association

I have two models (teams and students) and when creating a team I want to be able to add a student to the team using their email. I can do this in the rails console by doing team.students << student but I am unsure how to translate that functionality in the controller and view.
Team controller:
def new
#team = Team.new
end
def add_student
#team = Team.find(params[:id])
#team.students << Student.find(params[:student_email])
end
def create
#team = Team.new(team_params)
if #team.save
redirect_to teams_path
else
render 'new'
end
end
private
def team_params
params.require(:team).permit(:name, student_attributes:[])
end
def current_team
team = Team.find(params[:id])
end
end
Team view:
<%= form_with(model: #team, local: true) do |f| %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= fields_for :student do |s|%>
<%= s.label :email%>
<%= s.text_field :email, class: 'form-control' %>
<% end %>
<%= f.submit "Create Team", class: "btn btn-primary" %>
<% end %>
Thank you for your help
You can do a lot better then just using HABTM:
class Team < ApplicationRecord
has_many :memberships
has_many :students, through: :memberships
end
class Student < ApplicationRecord
has_many :memberships
has_many :teams, through: :memberships
end
# rails g model team student:belongs team:belongs_to
class Membership < ApplicationRecord
belongs_to :student
belongs_to :team
validates_uniqueness_of :student_id, scope: :team_id
end
This also creates a many to many assocation but it gives you an actual model so you can access additional columns on the table (like for example if you want to add roles or keep track of who added the student to the team) and its actually a real entity in your buisness logic instead of just a peice of plumbing.
HABTM is actually quite useless.
To add/remove members from a team you create and destroy memberships.
resources :teams do
resources :memberships,
only: [:new, :create, :destroy]
shallow: true
end
class MembershipsController < ApplicationController
before_action :set_team, only: [:new, :index, :create]
# GET /teams/1/memberships/new
def new
#students = Student.where.not(id: #team.students)
#membership = #team.memberships.new
end
# POST /teams/1/memberships
def create
#membership = #team.memberships.new(membership_params)
if #membership.save
redirect_to #team, notice: "Student added to team"
else
#students = Student.where.not(id: #team.students)
render :new
end
end
# DELETE /memberships/1
def destroy
#membership.destroy
redirect_to #membership.team, notice: "Student removed from team"
end
private
def set_team
#team = Team.find(params[:team_id])
end
def set_membership
#membership = Membership.find(params[:id])
end
def membership_params
params.require(:membership)
.permit(:student_id)
end
end
<%= form_with(model: #membership, local: true) do |f| %>
<div class="field">
<%= f.label :student_id %>
<%= f.collection_select :student_ids, #students, :id, :email %>
</div>
<%= f.submit %>
<% end %>
As a rule of thumb if you're creating a method on your controller thats not one of the standard CRUD methods and it contains a synonym to of one of them (add, remove, etc) you're almost certainly doing it wrong and should treat it as separate RESTful resource.

Rails 5 - find_or_create_by with nested attributes

I'm trying to create a new object with its associated records in the same form but would like the associated records to use find_or_create_by instead of just create (as the associated model records may and most of the time already will exist). I have spent the last two days digging through every post and article that I can find related to this subject trying to get this to work but still my form tries to create the new object only, not search for existing.
Models
#order.rb
has_many :order_owners, dependent: :destroy, inverse_of: :order
has_many :owners, through: :order_owners
accepts_nested_attributes_for :order_owners
#owner.rb
has_many :order_owners, dependent: :destroy
has_many :orders, through: :order_owners
validates :name, presence: true, uniqueness: { case_sensitive: false }
#order_owner.rb
belongs_to :owner
belongs_to :order
accepts_nested_attributes_for :owner
Form
orders/new.html.erb
<%= bootstrap_form_for(#orders, layout: :horizontal, label_col: "col-sm-2", control_col: "col-sm-6") do |f| %>
<%= render 'shared/error_messages', object: f.object %>
...
<%= f.fields_for :order_owners do |orderowner| %>
<%= render 'orders/new_order_owners', f: orderowner, render_partial: 'orders/new_order_owners' %>
<% end %>
...
<%= f.form_group do %>
<%= f.submit "Create Order", class: "btn btn-primary" %>
<%= link_to_add_association fa_icon("plus", text: "Add Owner"), f, :order_owners,
class: "btn btn-outline pull-right #{orderBtnDisable(#properties)}", partial: "orders/new_order_owners", id: "newOrderOwnerAdd" %>
<% end %>
<% end %>
orders/new_order_owners partial
<div class="m-t-md m-b-md border-bottom form-horizontal nested-fields">
<%= link_to_remove_association(fa_icon("remove", text: ""), f, { class: "btn btn-danger btn-outline pull-right" }) %>
<% f.object.build_owner unless f.object.owner %>
<%= f.fields_for :owner do |owner| %>
<%= owner.select :name, options_from_collection_for_select(#owners, "name", "name"),
{ label: "Name:", include_blank: true }, { id: "orderPropOwnerSelect", data: { placeholder: "Select an existing Owner or type a new one.."} } %>
<% end %>
</div>
Controller
orders/new
def new
#order = Order.new
#order.build_property
#order.order_owners.build.build_owner
#properties = Property.find_by_id(params[:property_id])
if #properties
#owners = #properties.owners
else
#owners = []
end
respond_to do |format|
format.html
format.js
end
end
orders/create
def create
#properties = Property.find(params[:order][:property_id])
#order = #properties.orders.create(order_params)
respond_to do |format|
format.html { if #order.save
if params[:order][:owners_attributes]
order_prop_owner_check(#order, #properties)
end
flash[:success] = "Order created successfully!"
redirect_to property_order_path(#properties, #order)
else
#properties
#owner = #properties.owner
render 'new'
end
}
format.js {
if #order.save
flash.now[:success] = "Order Updated Successfully!"
else
flash.now[:danger] = #order.errors.full_messages.join(", ")
end
}
end
end
So as you can see in the new action, I instantiate the new Order, build its associated property (its what the Order belongs_to), build the new order_owner relationship, and build the owner for that relationship. Then on submit it creates the order via #properties.orders.create(order_params).
The error that I get is "Order owners owner name already exists." so clearly its not looking up an owner by name. I have tried:
Redefining autosave_associated_records_for_owner in order.rb and order_owner.rb, using both belongs_to and has_many variations, but it seems like they never get called so I must be doing something wrong. (I have tried variations of almost every answer I could find on SO)
before_add: callback on both has_many :owners, through: :order_owners and has_many :order_owners in order.rb.
Extending has_many :owners and has_many :owners, through: :order_owners in order.rb as well as belongs_to :owner in order_order.rb
I've also tried different variations of calling associations and such within the form so I must be just misunderstanding something. I'm also using Cocoon to manage the nested forms but I've talked to the author on unrelated issues and Cocoon is essentially just a view helper for nested forms so the solution must something in the models/controller.
Any and all ideas welcome. Thanks in advance.
P.s. I left code in the controller actions that may/may not pertain to this exact post but I wanted to show the entire action for completeness. If it matters, I manually set the owners select via AJAX when a property is selected in another field. Basically it just looks up the property and adds existing owners to the owners select.
The owner is nested inside the order. So when you call order.save, it runs all validations (including owner's). If you want to use find_or_create_by you need to do it inside a before_save, so you can make modifications to the owner before the validation hits.
#order.rb
before_save :find_or_create_owner
def find_or_create_owner
self.order_owners.each do |order_owner|
order_owner.owner = Owner.find_or_create_by(name: final_owner.name)
end
end
Further customization may be needed depending on your form and business logic, but that's the main concept.

Nested form for has many :through join model, where join model has additional attribute

I have two models connected through a join model. The join model stores an additional attribute value, which is a boolean A Role has many Permissions through RolePermission, and RolePermission stores whether that Role can perform the action indicated by that Permission.
Every Role should have a RolePermission record for every Permission (of which there are ~10).
I'm having trouble creating my form, however. I want there to be a checkbox for each Permission, which is used to indicate the boolean value of the value attribute for RolePermission.
models
class Role < ApplicationRecord
has_many :role_permissions, dependent: :destroy
has_many :permissions, through: :role_permissions
accepts_nested_attributes_for :role_permissions
end
class RolePermission < ApplicationRecord
belongs_to :role
belongs_to :permission
end
class Permission < ApplicationRecord
has_many :role_permissions
has_many :roles, through: :role_permissions
end
table columns
Roles
name: string
description: string
Permissions
name: string
description: string
RolePermissions
role: references
permission: references
value: boolean
views/roles/_form.html.erb
<%= form_for [#chronicle, #role], url: url do |f| %>
...
<%= f.fields_for :role_permissions, Permission.all do |ff| %>
???
<%= ff.label :name %>
<%= ff.check_box :value %>
???
<% end %>
<%= f.submit text, class: 'btn btn-primary btn-block' %>
<% end %>
controllers/roles_controller.rb
class RolesController < ApplicationController
...
def create
#chronicle = Chronicle.find(params[:chronicle_id])
#role = #chronicle.roles.build(role_params)
???
#role_permissions = #role.role_permissions.build
???
if #chronicle.save
flash[:success] = 'Role successfully created.'
redirect_to chronicle_role_url(#chronicle, #role)
else
render 'new'
end
end
private
def role_params
params.require(:role).permit(:name, :description, role_permission_attributes: [] )
end
end
end
I still believe in the concept without the Role_permission model in the middle. Each role should have their own permissions (has_many), but the permission name and description are being reused when each role has their permission created.
Here is what i did.
I created a hash with the values for name and description in the private section of the role-controller
# roles_controller.rb
def permission_values
{
"Can edit" => "This is not good if you want to party",
"Can show" => "Yes, yes, yes. Go ahead",
"Can hike" => "Oh my! A hike-role?",
"And so on" => "What do we have here?"
}
end
Then when i create the Role and the permissions i use .build with the permission_values.
Couldn't do it with seeds.rb because the Role has to be created before assigning permissions
#roles_controller.rb
def new
#role = Role.new
permission_values.each do |titel, desc|
#role.permissions.build(name: titel, description: desc)
end
end
And in the form i use .object and .hidden_field to show and create the values.
# _form.html.erb
...
<%= f.fields_for :permissions do |ff|%>
<h4><%= ff.object.name %></h4>
<%= ff.hidden_field :name %>
<p><%= ff.object.description %></p>
<%= ff.hidden_field :description %>
<%= ff.check_box :permitted %>
<% end %>
And there you have it!
Now each user will have the same permissions displayed, but they can check permitted if they want and save it with their Role.
In the show, something like:
# show.html.erb
# I would make a helper-method
# And some index-symbol-power!
<%= "Hey! I'm permitted to do this!" if #role.permissions.find_by_name("Can
edit").permitted? %>
Note:
Remember the :id in the permission_attributes in the role_params, so the permissions doesn't duplicate on edit.
========================================================================
Is the RolePermission model necessary?
Let me know if i have misunderstood something. Why not put a boolean attribute on the Permission model?
table columns:
Roles
name: string
description: string
Permissions
referenced: role
name: string
description: string
permitted: boolean
Now you can have a checkbox field in the field_for without any problems
#In form_for [#chronicle, #role], url: url do |f|
...
<%= f.fields_for :permissions do |ff| %>
<%= ff.label :permitted %>
<%= ff.check_box :permitted %>
... <!-- description and name -->
And your controller
Remember not to call Permission.all in the view, but use .build in the action new in the RolesController.
def new
#Find #chronicle
#role = Role.new
10.times { #role.permissions.build }
end
def create
# If you set up your accepts_nested_attributes_for :permissions
# (delete the rest of role_permission stuff) in role.rb,
# and update your role_params with the permission_attributes everything should
be
# working fine. Find your role and save it.
end
And then you can do something like:
if role.permissions.first.permitted?
# DO SOMETHING CRAZY
end

Rails association doesn't work

I am having a weird issue that I can't figure out.
It is very basic rails programming : I want to create an association between a user model and a goal model.
goal.rb
class Goal < ActiveRecord::Base
belongs_to :user
end
user.rb
class User < ActiveRecord::Base
has_many :goals, dependent: :destroy
has_many :records
has_many :orders
end
When I am making the association from the console, it is working well, lets say :
$ goal = Goal.first
$ goal.user_id = 1
$ goal.save
$ goal.inspect
#<Goal id: 1, description: "loremipsum", created_at: "2016-11-26 12:39:34", updated_at: "2016-11-26 12:43:41", name: "ipsumlorem", user_id: 1>
But then, when I am creating a goal from my views, the association is not made, and the user_id of the goal user_id remain : nil.
Any ideas ?
EDIT AS REQUIRED :
_form.html.erb
<%= form_for(#goal) do |f| %>
<%= f.text_field :name, class: "form-control" %>
<%= f.text_area :description, class: "form-control" %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
goal_controller.rb
def new
#users = User.all
#goal = current_user.goals.build
end
def edit
end
def create
#goal = Goal.new(goal_params)
#goal.save
redirect_to root_path, notice: "Objectif sauvegardé"
end
def create
# Here!!!!
#goal = current_user.goals.new(goal_params)
#goal.save
redirect_to root_path, notice: "Objectif sauvegardé"
end

Passing Checkbox values to model

I am making a model where users can belong to multiple teams and teams have multiple people.
I have checkboxes but they don't pass the value onto the object.
class User < ActiveRecord::Base
attr_accessible :email, :name
has_many :teams
accepts_nested_attributes_for :teams
end
class Team < ActiveRecord::Base
has_many :users
attr_accessible :name
end
Here is the code in my controller
def create
#users = User.all
#user = User.new
#teams = Team.all
#user.attributes = {:teams => []}.merge(params[:user] || {})
end
Here is the code in my view file
<%= form_for #user, url: {action: "create"} do |f| %>
<%= f.label :teams%>
<% for team in #teams %>
<%= check_box_tag team.name, team.name, false, :teams => team.name%>
<%= team.name -%>
<% end %>
<%= submit_tag "Create User" %>
I am trying to show it into
<%= user.teams.name %>
But the only output is "Team"
Can someone tell me what I am doing wrong?
Actually, you can't do a many-to-many relationship that way... you need to do has_many :through or alternatively has_and_belongs_to_many Nice explanation here...
http://guides.rubyonrails.org/association_basics.html

Resources