Fix the paginator not detecting the first or last page correctly during sequential pagination. Caused by the fact that we fetch one more record than needed to detect whether we're on the last page, then throw that record away by overriding Rails' internal `records` method. An upstream refactoring meant that the `size` method now counts the number of records *after* the extra record is thrown away, where before it counted *before* the extra record was thrown away.
159 lines
4.8 KiB
Ruby
159 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# 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
|
|
load
|
|
@records.size <= records_per_page
|
|
end
|
|
end
|
|
|
|
def is_last_page?
|
|
case paginator_mode
|
|
when :numbered
|
|
current_page >= total_pages
|
|
when :sequential_before
|
|
load
|
|
@records.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
|