How to have many-to-many relationship in rails - ruby-on-rails

I am new to rails, and am trying to set up a many-to-many relationship in my rails project. I have a small strategy, but I am not sure if its the correct way.
Aim:
I have a table of users, and a table of groups. Users can be part of many groups, and each group may have many users.
Strategy:
Set up User migration to have name:string
Set up Group migration to have name:string
Set up a Join table migration
Set up User model such that it would have has_and_belongs_to_many :groups
Set up Group model such that it would have has_and_belongs_to_many :users
Would this be the correct strategy? Thanks!
Railcast Summary from answer:
For those that are interested - Railcast suggests you to use a has_many :through association since the strategy above has the limitation that you cannot add extra relation-specific information.
check out: http://kconrails.com/tag/has_many/

First, I assume, you have a user-model with a field "name" and a group-model with a field "name".
You need a model between users and groups. Let's call it grouping:
rails g model grouping user_name:string group_name:string
In the grouping-model (grouping.rb), you put:
belongs_to :user
belongs_to :group
In the user-model:
has_many :groupings, :dependent => :destroy
has_many :groups, :through => :groupings
And in the group-model:
has_many :groupings, :dependent => :destroy
has_many :users, :through => :groupings
In the _form file to edit or update a user's profile, you put:
<div class="field">
<%= f.label :group_names, "Groups" %>
<%= f.text_field :group_names %>
</div>
And, finally, the User-class must know, what to do with the information from the form. Insert into user.rb:
attr_writer :group_names
after_save :assign_groups
def group_names
#group_names || groups.map(&:name).join(' ')
end
private
def assign_groups
if #group_names
self.groups = #group_names.split(/\,/).map do |name|
if name[0..0]==" "
name=name.strip
end
name=name.downcase
Group.find_or_create_by_name(name)
end
end
end
assign_groups removes whitespace and downcases all words, so you won't have redundant tags.
Now, you can show the groups for a user in the show file of his or her profile:
<p>Groups:
<% #user.groups.each do |group|%>
<%= group.name %>
<% end %>
</p>
Hope, that helps.

Related

Rails has_many relationship with prefilled views

I have a pretty basic Rails 4 app, and am using Cocoon's nested forms to manage the has_many... :through model association.
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
# ... etc
end
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
# ... etc
end
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluation, reject_if: :all_blank
# ... etc
end
When I use Cocoon in the View, I want to use the New Assessment view to pre-fill all the Student records in order to create a new Evaluation for each one. I don't want to have to do some hacky logic on the controller side to add some new records manually, so how would I structure the incoming request? With Cocoon I see that requests have some number in the space where the id would go (I've replaced these with ?? below).
{"utf8"=>"✓", "authenticity_token"=>"whatever", "assessment"=>{"description"=>"quiz 3", "date(3i)"=>"24", "date(2i)"=>"10", "date(1i)"=>"2015", "assessments_attributes"=>{"??"=>{"student_id"=>"2", "grade" => "A"}, "??"=>{"student_id"=>"1", "grade" => "B"}, "??"=>{"student_id"=>"3", "grade"=>"C"}}, }}, "commit"=>"Create Assessment"}
I see in the Coccoon source code that this is somehow generated but I can't figure out how it works with the Rails engine to make this into a new record without an ID.
What algorithm should I use (or rules should I follow) to fill in the id above to make a new record?
"??"
Never a good sign in your params.
With Cocoon I see that requests have some number in the space where the id would go
That ID is nothing more than the next ID in the fields_for array that Rails creates. It's not your record's id (more explained below).
From your setup, here's what I'd do:
#app/models/student.rb
class Student < ActiveRecord::Base
has_many :evaluations
has_many :assessments, through: :evaluations
end
#app/models/evaluation.rb
class Evaluation < ActiveRecord::Base
belongs_to :student
belongs_to :assessment
end
#app/models/assessment.rb
class Assessment < ActiveRecord::Base
has_many :evaluations
has_many :students, through: :evaluations
accepts_nested_attributes_for :evaluations, reject_if: :all_blank
end
This will allow you to do the following:
#app/controllers/assessments_controller.rb
class AssessmentsController < ApplicationController
def new
#assessment = Assessment.new
#students = Student.all
#students.each do
#assessment.evaluations.build
end
end
end
Allowing you:
#app/views/assessments/new.html.erb
<%= form_for #assessment do |f| %>
<%= f.fields_for :evaluations, #students do |e| %>
<%= e.hidden_field :student_id %>
<%= e.text_field :grade %>
<% end %>
<%= f.submit %>
<% end %>
As far as I can tell, this will provide the functionality you need.
Remember that each evaluation can connect with existing students, meaning that if you pull #students = Student.all, it will populate the fields_for accordingly.
If you wanted to add new students through your form, it's a slightly different ballgame.
Cocoon
You should also be clear about the role of Cocoon.
You seem like an experienced dev so I'll cut to the chase - Cocoon is front-end, what you're asking is back-end.
Specifically, Cocoon is meant to give you the ability to add a number of fields_for associated fields to a form. This was discussed in this Railscast...
Technically, Cocoon is just a way to create new fields_for records for a form. It's only required if you want to dynamically "add" fields (the RailsCast will tell you more).
Thus, if you wanted to just have a "static" array of associative data fields (which is I think what you're asking), you'll be able to use fields_for as submitted in both Max and my answers.
Thanks to #rich-peck I was able to figure out exactly what I wanted to do. I'm leaving his answer as accepted because it was basically how I got to my own. :)
assessments/new.html.haml (just raw, no fancy formatting)
= form_for #assessment do |f|
= f.fields_for :evaluations do |ff|
.meaningless-div
= ff.object.student.name
= ff.hidden_field :student_id, value: ff.object.student_id
= ff.label :comment
= ff.text_field :comment
%br/
assessments_controller.rb
def new
#assessment = Assessment.new
#students = Student.all
#students.each do |student|
#assessment.evaluations.build(student: student)
end
end

Ruby on Rails - Bullet/N+1

On Rails 4. I recently installed the bullet gem for my development environment to clear up my app's N+1 queries. Relevant models:
Submissions: Belongs to Categories and Users. Has many SubmissionDetails.
Users: Has many Submissions
Categories: Has many Submissions. Belongs to Awards.
Awards: Has many Categories (and Submissions through Categories)
SubmissionDetails: Belongs to Submissions
In my submission's index page, I have an each do statement to display each submission made by the current user.
<% current_user.submissions.order('created_at DESC').in_groups_of(3, false) do |group| %>
<div class="row">
<% group.each do |submission| %>
After that, I list information about the submission, including its associated category, award, and submission details information. Bullet is saying I'm having N+1 issues with this statement:
N+1 Query detected Submission => [:category] Add to your finder: :include => [:category]
N+1 Query detected Submission => [:user] Add to your finder: :include => [:user]
N+1 Query detected Submission => [:submission_details] Add to your finder: :include => [:submission_details]
Every time I try to add .includes with all three of those models, it only picks the first one I list (this is not surprising). I figure I need to go a different route when multiple models are involved--perhaps a join statement?
(When I make :category the first item, it adds this notice):
N+1 Query detected Category => [:award] Add to your finder: :include => [:award]
(So I also need to include as part of the statement a way to make Award fit in there as well, which, again, has many Submissions through Categories).
So assuming I can't do one .includes for three different models, is there another way to go about this? Thanks.
Just to be more clear, let me make the details more visible:
class User < ActiveRecord::Base
has_many :submissions
end
class Submission < ActiveRecord::Base
belongs_to :category
belongs_to :user
has_many :submission_details
end
class SubmissionDetail < ActiveRecord::Base
belongs_to :submission
end
class Category < ActiveRecord::Base
belongs_to :award
has_many :submissions
end
class Award < ActiveRecord::Base
has_many :categories
has_many :submissions, through: :categories
end
If I understand correctly, for your current_user, you are listing his submissions.
For each submission you want to list submission_details and the category it belongs.
For every category you list the award too.
<% current_user.submissions.order('created_at DESC').in_groups_of(3, false) do |group| %>
<div class="row">
<% group.each do |submission| %>
...
<div><%= submission.category %></div>
<div><%= submission.category.award %></div>
<%= submission.submissions_details.each do |submission_detail| %>
...
<% end %>
<% end %>
</div>
<% end %>
You can remove N+1 problem by using includes in the following manner:
current_user.submissions.includes(:submission_details, :category => :award)
For more details about includes please refer to:
Rails guides eager-loading-associations
Rails guides eager-loading-multiple-associations
Rails api - includes
associations
To include those associations, I would create a scope for submissions. While you're at it, add a latest scope.
class Submission
scope :eager, -> { includes(:submission_details, :category => [:award]) }
scope :latest, -> { order("created_at DESC") }
end
Then simply
current_user.submissions.latest.eager [...]
You shouldn't have to include :user, but I've noticed Rails isn't too clever about such references.

Update join model in has_many through association, using the two other models

I've read through many other topics here (1, 2, 3...) but none really solved my problem.
Here are my 3 models.
User
has_many :memberships
has_many :accounts, :through => :memberships
accepts_nested_attributes_for :memberships
end
Account
has_many :memberships
has_many :users, :through => :memberships
accepts_nested_attributes_for :memberships
end
Membership
attr_accessible :account_id, :url, :user_id
belongs_to :account
belongs_to :user
end
As you can see, my join model Membership has an additional attribute: :url.
In my Accounts table, I store names of online services, such as GitHub, Stack Overflow, Twitter, Facebook, LinkedIn.. I have 9 in total. It's a fixed amount of accounts that I don't tend to update very often.
In my User form, I'd like to create this:
The value entered in any of these field should be submitted in the Memberships table only, using 3 values:
url (the value entered in the text field)
user_id (the id of the current user form)
account_id (the id of the related account, e.g. LinkedIn is '5')
I have tried 3 options. They all work but only partially.
Option #1
<% for account in #accounts %>
<%= f.fields_for :memberships do |m| %>
<div class="field">
<%= m.label account.name %><br>
<%= m.text_field :url %>
</div>
<% end %>
<% end %>
I want to have 9 text field, one for each account. So I loop through my accounts, and create a url field related to my memberships model.
It shows my fields correctly on the first time, but the next time it'll display 81 fields:
Option #2
<% #accounts.each do |account| %>
<p>
<%= label_tag(account.name) %><br>
<%= text_field_tag("user[memberships_attributes][][url]") %>
<%= hidden_field_tag("user[memberships_attributes][][account_id]", account.id) %>
<%= hidden_field_tag("user[memberships_attributes][][user_id]", #user.id) %>
</p>
<% end %>
I'm trying to manually enter the 3 values in each column of my Memberships tables.
It works but :
displaying both account and user id's doesn't seem very secure (no?)
it will reset the fields everytime I edit my user
it will duplicate the values on each submit
Option #3 (best one yet)
<%= f.fields_for :memberships do |m| %>
<div class="field">
<%= m.label m.object.account.name %><br>
<%= m.text_field :url %>
</div>
<% end %>
I'm creating a nested form in my User form, for my Membership model.
It works almost perfectly:
exactly 9 fields, one for each account
no duplicates
But, it only works if my Memberships table is already populated! (Using Option #2 for example).
So I tried building some instances using the UsersController:
if (#user.memberships.empty?)
#user.memberships.build
end
But I still get this error for my m.label m.object.account.name line.
undefined method `name' for nil:NilClass
Anyway, I'm probably missing something here about has_many through models. I've managed to create has_and_belongs_to_many associations but here, I want to work on that join model (Membership), through the first model (User), using information about the third model (Account).
I'd appreciate your help. Thank you.
in the controller, fetch the list of memberships for a particular user
# controller
# no need to make this an instance variable since you're using fields_for in the view
# and we're building additional memberships later
memberships = #user.memberships
then loop through each account and build a membership if the user has no membership for an account yet.
# still in the controller
Account.find_each do |account|
unless memberships.detect { |m| m.account_id == account.id }
#user.memberships.build account_id: account.id
end
end
then in your view, you change nothing :)
I would use the following data-design approach. All users in your system should have the
memebership entries for all possible accounts. The active configurations will have a value for the url field.
User
has_many :memberships
has_many :accounts, :through => :memberships
has_many :active_accounts, :through => :memberships,
:source => :account, :conditions => "memberships.url IS NOT NULL"
accepts_nested_attributes_for :memberships
end
Now
curent_user.active_accounts # will return the accounts with configuration
curent_user.accounts # will return all possible accounts
Add a before_filter to initialize all the memberships that a user can have.
class UsersController
before_filter :initialize_memberships, :only => [:new, :edit]
private
def initialize_memberships
accounts = if #user.accounts.present?
Account.where("id NOT IN (?)", #user.account_ids)
else
Account.scoped
end
accounts.each do |account|
#user.memberships.build(:account_id => account.id)
end
end
end
In this scenario you need to initialize the memeberships before the new action and all the memberships should
be saved in the create action ( even the ones without url).
Your edit action doesn't need to perform any additional data massaging.
Note:
I am suggesting this approach as it makes the management of the form/data straight forward. It should only
be used if the number of Account's being associated is handful.

view role titles of the user through assignment table

Hey I hope you can help me here.
I got a Role model:
has_many :users, :through => :role_assignments
has_many :role_assignments
a role assignment model:
belongs_to :user
belongs_to :role
and a user model:
has_many :roles, :through => :role_assignments
has_many :role_assignments
I want to display the users roles in a view.
I tried some stuff like: user.roles.names but it didnt work
Since user.roles is an collection (array) of roles, you can't call names directly. Now I assume that the attribute you want to access is name so in that case you could do:
user.roles.map(&:name).join(", ")
which will collect all the names from the roles and then join them into a string seperated by commas. That is very simple and not very flexible. If you instead want to style it in some way you could do like this:
<% user.roles.each do |role| %>
<p>Role: <%= role.name %></p>
<% end %>

How to write properly a nested form with a N-N relation

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
I hope I understand you correctly?
The groups are predefined and you want to be able to add a user to a group.
On the edit User screen you show all or some of the predefined groups.
You want to add a user by ticking the checkbox and saving the record.
If you untick the checkbox the membership of this user in the unticked group should be nil.
Here's how I am doing this with companies and projects:
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 %>
I list all Companies and check the ones I want to include in the project.
Please tell me if this already helps you? I believe you are using haml in your example. I am not really used to that notation.
If you want to use a subset you could use a scope:
scope :recent, Company.where(:created_at => (Time.now.midnight - 1.day)..Time.now.midnight)
Then you can use this scope like the .all method:
Company.recent
Does this help?

Resources