How to modify nested params in Rails? - ruby-on-rails

I am working on similar app.I have 3 models: User, Group and Relation.I want to make a form where a logged in user can create a group and invite a college(other user registered in db).I am using has_many through association.Here are these models:
class Group < ApplicationRecord
has_many :relations
has_many :users, through: :relations
accepts_nested_attributes_for :relations
end
class Relation < ApplicationRecord
belongs_to :group
belongs_to :user
end
class User < ApplicationRecord
attr_accessor :remember_token
has_many :transactions, dependent: :destroy
has_many :relations
has_many :groups, through: :relations
<some validation >
end
My GroupsController
class GroupsController < ApplicationController
def new
#group = Group.new
#group.relations.build
end
def create
#group = Group.new(groups_params)
if #group.save
flash[:success] = "Group has been created!"
redirect_to root_path
else
flash[:danger] = "Something went wrong!"
render 'groups/new'
end
end
private
def groups_params
params.require(:group).permit(:group_name, :group_from, :group_to,
relations_attributes: Relation.attribute_names.map(&:to_sym) )
#Relation.attribute_names.map(&:to_sym) this grabs all the column names
#of the relations tabel, puts it in the array and maps each element with hash
#[:id, :group_id, :user_id, :created_at, :updated_at]
end
end
And my view for "new" action
<%= provide(:title, "New Group") %>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for #group do |group| %>
<%= group.label :group_name, "Group name"%>
<%= group.text_field :group_name %>
<%= group.label :group_from, "Group from"%>
<%= group.date_field :group_from %>
<%= group.label :group_to, "Group to"%>
<%= group.date_field :group_to %>
<!-- :relations is a symbol for assosiation between the group and user -->
<%= group.fields_for :relations do |relations_form| %>
<%= relations_form.label :user_id, "Member #1" %>
<%= relations_form.text_field :user_id %>
<% end %>
<%= group.submit "Create a group!" %>
<% end %>
</div>
</div>
screenshot: https://i.stack.imgur.com/u1LDX.png
With this code a logged in user is able to create group record and relation record simultaneously but it has to pass an id of user it wants to invite to a group(for example: "7" not "John" - the name of the user with id 7)
What I want to achieve is to get the id of the user names pass in the "Member #1" field.Example:
1.A logged in user puts in the "Member #1" field: "John"
2.Some function to get the id of the "John" - modify params
3.if "John" exists in users tabel then save the group -> create a group record and relation record.
I think I have to modify nested params, but I do not know how to do this.
Thanks for any help,
Lukas

Instead of an input text field, you should try out a select dropdown where you can choose the existing members. Like this it will show the name to the user but it will select the ID which will be sent to the controller.

Related

Repeating form fields and updating to database

any help would be most appreciated, I am rather new to Rails.
I have two models a Shopping List and a Product. I'd like to save/update multiple products to a shopping list at a time.
The suggested changes are not updating the models. I've been googling and is "attr_accessor" or find_or_create_by the answer(s)?
Attempt 1 - Existing code
Error
> unknown attribute 'products_attributes' for Product.
Request
Parameters:
{"_method"=>"patch",
"authenticity_token"=>"3BgTQth38d5ykd3EHiuV1hkUqBZaTmedaJai3p9AR1N2bPlHraVANaxxe5lQYaVcWNoydA3Hb3ooMZxx15YnOQ==",
"list"=>
{"products_attributes"=>
{"0"=>{"title"=>"ten", "id"=>"12"},
"1"=>{"title"=>"two", "id"=>"13"},
"2"=>{"title"=>"three", "id"=>"14"},
"3"=>{"title"=>"four", "id"=>"15"},
"4"=>{"title"=>"five", "id"=>"16"},
"5"=>{"title"=>""},
"6"=>{"title"=>""},
"7"=>{"title"=>""},
"8"=>{"title"=>""},
"9"=>{"title"=>""},
"10"=>{"title"=>""}}},
"commit"=>"Save Products",
"id"=>"7"}
Attempt 2 - no errors the page reloads and none of the expected fields are updated. In earnest, I am Googling around and copying and pasting code snippets in the vain hope of unlocking the right combo.
Added to Products mode
class Product < ApplicationRecord
attr_accessor :products_attributes
belongs_to :list, optional: true
end
<%= content_tag(:h1, 'Add Products To This List') %>
<%= form_for(#list) do |f| %>
<%= f.fields_for :products do |pf| %>
<%= pf.text_field :title %><br>
<% end %>
<p>
<%= submit_tag "Save Products" %>
</p>
<% end %>
<%= link_to "Back To List", lists_path %>
list controller
def update
#render plain: params[:list].inspect
#list = List.find(params[:id])
if #list.products.update(params.require(:list).permit(:id, products_attributes: [:id, :title]))
redirect_to list_path(#list)
else
render 'show'
end
list model
class List < ApplicationRecord
has_many :products
accepts_nested_attributes_for :products
end
original do nothing - product model
class Product < ApplicationRecord
belongs_to :list, optional: true
end
If you just want a user to be able to select products and place them on a list you want a many to many association:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
end
class ListItem < ApplicationRecord
belongs_to :list
belongs_to :product
end
class Product < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_products
end
This avoids creating vast numbers of duplicates on the products table and is known as normalization.
You can then select existing products by simply using a select:
<%= form_for(#list) do |f| %>
<%= f.label :product_ids %>
<%= f.collection_select(:product_ids, Product.all, :name, :id) %>
# ...
<% end %>
Note that this has nothing to with nested routes or nested attributes. Its just a select that uses the product_ids setter that's created by the association. This form will still submit to /lists or /lists/:id
You can whitelist an array of ids by:
def list_params
params.require(:list)
.permit(:foo, :bar, product_ids: [])
end
To add create/update/delete a bunch of nested records in one form you can use accepts_nested_attributes_for together with fields_for:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
accepts_nested_attributes_for :products
end
<%= form_for(#list) do |f| %>
<%= form.fields_for :products do |pf| %>
<%= pf.label :title %><br>
<%= pf.text_field :title %>
<% end %>
# ...
<% end %>
Of course fields_for won't show anything if you don't seed the association with records. That's where that loop that you completely misplaced comes in.
class ListsController < ApplicationController
# ...
def new
#list = List.new
5.times { #list.products.new } # seeds the form
end
def edit
#list = List.find(params[:id])
5.times { #list.products.new } # seeds the form
end
# ...
def update
#list = List.find(params[:id])
if #list.update(list_params)
redirect_to #list
else
render :new
end
end
private
def list_params
params.require(:list)
.permit(
:foo, :bar,
product_ids: [],
products_attrbutes: [ :title ]
)
end
end
Required reading:
Rails Guides: Nested forms
ActiveRecord::NestedAttributes
fields_for

Rails 5.1 add a specific column as foreign key

I have a issue with foreign keys.
I have 3 tables : Users, Members and Groups.
Basically I want the user to be able to create groups. The user provide a name, a description and on create a token is generated. Then other users can join this group via the members table. The members table has user_id and group_id. User can join the group providing the group_id which is not very cool, instead I would like the users to join the group providing the token.
Everything is working except the fact that they can join with the token. My question is how can I use the token instead of the group_id ?
I already tried to add foreign keys like this but nothing is going on in the schema :
add_foreign_key :members, :groups, column: :group_id, primary_key: :auth_token
add_foreign_key :members, :groups, column: :auth_token, primary_key: :auth_token
I also tried to add foreign key in my model :
belongs_to :group, foreign_key: "group_auth_token"
has_many :members, foreign_key: "group_auth_token", class_name: "Group"
Here are my actual models in case :
Group.rb
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, :through => :members
end
User.rb
class User < ApplicationRecord
has_secure_password
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
end
Member.rb
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
I'm really confused with all this, I'm pretty sure the answer is obvious but I'm just lost, anybody to help me out ?
Thanks
EDIT : It looks like it's not clear enough so I'm adding a few things. When the user create the group this is what's happening in my controller :
def create
#group = Group.new group_params
#group.owner_id = current_user.id if current_user
if #group.save
#user = current_user.id if current_user
Member.create(group_id: "#{#group.id}", user_id: "#{#user}")
flash[:success] = "The Group has been created."
redirect_to group_path(#group)
else
render 'new'
end
end
The Member.create is to add the User automatically in the group. If anybody want's to join a group this is what is going on in my member controller :
def create
#member = Member.new member_params
#member.user_id = current_user.id if current_user
if #member.save
flash[:success] = "You Joined the Group."
redirect_to mygroups_path
else
flash.now[:error] = "Something went wrong. Are you in the group already ?"
render 'new'
end
end
And of course there is a form to create a new member (join the group) where the user provide the group_id (member_params) in the controller.
I want to be able to use the token instead of the group_id.
EDIT 2 :
So I have a error with your code jvillian. I paste it in the MembersController. In my form I changed the :group_id by :auth_token and this is my form :
<%= form_for #member do |f| %>
<%= f.label :auth_token %>
<%= f.text_field :auth_token %>
<%= f.submit "Join the Group" %>
<% end %>
This code may not be exactly correct, but it should be in the right direction.
def create
if #group = Group.find_by(auth_token: params[:auth_token]) && current_user
if #group.users << current_user
flash[:success] = "You Joined the Group."
redirect_to mygroups_path
else
flash.now[:error] = "Something went wrong. Are you in the group already ?"
render 'new'
end
end
end
I put in:
params[:token]
Naturally, that may not be quite right. Adjust to fit your own params.
This bit:
if #group.users << current_user
may not be correct as I forget what the shovel operator returns. But, you can look that up in the guide.
TO CORRECT YOUR FORM
Presumably, you're doing something like:
class MembersController < ApplicationController
def new
#member = Member.new
end
...
end
So, when you do this:
<%= form_for #member do |f| %>
<%= f.label :auth_token %>
<%= f.text_field :auth_token %>
<%= f.submit "Join the Group" %>
<% end %>
form_for can access your #member instance.
BUT, when you do this:
<%= f.text_field :auth_token %>
You're getting an error because form_for is trying to access the .auth_token attribute on #member which, naturally, doesn't exist. But remember, all you're trying to do here is submit a form that has a token. So, instead, do something like:
<%= form_for #member do |f| %>
<%= label_tag 'auth_token', 'Token' %>
<%= text_field_tag 'auth_token' %>
<%= f.submit "Join the Group" %>
<% end %>
In this way, the auth_token field is no longer directly derived from #member and you're no longer trying to access a non-existent method on #member.
Your params will look something like:
Parameters: {"auth_token"=>"whatever the user entered"}
If you want the auth_token to be nested inside group, like this:
Parameters: {"group"=>{"auth_token"=>"whatever the user entered"}}
Then do:
<%= form_for #member do |f| %>
<%= label_tag 'group[auth_token]', 'Token' %>
<%= text_field_tag 'group[auth_token]' %>
<%= f.submit "Join the Group" %>
<% end %>
Your text is abit confusing, but maybe this setup of the references are what you are looking for?
add_reference :members, :group_auth_token, foreign_key: { to_table: :groups }, references: :auth_token

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

has_one and belongs to form

I'm rather new to rails and I'm stuck with this has_one and belongs_to form. I'm trying to create a team that has two speakers (from class 'User') through a form ,in the following manner:
class Team<ActiveRecord::Base
belongs_to :league
belongs_to :seed
has_many :speakers do
def user(level="1")
find_by(level: level).user
end
end
end
my user model looks like this :
class User < ActiveRecord::Base
belongs_to :team
end
user model:
class User
speaker model:
class Speaker < ActiveRecord::Base
belongs_to :team
belongs_to :user
end
my issue is (i think ) primarily in my controllers and form.controller looks like:
class TeamsController<ApplicationController
def new
#seed=Seed.find_by_id(params[:seed_id])
#league=current_admin.league
#team=current_admin.league.teams.build(:seed_id=>#seed,:approved=>false)
#usernames= #mca.connections.connected.each do |x| x.user end
end
def create
#league=current_admin.league
#team = #league.teams.build(team_params)
if #team.save
flash[:notice] = "Team Request Sent!."
redirect_to '/'
else
flash[:error] = "Unable to request team."
redirect_to :back
end
end
form looks like:
<div class="panel-body">
<div class="container">
<%= form_for #team do |f| %>
<%= f.hidden_field :seed_id, :value => #seed.id %>
<%= f.hidden_field :league_id, :value => #league.id %>
<div class="row">
<!-- <div class="col-md-8"> -->
<div class="form-group">
<%= f.collection_select :speaker, #usernames,:user,:fullname, multiple:true %>
</div>
<!-- </div> -->
</div>
<div class="actions">
<%= f.submit "Create" , class:"btn btn-primary" %>
</div>
<% end %>
</div>
</div>
I would really appreciate some help because it keeps throwing the following error:
NoMethodError in TeamsController#create
undefined method `each' for "2":String
The surface issue you have is that you're passing a string when Rails is expecting an object:
User(#69980837338020) expected, got String(#69980808947560)
This means you should be sending #user rather than "username" etc.
The error will likely be on this line:
#team = #league.teams.build team_params
... which means that you're passing :speaker (which Rails needs as an object) when you should be passing the speaker_id foreign key. Yury Lebedev's answer explains how to do this.
There is a deeper issue.
I don't see how each User can only belong to a Team:
class AddFieldsToUser < ActiveRecord::Migration
def change
add_column :users, :speaker_id, :integer
add_column :users, :speaker2_id, :integer
end
end
For this to work, your users can only be a member of one team.
Whilst this might work for a smaller scale product, I personally feel it to be an incorrect schema setup.
If anything, you'd expect the team to have speaker_1 and speaker_2, which would mean those two options being stored in the teams database (not user).
I think this is the cause of your problem (you're trying to set the speaker_1 and speaker_2 params when they don't exist in the teams db).
-
I would recommend the following:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :speaking_engagements, class_name: "Speaker"
has_many :teams, through: :speaking_engagements
end
#app/models/speaker.rb
class Speaker < ActiveRecord::Base
#columns team_id | user_id | level | created_at | updated_at
belongs_to :team
belongs_to :user
end
#app/models/team.rb
class Team < ActiveRecord::Base
has_many :speakers do
def user(level="1")
find_by(level: level).user
end
end
end
This will give you the ability to call:
#team = Team.find params[:id]
#speakers = #team.speakers
#user.speaking_engagements.where(team: #team)
To save it, you'll be able to use the following:
#app/controllers/teams_controller.rb
class TeamsController < ApplicationController
def new
...
#team = current_admin.league.teams.build seed: #seed, approved: false
end
def create
#league = current_admin.league
#team = #league.teams.build team_params
if #team.save
...
end
private
def team_params
params.require(:team).permit(:name, :speakers) #-> not sure about "speakers"
end
end
This should allow you to define the following:
#app/views/teams/new.html.erb
<%= form_for #team do |f| %>
<%= f.collection_select :speakers, #usernames, :id, :name, multiple: true %>
<%= f.submit %>
<% end %>

Rails 4 has_many :through w/ check_box_tag - only one side of associate being saved to join table

I'm fairly new to Rails and I've been trying to extend Michael Hartl's tutorial in various ways. One of which is to model user interests using a has_many :through association. I have the following models set up:
class Interest < ActiveRecord::Base
has_many :user_interests, dependent: :destroy
has_many :users, through: :user_interests
end
class User < ActiveRecord::Base
has_many :user_interests, dependent: :destroy
has_many :interests, through: :user_interests
end
class UserInterest < ActiveRecord::Base
belongs_to :user
belongs_to :interest
end
My user_interests controller:
def index
end
def create
#user_interest = current_user.user_interests.build(params[:interest_ids])
if #user_interest.save
redirect_to current_user
flash[:success] = "Interests updated!"
else
render 'index'
end
end
def destroy
#user_interest = UserInterest.find(params[:user_id][:interest_ids])
current_user.user_interests(#user_interest).destroy
end
The view:
<%= form_for(current_user.user_interests.build(params[:interest_ids])) do |f| %>
<%= hidden_field_tag "user[interest_ids][]", nil %>
<% Interest.all.each do |interest| %>
<%= check_box_tag "user[interest_ids][]", interest.id, current_user.interest_ids.include?(interest.id), id: dom_id(interest) %>
<%= label_tag dom_id(interest), interest.activity %><br>
<% end %>
<%= f.submit "Update interests", class: "btn btn-large btn-primary" %>
<% end %>
When I run the app I can select a check box and click the submit button but only the user id is saved in the user_interests table. So it will look like:
id integer user_id interest_id created_at_timestamp updated_at_timestamp
1 2155 2014-04-06 ect... 2014-04-06 ect...
At first I was trying to use the users controller to create the association, but that was causing issues because I didn't have the interests check boxes displayed on the users#edit action, I wanted them to have their own page. What do I need to do to get the interest ids to save to the user_interests table along with the user id?
Please have a try with the following code.
def create
interests = Interest.where(id: params[:user][:interest_ids])
current_user.interests.push(interests)
flash[:success] = "Interests updated!"
redirect_to current_user
end

Resources