Background:
I followed the tutorial here to setup a polymorphic User favorites data model in my application. This allows me to let a User make pretty much any Entity in the system which I add 'has_many :favorites, :as => :favorable' line to its model a favorite. I plan on using this to implement a Facebook style 'Like' system as well as several other similar systems.
To start off I added the favoritability to a Post model (each user can create status updates like on Facebook). I have it all done and unit tested so I know the data model is sound and functioning from either side of the relationship (User and Post).
Details:
I have a Home controller with a single index method and view.
on the index view I render out the posts for the user and the user's friends
I want the user to be able to like posts from their friends
The Posts controller has only a create and a destroy method with associated routes (not a full fledged resource) and through the Post method via AJAX posts are created and deleted without issue
Where I am stuck
How do I add the link or button to add the post to the user's Favorites?
According to the tutorial the way to create a new Favorite through the polymorphic association is to do it from the Post.favorites.build(:user_id => current_user.id). From this direction the build handles pulling out the Post's ID and TYPE and all I have to do is pass in the user's id
Do I use an AJAX form post to a Favorites controller with a Create and Destroy method similar to the Post controller?
I am still struggling to uncross the wires in my brain from ASP.Net N-Tier web application development over to Rails MVC. Hasn't been too bad until now ;)
I bet there are Gems out there that might do this but I need to learn and the best way is to suffer through it. Maybe a tutorial or sample code from someone who has implemented liking functionality within their application would be helpful.
Thanks in advance for the assistance!
Jaap, I appreciate your comment on my question. After writing the question I pretty much didn't want to wait because the real learning takes place through trial and error, so I errored it up ;)
It turns out that what you suggested was pretty much in line with exactly what I ended up doing myself (it's always nice to find out that what you decide to do is what others would do as well, I love the sanity check value of it all).
So here is what I did and it is all working through post-backs. Now I just need to implement AJAX and style it:
My favorite model because my Polymorphic Favorites model requires that an Entity can only be favorited once by a user I added to the validations 'Scopes' which indicate that for each attribute it has to be unique in the scope of the other 2 required attributes. This solves the issue of multiple favorites by the same user.
class Favorite < ActiveRecord::Base
before_save :associate_user
belongs_to :favorable
belongs_to :user
# Validations
validates :user_id, :presence => true,
:uniqueness => {:scope => [:favorable_id, :favorable_type], :message => "item is already in favorites list."}
validates :favorable_id, :presence => true,
:uniqueness => {:scope => [:user_id, :favorable_type], :message => "item is already in favorites list."}
validates :favorable_type, :presence => true,
:uniqueness => {:scope => [:favorable_id, :user_id], :message => "item is already in favorites list."}
# Callbacks
protected
def associate_user
unless self.user_id
return self.user_id = session[:user_id] if session[:user_id]
return false
end
end
end
My User Model (that which is relevant): I added 2 methods, the get_favorites which is the same as favorable one from the tutorial and a Favorite? method which checks to see if the Entity in question has already been added to the user's favorites.
class User < ActiveRecord::Base
# Relationships
has_many :microposts, :dependent => :destroy
has_many :favorites
# Methods
def favorite?(id, type)
if get_favorites({:id => id, :type => type}).length > 0
return true
end
return false
end
def get_favorites(opts={})
# Polymorphic Favoritability: allows any model in the
# application to be favorited by the user.
# favorable_type
type = opts[:type] ? opts[:type] : :topic
type = type.to_s.capitalize
# add favorable_id to condition if id is provided
con = ["user_id = ? AND favorable_type = ?", self.id, type]
# append favorable id to the query if an :id is passed as an option into the
# function, and then append that id as a string to the "con" Array
if opts[:id]
con[0] += " AND favorable_id = ?"
con << opts[:id].to_s
end
# Return all Favorite objects matching the above conditions
favs = Favorite.all(:conditions => con)
case opts[:delve]
when nil, false, :false
return favs
when true, :true
# get a list of all favorited object ids
fav_ids = favs.collect{|f| f.favorable_id.to_s}
if fav_ids.size > 0
# turn the Capitalized favorable_type into an actual class Constant
type_class = type.constantize
# build a query that only selects
query = []
fav_ids.size.times do
query << "id = ?"
end
type_conditions = [query.join(" AND ")] + fav_ids
return type_class.all(:conditions => type_conditions)
else
return []
end
end
end
end
My Micropost Model (that which is relevant): note the Polymorphic association in the has_many relationship titled :favorites.
class Micropost < ActiveRecord::Base
attr_accessible :content
# Scopes
default_scope :order => 'microposts.created_at DESC'
# Relationships
belongs_to :user
has_many :favorites, :as => :favorable # Polymorphic Association
# Validations
validates :content, :presence => true, :length => { :minimum => 1, :maximum => 140 }
validates :user_id, :presence => true
end
My Micropost Form: as you can see I am passing in the entity that will be mapped to the Favorite model as a local variable to the 2 Favorite forms as 'local_entity'. This way I can pull out the ID and the TYPE of the Entity for the Polymorphic association.
<div class="post">
<span class="value">
<%= micropost.content %>
</span>
<span>
<% if current_user.favorite?(micropost.id, micropost.class.to_s) %>
<%= render :partial => 'favorites/remove_favorite', :locals => {:local_entity => micropost} %>
<% else %>
<%= render :partial => 'favorites/make_favorite', :locals => {:local_entity => micropost} %>
<% end %>
</span>
<span class="timestamp">
Posted <%= time_ago_in_words(micropost.created_at) %> ago.
</span>
<div class="clear"></div>
</div>
My Make Favorite Form:
<%= form_for current_user.favorites.build do |f| %>
<div><%= f.hidden_field :favorable_id, :value => local_entity.id %></div>
<div><%= f.hidden_field :favorable_type, :value => local_entity.class.to_s %></div>
<div class="actions"><%= f.submit "make favorite" %></div>
<% end %>
My Remove Favorite Form:
<%= form_for current_user.get_favorites(
{:id => local_entity.id,
:type => local_entity.class.to_s}),
:html => { :method => :delete } do |f| %>
<div class="actions"><%= f.submit "remove favorite" %></div>
<% end %>
If you don't want to call this on the current_user, you would have to have these routes in your config/routes.rb to make nested routes for favorites on a user. I assume you have a Favorite model which belongs_to :user:
resources :users do
resources :favorites
end
Then make sure your favorites controller loads the user in some kind of before_filter:
def load_user
#user = User.load params[:user_id]
end
And then you can render a remote form to create a new favorite for any kind of object (it will only show a button):
<%= remote_form_for [#user, Favorite.new] do |f| -%>
<%= f.hidden_field :favorable_type, object.class.to_s %>
<%= f.hidden_field :favorable_id, object.id %>
<%= f.submit 'Like' %>
<%- end -%>
You would have to render that form as a partial sending along an object (e.g. a Post) and then it will create an AJAX POST call to /users/:id/favorites/ which will create the favorite object and render some kind of javascript response in a create.rjs file.
I hope this helps. The code itself is untested, but it might get you moving.
Related
I have a bit of a problem with a "has one" association on my app.
What I want to achieve is to be able to attach an optional quote to the topic. The quote can only be used once (in other words, if it's used for topic 1, it can't be used for any other topics).
I have a Topic model and a Quote model.
Topic has one quote.
Quote belongs to topic.
I also want to be able to attach a quote to other models (ex. Profile Model).
I'm really confused on what to do on my "edit topic" view as well as in the controller. I thought it would work like a "one to many" association, which I had no problem configuring. Somehow the "has one" is more complicated (for me!)
What I'd like is to have in the "edit topic" view a radio list of the available quotes which I can freely update. (Same for the "new topic" view).
My current controller:
def edit
#topic = Topic.find(params[:id])
#quote = #topic.quote
#packages = #topic.packages
#books = #topic.books
#tasklists = #topic.tasklists
#links = #topic.links
#terms = #topic.terms
end
def update
#topic = Topic.find(params[:id])
if #topic.update_attributes(topic_params)
flash[:success] = t('helpers.success-update', model: "topic")
redirect_to backend_topics_url
else
render partial: 'edit'
end
end
def topic_params
params.require(:topic).permit(:topic_id, :theme_id, :cover, :topic_status, :topic_access, :slug, *Topic.globalize_attribute_names, :quote_attributes => [:id, :topic_id], :package_ids => [], :book_ids => [], :link_ids => [], :tasklist_ids => [], :term_ids => [])
end
My current Topic model:
has_one :quote
accepts_nested_attributes_for :quote
My current Quote model:
belongs_to :topic
And my "Edit Topic" view:
<h4>Quote</h4>
<% if #quote %>
<h5>Current quote</h5>
<%= #quote.quote %> <%= link_to('[change]', '#') %>
<% end %>
<%= f.input :quote, :collection => Quote.all, :label_method => :quote, :label_value => :id, :checked => #quote.id, as: :radio_buttons %>
I'm sure there is something obvious that I'm missing but I can't figure out what.
Any ideas?
Thanks!
- Vincent
First off, if you want to have quote belong to multiple models, you will need a polymorphic association. Otherwise, you would need to add multiple foreign ids to quote like this: topic_id, profile_id etc and that will get messy fast. You can view a screencast on polymorphism here: http://railscasts.com/episodes/154-polymorphic-association-revised
has_one and belongs_to is basically the exact same as has_many and belongs_to except you are only dealing with 1 record instead of a collection of records.
For your current setup - in your edit action you need to fetch all the quotes that are not associated to any Topics. You can do that like this:
#available_quotes = Quote.where(topic_id: nil)
and then:
<%= f.input :quote, :collection => #available_quotes, :label_method => :quote, :label_value => :id, :checked => #quote.id, as: :radio_buttons %>
instead of
Quote.all in your form which is returning all quotes.
If you move to a polymorphic model, watch the screencast, and you would replace "commentable_id" in the screencast with something like "quotable_id" and then in your edit action to find the unassigned quotes you would do this:
#quotes = Quote.where(quotable_id: nil)
I have a form that prompts for a job and job_files to attach. When the job save fails due to validation, the selected job_files disappear when the form redisplays. How do I retain the child fields? All parent fields are retained in the form but the child fields are gone.
job model:
class Job < ActiveRecord::Base
has_many :job_files, :dependent => :destroy
accepts_nested_attributes_for :job_files, :allow_destroy => true
validates_acceptance_of :disclaimer, :allow_nil => false, :accept => true, :on => :create, :message=>'Must accept Terms of Service'
end
job_files model:
class JobFile < ActiveRecord::Base
belongs_to :job
has_attached_file :file
end
jobs_controller:
def new
#upload_type = UploadType.find_by_id(params[:upload_job_id])
#job = Job.new
#job.startdate = Time.now
10.times {#job.job_files.build}
#global_settings = GlobalSettings.all
end
def create
#global_settings = GlobalSettings.all
if params[:cancel_button]
redirect_to root_path
else
#job = Job.new(params[:job])
#job.user_id = current_user.id
#upload_type = UploadType.find_by_id(#job.upload_type_id)
if #job.save
JobMailer.job_uploaded_notification(#upload_type,#job).deliver
flash[:notice] = "Job Created"
redirect_to root_path
else
# retain selected files if save fails
(10 - #job.job_files.size).times { #job.job_files.build }
flash[:error] = "Job Creation Failed"
render :action => :new
end
end
end
job_files partial within job form:
<%= form.fields_for :job_files, :html=>{ :multipart => true } do |f| %>
<li class="files_to_upload">
<%= f.file_field :file %>
</li>
<% end %>
Any help would be appreciated since I can not find any solution online.
That's standard-issue behavior. Really, the only way to workaround it would be to persist the files first, regardless of whether validation passes. Then, run your validations. But that's definitely a workaround.
You could also do client-side validations before the server-side ones; that way, you'd have better assurances that the user's data is valid BEFORE they submit to the server, such that they won't encounter any server-side validation failures, and the files will always make it. I'd probably go that route if I were in your shoes. Note that client-side validation is NOT a replacement for server-side validation, just will help your users in this case.
Hope that helps!
I´m trying to make a User - Infos association where my User has_many Infos.
I´m trying to call my User Infos on the index form from Users.
By calling <%= user.infos %> on my index.html.erb (Users) it returns me all datas from the Infos table like this:
[#<Info id: 2, name: "Thales Miguel", date: "1989-07-14", area: "An\xC3\xA1lise de Sistemas", comment: "Analista j\xC3\xBAnior de sistemas.", user_id: 1, created_at: "2012-01-16 15:54:29", updated_at: "2012-01-16 15:54:29">]
I assumed that by doing <%= user.infos.comment %> it would return me the "comment" from that user, but all I get is this error:
undefined method `comment' for #<ActiveRecord::Relation:0x3b8ebe8>
What am I doing wrong?
infos_controller:
class InfosController < ApplicationController
def create
#user = User.find(params[:user_id])
#info = #user.infos.create(params[:info])
redirect_to user_path(#user)
end
def destroy
#user = User.find(params[:user_id])
#info = #user.infos.find(params[:id])
#info.destroy
redirect_to user_path(#user)
end
def new
#user = User.new
#user.build_info
end
end
info model:
class Info < ActiveRecord::Base
belongs_to :user
end
user model:
class User < ActiveRecord::Base
validates :login, :presence => true
validates :password, :presence => true,
:length => {:minimum => 5}
has_many :infos, :dependent => :destroy
end
This:
[#<Info id: 2, name: "Thales Miguel", date: "1989-07-14", area: "An\xC3\xA1lise de Sistemas", comment: "Analista j\xC3\xBAnior de sistemas.", user_id: 1, created_at: "2012-01-16 15:54:29", updated_at: "2012-01-16 15:54:29">]
is an array. You have to choose one of the objects. You can't call an attribute value without selecting one of theses objects. Okay, in this case there is only one object but that doesn't matter.
So, collecting all info what written above:
You cannot call user.infos.comment, because the return value of user.infos is an ActiveRecord::Relation in Rails 3.2 and an array in Rails 3.1 and below. Neither ActiveRecord::Relation nor array do not know what it should do with comment method because they don't know anything of the contained objects.
You have multiple solutions for solve this problem.
If user is limited to have only one info:
class User < ActiveRecord::Base
has_one :info, :dependent => :destroy
end
<%= user.info.comment %>
If user can have multiple info objects:
<%- user.infos.each do |info| %>
<%= info.comment %>
<% end -%>
Or
<ul><li>
<%= user.infos.collect(&:comment).join('</li><li>').html_safe %>
</li></ul>
Or if you want to display only the first info:
<%= user.infos.any? ? user.infos.first.comment : "N/A" %>
Be careful the latest solution: user.infos never be nil, but user.infos.first can be nil, so you must check available infos with user.infos.any?
You should do this way:
<% user.infos.each do |info| %>
<%= info.comment %>
<% end %>
As davidb said, <% user.infos %> returns an array. The object of .comment should be an instance.
The problem is
info is a relationship link
that returns an array of info records, if you want to show the first one for example
user.infos[0].comment
or
user.infos.first.comment
if that user don't have infos then return nil
In my application I have a very simple association. A User has_many Emails, this association works as expected with create/update/delete.
How ever when I attempt to display the information this is where things become some what difficult.
In my controller I have the following bit of code:
def prospective_user
#users = Account::User.all_by_user_status(0)
#users.each do |u|
u.email = u.email.get_primary_email
end
end
What this should do is create an instance of the Account::Email model with a single record.
The method get_primary_email looks like this:
def self.get_primary_email
first :conditions => ["is_primary = 1"]
end
The issue I am seeing is with in my view, I am getting the exception below. This is a bit confusing as I am not looping over the email object.
undefined method each' for #<Account::Email:0x7fcc3a5c49d8>
The code I am using to test with is this:
<% #users.each do |u|%>
<p>
<%=debug(u.email)%>
</p>
<% end %>
Do something like that instead:
class User < ActiveRecord::Base
has_many :emails
has_one :primary_email, :class_name => 'Email',
:conditions => { is_primary: 1 }
end
Then you can call user.primary_email directly
# your controller
def prospective_user
#users = Account::User.includes(:primary_email).where(:status => 0)
end
# your view
<% for user in #users %>
<%= user.primary_email %>
<% end %>
I have a model named UserPrice which has the attribute :purchase_date(a date_select) in its table. With my form I can create multiple user_prices at once but for user convenience I made a virtual attribute inside of my UserPrice model called :all_dates that's also a date_select field and its job is to be the replacement of the :purchase_dates so users only have to select the :all_dates field for the date.
Problem & Question
The :all_dates field is not updating the :purchase_date fields of my user_prices that are being created. What do I need to do in order to get my :all_dates field to update the :purchase_date fields of my new UserPrices?
Does anyone have any tips on how to do this?
Parameters
Parameters:
"user_price"=> {
"all_dates(2i)"=>"10",
"all_dates(3i)"=>"27",
"all_dates(1i)"=>"2011"
},
"user_prices"=>
{
"0"=>{"product_name"=>"Item1", "store"=>"Apple Store","price"=>"6"},
"1"=>{"product_name"=>"Item2", "store"=>"Apple Store", "price"=>"7"}
},
"commit"=>"Submit"}
Code
class CreateUserPrices < ActiveRecord::Migration
def self.up
create_table :user_prices do |t|
t.decimal :price
t.integer :product_id
t.date :purchase_date
t.timestamps
end
end
end
I took out the :purchase_date field so it isn't inside of the user_price loop.
<%= form_tag create_multiple_user_prices_path, :method => :post do %>
<%= date_select("user_price", "all_dates" ) %>
<% #user_prices.each_with_index do |user_price, index| %>
<%= fields_for "user_prices[#{index}]", user_price do |up| %>
<%= render "user_price_fields", :f => up %>
<% end %>
<% end %>
<% end %>
class UserPrice < ActiveRecord::Base
attr_accessible :price, :product_name, :purchase_date, :all_dates, :store
attr_accessor :all_dates
after_save :save_all_dates_to_user_prices
composed_of :all_dates, :class_name => "DateTime",
:mapping => %w(Time to_s),
:constructor => Proc.new { |item| item },
:converter => Proc.new { |item| item }
def user_prices
#user_prices = Array.new() { UserPrice.new }
end
protected
def save_all_dates_to_user_prices
if !self.all_dates.nil?
self.user_prices.each {|up| up.purchase_date = self.all_dates if up.new_record?}
end
end
class UserPricesController < ApplicationController
def new
#user_prices = Array.new(5) { UserPrice.new }
end
def create_multiple
#user_prices = params[:user_prices].values.collect { |up| UserPrice.new(up) }
if #user_prices.all?(&:valid?)
#user_prices.each(&:save!)
redirect_to :back, :notice => "Successfully added prices."
else
redirect_to :back, :notice => "Error, please try again."
end
end
This is a case of trying to do in a model what is better left to the controller. All you're trying to do here is to auto-assign a certain attribute on creation from a parameter not directly tied to your model. But you're not even passing that extra parameter to the model anywhere - you're creating your model instances from the user_prices parts of the parameter hash, but the user_price sub-hash is not used anywhere. In any case, this is behavior that is more closely related to the view and action taken than the model, so keep it in the controller.
Try this:
Throw out the virtual attribute, and get rid of the whole after_save callback stuff
Throw away the user_prices method in your model
Change the all_dates attribute name back to purchase_date in the form
Then your parameter hash should look like this:
{"user_price"=> {
"purchase_date(2i)"=>"10",
"purchase_date(3i)"=>"27",
"purchase_date(1i)"=>"2011"
},
"user_prices"=>
{
"0"=>{"product_name"=>"Item1", "store"=>"Apple Store","price"=>"6"},
"1"=>{"product_name"=>"Item2", "store"=>"Apple Store", "price"=>"7"}
}}
All that's left to do is to merge the single user_price attributeS into each user_prices sub-hash in your create_multiple action. Replace the first line in that action with this:
#user_prices = params[:user_prices].values.collect do |attributes|
UserPrice.new(attributes.merge(params[:user_price]))
end
I'm not sure why you are even using that virtual attribute is there more to this implementation? If you are just trying to save an associated model, you might simply want a accepts_nested_attributes_for :user_prices in your User model
This works great and many developers use this method, so it's nice to know for working on other projects as well as for the people who might end up maintaining yours.
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2