Fix a bug where if you did a slow search that took too long to calculate the page count, and you had 200 posts per page, then we would show page 5000 as the last page of the search. This was because we were artificially returning 1,000,000 as the post count to signal that the count timed out, but at 200 posts per page this would show 5000 as the last page of the search.
155 lines
4.8 KiB
Ruby
155 lines
4.8 KiB
Ruby
# A mixin that adds a `#paginate` method to an ActiveRecord relation.
|
|
#
|
|
# There are two pagination techniques. The first is page-based (numbered):
|
|
#
|
|
# https://danbooru.donmai.us/posts?page=1
|
|
# https://danbooru.donmai.us/posts?page=2
|
|
# https://danbooru.donmai.us/posts?page=3
|
|
#
|
|
# The second is id-based (sequential):
|
|
#
|
|
# https://danbooru.donmai.us/posts?page=a1000&limit=100
|
|
# https://danbooru.donmai.us/posts?page=a1100&limit=100
|
|
# https://danbooru.donmai.us/posts?page=a1200&limit=100
|
|
#
|
|
# https://danbooru.donmai.us/posts?page=b1000&limit=100
|
|
# https://danbooru.donmai.us/posts?page=b900&limit=100
|
|
# https://danbooru.donmai.us/posts?page=b800&limit=100
|
|
#
|
|
# where a1000 means "after id 1000" and b1000 means "before id 1000".
|
|
#
|
|
module PaginationExtension
|
|
class PaginationError < StandardError; end
|
|
|
|
attr_accessor :current_page, :records_per_page, :paginator_count, :paginator_mode, :paginator_page_limit
|
|
|
|
# Paginate an ActiveRecord relation. Returns a relation for the given page and number of posts per page.
|
|
#
|
|
# @param page [String] the page number, or an "aNNN" or "bNNN" string
|
|
# @param limit [Integer] the number of posts per page
|
|
# @param max_limit [Integer] the maximum number of posts per page the user can view
|
|
# @param page_limit [Integer] the highest page the user can view
|
|
# @param count [Integer] the precalculated number of search results, or nil to calculate it
|
|
# @param search_count [Object] if truthy, don't calculate the number of results; assume a large number of results
|
|
def paginate(page, limit: nil, max_limit: 1000, page_limit: CurrentUser.user.page_limit, count: nil, search_count: nil)
|
|
@records_per_page = limit || Danbooru.config.posts_per_page
|
|
@records_per_page = @records_per_page.to_i.clamp(1, max_limit)
|
|
@paginator_page_limit = page_limit
|
|
|
|
if count.present?
|
|
@paginator_count = count
|
|
elsif !search_count.nil? && search_count.blank?
|
|
@paginator_count = Float::INFINITY
|
|
end
|
|
|
|
if page.to_s =~ /\Ab(\d+)\z/i
|
|
@paginator_mode = :sequential_before
|
|
paginate_sequential_before($1, records_per_page)
|
|
elsif page.to_s =~ /\Aa(\d+)\z/i
|
|
@paginator_mode = :sequential_after
|
|
paginate_sequential_after($1, records_per_page)
|
|
elsif page.to_i > page_limit
|
|
raise PaginationError
|
|
elsif page.to_i == page_limit
|
|
@paginator_mode = :sequential_after
|
|
paginate_numbered(page.to_i, records_per_page)
|
|
else
|
|
@paginator_mode = :numbered
|
|
@current_page = [page.to_i, 1].max
|
|
|
|
paginate_numbered(current_page, records_per_page)
|
|
end
|
|
end
|
|
|
|
def paginate_sequential_before(before_id, limit)
|
|
where("#{table_name}.id < ?", before_id).reorder("#{table_name}.id desc").limit(limit + 1)
|
|
end
|
|
|
|
def paginate_sequential_after(after_id, limit)
|
|
where("#{table_name}.id > ?", after_id).reorder("#{table_name}.id asc").limit(limit + 1)
|
|
end
|
|
|
|
def paginate_numbered(page, limit)
|
|
offset((page - 1) * limit).limit(limit)
|
|
end
|
|
|
|
def is_first_page?
|
|
case paginator_mode
|
|
when :numbered
|
|
current_page == 1
|
|
when :sequential_before
|
|
false
|
|
when :sequential_after
|
|
size <= records_per_page
|
|
end
|
|
end
|
|
|
|
def is_last_page?
|
|
case paginator_mode
|
|
when :numbered
|
|
current_page >= total_pages
|
|
when :sequential_before
|
|
size <= records_per_page
|
|
when :sequential_after
|
|
false
|
|
end
|
|
end
|
|
|
|
def prev_page
|
|
if is_first_page?
|
|
nil
|
|
elsif paginator_mode == :numbered
|
|
current_page - 1
|
|
elsif records.present?
|
|
"a#{records.first.id}"
|
|
else
|
|
nil
|
|
end
|
|
rescue ActiveRecord::QueryCanceled
|
|
nil
|
|
end
|
|
|
|
def next_page
|
|
if is_last_page?
|
|
nil
|
|
elsif paginator_mode == :numbered
|
|
current_page + 1
|
|
elsif records.present?
|
|
"b#{records.last.id}"
|
|
else
|
|
nil
|
|
end
|
|
rescue ActiveRecord::QueryCanceled
|
|
nil
|
|
end
|
|
|
|
# XXX Hack: in sequential pagination we fetch one more record than we
|
|
# need so that we can tell when we're on the first or last page. Here
|
|
# we override a rails internal method to discard that extra record. See
|
|
# #2044, #3642.
|
|
def records
|
|
case paginator_mode
|
|
when :sequential_before
|
|
super.first(records_per_page)
|
|
when :sequential_after
|
|
super.first(records_per_page).reverse
|
|
when :numbered
|
|
super
|
|
end
|
|
end
|
|
|
|
# Return the number of pages of results, or infinity if it takes too long to count.
|
|
def total_pages
|
|
return Float::INFINITY if total_count.infinite?
|
|
(total_count.to_f / records_per_page).ceil
|
|
end
|
|
|
|
# Return the number of results, or infinity if it takes too long to count.
|
|
def total_count
|
|
@paginator_count ||= unscoped.from(except(:offset, :limit, :order).reorder(nil)).count
|
|
rescue ActiveRecord::StatementInvalid => e
|
|
raise unless e.to_s =~ /statement timeout/
|
|
@paginator_count ||= Float::INFINITY
|
|
end
|
|
end
|