Rails - how to store "has_many" checkboxes association in database? - ruby-on-rails

I have the table User and the table Categories.
The relation between these two tables:
user has_many :categories
category belongs_to :user
I would like to display on the user's (standard) edit page the list of categories with checkboxes. When this user would check some of the checkboxes, I would like to save them and then display as checked when he open the page the next time.
But how to render the checkboxes in the view? And how to store information about what checkboxes the user checked in database?
My first idea was like to create a table like user_categories with user_id and category_id, but not sure how effective this approach is.
What's the best way to implement this task nowadays?

Not much has changed recently except for the introduction of strong parameters. You still want to use either a has_and_belongs_to_many or has_many :through relationship and then just assign the ids of the relationship directly. You'll need to setup a form that passes an array of ids that are selected so that they come in like {user: {category_ids => [1,2,3]}.
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
In the controller
def update
current_user.update(user_params)
end
def user_params
params[:user].permit(
{:category_ids => []}
)
end

you can take the habtm approach.
then in user form partial, add field
<div class="field">
<%= f.label "Categories" %><br />
<% for category in Category.all %>
<%= check_box_tag 'user[category_ids][]', category.id,
#user.category_ids.include?(category.id), :id => dom_id(category) %>
<%= label_tag dom_id(category), category.name, :class => "check_box_label" %>
<% end %>
</div>
where category.name is the field in your category model.
this should work.
then in your user index view,
<% #users.each do |user| %>
<%= user.categories.collect(&:name).join(",") %>
<% end %>
it should show the associated category of the user. dont forget to add :category_ids in your user model attr_acessible field. else mass assingment security will pop up

You probably want to implement a many to many relationship using has_many :through rather than having categories belong to a user (means a category can only ever have one user, is that what you want?
Have a read of has_many :through to get you started. Once you've done that the checkboxes are easily implemented using the collection_check_boxes helper.

Related

How to accept attributes for models that don't have an active record association?

I'm making a simple team allocation app. I have three models - Players, Teams and Rounds. The idea is to have a list of players from which a user can select those who are actually there on the day to play in a round. So far I have the code that takes all the current players in the db and randomly allocates them to a team (the teams are created when a round is created).
class Player < ActiveRecord::Base
has_and_belongs_to_many :teams
end
class Team < ActiveRecord::Base
has_and_belongs_to_many :players
belongs_to :round
end
class Round < ActiveRecord::Base
has_many :teams
end
I've written a function that is called during round create to create and populate teams. That's working fine, but it is just getting the input list of players from Player.all.
What I would like is in rounds/new.html.erb to have something like this:
<%= f.fields_for(#players) do |player| %>
<%= check_box_tag 'player_ids[]', player.id, true %>
<%= player.first_name %> <%= player.surname %>
<% end %>
That way on submit I can process the array of selected users for this round and only assign those players to a team. I thought about using
accepts_nested_attributes_for :players
in the rounds model, but it doesn't actually have (or need) an association with the players. Is there a way I can achieve this or something similar? Should I instead setup an action in the players controller that manages the checkbox view/submit and then store the user's selection in session data?
If you add the following code to your form:
<% #players.each do |p| %>
<%= label_tag do %>
<%= check_box_tag 'players[]', p.id, false %>
<%= "#{p.first_name} #{p.surname}" %>
<% end %>
<% end %>
Then there will be and array of ids in params[:players] and then you can use the comment from #iced.
Your model do not have to know anything about how you procured these ids and that is nice due to Separation of concerns.

How to setup this relationship and display the correct data

Not sure how to set this relationship up correctly.
3 tables: Users, Posts, Statuses
Models -
User Model
has_many :posts
Post Model
belongs_to :user
has_one :status
Status Model
#Not sure about this.
Tables –
Users: name, email, password_digest
Posts: user_id, status_id, post_title, post_content
Statuses: user_id, status_title, status_active
The user can create their own statuses, so I am not sure if the status should belong to the User or the Post.
In the Users controller, for the new action I have the following to help generate the status options in a dropdown list when making a new Post:
def new
#post = Post.new
#options_statuses = Status.where(:admin_user_id => session[:id])
end
The code to create the new Post(from within a partial):
<%= f.label(:status_id, "Status") %><br/>
<%= f.select(:status_id, #options_statuses.map { |s| [ s.status_title, s.id ] }) %><br/><br/>
<%= f.label(:post_title, "Title") %><br/>
<%= f.text_field(:post_title) %><br/><br/>
<%= f.label(:post_content, "Content") %><br/>
<%= f.text_area(:post_content) %><br/><br/>
The Post info is saved correctly to the database. The problem is when i want to show the posts via the Index or Show actions. Currently I have the following:
<% #posts.each do |post| %>
<%= post.status_id %> - <%= post.created_at %><br/>
<%= post.post_title %> - <%= post.post_content %>
<% end %>
Instead of showing the status_id, I would actually like to display the status_title but am not sure how to do this.
Any help is greatly appreciated.
First of all, since your Post model has a status_id attribute, this means Post should belong_to :status instead of the has_one :status. Status should then either has_one :post or has_many :posts.
Now, for your question: You can achieve this by delegating title to the associated status object. (Hint: think of these objects as objects here -- forget about the database behind them!) Here's how:
# Post model
delegate :title, to: :status, prefix: true
This will define a method behind the scenes that looks like this:
# Post model
def status_title
status.title
end
For more on delegate see the api docs.

Add star ratings to my model rails3

I have a model called teacher that I'd like to add ratings to (5 star). Currently, I implement this by adding a ratings nested route (resource rating) inside of my teacher resource. Then I created a model: rating with (id, user_id, teacher_id, ratings, ...). Then I created a form with hidden fields, one of which is called stars. When a user clicks on a star, I use jQuery to send an AJAX request to create/update the rating for that user and teacher.
My confusion is this: I'm having two separate forms on the page. I have a form for writing the reviewers comments. This form has two fields: title, comments (and submit). Then I have the ratings form with hidden fields. Is this the right way to go about something like this? It seems to me that I should really have the ratings model fields somehow embedded in the main review form.
Any help highly appreciated. Thank you.
[EDIT]
I've updated my application so that instead of rating a teacher object, users now rate a comment on a teacher
my setup is something like this:
routes
resources :comments as :teacher_comments do
resource :rating
end
models
comment
has_one :rating
attr_accessible :body, :rating_attributes
accepts_nested_attributes_for :rating
rating
belongs_to :comment
attr_accessible :stars, :user_id, :teacher_id, :comment_id
view
<%= form_for( #comment, :remote => true, :url => teacher_comments_path ) do |tc| %>
<%= tc.text_area :body, :maxlength => 450 %>
<%= tc.fields_for :rating do |builder| %>
<%= builder.text_field :stars %>
<% end %>
<% end %>
I don't see the text_field for the stars. It's just not showing up. Is there something I missed?
Indeed, it's generally better to have all these fields in a single form (good for user experience).
Edit:
You might use the method accepts_nested_attributes_for (as you suggested in the comments below). Put the following in your parent Model (teacher); then you should be able to create a single form to handle inputs for both Models:
in the model:
class Comment < ActiveRecord::Base
has_one :rating
accepts_nested_attributes_for :rating
end
in the controller:
def new
#comment = Comment.new
#comment.rating = Rating.new
end
Ryan Bates gives a detailed screencast on the use of these concepts here: Nested Model Form. I recommend it for users who want to know more ins and outs.
Original:
This means that you'll need to point the form to an action that can handle both types of input. You can still use form_for if you like, but specify an action other than your default (or change the code within the default action in your teacher_controller.rb file):
<%= form_for #teacher, :url => {:action => 'create_and_rate'} do |f| %>
Since rating is a Model distinct from teacher (whose form we just created), you'll want to use the generic _tag form helpers for the rating fields.
<%= text_field_tag :rating, :name %> # rating's fields should use the generic form helper
<%= f.text_field :name %> # teacher's fields can use the specific form helper
Since you are pointing to a non-RESTful action, add it to your routes file.
resources :teacher do
:collection do
post 'create_and_rate' # this will match /teachers/create_and_rate to TeachersController#create_and_rate
end
end

Many-to-many associations and REST?

Newbie question, you've been warned!
I'm trying to implement a sample Rails app with a many-to-many association, people owning movies, and I'm trying to figure out how exactly to implement the UI for it. It's my understanding that REST requires everything to be a resource, so in this case "User" (person), "Movie" and "Possession" (the joint table) (oh, the puns).
Now the interesting part, the UX. Let's say I have a user dashboard where all of your movies are listed.
Let's say the user wants to add a movie that he owns. How do you do this in REST? It's trivial with a custom action that one could add to the User controller, but the point is not to go beyond the basic 7 REST actions, right? Therefore I'd have to first do a "new" on a movie and then do a "new" on a possession, which are two operations. How do I collapse them into one?
Basically I feel I'm not quite understanding how to maintain REST as soon as multiple models are involved and would appreciate a tip.
Thanks!
Happily, Rails has some magic just for this common scenario. Assuming a model like this:
class Movie
has_many :users, :through => :possessions
end
Your view:
<%= form_for [current_user, Movie.new] do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<% end %>
Basically this form will POST to MoviesController#create and will pass along current_user.id as a user_id parameter that (the default) MoviesController#create will know to associate with the Movie it creates. Take a look at the documentation for FormBuilder#form_for for more information.
You could also do this the other way around, by the way:
class User
has_many :movies, :through => :possessions
accepts_nested_attributes_for :movies # magic!
end
And the view:
<%= form_for current_user |user_form| %>
<%= user_form.fields_for current_user.movies.build |movie_fields| %>
<%= movie_fields.label :title %>
<%= movie_fields.text_field :title %>
<% end %>
<% end %>
In this case the form will submit to UsersController#update and its parameters will look like this:
{ :id => 123,
:movie => {
:title => "The Red Balloon"
}
}
...and the controller will know to create the Movie object. For more information check the documentation for FormHelper#fields_for.

Use accepts_nested_attributes_for to create new records or update existing

Read the big update for the latest information.
Hey everyone,
I've got a many-to-many relationship in a rails app that involves three tables: a user table, an interests table, and a join user_interests table that also has a rating value so a user can rate each of their interests on a 1-10 scale.
I am basically looking for a way for a new user to create their rating when they sign up and edit them at a future date along with any of their profile information at the same time.
I tried to follow this question Rails nested form with has_many :through, how to edit attributes of join model? but the problem I'm having is trying to incorporate a select list into the mix and having multiple interests to rate for the user.
Model Code:
user.rb
has_many :user_interests, :dependent => :destroy
has_many :interests, :through => :user_interests, :foreign_key => :user_id
accepts_nested_attributes_for :user_interests
interest.rb
has_many :user_interests, :dependent => :destroy
has_many :users, :through => :user_interests, :foreign_key => :interest_id, :dependent => :destroy
user_interest.rb
belongs_to :user
belongs_to :interest
View Code:
app/views/user/_form.html.erb
<%= form_for(#user) do |form| %>
... user fields
<%= form.fields_for :user_interests do |ui_form| %>
... loop through ALL interests
<% Interest.all.each do |interest| %>
<%= ui_form.select :rating, options_for_select(1..10) %>
<%= ui_form.hidden_field :interest_id, :value => interest.id %>
<% end %>
<% end %>
<% end %>
I also included the following in the new/edit actions in my controller #user.interests.build.build_interest
The problem I'm running into is that only one interest rating is being passed in the params hash when I want to have multiple. Also I am getting an exception thrown by rails
Interest(#2172840620) expected, got Array(#2148226700)
What tiny detail did I miss or get wrong that is causing the problem?
EDIT:
I found a way to force this to work but it requires manually editing the HTML in chrome developer tools, the :name attribute of my form elements are being generated as user[user_interests_attributes][rating] but if I change it to user[user_interests_attributes][][rating] it will work when I update a record. However I can't manually specify the :name of a form element that is tied to a form object. So what can I do to show that multiple interest ratings are being passed instead of just one that rails thinks?
BIG Update:
I got a semi functional version going with some slight changes:
View code:
<% form.fields_for :user_interests do |ui_form| %>
<p>
<%= ui_form.select :rating, options_for_select(1..5), :selected => :rating %>
<%= ui_form.label :interest_title %>
<%= ui_form.hidden_field :interest_id %>
</p>
<% end %>
Controller code:
def new
#user = User.new
Interest.all.each { |int| #user.user_interests.build({ :interest_id => int.id }) }
end
def edit
#user = #current_user
Interest.unrated_by_user_id(#user.id).each { |int| #user.user_interests.build({ :interest_id => int.id }) }
end
Now I am able to edit and get my user_interests updated or created if no rating exists, but I get an error that user is empty when I try to create a new user. Also I am unable to access any of the interest attributes in the form to display the interest the user is actually rating. Can anyone help with those caveats?
You only need #user.interests.build because its a has_many relationship. build_interest is for when there is a has_one/belongs_to relationship.
When using fields_for :user_interests you're telling the User model that an instance of one or more user_interest objects will be in the parameters hash when the user is created/updated. The form is not creating or updating any user_interests but it is sending back an array of user_interest_attributes hashes that represent the user_interests for the user the form references. This is an array of user_interests rating values for which no user_interests exist as you reference them in the form which is the reason you get the error.
Since you are passing a range to the select form helper you aren't actually providing any interests to the form for selection. The select will set a value for the rating column in the user_interests table with a value between 1 and 10. No user_interest exists for the rating to be set on even if the user_interests table has a rating column.
passing :multiple => true in the options hash of the select tag will create a multiple select list but I don't think that is what you want. I think you want many items on a page the user can put an interest rating on.
If you do want a user to be able to select many interests this is how to use fields_for with accepts_nested_attributes_for on a has_many :through relationship:
<%= form_for(#user) do |f| %>
<% f.fields_for :interest_ids do |interest| %>
<ul>
<% Interest.all.each do |choice,i| %>
<li class="selection">
<%= interest.check_box [], { :checked => f.object.user_interest_ids.include?(choice.id) }, choice.id, '' %>
<%= interest.label [], choice.name %>
</li>
<% end %>
</ul>
<% end %>
<% end %>

Resources