I have three models: User, Comment and Upvote. User-to-Comment has a one-to-many relation, Comment-to-Upvote has a one-to-many relation and User-to-Upvote has a one-to-many relation.
I want to do something similar to the upvoting done on Stackoverflow. So when you upvote/downvote the arrow will highlight and remain highlighted even if you refresh the page or come back to the page days/weeks later.
Currently I am doing this:
<% if Upvote.voted?(#user.id, comment.id) %>
<%= link_to '^', ... style: 'color: orange;'%>
<% else %>
<%= link_to '^', ... style: 'color:black;'%>
<% end %>
where the voted? method looks like this:
def self.voted?(user_id, comment_id)
find_by(comment_id: comment_id, user_id: user_id).present?
end
So if I have 10 comments on a page, this will load an upvote from my database 10 times, just to check if it exist!
There has to be a better way to go about doing this, but I think my brain stopped working, so I can't think of any.
Assuming you have properly set relations
# user.rb
class User
has_many :upvotes
end
we can load comments, current user and his upvotes:
# comments_controller.rb
def index
#comments = Comment.limit(10)
#user = current_user
user_upvotes_for_comments = current_user.upvotes.where(comment_id: #comments.map(&:id))
#upvoted_comments_ids = user_upvotes_for_comments.pluck(:comment_id)
end
And then change if condition in view:
# index.html.erb
<% if #upvoted_comments_ids.include?(comment.id) %>
<%= link_to '^', ... style: 'color: orange;'%>
<% else %>
<%= link_to '^', ... style: 'color:black;'%>
<% end %>
It will require only 2 DB queries. Hope it helps.
We can do it the following way if you want it to be handled by a single query.
Lets make sure the relations are proper
# user.rb
class User < ActiveRecord::Base
has_many :comments
has_many :upvotes
end
# comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
has_many :upvotes
end
# upvote.rb
class Upvote < ActiveRecord::Base
belongs_to :user
belongs_to :comment
end
Then in the controller
def index
current_user = User.first # current_user may come from devise or any authentication logic you have.
#comments = Comment.select('comments.*, upvotes.id as upvote').joins("LEFT OUTER JOIN upvotes ON comments.id = upvotes.comment_id AND upvotes.user_id = #{current_user.id}")
end
And in view
# index.html.erb
<% #comment.each do |comment| %>
<% link_color = comment.upvote ? 'orange' : 'black' %>
<%= link_to '^', ...style: "color: #{link_color}" %>
<% end %>
# And all of your logics ;)
If you're limited to N comments per page then you can probably do this in two queries using the limit and offset methods to return the 1st, 2nd, ... ith set of N comments for the ith page, something like (syntax may be off, Ruby isn't my primary language)
comment_ids =
Comments.select("comment_id")
.where(user_id: user_id)
.order(post_date/comment_id/whatever)
.offset(per_page * (page_number - 1)) // assumes 1-based page index
.limit(per_page)
This gives you a list of comment_ids which you can use to query Upvote:
upvoted_comments =
Upvotes.select("comment_id")
.where(user_id: user_id, comment_id: comment_ids)
If you're sorting the comment_ids by a column that also exists in Upvote (e.g. if you're sorting by comment_id) then you can replace the Upvote set query with a range query.
Put upvoted_comments in a hash and you're good to go - if the comment_id is in the hash then it's been upvoted, else not.
I'm not sure this will avoid excess queries in this state but maybe you could include the upvotes when you fetch comments:
#comments = Comment.includes(:upvotes).where(foo: 'bar').limit(10)
Then in the view:
<%=
link_color = comment.upvotes.map(&:user_id).include?(#user.id) ? 'orange' : 'red'
link_to '^', ...style: "color: #{link_color}"
%>
Related
I created a rails app that allows the user to read a magazine.
To do so, I created two scaffolds, one for the magazine and an other for the pages inside of it. I then made a one-to-many relationship, so the pages belong to the magazine.
Each page is an image, since they are digitized then uploaded in a multi-upload form.
Recently, the group I work for asked me to find a way to allow the user to read two pages at the same time, so I made some tweaks, and it works like a charm.
However, I now have a problem: I want to set some "previous" and "next" links, but I can't find a way to do so
Here is what I have done so far:
magazine_controller.rb
def create
#magazine = Magazine.new(magazine_params)
if #magazine.save
#index = 0
(params[:images] || []).each_with_index do |image, index|
if index.even?
#This way, #index increments every other upload
#So I'm sure I have two images with the same page_number
#index += 1
end
#magazine.pages.create(image: image, page_number: #index)
end
redirect_to #magazine, notice: 'Magazine créé'
else
render :new
end
end
models/page.rb
class Page < ApplicationRecord
belongs_to :magazine
validates_presence_of :magazine
mount_uploader :image, PageUploader
def previous
self.class.first.where('page_number < ?', page_number).limit(1).first
end
def next
self.class.first.where('page_number > ?', page_number).limit(1).last
end
end
views/pages/show.html.erb
<% #page.each do |p| %>
<%= image_tag p.image %>
<%= p.inspect %>
<% end %>
<br />
<%= #page.first.page_number %>
<%= link_to '< Précédent', magazine_page_path(magazine_id: #magazine.slug, id: #page.previous) if #page.previous %>
<%= link_to 'Suivant >', magazine_page_path(magazine_id: #magazine.slug, id: #page.next) if #page.next %>
<br />
<%= link_to 'Back', magazines_path %>
page_controller.rb
private
def set_page
#magazine = Magazine.find_by(slug: params[:magazine_id])
#was 'find_by' before I was asked to show two records at the same time
#page = #magazine.pages.where(page_number: params[:id])
end
So with this code, I'm getting the error undefined method 'previous' for #<Page::ActiveRecord_AssociationRelation:0x007ff4f702ad48>. I don't have a clue about how to find if there is a following "page" or not.
Any idea welcome!
Thank you in advance
Remember that #page is no longer a single record, it's an association with two records.
You can create previous and next page methods in Page class for the association instead of the object (self.previous instead of previous). It will get a new association for the previous (or next) page number. Note the addiitional code to make sure you're getting the same magazine (which you don't have in your current code that worked for single pages).
Also note that if the association has no records (count == 0) the methods return nil... this is to accommodate your if #page.previous test for no previous (and if #page.next if no next)
def self.previous
new_page_set = Page.where(page_number: (first.page_number - 1), magazine: first.magazine)
return new_page_set.count == 0 ? nil : page_set
end
def self.next
new_page_set = Page.where(page_numberL (first.page_number + 1), magazine: first.magazine)
return new_page_set.count == 0 ? nil : page_set
end
I'm adding a new model to my equasion and I'm wondering if there is a way to associate two models into one model then display any/all results within a view. For example, here is what I've currently have;
#tweet_category.order("position").each do |tweet|
<%= tweet.title %>
end
just a short example... now what if I added facebook into this. I was first thinking of creating a model thats named stuff then associate it to tweet_category and facebook_category like so;
class Stuff < ActiveRecord::Base
attr_accessible :title
belongs_to :user
has_many :tweet_category
has_many :facebook_category
end
Now in my controller I'm guessing I would do the following;
class StuffController < ApplicationController
def index
#stuff_list = Stuff.find(:all)
end
end
and in my view I would just simply do the following from above view;
#stuff_list.order("position").each do |stuff|
<%= stuff.title %>
end
am I understanding the logic here??? would that work having two models / two tables db.. etc..
First of all, I don't understand why you would need that "stuff" model. It belongs to users and has_many tweet_category and facebook_category, and just does nothing but offering a "title", when your User model could do the job ( I mean, each user could have many tweets and fb category, instead of having one or several "stuff" which has/have many of them ).
Anyway, if you want to make links between your models and then display everything in a view, first in your User model you just have to do :
class User < ActiveRecord::Base
...
has_many :facebook_categories #( I don't know how rails would pluralize it, btw, I'm just making an assumption )
has_many :tweeter_categories
end
and
class Facebook_category
...
belongs_to :user
end
and do the same fot the tweeter category
Then in your controller :
def show_everything #Here it's a custom action, but you can call it wherever you want
#users = User.all
end
And finally in your view :
<% #users.each do |user| %>
<% user.facebook_categories.all.each do |fb_c| %>
<%= fb_c.title %>
<% end %>
<% user.tweeter_categories.all.each do |t_c| %>
<%= t_c.title %>
<% end %>
<% end %>
Maybe just try to grab a better name for your models, so the pluralization doesn't get messy ( and I saw that the ".all" method is deprecated, so maybe replace it with something
Hope it helps !
Edit :
Basically, when you're doing
#users = User.all
What rails' doing is putting every hash defining every "User" in an array. So, if you want to mix two tables' arrays inside a single array, you can do something like this :
#categories = [] << Facebook_category.all, Tweeter_category.all
You will then have an array ( #category ), filled with 2 arrays ( one ActiveRecord relation for Facebook_category and one for Tweeter_category ). Themselves filled with hashes of their model. So, what you need to do is :
#categories.flatten!
Here's the API for what flatten does ( basically removing all your nested arrays inside your first tarray )
Now, you got a single array of hashes, being the informations from both your model's instances. And, if these informations can be ordered, in your view, you just have to :
<% #categories.order("updated_at").each do |i| %>
<%= i.title %>
<% end %>
I'm new to rails. I've searched and been stuck on this problem for a couple days now.
I am building a site where there are users, bands (that users can join), tours (that belong to bands), and stops (stops on the tours). I have tables for each, as well as additional tables that link them together by id (bandmembership, bandtourmembership, tourstopmembership).
I followed a few tutorials and have used belongs_to, has_many => through to link these all together and I have used nested attributes to display data from one level deep successfully.
The final format I'm trying to display is
User Name
=> Band Name #1
====> Tour Name #1
========> Tour Stop #1
========> Tour Stop #2
====> Tour Name #2
========> Tour Stop #1
========> Tour Stop #2
=> Band Name #2
====> Tour Name #3
========> Tour Stop #1
========> Tour Stop #2
etc.
Currently I only can get the band name to display without an error, but it displays the same band name 3 times (there are 3 in the database). When I try to add in tours it just gives an error. I'd also like to try to use a partial and a collection to break out the rendering of each time of item.
My questions are
Why is the partial displaying the same name 3 times and how do I get it to display the correct name?
Why am I not able to access tours from bands and how do I get it to cooperate?
views/users/show.html.erb
<h1>Dashboard</h1>
<%= render partial: 'shared/user_item' %>
<% if #user.bands.any? %>
<h2>You are in <%= #user.bands.count %> bands:</h2>
<%= render partial: 'shared/band_item', collection: #band_items %>
<% else %>
shared/_band_item.html.erb
<%= #band.name %>
shared/_tour_item.html.erb
<%= #tour.name %>
shared/_stop_item.html.erb
<%= #stop.name %>
controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
sign_in #user
flash[:success] = "You are now signed in"
redirect_to #user
else
render 'new'
end
end
def show
#user = User.find(params[:id])
#band_items = Bandmembership.where(user_id: #user.id)
#band = Band.find(params[:id])
end
def new
#user = User.new
end
private
def user_params
params.require(:user).permit(:firstname, :lastname, :email, :password, :password_confirmation)
end
end
models/user.rb
class User < ActiveRecord::Base
has_many :bandmemberships
has_many :bands, :through => :bandmemberships
has_many :tours, :through => :bands
accepts_nested_attributes_for :bands, :tours
end
models/bandmembership.rb
class Bandmembership < ActiveRecord::Base
belongs_to :user
belongs_to :band
end
models/tour.rb
class Tour < ActiveRecord::Base
has_many :tourstopmemberships
has_many :stops, :through => :tourstopmemberships
has_many :bandtourmemberships
has_many :bands, :through => :bandtourmemberships
accepts_nested_attributes_for :stops
end
This smells like something solvable by a class-level delegate method (Rails Antipatterns, pp 6-7)
You've got the show method pulling the params for both user and band. Is that something like tld.com/user/1/band/3?
If you don't have params for both in the ID, then it's pulling the user's ID for band or vice versa.
From a code perspective, you should be able to refactor towards something like this:
<h1>Dashboard</h1>
<%# this should render app/views/users/_user.html.erb %>
<%= render #user %>
<% if #user.bands %>
<h2>
You are in <%= #user.bands.count %> bands:
</h2>
<ul>
<%= #user.bands.each do band %>
<%# this should render app/views/bands/_band.html.erb %>
<%= render #band,
locals: (band: band) %>
<% end %>
</ul>
<%- end -%>
Your _band.html.erb would be
<li class="band-name">
<%= band.name %>
</li>
The code may not be 100% right as I keep jumping between a 2.3 app and a 4.x app. But the principle is this:
1.) Use ActiveRecord's power to your benefit. You've got the model association, so you don't need to do the lookup yourself. #user.bands should return an array of bands the user belongs to since they belong to those bands THROUGH bandmemberships.
2.) If you need to get to something, don't walk the tree over 2 or 3 items. e.g. #user.band.first.tour.first is bad bad juju. Create a method that finds this on the User model and then go from there, e.g.
def next_tour
User.tour.first etc etc
end
and then call it in the view as #user.next_tour or whatever.
3.) Use the power of render #collection_name and use the defaults to clean your code up. It's easier to read and better than a lot of partials floating around in shared/.
This is something I harp on a lot when I give my Rails View talks, the partials should belong in the folder for the controller they exist under. /app/views/tours/_tour.html.erb and so forth would be better than the tour_item under shared. It's the rendering of a single tour entry for anywhere in the app.
I'm not sure about tourstopmemberships as a join model either. What are you joining it to? Why not just have a tour has many stops and stops belong to a tour? If you're looking at a venue model as well, then perhaps stops is the join model between tours and venues. That then allows you to add additional meta data onto the stop
stop.tour
stop.venue
stop.start_time
stop.support_act (which could be a different relationship)
etc.
I've googling and trying everything I could think of for the past couple of days to solve a relatively simple (I presume) issue with has_and_belongs_to_many relation.
I managed to successful use the HABTM relation to submit a single relation value. Here's the sample code:
Model:
class Livre < ActiveRecord::Base
has_and_belongs_to_many : auteurs
end
class Auteur < ActiveRecord::Base
has_and_belongs_to_many :livres
end
Controller:
def new
#livre = Livre.new
#auteurs = Auteur.all
end
def create
#livre = Livre.new(livre_params)
if #livre.save
redirect_to [:admin, #livre]
else
render 'new'
end
end
private
def livre_params
params.require(:livre).permit(:name, :auteur_ids)
end
View:
<% f.label :auteur %><br>
<% f.collection_select(:auteur_ids, #auteurs, :id, :name) %>
Posted Params:
{"utf8"=>"✓",
"authenticity_token"=>"mAXUm7MRDgJgCH00VPb9bpgC+y/iOfxBjXSazcthWYs=",
"livre"=>{"name"=>"sdfsdfd",
"auteur_ids"=>"3"},
"commit"=>"Create Livre"}
But when I try to add "multiple true" to the view's collection_select helper, the (now multiple) relation doesn't get saved anymore. Sample code:
(both Model and Controller unchanged)
View:
<% f.label :auteur %><br>
<% f.collection_select(:auteur_ids, #auteurs, :id, :name, {}, {:multiple => true}) %>
Posted Params:
{"utf8"=>"✓",
"authenticity_token"=>"mAXUm7MRDgJgCH00VPb9bpgC+y/iOfxBjXSazcthWYs=",
"livre"=>{"name"=>"sdfsdf",
"auteur_ids"=>["1",
"5",
"3"]},
"commit"=>"Create Livre"}
As you can see, the params for "auteur_ids" is now an array. That's the only difference.
What am I doing wrong?
Just to clarify: both piece of code are able to add a new record to the livres db table, but only the 1st code is able to add the appropriate record to the auteurs_livres db table. The second one simply does not insert anything into auteurs_livres.
(I run on ruby 1.9.3p194 and rails 4.0.1)
Thanks!
Answer
For the fine folks stuck with the same problem, here's the answer:
Edit your controller and change the permitted parameter from :auteur_ids to {:auteur_ids => []}
params.require(:livre).permit(:name, {:auteur_ids => []})
And it now works :)
For the fine folks stuck with the same problem, here's the answer:
Edit your controller and change the permitted parameter from :auteur_ids to {:auteur_ids => []}
params.require(:livre).permit(:name, {:auteur_ids => []})
And it now works :)
You worked out the solution because Rails now expects auteur_ids to be an array, rather than a single item. This means that instead of just passing a single entity to the model, it will package the params as [0][1][2] etc, which is how you can submit your HABTM records now
There is a more Rails way to do this, which is to use accepts_nested_attributes_for. This is going to seem like a lot more work, but it will dry up your system, and also ensure convention over configuration:
Model
class Livre < ActiveRecord::Base
has_and_belongs_to_many : auteurs
accepts_nested_attributes_for :auteurs
end
class Auteur < ActiveRecord::Base
has_and_belongs_to_many :livres
end
Controller
def new
#livre = Livre.new
#livre.auteurs.build
#auteurs = Auteur.all
end
def create
#livre = Livre.new(livre_params)
if #livre.save
redirect_to [:admin, #livre]
else
render 'new'
end
end
private
def livre_params
params.require(:livre).permit(:name, auteur_attributes: [:auteur_id])
end
Form
<%= form_for #livre do |f| %>
<%= f.text_field :your_current_vars %>
<%= f.fields_for :auteurs do |a| %>
<%= a.collection_select(:auteur_id, #auteurs, :id, :name, {}) %>
<% end %>
<% end %>
This will submit the auteur_id for you (and automatically associate the livre_id foreign key in the HABTM model. Currently, this will only submit the number of objects which have been built in the new action -- so in order to add more items, you'll have to build more
I want to achieve such function.
I am currently viewing exact product, like, John Deer tractor, who is under category Tractors.
How can I generate link so that clicking on it it will send me to next tractor under Tractors category?
I am familar with some pagination like Kaminari and Will paginate, but can I do such thing without pagination gems ?
At this moment I don't have any idea to show, or what I have tried.
Thanks
Sure you can, let's say you have this model
class Category < ActiveRecord::Base
has_many :products
def previous_product(product)
products.where("created_at < ?", product.created_at).last
end
def next_product(product)
products.where("created_at > ?", product.created_at).first
end
end
You could also move the scoping logic to the Product class too :)
-- EDIT --
Inside the view, something like
<% next_product = #category.next_product(#product) %>
<% if next_product %>
<%= link_to next_product.title, product_url(next_product) %>
<% end %>