journalduhacker/app/controllers/home_controller.rb
joshua stein 0d6f4ed60c fix story pagination and caching for logged-out users
use a file store so each unicorn process can share the same set of
files (using redis would probably be cleaner)

put page number into how hash when creating the cache key
2014-02-16 09:57:05 -06:00

255 lines
6.7 KiB
Ruby

class HomeController < ApplicationController
STORIES_PER_PAGE = 25
# how many points a story has to have to probably get on the front page
HOT_STORY_POINTS = 5
# how many days old a story can be to get on the bottom half of /recent
RECENT_DAYS_OLD = 3
# for rss feeds, load the user's tag filters if a token is passed
before_filter :find_user_from_rss_token, :only => [ :index, :newest ]
def index
@stories = find_stories
@rss_link ||= "<link rel=\"alternate\" type=\"application/rss+xml\" " <<
"title=\"RSS 2.0\" href=\"/rss" <<
(@user ? "?token=#{@user.rss_token}" : "") << "\" />"
@heading = @title = ""
@cur_url = "/"
respond_to do |format|
format.html { render :action => "index" }
format.rss {
if @user && params[:token].present?
@title = "Private feed for #{@user.username}"
end
render :action => "rss", :layout => false
}
format.json { render :json => @stories }
end
end
def newest
@stories = find_stories({ :newest => true })
@heading = @title = "Newest Stories"
@cur_url = "/newest"
@newest = true
@rss_link = "<link rel=\"alternate\" type=\"application/rss+xml\" " <<
"title=\"RSS 2.0 - Newest Items\" href=\"/newest.rss" <<
(@user ? "?token=#{@user.rss_token}" : "") << "\" />"
respond_to do |format|
format.html { render :action => "index" }
format.rss {
if @user && params[:token].present?
@title += " - Private feed for #{@user.username}"
end
render :action => "rss", :layout => false
}
format.json { render :json => @stories }
end
end
def newest_by_user
by_user = User.where(:username => params[:user]).first!
@stories = find_stories({ :by_user => by_user })
@heading = @title = "Newest Stories by #{by_user.username}"
@cur_url = "/newest/#{by_user.username}"
@newest = true
@for_user = by_user.username
render :action => "index"
end
def recent
@stories = find_stories({ :recent => true })
@heading = @title = "Recent Stories"
@cur_url = "/recent"
@recent = true
render :action => "index"
end
def tagged
@tag = Tag.where(:tag => params[:tag]).first!
@stories = find_stories({ :tag => @tag })
@heading = @title = @tag.description.blank?? @tag.tag : @tag.description
@cur_url = tag_url(@tag.tag)
@rss_link = "<link rel=\"alternate\" type=\"application/rss+xml\" " <<
"title=\"RSS 2.0 - Tagged #{CGI.escape(@tag.tag)} " <<
"(#{CGI.escape(@tag.description.to_s)})\" href=\"/t/" +
"#{CGI.escape(@tag.tag)}.rss\" />"
respond_to do |format|
format.html { render :action => "index" }
format.rss { render :action => "rss", :layout => false }
end
end
def privacy
begin
render :action => "privacy"
rescue
render :text => "<div class=\"box wide\">" <<
"You apparently have no privacy." <<
"</div>", :layout => "application"
end
end
def about
begin
render :action => "about"
rescue
render :text => "<div class=\"box wide\">" <<
"A mystery." <<
"</div>", :layout => "application"
end
end
private
def find_stories(how = {})
@page = how[:page] = 1
if params[:page].to_i > 0
@page = how[:page] = params[:page].to_i
end
# guest views have caching, but don't bother for logged-in users or dev or
# when the user has tag filters
if Rails.env.development? || @user || tags_filtered_by_cookie.any?
stories, @show_more = _find_stories(how)
else
stories, @show_more = Rails.cache.fetch("stories " <<
how.sort.map{|k,v| "#{k}=#{v.to_param}" }.join(" "),
:expires_in => 45) do
_find_stories(how)
end
end
stories
end
def _find_stories(how)
stories = Story.where(:is_expired => false)
if @user && !(how[:newest] || how[:by_user])
# exclude downvoted items
stories = stories.where(
Story.arel_table[:id].not_in(
Vote.arel_table.where(
Vote.arel_table[:user_id].eq(@user.id)
).where(
Vote.arel_table[:vote].lt(0)
).where(
Vote.arel_table[:comment_id].eq(nil)
).project(
Vote.arel_table[:story_id]
)
)
)
end
filtered_tag_ids = []
if @user
filtered_tag_ids = @user.tag_filters.map{|tf| tf.tag_id }
else
filtered_tag_ids = tags_filtered_by_cookie.map{|t| t.id }
end
if how[:tag]
stories = stories.where(
Story.arel_table[:id].in(
Tagging.arel_table.where(
Tagging.arel_table[:tag_id].eq(how[:tag].id)
).project(
Tagging.arel_table[:story_id]
)
)
)
elsif how[:by_user]
stories = stories.where(:user_id => how[:by_user].id)
elsif filtered_tag_ids.any?
stories = stories.where(
Story.arel_table[:id].not_in(
Tagging.arel_table.where(
Tagging.arel_table[:tag_id].in(filtered_tag_ids)
).project(
Tagging.arel_table[:story_id]
)
)
)
end
if how[:recent] && how[:page] == 1
# try to help recently-submitted stories that didn't gain traction
story_ids = []
10.times do |x|
# grab the list of stories from the past n days, shifting out popular
# stories that did gain traction
story_ids = stories.select(:id, :upvotes, :downvotes).
where(Story.arel_table[:created_at].gt((RECENT_DAYS_OLD + x).days.ago)).
order("stories.created_at DESC").
reject{|s| s.score > HOT_STORY_POINTS }
if story_ids.length > STORIES_PER_PAGE + 1
# keep the top half (newest stories)
keep_ids = story_ids[0 .. ((STORIES_PER_PAGE + 1) * 0.5)]
story_ids = story_ids[keep_ids.length - 1 ... story_ids.length]
# make the bottom half a random selection of older stories
while keep_ids.length <= STORIES_PER_PAGE + 1
story_ids.shuffle!
keep_ids.push story_ids.shift
end
stories = Story.where(:id => keep_ids)
break
end
end
end
stories = stories.includes(
:user, :taggings => :tag
).limit(
STORIES_PER_PAGE + 1
).offset(
(how[:page] - 1) * STORIES_PER_PAGE
).order(
(how[:newest] || how[:recent]) ? "stories.created_at DESC" : "hotness"
).to_a
show_more = false
if stories.count > STORIES_PER_PAGE
show_more = true
stories.pop
end
if @user
votes = Vote.votes_by_user_for_stories_hash(@user.id, stories.map(&:id))
stories.each do |s|
if votes[s.id]
s.vote = votes[s.id]
end
end
end
[ stories, show_more ]
end
end