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
- Prepended the pagy_calendar_url_at to the Frontend and Backend
- Added calendar showtime
  • Loading branch information
ddnexus committed Jan 8, 2024
1 parent 8405937 commit 87cc1a0
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 38 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<span>[![Gem Version](https://img.shields.io/gem/v/pagy.svg?label=pagy&colorA=99004d&colorB=cc0066)](https://rubygems.org/gems/pagy)</span> <span>
[![ruby](https://img.shields.io/badge/ruby-2.5+%20*-ruby.svg?colorA=99004d&colorB=cc0066)](https://ddnexus.github.io/pagy/docs/prerequisites/#ruby)</span> <span>
[![Build Status](https://img.shields.io/github/actions/workflow/status/ddnexus/pagy/pagy-ci.yml?branch=master)](https://github.com/ddnexus/pagy/actions/workflows/pagy-ci.yml?query=branch%3Amaster)</span> <span>
[![CodeCov](https://img.shields.io/codecov/c/github/ddnexus/pagy.svg?colorA=1f7a1f&colorB=2aa22a)](https://codecov.io/gh/ddnexus/pagy)</span> <span>
[![codecov](https://codecov.io/gh/ddnexus/pagy/graph/badge.svg?token=S7wBqMwPlQ)](https://codecov.io/gh/ddnexus/pagy)</span> <span>
![Rubocop Status](https://img.shields.io/badge/rubocop-passing-rubocop.svg?colorA=1f7a1f&colorB=2aa22a)</span> <span>
[![MIT license](https://img.shields.io/badge/license-MIT-mit.svg?colorA=1f7a1f&colorB=2aa22a)](http://opensource.org/licenses/MIT)</span> <span>
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4329/badge)](https://bestpractices.coreinfrastructure.org/projects/4329)</span> <span>
Expand Down
5 changes: 4 additions & 1 deletion apps/pagy_calendar_app.ru
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,13 @@ __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>

<p>Showtime: <%= @calendar.showtime %></p>

<!-- calendar filtering navs -->
<% if @calendar %>
<%= pagy_bootstrap_nav(@calendar[:year]) %> <!-- year nav -->
Expand Down
19 changes: 16 additions & 3 deletions docs/extras/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,19 @@ It returns an array with one more item than the usual two:
```
|||

The `@calendar` contains the hash of the generated `Pagy::Calendar::*` objects that can be used in the UI.
The `@calendar` is the hash of the generated `Pagy::Calendar::*` objects that can be used in the UI.

It also provides the `showtime` helper method that returns the `DateTime` of the smallest time unit currently shown in your calendar. For example:

```erb
<!-- Link to go to a specific page in the calendar -->
<a href="<%= pagy_calendar_url_at(@calendar, Time.zone.parse('2022-03-03')) %>">Go to the 2022-03 Page</a>
<!-- Showtime shows the `DateTime` beginning of the smallest time unit currently shown in the calendar -->
<p>Showtime: <%= @calendar.showtime %></p>
```

See also the the single-file self-contained [pagy_calendar_app.ru](https://github.com/ddnexus/pagy/blob/master/apps/pagy_calendar_app.ru) for an interactive demo.

### `collection` argument

Expand Down Expand Up @@ -270,13 +282,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
36 changes: 30 additions & 6 deletions lib/pagy/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, :to

# Merge and validate the options, do some simple arithmetic and set a few instance variables
def initialize(vars) # rubocop:disable Lint/MissingSuper
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
16 changes: 12 additions & 4 deletions lib/pagy/calendar/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?
Expand Down Expand Up @@ -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
15 changes: 10 additions & 5 deletions lib/pagy/extras/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' \
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/pagy/extras/standalone.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/pagy/url_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
61 changes: 50 additions & 11 deletions test/pagy/calendar_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ def pagy(unit: :month, **vars)
end

describe 'pagy/calendar' do
describe "Unit subclasses feedback methods" do
it "raises NoMethodError for starting_time_for" do
_ { pagy.starting_time_for }.must_raise NoMethodError
end
it "raises NoMethodError for page_offset_at" do
_ { pagy.page_offset_at }.must_raise NoMethodError
end
end

describe 'instance methods and variables' do
it 'defines calendar specific accessors' do
assert_respond_to pagy, :order
Expand Down Expand Up @@ -277,72 +286,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
Loading

0 comments on commit 87cc1a0

Please sign in to comment.