From 7a8c18eb3be512fcfde9f6028c0021dc1625dc9e Mon Sep 17 00:00:00 2001
From: Domizio Demichelis
Date: Wed, 3 Jan 2024 20:39:24 +0700
Subject: [PATCH] Calendar improvements:
- Added the :fit_time option to page_at and pagy_calendar_url_at methods. It avoids the OutOfRangeError by returning the first or last page
- Added starting_time_for and page_offset_at feedback methods to the Calendar base class
---
apps/pagy_calendar_app.ru | 3 +-
docs/extras/calendar.md | 5 ++-
lib/pagy/calendar.rb | 34 +++++++++++++---
lib/pagy/calendar/helper.rb | 4 +-
lib/pagy/extras/calendar.rb | 4 +-
test/coverage_setup.rb | 3 +-
test/pagy/calendar_test.rb | 52 +++++++++++++++++++------
test/pagy/extras/calendar_extra_test.rb | 14 +++++--
test/test_helper.rb | 11 ++++++
9 files changed, 102 insertions(+), 28 deletions(-)
diff --git a/apps/pagy_calendar_app.ru b/apps/pagy_calendar_app.ru
index e98420f1f..bfe48d5c1 100644
--- a/apps/pagy_calendar_app.ru
+++ b/apps/pagy_calendar_app.ru
@@ -112,7 +112,8 @@ __END__
<% else %>
Hide Calendar
- Go to current Page
+ Go to the 2022-03 Page
+
<% end %>
diff --git a/docs/extras/calendar.md b/docs/extras/calendar.md
index 94d1518fa..b50c95777 100644
--- a/docs/extras/calendar.md
+++ b/docs/extras/calendar.md
@@ -270,13 +270,14 @@ You can use the calendar objects with any `pagy_*nav` and `pagy_*nav_js` helpers
The `pagy_*combo_nav_js` keeps into account only page numbers and not labels, so it is not very useful (if at all) with `Pagy::Calendar::*` objects.
-==- `pagy_calendar_url_at(@calendar, time)`
+==- `pagy_calendar_url_at(@calendar, time, **opts)`
This helper takes the `@calendar` and a `TimeWithZone` objects and returns the url complete with all the params for the pages in each bars that include the passed time.
For example: `pagy_calendar_url_at(@calendar, Time.zone.now)` will select the the bars pointing to today. You can see a working example in the [pagy_calendar_app.ru](https://github.com/ddnexus/pagy/blob/master/apps/pagy_calendar_app.ru) file.
-If `time` is outside the pagination range it raises a `Pagy::Calendar::OutOfRangeError`.
+If `time` is outside the pagination range it raises a `Pagy::Calendar::OutOfRangeError`, however you can pass the option `fit_time: true` to avoid the error and get the url to the page closest to the passed time argument (first or last page).
+
===
### Label format
diff --git a/lib/pagy/calendar.rb b/lib/pagy/calendar.rb
index 35c7c837f..af205ddf0 100644
--- a/lib/pagy/calendar.rb
+++ b/lib/pagy/calendar.rb
@@ -13,7 +13,7 @@ class Pagy # :nodoc:
# Base class for time units subclasses (Year, Quarter, Month, Week, Day)
class Calendar < Pagy
# Specific out of range error
- class OutOfRangeError < StandardError; end
+ class OutOfRangeError < VariableError; end
# List of units in desc order of duration. It can be used for custom units.
UNITS = %i[year quarter month week day] # rubocop:disable Style/MutableConstant
@@ -49,10 +49,24 @@ def label_for(page, opts = {})
protected
# The page that includes time
- def page_at(time)
- raise OutOfRangeError unless time.between?(@initial, @final)
-
- offset = page_offset_at(time) # offset starts from 0
+ # In case of out of range time, the :fit_time option avoids the outOfRangeError
+ # and returns the closest page to the passed time argument (first or last page)
+ def page_at(time, **opts)
+ fit_time = time
+ fit_final = @final - 1
+ unless time.between?(@initial, fit_final)
+ raise OutOfRangeError.new(self, :time, "between #{@initial} and #{fit_final}", time) unless opts[:fit_time]
+
+ if time < @final
+ fit_time = @initial
+ ordinal = 'first'
+ else
+ fit_time = fit_final
+ ordinal = 'last'
+ end
+ Warning.warn "Pagy::Calendar#page_at: Rescued #{time} out of range by returning the #{ordinal} page."
+ end
+ offset = page_offset_at(fit_time) # offset starts from 0
@order == :asc ? offset + 1 : @pages - offset
end
@@ -84,6 +98,16 @@ def active_period
[[@starting, @from].max, [@to - 1, @ending].min] # -1 sec: include only last unit day
end
+ # This method must be implemented by the unit subclass
+ def starting_time_for(*)
+ raise NoMethodError, 'the starting_time_for method must be implemented by the unit subclass'
+ end
+
+ # This method must be implemented by the unit subclass
+ def page_offset_at(*)
+ raise NoMethodError, 'the page_offset_at method must be implemented by the unit subclass'
+ end
+
class << self
# Create a subclass instance by unit name (internal use)
def create(unit, vars)
diff --git a/lib/pagy/calendar/helper.rb b/lib/pagy/calendar/helper.rb
index 906118791..36a122087 100644
--- a/lib/pagy/calendar/helper.rb
+++ b/lib/pagy/calendar/helper.rb
@@ -44,13 +44,13 @@ def init(conf, period, params)
[replace(calendar), object.from, object.to]
end
- def last_object_at(time)
+ def last_object_at(time, **opts)
conf = Marshal.load(Marshal.dump(@conf))
page_params = {}
@units.inject(nil) do |object, unit|
conf[unit][:period] = object&.send(:active_period) || @period
conf[unit][:page] = page_params[:"#{unit}_#{@page_param}"] \
- = Calendar.send(:create, unit, conf[unit]).send(:page_at, time)
+ = Calendar.send(:create, unit, conf[unit]).send(:page_at, time, **opts)
conf[unit][:params] ||= {}
conf[unit][:params].merge!(page_params)
Calendar.send(:create, unit, conf[unit])
diff --git a/lib/pagy/extras/calendar.rb b/lib/pagy/extras/calendar.rb
index 133edd1f4..0909c95dc 100644
--- a/lib/pagy/extras/calendar.rb
+++ b/lib/pagy/extras/calendar.rb
@@ -28,8 +28,8 @@ def pagy_calendar(collection, conf)
[calendar, pagy, results]
end
- def pagy_calendar_url_at(calendar, time)
- pagy_url_for(calendar.send(:last_object_at, time), 1)
+ def pagy_calendar_url_at(calendar, time, **opts)
+ pagy_url_for(calendar.send(:last_object_at, time, **opts), 1)
end
# This method must be implemented by the application
diff --git a/test/coverage_setup.rb b/test/coverage_setup.rb
index 7f9131cc8..dc3453758 100644
--- a/test/coverage_setup.rb
+++ b/test/coverage_setup.rb
@@ -12,6 +12,7 @@
command_name "Task##{$PROCESS_ID}"
merge_timeout 20
enable_coverage :branch
+ add_filter "/test/"
add_group 'Core', %w[ lib/pagy.rb
lib/pagy/backend.rb
lib/pagy/console.rb
@@ -21,7 +22,7 @@
lib/pagy/i18n.rb
lib/pagy/url_helpers.rb ]
add_group 'Extras', 'lib/pagy/extras'
- add_group 'Tests', 'test'
+ # add_group 'Tests', 'test'
end
SimpleCov.formatter = SimpleCov::Formatter::SimpleFormatter unless ENV.fetch('HTML_REPORTS', nil) == 'true'
diff --git a/test/pagy/calendar_test.rb b/test/pagy/calendar_test.rb
index 066cc3176..7342229da 100644
--- a/test/pagy/calendar_test.rb
+++ b/test/pagy/calendar_test.rb
@@ -277,14 +277,20 @@ def pagy(unit: :month, **vars)
_(p.send(:page_at, Time.zone.local(2021, 10, 21, 13, 18, 23, 0))).must_equal 1
_(p.send(:page_at, Time.zone.local(2022, 1, 1, 13, 18, 23, 0))).must_equal 2
_(p.send(:page_at, Time.zone.local(2023, 11, 13, 15, 43, 40, 0))).must_equal 3
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 3
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 1
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :year desc' do
p = pagy(unit: :year, order: :desc)
_(p.send(:page_at, Time.zone.local(2021, 10, 21, 13, 18, 23, 0))).must_equal 3
_(p.send(:page_at, Time.zone.local(2022, 1, 1, 13, 18, 23, 0))).must_equal 2
_(p.send(:page_at, Time.zone.local(2023, 11, 13, 15, 43, 40, 0))).must_equal 1
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 1
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 3
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :quarter' do
@@ -292,28 +298,40 @@ def pagy(unit: :month, **vars)
_(p.send(:page_at, Time.zone.local(2021, 10, 21, 13, 18, 23, 0))).must_equal 1
_(p.send(:page_at, Time.zone.local(2022, 1, 1, 13, 18, 23, 0))).must_equal 2
_(p.send(:page_at, Time.zone.local(2023, 11, 13, 15, 43, 40, 0))).must_equal 9
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 9
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 1
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :quarter desc' do
p = pagy(unit: :quarter, order: :desc)
_(p.send(:page_at, Time.zone.local(2021, 10, 21, 13, 18, 23, 0))).must_equal 9
_(p.send(:page_at, Time.zone.local(2022, 1, 1, 13, 18, 23, 0))).must_equal 8
_(p.send(:page_at, Time.zone.local(2023, 11, 13, 15, 43, 40, 0))).must_equal 1
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 1
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 9
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :month' do
p = pagy(unit: :month)
_(p.send(:page_at, Time.zone.local(2021, 10, 21, 13, 18, 23, 0))).must_equal 1
_(p.send(:page_at, Time.zone.local(2022, 1, 1, 13, 18, 23, 0))).must_equal 4
_(p.send(:page_at, Time.zone.local(2023, 11, 13, 15, 43, 40, 0))).must_equal 26
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 26
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 1
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :month desc' do
p = pagy(unit: :month, order: :desc)
_(p.send(:page_at, Time.zone.local(2021, 10, 21, 13, 18, 23, 0))).must_equal 26
_(p.send(:page_at, Time.zone.local(2022, 1, 1, 13, 18, 23, 0))).must_equal 23
_(p.send(:page_at, Time.zone.local(2023, 11, 13, 15, 43, 40, 0))).must_equal 1
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 1
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 26
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :week' do
@@ -321,28 +339,40 @@ def pagy(unit: :month, **vars)
_(p.send(:page_at, Time.zone.local(2021, 10, 21))).must_equal 1
_(p.send(:page_at, Time.zone.local(2021, 10, 26))).must_equal 2
_(p.send(:page_at, Time.zone.local(2023, 11, 13))).must_equal 109
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 109
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 1
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
- it 'returns the page number for :day desc' do
+ it 'returns the page number for :week desc' do
p = pagy(unit: :week, order: :desc)
_(p.send(:page_at, Time.zone.local(2021, 10, 21))).must_equal 109
_(p.send(:page_at, Time.zone.local(2021, 10, 26))).must_equal 108
_(p.send(:page_at, Time.zone.local(2023, 11, 13))).must_equal 1
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 1
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 109
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :day' do
p = pagy(unit: :day)
_(p.send(:page_at, Time.zone.local(2021, 10, 21))).must_equal 1
_(p.send(:page_at, Time.zone.local(2021, 10, 26))).must_equal 6
_(p.send(:page_at, Time.zone.local(2023, 11, 13))).must_equal 754
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 754
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 1
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
it 'returns the page number for :day desc' do
p = pagy(unit: :day, order: :desc)
_(p.send(:page_at, Time.zone.local(2021, 10, 21))).must_equal 754
_(p.send(:page_at, Time.zone.local(2021, 10, 26))).must_equal 749
_(p.send(:page_at, Time.zone.local(2023, 11, 13))).must_equal 1
- _ { p.send(:page_at, Time.zone.local(2030)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _(p.send(:page_at, Time.zone.local(2100), fit_time: true)).must_equal 1
+ _(p.send(:page_at, Time.zone.local(2000), fit_time: true)).must_equal 754
+ _ { p.send(:page_at, Time.zone.local(2100)) }.must_raise Pagy::Calendar::OutOfRangeError
+ _ { p.send(:page_at, Time.zone.local(2000)) }.must_raise Pagy::Calendar::OutOfRangeError
end
end
diff --git a/test/pagy/extras/calendar_extra_test.rb b/test/pagy/extras/calendar_extra_test.rb
index 6aa9bdf06..7b3224f0c 100644
--- a/test/pagy/extras/calendar_extra_test.rb
+++ b/test/pagy/extras/calendar_extra_test.rb
@@ -228,11 +228,17 @@ def app(**opts)
_(app.send(:pagy_calendar_url_at, calendar, Time.zone.local(2023, 11, 10)))
.must_equal "/foo?page=1&year_page=3&month_page=11"
- _ { app.send(:pagy_calendar_url_at, calendar, Time.zone.local(2024, 1, 10)) }
- .must_raise Pagy::Calendar::OutOfRangeError
+ _(app.send(:pagy_calendar_url_at, calendar, Time.zone.local(2100), fit_time: true))
+ .must_equal "/foo?page=1&year_page=3&month_page=11"
- _ { app.send(:pagy_calendar_url_at, calendar, Time.zone.local(2021, 9, 10)) }
- .must_raise Pagy::Calendar::OutOfRangeError
+ _(app.send(:pagy_calendar_url_at, calendar, Time.zone.local(2000), fit_time: true))
+ .must_equal "/foo?page=1&year_page=1&month_page=1"
+
+ _ { app.send(:pagy_calendar_url_at, calendar, Time.zone.local(2100)) }
+ .must_raise Pagy::Calendar::OutOfRangeError
+
+ _ { app.send(:pagy_calendar_url_at, calendar, Time.zone.local(2000)) }
+ .must_raise Pagy::Calendar::OutOfRangeError
end
end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 23e44e79f..44d534c36 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -15,3 +15,14 @@
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
require 'pagy'
require 'minitest/autorun'
+
+module PagyCalendarWarningFilter
+ def warn(message, category: nil, **kwargs)
+ if message.match?('Calendar#page_at')
+ # ignore
+ else
+ super
+ end
+ end
+end
+Warning.extend PagyCalendarWarningFilter