diff --git a/apps/pagy_calendar_app.ru b/apps/pagy_calendar_app.ru index e98420f1f..a2cbe8060 100644 --- a/apps/pagy_calendar_app.ru +++ b/apps/pagy_calendar_app.ru @@ -112,10 +112,13 @@ __END__ <% else %> Hide Calendar
- Go to current Page + Go to the 2022-03 Page + <% end %>

+

Showtime: <%= @calendar.showtime %>

+ <% if @calendar %> <%= pagy_bootstrap_nav(@calendar[:year]) %> 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..4a1c0f551 100644 --- a/lib/pagy/calendar.rb +++ b/lib/pagy/calendar.rb @@ -13,12 +13,12 @@ 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 - attr_reader :order + attr_reader :order, :from # Merge and validate the options, do some simple arithmetic and set a few instance variables def initialize(vars) # rubocop:disable Lint/MissingSuper @@ -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..44b491b85 100644 --- a/lib/pagy/calendar/helper.rb +++ b/lib/pagy/calendar/helper.rb @@ -3,8 +3,7 @@ class Pagy # :nodoc: class Calendar # :nodoc: # Initializes the calendar objects, reducing complexity in the extra - # The returned calendar is a simple hash of units/objects with an added helper - # returning the last_object_at(time) used in the extra + # The returned calendar is a simple hash of units/objects class Helper < Hash class << self private @@ -16,6 +15,7 @@ def init(conf, period, params) private + # Create the calendar def init(conf, period, params) @units = Calendar::UNITS & conf.keys # get the units in time length desc order raise ArgumentError, 'no calendar unit found in pagy_calendar @configuration' if @units.empty? @@ -44,18 +44,26 @@ def init(conf, period, params) [replace(calendar), object.from, object.to] end - def last_object_at(time) + # Return the calendar object at time + def calendar_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]) end end + + public + + # Return the current time of the smallest time unit shown + def showtime + self[@units.last].from + end end end end diff --git a/lib/pagy/extras/calendar.rb b/lib/pagy/extras/calendar.rb index 133edd1f4..2f1266f85 100644 --- a/lib/pagy/extras/calendar.rb +++ b/lib/pagy/extras/calendar.rb @@ -28,10 +28,6 @@ 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) - end - # This method must be implemented by the application def pagy_calendar_period(*) raise NoMethodError, 'the pagy_calendar_period method must be implemented by the application ' \ @@ -44,6 +40,15 @@ def pagy_calendar_filter(*) '(see https://ddnexus.github.io/pagy/docs/extras/calendar/#pagy-calendar-filter-collection-from-to)' end end + + # Additions for the Frontend module + module UrlHelper + # Return the url for the calendar page at time + def pagy_calendar_url_at(calendar, time, **opts) + pagy_url_for(calendar.send(:calendar_at, time, **opts), 1, **opts) + end + end end - Backend.prepend CalendarExtra::Backend + Backend.prepend CalendarExtra::Backend, CalendarExtra::UrlHelper + Frontend.prepend CalendarExtra::UrlHelper end diff --git a/lib/pagy/extras/standalone.rb b/lib/pagy/extras/standalone.rb index 55d732cff..9b90311ee 100644 --- a/lib/pagy/extras/standalone.rb +++ b/lib/pagy/extras/standalone.rb @@ -38,7 +38,7 @@ def build_nested_query(value, prefix = nil) # Return the URL for the page. If there is no pagy.vars[:url] # it works exactly as the regular #pagy_url_for, relying on the params method and Rack. # If there is a defined pagy.vars[:url] variable it does not need the params method nor Rack. - def pagy_url_for(pagy, page, absolute: false, html_escaped: false) + def pagy_url_for(pagy, page, absolute: false, html_escaped: false, **_) return super unless pagy.vars[:url] vars = pagy.vars diff --git a/lib/pagy/url_helpers.rb b/lib/pagy/url_helpers.rb index 0cfe728e2..7b4bc06c9 100644 --- a/lib/pagy/url_helpers.rb +++ b/lib/pagy/url_helpers.rb @@ -6,7 +6,7 @@ module UrlHelpers # Return the URL for the page, relying on the params method and Rack by default. # It supports all Rack-based frameworks (Sinatra, Padrino, Rails, ...). # For non-rack environments you can use the standalone extra - def pagy_url_for(pagy, page, absolute: false, html_escaped: false) + def pagy_url_for(pagy, page, absolute: false, html_escaped: false, **_) vars = pagy.vars request_path = vars[:request_path].to_s.empty? ? request.path : vars[:request_path] page_param = vars[:page_param].to_s 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