Skip to content

Commit

Permalink
Calendar improvements:
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
ddnexus committed Jan 6, 2024
1 parent 2d313e8 commit 7a8c18e
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 28 deletions.
3 changes: 2 additions & 1 deletion apps/pagy_calendar_app.ru
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ __END__
<% else %>
<a href="?skip=true" >Hide Calendar</a>
<br>
<a href="<%= pagy_calendar_url_at(@calendar, Time.zone.now) %>">Go to current Page</a>
<a href="<%= pagy_calendar_url_at(@calendar, Time.zone.parse('2022-03-03')) %>">Go to the 2022-03 Page</a>
<!-- You can use Time.zone.now to find the current page if your time period include today -->
<% end %>
</p>

Expand Down
5 changes: 3 additions & 2 deletions docs/extras/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 29 additions & 5 deletions lib/pagy/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lib/pagy/calendar/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
4 changes: 2 additions & 2 deletions lib/pagy/extras/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion test/coverage_setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down
52 changes: 41 additions & 11 deletions test/pagy/calendar_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -277,72 +277,102 @@ 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
p = pagy(unit: :quarter)
_(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
p = pagy(unit: :week)
_(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

Expand Down
14 changes: 10 additions & 4 deletions test/pagy/extras/calendar_extra_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 7a8c18e

Please sign in to comment.