2

I need to get 5 most viewed articles by users. For this I have implemented "impression" functionality and there is polymorphic relation between article and impression model.Using this, I can get no of views each article gets from user. But now I need to fetch 5 most viewed articles from db. I know It is silly question but I tried and could not find answer.

schema.rb (i have not pasted complete file but only relevant)

ActiveRecord::Schema.define(:version => 20130419101017) do

  create_table "articles", :force => true do |t|
    t.string   "title"
    t.text     "body"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
    t.integer  "user_id"
  end

  create_table "comments", :force => true do |t|
    t.text     "content"
    t.integer  "user_id"
    t.string   "article_id"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
  end

  create_table "impressions", :force => true do |t|
    t.string   "impressionable_type"
    t.integer  "impressionable_id"
    t.integer  "user_id"
    t.string   "ip_address"
    t.datetime "created_at",          :null => false
    t.datetime "updated_at",          :null => false
  end

articles_controller.rb

class ArticlesController < ApplicationController
  before_filter :is_user_admin, only: [:new, :create, :edit, :destroy]
  before_filter :log_impression, :only=> [:show]

    def is_user_admin
      redirect_to(action: :index) unless current_user.try(:is_admin?) 
      return false 
    end

   def log_impression
     @article = Article.find(params[:id])
     # this assumes you have a current_user method in your authentication system
      @article.impressions.create(ip_address: request.remote_ip,user_id:current_user.id)
   end

      def index
          @articles = Article.all(:order => "created_at DESC")
      @article_titles = Article.first(10)
      @article_popular = Article. 
      @tags = Tag.all
      end

    def show
       @article = Article.find(params[:id])
       @related_articles = Article.joins(:taggings).
       where('articles.id != ?', @article.id)
       .where(taggings: { tag_id: @article.tag_ids }) 
    end

      def new
      @article = Article.new
      end

    def create
      @article = Article.new(params[:article])
      @article.user_id = current_user.id
      if @article.save
        flash[:success] = "article created!"
        redirect_to article_path(@article)
      else
        render 'new' 
      end 
    end

    def destroy
      @article = Article.find(params[:id])
      @article.destroy
      redirect_to action:  'index'  
    end

    def edit
      @article = Article.find(params[:id])
    end

    def update
      @article = Article.find(params[:id])
      if @article.update_attributes(params[:article])
       flash.notice = "Article '#{@article.title}' Updated!"
       redirect_to article_path(@article)
      else 
        render 'edit'
      end
    end
end

article.rb

class Article < ActiveRecord::Base
   attr_accessible :title, :body
   attr_accessible :tag_list
   has_many :comments
   belongs_to :user
   has_many :taggings
   has_many :tags, through: :taggings
   has_many :impressions, :as=>:impressionable
   validates :title, :body, :tag_list,  :presence => true

  def impression_count
    impressions.size
  end

  def unique_impression_count
    impressions.group(:ip_address).size #UNTESTED: might not be correct syntax
  end


   def tag_list
     self.tags.collect do |tag|
      tag.name
     end.join(", ")
   end

   def tag_list=(tags_string)
     tag_names = tags_string.split(",").collect{|s| s.strip.downcase}.uniq
     new_or_found_tags = tag_names.collect { |name| Tag.find_or_create_by_name(name) }
     self.tags = new_or_found_tags
   end
end

impression.rb

class Impression < ActiveRecord::Base
  attr_accessible :impressionable_id, :impressionable_type, :ip_address, :user_id
  belongs_to :impressionable, :polymorphic=>true 
end

articles/show.html.erb

Viewed: <%=@article.impression_count %> times

This is how I calculate no of views from users. Now I want to get 5 most viewed articles by users. Any help would be appreciating. For more info, I have used (simple hit counter for page views in rails) this to get user views.

Community
  • 1
  • 1
Vieenay Siingh
  • 867
  • 2
  • 17
  • 44

2 Answers2

3

I think the best way to go here, to avoid mapping of ruby objects, is to use a counter cache:

# add the impression count column to your tables
t.integer :articles, :impressions_count
t.integer :comments, :impressions_count

In your models:

class Article < AR
  has_many :impressions, as: :impressionable
  has_many :comments
end

class Comment < AR
  belongs_to :article
  has_many :impressions, as: :impressionable
end

class Impression < AR
  # the counter cache here is the important option
  belongs_to :impressionable, polymorphic: true, counter_cache: :impressions_count
end

With this setup every time you create an impression the impressions_count column in the respective model is being updated. So then you can simply sort the number of impressions to get the 5 top most:

Article.order('articles.impressions_count DESC').limit(5)

respectively

Comment.order('comments.impressions_count DESC').limit(5)

And of course you can do @article.impressions_count as well...

Vapire
  • 4,568
  • 3
  • 24
  • 41
  • hi Vampire, Thanks for your suggestion. I need little more info. If I implement your suggestion, do i need to change my existing setup for impression count. any suggestions. – Vieenay Siingh Apr 21 '13 at 10:58
  • Not as far as I can see... The counter cache would get updated with the `@article.impressions.create` call in your controller. But what you could/should ditch is the `impression_count` method in your `Article` and `Comment` model - since you would be using the counter cache column instead. I also think this would benefit performance, since you can simply leave the ordering to the database and don't need to do it in ruby... – Vapire Apr 21 '13 at 11:06
  • ps: you really should read up on this possibility in the rails guides anyway though... – Vapire Apr 21 '13 at 11:07
  • hi Vampire, i tried to implement your suggestion, it gives me no output. I just implemented for article only. first of all, i added colomn to article table and make changes to article and impression model as per your suggestion and put your join query in show action of article and use that instance variable in views of article but no output. any idea – Vieenay Siingh Apr 21 '13 at 12:42
  • hi Vampire, this is how i use your suggestion in show action of article.-: @article_popular = Article.joins(:impressions).order('articles.impressions_count DESC').limit(5) – Vieenay Siingh Apr 21 '13 at 12:47
  • hi Vampire, there was typo mistake from my side. Sorry for this spam. now your suggestion working but not giving exact output. It shows same article for 5 times. but we need 5 top articles which are most viewed. but it is showing same article 5 times (thought it is most viewed). any idea how to improve this query. – Vieenay Siingh Apr 21 '13 at 12:51
  • Did you set it up on existing data? If so, then you need to update the counter cache column manually first, since if you migrate it it's gonna be NULL by default... – Vapire Apr 21 '13 at 13:18
  • hi Vampire, i did on existing data. i am getting output but it gives me same article 5 times instead of 5 different articles. should i first delete all data . – Vieenay Siingh Apr 21 '13 at 13:42
  • hi Vampire, i deleted all the data and inserted new data. problem is same as it shows same article five times which is viewed maximum time but not five different articles. it may be problem with join query that you suggested. any idea. – Vieenay Siingh Apr 21 '13 at 13:57
  • See my edit - you don't have to join the impressions with this setup, of course... sorry – Vapire Apr 21 '13 at 14:02
  • hi, It worked like magic. Thanks. sorry for lengthy discussion. – Vieenay Siingh Apr 21 '13 at 14:08
1

Have you tried with:

impressions = Impression.select("impressionable_id, count(*) as qty").where("impressionable_type = ?", 'Article').group('impressionable_type, impresionable_id').order('qty desc').limit(5)
Article.joins(:impressions).where(impressions: {impresionable_id: impressions.map(&:impressionable_id)})
rorra
  • 9,593
  • 3
  • 39
  • 61
  • hi rorra,thanks for your response. will this join query written in articles_controller.rb or somewhere else. If it is in articles_controller.rb, then how to accesss in views. Do I need to put it in impression.rb(model). Any suggestion. – Vieenay Siingh Apr 21 '13 at 10:36
  • \@articles = Article.joins(:impressions).where(impressions: {impresionable_id: impressions.map(&:impressionable_id)}) You can write it on any part, controller, views, etc. as long as you have access to the model. – rorra Apr 21 '13 at 10:38
  • hi rorra, when I tried to implement your suggestion in articles_controller.rb/show action, I got following error:- ActiveRecord::StatementInvalid in ArticlesController#show SQLite3::SQLException: no such column: impresionable_id: SELECT impressionable_id, count(*) as qty FROM "impressions" WHERE (impressionable_type = 'Article') GROUP BY impressionable_type, impresionable_id ORDER BY qty desc LIMIT 5 . It says "StatementInvalid in ArticlesController#show". Any suggestion. – Vieenay Siingh Apr 21 '13 at 10:55
  • It's just a typo in the group clause. `impresSionable_id` – Vapire Apr 21 '13 at 11:02
  • yes, I have changed typo mistake. your suggestion somehow worked but It gave me articles multiple times. Suppose, An article is viewed 10 times , It showed that article 10 times, rather than it should show that article one time only. This is how i used it in views :- <%= articles_popular.title %>. – Vieenay Siingh Apr 21 '13 at 11:14
  • that's weird, have you already debugged, to see what you get on the variable **impressions** ? You should get an array of five records, where the first column is the id of the impression, and the second column is the number of impressions. – rorra Apr 21 '13 at 11:18
  • hi rorra, your query fetches top 5 viewed articles but each article is shown multiple times(suppose a particular article is shown 10 times, it is displayed 10 times. but it should be shown only one time). Any suggestion how to optimize this query. – Vieenay Siingh Apr 21 '13 at 11:20
  • Would you mind to join me on [link](http://chat.stackoverflow.com/rooms/5676/ruby-on-rails) – rorra Apr 21 '13 at 11:25
  • hi rorra, I got solution by implementing Vapire's suggestion. Anyhow thanks. – Vieenay Siingh Apr 21 '13 at 16:11