major refactoring of post sets and pagination, incomplete

This commit is contained in:
albert
2011-06-03 19:47:16 -04:00
parent ce0695c606
commit ca3e9bb6db
21 changed files with 475 additions and 204 deletions

View File

@@ -4,7 +4,7 @@ class PostsController < ApplicationController
respond_to :html, :xml, :json
def index
@post_set = PostSets::Post.new(params[:tags], :page => params[:page], :before_id => params[:before_id])
@post_set = PostSets::Post.new(params[:tags], params)
respond_with(@post_set)
end

View File

@@ -0,0 +1,78 @@
module PaginationHelper
def smart_paginator(set, &block)
if set.page && set.page > 1000
sequential_paginator(set)
else
numbered_paginator(set, &block)
end
end
def sequential_paginator(set)
html = "<menu>"
unless set.first_page?
html << '<li>' + link_to("&laquo; Previous", params.merge(:after_id => set.first_id)) + '</li>'
end
unless set.last_page?
html << '<li>' + link_to("Next &raquo;", params.merge(:before_id => set.last_id)) + '</li>'
end
html << "</menu>"
html.html_safe
end
def numbered_paginator(set, &block)
html = "<menu>"
window = 3
if set.total_pages <= (window * 2) + 5
1.upto(set.total_pages) do |page|
html << numbered_paginator_item(page, set.current_page, &block)
end
elsif set.current_page <= window + 2
1.upto(set.current_page + window) do |page|
html << numbered_paginator_item(page, set.current_page, &block)
end
html << numbered_paginator_item("...", set.current_page, &block)
html << numbered_paginator_final_item(set.total_pages, set.current_page, &block)
elsif set.current_page >= set.total_pages - (window + 1)
html << numbered_paginator_item(1, set.current_page, &block)
html << numbered_paginator_item("...", set.current_page, &block)
(set.current_page - window).upto(set.total_pages) do |page|
html << numbered_paginator_item(page, set.current_page, &block)
end
else
html << numbered_paginator_item(1, set.current_page, &block)
html << numbered_paginator_item("...", set.current_page, &block)
(set.current_page - window).upto(set.current_page + window) do |page|
html << numbered_paginator_item(page, set.current_page, &block)
end
html << numbered_paginator_item("...", set.current_page, &block)
html << numbered_paginator_final_item(set.total_pages, set.current_page, &block)
end
html << "</menu>"
html.html_safe
end
def numbered_paginator_final_item(total_pages, current_page, &block)
if total_pages <= 1000
numbered_paginator_item(total_pages, current_page, &block)
else
""
end
end
def numbered_paginator_item(page, current_page, &block)
html = "<li>"
if page == "..."
html << "..."
elsif page == current_page
html << page.to_s
else
html << capture(page, &block)
end
html << "</li>"
html.html_safe
end
end

View File

@@ -5,6 +5,20 @@ class Favorite
"favorites_#{user_id.to_i % 10}"
end
def self.sql_order_clause(post_ids, posts_table_alias = "posts")
if post_ids.empty?
return "#{posts_table_alias}.id desc"
end
conditions = []
post_ids.each_with_index do |post_id, n|
conditions << "when #{post_id} then #{n}"
end
"case #{posts_table_alias}.id " + conditions.join(" ") + " end"
end
def self.create(attributes)
user_id = attributes[:user_id]
post_id = attributes[:post_id]
@@ -26,6 +40,19 @@ class Favorite
destroy_for_post(conditions[:post_id])
end
end
def self.find_post_ids(user_id, options)
limit = options[:limit] || 1 || Danbooru.config.posts_per_page
if options[:before_id]
select_values_sql("SELECT post_id FROM #{table_name_for(user_id)} WHERE id < ? ORDER BY id DESC LIMIT ?", options[:before_id], limit)
elsif options[:after_id]
select_values_sql("SELECT post_id FROM #{table_name_for(user_id)} WHERE id > ? ORDER BY id ASC LIMIT ?", options[:after_id], limit).reverse
elsif options[:offset]
select_values_sql("SELECT post_id FROM #{table_name_for(user_id)} ORDER BY id DESC LIMIT ? OFFSET ?", limit, options[:offset])
else
select_values_sql("SELECT post_id FROM #{table_name_for(user_id)} ORDER BY id DESC LIMIT ?", limit)
end
end
def self.exists?(conditions)
if conditions[:user_id] && conditions[:post_id]
@@ -39,26 +66,30 @@ class Favorite
end
end
private
def self.destroy_for_post_and_user(post_id, user_id)
execute_sql("DELETE FROM #{table_name_for(user_id)} WHERE post_id = #{post_id} AND user_id = #{user_id}")
end
def self.destroy_for_post(post)
0.upto(9) do |i|
execute_sql("DELETE FROM favorites_#{i} WHERE post_id = #{post.id}")
end
end
def self.destroy_for_user(user)
execute_sql("DELETE FROM #{table_name_for(user)} WHERE user_id = #{user.id}")
end
def self.select_value_sql(sql, *params)
ActiveRecord::Base.select_value_sql(sql, *params)
end
def self.execute_sql(sql, *params)
ActiveRecord::Base.execute_sql(sql, *params)
def self.destroy_for_post_and_user(post_id, user_id)
execute_sql("DELETE FROM #{table_name_for(user_id)} WHERE post_id = #{post_id} AND user_id = #{user_id}")
end
def self.destroy_for_post(post)
0.upto(9) do |i|
execute_sql("DELETE FROM favorites_#{i} WHERE post_id = #{post.id}")
end
end
def self.destroy_for_user(user)
execute_sql("DELETE FROM #{table_name_for(user)} WHERE user_id = #{user.id}")
end
def self.select_value_sql(sql, *params)
ActiveRecord::Base.select_value_sql(sql, *params)
end
def self.select_values_sql(sql, *params)
ActiveRecord::Base.select_values_sql(sql, *params)
end
def self.execute_sql(sql, *params)
ActiveRecord::Base.execute_sql(sql, *params)
end
end

View File

@@ -1,45 +1,78 @@
# A PostSet represents a paginated slice of posts. It is used in conjunction
# with the helpers to render the paginator.
#
# Usage:
#
# @post_set = PostSets::Base.new(params)
# @post_set.extend(PostSets::Sequential)
# @post_set.extend(PostSets::Post)
module PostSets
class Base
attr_accessor :page, :before_id, :count, :posts
def initialize(options = {})
@page = options[:page] ? options[:page].to_i : 1
@before_id = options[:before_id]
load_posts
attr_reader :params, :posts
delegate :to_xml, :to_json, :to => :posts
def initialize(params)
@params = params
end
def has_wiki?
false
end
def use_sequential_paginator?
!use_numbered_paginator?
end
def use_numbered_paginator?
before_id.nil?
end
def load_posts
# Should a return a paginated array of posts. This means it should have
# at most <limit> elements.
def posts
raise NotImplementedError
end
def to_xml
posts.to_xml
# Does this post set have a valid wiki page representation?
def has_wiki?
raise NotImplementedError
end
def to_json
posts.to_json
# Should return an array of strings representing the tags.
def tags
raise NotImplementedError
end
# Given an ActiveRelation object, perform the necessary pagination to
# extract at most <limit> elements. Should return an array.
def slice(relation)
raise NotImplementedError
end
# For cases where we're not relying on the default pagination
# implementation (for example, if the ids are cached in a string)
# then pass in the offset/before_id/after_id parameters here.
def pagination_options
raise NotImplementedError
end
# This method should throw an exception if for whatever reason the query
# is invalid or forbidden.
def validate
end
# Clear out any memoized instance variables.
def reload
@posts = nil
@presenter = nil
@tag_string = nil
end
def tag_string
@tag_string ||= tags.join(" ")
end
def is_first_page?
raise NotImplementedError
end
def is_last_page?
posts.size == 0
end
def presenter
@presenter ||= PostSetPresenter.new(self)
end
def offset
((page < 1) ? 0 : (page - 1)) * count
end
def limit
Danbooru.config.posts_per_page
end

View File

@@ -1,18 +1,29 @@
module PostSets
class Favorite < Base
attr_accessor :user
def initialize(user)
@user = user
super()
module Favorite
def user
@user ||= User.find(params[:id])
end
def tags
"fav:#{user.name}"
@tags ||= ["fav:#{user.name}"]
end
def has_wiki?
false
end
def reload
super
@user = nil
@count = nil
end
def count
@count ||= Favorite.count(user.id)
end
def load_posts
@posts = user.favorite_posts(:before_id => before_id)
def posts
@posts ||= user.favorites(pagination_options)
end
end
end

View File

@@ -0,0 +1,35 @@
module PostSets
module Numbered
attr_reader :page
def initialize(params)
super
@page = options[:page] ? options[:page].to_i : 1
end
def total_pages
@total_pages ||= (count / limit.to_f).ceil.to_i
end
def reload
super
@total_pages = nil
end
def slice(relation)
relation.offset(offset).all
end
def pagination_options
{:offset => offset}
end
def is_first_page?
offset == 0
end
def offset
((page < 1) ? 0 : (page - 1)) * limit
end
end
end

View File

@@ -1,33 +1,30 @@
# This only works with the numbered paginator because of the way
# the association is stored.
module PostSets
class Pool < Base
attr_reader :pool
def initialize(pool, options = {})
@pool = pool
@count = pool.post_id_array.size
super(options)
module Pool
def pool
@pool ||= Pool.find(params[:id])
end
def tags
"pool:#{pool.name}"
["pool:#{pool.name}"]
end
def has_wiki?
true
end
def count
pool.post_count
end
def load_posts
@posts = pool.posts(:limit => limit, :offset => offset).order("posts.id")
def posts
@posts ||= pool.posts(pagination_options)
end
def sorted_posts
sort_posts(@posts)
end
private
def sort_posts(posts)
posts_by_id = posts.inject({}) do |hash, post|
hash[post.id] = post
hash
end
@pool.post_id_array.map {|x| posts_by_id[x]}
def reload
super
@pool = nil
end
end
end

View File

@@ -1,78 +1,56 @@
module PostSets
class Post < Base
module Post
class Error < Exception ; end
attr_accessor :tags, :errors, :count
attr_accessor :wiki_page, :artist, :suggestions
def initialize(tags, options = {})
super(options)
@tags = Tag.normalize(tags)
@errors = []
load_associations
load_suggestions
validate
attr_accessor :tags, :count, :wiki_page, :artist, :suggestions
def tags
@tags ||= Tag.normalize(params[:tags])
end
def count
@count ||= ::Post.fast_count(tag_string)
end
def posts
@posts ||= slice(::Post.tag_match(tags).limit(limit))
end
def reload
super
@tags = nil
@tag_string = nil
@count = nil
@wiki_page = nil
@artist = nil
end
def wiki_page
@wiki_page ||= ::WikiPage.titled(tag_string).first
end
def artist
@artist ||= ::Artist.find_by_name(tag_string)
end
def has_wiki?
is_single_tag?
end
def has_errors?
errors.any?
end
def offset
x = (page - 1) * limit
if x < 0
x = 0
end
x
end
def is_single_tag?
tag_array.size == 1
end
def date_tag
tag_array.grep(/date:/).first
end
def load_associations
if is_single_tag?
@wiki_page = ::WikiPage.titled(tags).first
@artist = ::Artist.find_by_name(tags)
end
end
def load_posts
@count = ::Post.fast_count(tags)
@posts = ::Post.tag_match(tags).before_id(before_id).all(:order => "posts.id desc", :limit => limit, :offset => offset)
end
def load_suggestions
if count < limit && is_single_tag?
@suggestions = Tag.find_suggestions(tags)
else
@suggestions = []
end
end
def tag_array
@tag_array ||= Tag.scan_query(tags)
end
def tag
tag_array.first
end
def validate
super
validate_page
validate_query_count
rescue Error => x
@errors << x.to_s
end
def validate_page
if page > 1_000
raise Error.new("You cannot explicitly specify the page after page 1000")

View File

@@ -0,0 +1,29 @@
module PostSets
module Sequential
attr_reader :before_id, :after_id
def initialize(params)
super
@before_id = params[:before_id]
@after_id = params[:after_id]
end
def slice(relation)
if before_id
relation.where("id < ?", before_id).all
elsif after_id
relation.where("id > ?", after_id).order("id asc").all.reverse
else
relation.all
end
end
def pagination_options
{:before_id => before_id, :after_id => after_id}
end
def is_first_page?
before_id.nil?
end
end
end

View File

@@ -1,34 +1,29 @@
module PostSets
class WikiPage < Base
attr_reader :tag_name
def initialize(tag_name)
@tag_name = tag_name
super()
module WikiPage
def wiki_page
@wiki_page ||= begin
if params[:id]
::WikiPage.find(params[:id])
elsif params[:tags]
::WikiPage.titled(params[:tags]).first
end
end
end
def load_posts
@posts = ::Post.tag_match(tag_name).all(:order => "posts.id desc", :limit => limit, :offset => offset)
end
def limit
8
end
def offset
0
def has_wiki?
true
end
def tags
[@tag_name]
@tags ||= Tag.normalize(wiki_page.title)
end
def use_sequential_paginator?
false
def posts
@posts ||= slice(::Post.tag_match(tag_string))
end
def use_numbered_paginator?
false
def count
@count ||= ::Post.fast_count(tag_string)
end
end
end

View File

@@ -72,9 +72,9 @@ class Pool < ActiveRecord::Base
def posts(options = {})
offset = options[:offset] || 0
limit = options[:limit] || 20
limit = options[:limit] || Danbooru.config.posts_per_page
ids = post_id_array[offset, limit]
Post.where(["id IN (?)", ids])
Post.where(["id IN (?)", ids]).order(Favorite.sql_order_clause(ids))
end
def post_id_array

View File

@@ -1,6 +1,7 @@
class Post < ActiveRecord::Base
class ApprovalError < Exception ; end
class DisapprovalError < Exception ; end
class SearchError < Exception ; end
attr_accessor :old_tag_string, :old_parent_id
after_destroy :delete_files
@@ -39,6 +40,7 @@ class Post < ActiveRecord::Base
scope :available_for_moderation, lambda {where(["id NOT IN (SELECT pd.post_id FROM post_disapprovals pd WHERE pd.user_id = ?)", CurrentUser.id])}
scope :hidden_from_moderation, lambda {where(["id IN (SELECT pd.post_id FROM post_disapprovals pd WHERE pd.user_id = ?)", CurrentUser.id])}
scope :before_id, lambda {|id| id.present? ? where(["posts.id < ?", id]) : where("TRUE")}
scope :after_id, lambda {|id| id.present? ? where("posts.id > ?", id) : where("true")}
scope :tag_match, lambda {|query| Post.tag_match_helper(query)}
search_methods :tag_match
@@ -374,36 +376,33 @@ class Post < ActiveRecord::Base
Favorite.destroy_for_post(self)
end
def add_favorite(user)
if user.is_a?(ActiveRecord::Base)
user_id = user.id
else
user_id = user
def favorited_by?(user_id)
fav_string =~ /(?:\A| )fav:#{user_id}(?:\Z| )/
end
def add_favorite(user_id)
if user_id.is_a?(ActiveRecord::Base)
user_id = user_id.id
end
if favorited_by?(user_id)
return false
end
return false if fav_string =~ /(?:\A| )fav:#{user_id}(?:\Z| )/
self.fav_string += " fav:#{user_id}"
self.fav_string.strip!
# in order to avoid rerunning the callbacks, just update through raw sql
execute_sql("UPDATE posts SET fav_string = ? WHERE id = ?", fav_string, id)
update_attribute(:fav_string, fav_string)
Favorite.create(:user_id => user_id, :post_id => id)
end
def remove_favorite(user)
if user.is_a?(ActiveRecord::Base)
user_id = user.id
else
user_id = user
def remove_favorite(user_id)
if user_id.is_a?(ActiveRecord::Base)
user_id = user_id.id
end
self.fav_string.gsub!(/(?:\A| )fav:#{user_id}(?:\Z| )/, " ")
self.fav_string.strip!
# in order to avoid rerunning the callbacks, just update through raw sql
execute_sql("UPDATE posts SET fav_string = ? WHERE id = ?", fav_string, id)
update_attribute(:fav_string, fav_string)
Favorite.destroy(:user_id => user_id, :post_id => id)
end
@@ -413,9 +412,9 @@ class Post < ActiveRecord::Base
end
module SearchMethods
class SearchError < Exception ; end
def add_range_relation(arr, field, relation)
return relation if arr.nil?
case arr[0]
when :eq
relation.where(["#{field} = ?", arr[1]])
@@ -513,10 +512,10 @@ class Post < ActiveRecord::Base
relation = add_range_relation(q[:character_tag_count], "posts.tag_count_character", relation)
relation = add_range_relation(q[:tag_count], "posts.tag_count", relation)
if q[:md5].any?
if q[:md5]
relation = relation.where(["posts.md5 IN (?)", q[:md5]])
end
if q[:status] == "pending"
relation = relation.where("posts.is_pending = TRUE")
elsif q[:status] == "flagged"
@@ -525,11 +524,11 @@ class Post < ActiveRecord::Base
relation = relation.where("posts.is_deleted = TRUE")
end
if q[:source].is_a?(String)
if q[:source]
relation = relation.where(["posts.source LIKE ? ESCAPE E'\\\\'", q[:source]])
end
if q[:subscriptions].any?
if q[:subscriptions]
relation = add_tag_subscription_relation(q[:subscriptions], relation)
end

View File

@@ -208,7 +208,7 @@ class Tag < ActiveRecord::Base
end
def parse_query(query, options = {})
q = Hash.new {|h, k| h[k] = []}
q = {}
q[:tags] = {
:related => [],
:include => [],
@@ -237,6 +237,7 @@ class Tag < ActiveRecord::Base
q[:tags][:related] << "fav:#{User.name_to_id($2)}"
when "sub"
q[:subscriptions] ||= []
q[:subscriptions] << $2
when "md5"

View File

@@ -117,16 +117,13 @@ class User < ActiveRecord::Base
end
module FavoriteMethods
def favorite_posts(options = {})
favorites_table = Favorite.table_name_for(id)
if options[:before_id]
before_id_sql_fragment = ["favorites.id < ?", options[:before_id]]
def favorites(options = {})
post_ids = Favorite.find_post_ids(id, options)
if post_ids.any?
Post.where("id in (?)", post_ids).order(Favorite.sql_order_clause(post_ids))
else
before_id_sql_fragment = "TRUE"
Post.where("false")
end
limit = options[:limit] || 20
Post.joins("JOIN #{favorites_table} AS favorites ON favorites.post_id = posts.id").where("favorites.user_id = ?", id).where(before_id_sql_fragment).order("favorite_id DESC").limit(limit).select("posts.*, favorites.id AS favorite_id")
end
end

View File

@@ -0,0 +1,58 @@
module Paginators
class Numbered
attr_reader :template, :source
delegate :url, :total_pages, :current_page, :to => :source
def initialize(template, source)
@template = template
@source = source
end
def pagination_html
html = "<menu>"
window = 3
if total_pages <= (window * 2) + 5
1.upto(total_pages) do |page|
html << pagination_item(page, current_page)
end
elsif current_page <= window + 2
1.upto(current_page + window) do |page|
html << pagination_item(page, current_page)
end
html << pagination_item("...", current_page)
html << pagination_item(total_pages, current_page)
elsif current_page >= total_pages - (window + 1)
html << pagination_item(1, current_page)
html << pagination_item("...", current_page)
(current_page - window).upto(total_pages) do |page|
html << pagination_item(page, current_page)
end
else
html << pagination_item(1, current_page)
html << pagination_item("...", current_page)
(current_page - window).upto(current_page + window) do |page|
html << pagination_item(page, current_page)
end
html << pagination_item("...", current_page)
html << pagination_item(total_pages, current_page)
end
html << "</menu>"
html.html_safe
end
protected
def pagination_item(page, current_page)
html = "<li>"
if page == "..."
html << "..."
elsif page == current_page
html << page.to_s
else
html << template.link_to(page, url(template, :page => page))
end
html << "</li>"
html.html_safe
end
end
end

View File

@@ -15,6 +15,7 @@ module Paginators
[1, post_set.page].max
end
# TODO: this is not compatible with paginating favorites
def sequential_link(template)
template.posts_path(:tags => template.params[:tags], before_id => post_set.posts[-1].id, :page => nil)
end

View File

@@ -0,0 +1,29 @@
module Paginators
class Sequential
attr_reader :template, :source
delegate :url, :to => :source
def initialize(template, source)
@template = template
@source = source
end
def pagination_html
html = "<menu>"
html << '<li>' + template.link_to("&laquo; Previous", prev_url) + '</li>'
if next_url
html << '<li>' + template.link_to("Next &raquo;", next_url) + '</li>'
end
html << "</menu>"
html.html_safe
end
def prev_url
template.request.env["HTTP_REFERER"]
end
def next_url
@next_url ||= url(template)
end
end
end

View File

@@ -1,11 +1,5 @@
<div id="c-posts">
<div id="a-index">
<% if @post_set.suggestions.any? %>
<div class="notice">
Maybe you meant: <%= @post_set.suggestions.map {|x| link_to(x, posts_path(:tags => x), :class => "tag-type-#{Tag.type_name(x)}" )}.to_sentence(:last_word_connector => ", or ", :two_words_connector => " or ") %>
</div>
<% end %>
<aside id="sidebar">
<section id="search-box">
<h1>Search</h1>

View File

@@ -5,5 +5,7 @@
<div class="clearfix"></div>
<div class="paginator">
<%= post_set.presenter.pagination_html(self) %>
<%= smart_paginator(post_set) do |page| %>
<%= link_to(page, posts_path(:page => page, :tags => params[:tags])) %>
<% end %>
</div>

View File

@@ -1,5 +1,7 @@
<div id="c-uploads">
<div id="a-new">
<h1>Upload Post</h1>
<div id="upload-guide-notice">
<p>Before uploading, please read the <%= link_to "how to upload guide", wiki_pages_path(:title => "howto:upload") %>.</p>
</div>

View File

@@ -6,6 +6,7 @@ class CreatePools < ActiveRecord::Migration
t.column :description, :text
t.column :is_active, :boolean, :null => false, :default => true
t.column :post_ids, :text, :null => false, :default => ""
t.column :post_count, :integer, :null => false, :default => 0
t.timestamps
end