© 2017-2022 Domizio DemichelisMIT License

➡ Chat Support on Gitter ➡

Calendar Extra

Add pagination filtering by calendar time unit: year, quarter, month, week, day (and your own custom time units).

This extra adds single or multiple chained calendar navs that act as calendar filters on the collection records, placing each record in its time unit.

calendar_app Screenshot from the single-file self-contained pagy_calendar_app.ru demo

Use cases

This extra makes sense when the result to paginate have some time continuity and you want to provide a simple chronological browsing. For example: a movie catalog could allow the user to browse all the movies by year, or you may want to browse a long list of events by jumping and narrowing between years, months, days.

On the other hand it does not make much sense for the result of a search that hits just a few sparse records scattered over a possibly long period of time. For that case the calendar extra has an :active flag that can be used to inactivate the calendar and fallback to the regular pagination. No need to maintain different UIs for wide browsing and narrow searching.

Synopsis

See extras for general usage info.

Require and configure it in the pagy.rb initializer:

require 'pagy/extras/calendar'
# Optional: customize the default

Implement and use it in your controllers:

# Define the pagy_calendar_period method in your application
def pagy_calendar_period(collection)
  return_period_array_using(collection)
end

# Define the pagy_calendar_filter method in your application  
def pagy_calendar_filter(collection, from, to)
  return_filtered_collection_using(collection, from, to)
end

# Use it in your actions:
@calendar, @pagy, @records = pagy_calendar(collection, year:  { size:  [1, 1, 1, 1], ... },
                                                       month: { size:  [0, 12, 12, 0], ... },
                                                       pagy:  { items: 10, ...})

Use the calendar and pagy objects in your views:

<!-- calendar filtering -->
<%== pagy_nav(@calendar[:year]) %>
<%== pagy_nav(@calendar[:month]) %>

<!-- pagy info extended for the calendar unit -->
<%== pagy_info(@pagy) %> for <%== @calendar[:month].label(format: '%B %Y') %>

... display @records ...

<!-- standard pagination of the selected month -->
<%== pagy_nav(@pagy) %>

See also a few examples about How to wrap existing pagination with pagy_calendar.

Notice For a complete and detailed example, see the pagy_calendar_app.ru.

Usage

Since the time can be stored or calculated in many different ways in different collections, this extra requires you to implement a couple of simple methods in your app and configure the objects that it creates and coordinates for you.

The whole usage boils down to these steps:

  1. Define the pagy_calendar_period method in your controller
  2. Define the pagy_calendar_filter method in your controller
  3. Configure the pagy_calendar method in your action
  4. Use it in your UI

You can play with a quick demo app, working without any additional configuration with:

git clone --depth 1 https://github.com/ddnexus/pagy
cd pagy
rackup -o 0.0.0.0 -p 8080 apps/pagy_calendar_app.ru

Then point your browser to http://0.0.0.0:8080.

Variables and Accessors

See Pagy::Calendar

Files

Methods

All the methods in this module are prefixed with the "pagy_calendar" string in order to avoid any possible conflict with your own methods when you include the module in your controller. They are also all private, so they will not be available as actions.

pagy_calendar(collection, configuration)

This method wraps one or more levels of calendar filtering on top of another backend pagination method (e.g. pagy, pagy_arel, pagy_array, pagy_searchkick, pagy_elasticsearch_rails, pagy_meilisearch, …).

It filters the collection by the selected time units in the configuration (e.g. year, month, day, …), and forwards it to the wrapped method.

It returns an array with one more item than the usual two:

@calendar, @pagy, @results = pagy_calendar(...)

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

collection argument

The collection argument (from ActiveRecord, ElasticSearchRails, Searchkick, Meilisearch, …) is filtered by the pagy_calendar_filter and forwarded to the wrapped method for its final pagination, so ensure that you pass the same type of argument expected by the wrapped method.

configuration argument

The configuration argument must be a Hash structure with the keys representing the type of configuration and the values being the Hash of the variables that you want to pass for the creation of the specific pagy object (or a boolean for the Active flag).

The configuration hash can be composed by the following types of configuration:

Calendar configuration

The calendar configuration determines the calendar objects generated. These are used for filtering the collection to the selected time units.

You can add one or more levels with keys like :year, :quarter, :month, :week, :day. Each key must be set to the hash of the variables that will be used to initialize the relative Pagy::Calendar::* object. Use an empty hash for default values. E.g.: year: {}, month: {}, ....

Restrictions: The :page, :page_param, :params and :period variables for the calendar objects are managed automatically by the extra. Setting them explicitly has no effect. (See also Calendar params for solutions in case of conflicts)

Pagy configuration

This is the optional configuration of the final pagination object (produced by the wrapped method) which is always used regardless the value of the Active flag.

You can pass one optional :pagy key, set to the hash of the variables to initialize the Pagy object. It has none of the restriction mentioned in the Calendar configuration.

Besides the usual pagy variables, you can add a :backend variable, set to the name of the backend extra method that you want to use for managing the collection:

{ pagy: { backend: :pagy_searchkick, items: 10, ... } }

Notice that the collection argument must be exactly what you would pass to the wrapped backend method.

If the :pagy key/value is omitted, a default Pagy instance will be created by the default :pagy backend method.

Active flag

The calendar is active by default, however you can add an optional :active boolean flag to the configuration hash in order to switch it ON or OFF, depending on its usefulness in different conditions (see the Use cases).

Take a look at the pagy_calendar_app.ru for a simple example of a manual toggle in the UI.

pagy_calendar_period(collection)

This method must be implemented by the application.

It receives a collection argument that must not be changed by the method, but can be used to return the starting and ending local TimeWithZone objects array defining the calendar :period. See the Pagy::Calendar Variables for details.

Depending on the type of storage, the collection argument can contain a different kind of object:

ActiveRecord managed storage

If you use ActiveRecord the collection is going to be an ActiveRecord::Relation object. You can use it to return the starting and ending local TimeWithZone objects array. Here are a few examples with the created_at field (but you can pull the time from anywhere):

# Simpler version (2 queries)
def pagy_calendar_period(collection)
  starting = collection.minimum('created_at')
  ending   = collection.maximum('created_at') 
  [starting.in_time_zone, ending.in_time_zone]
end

# Faster version with manual pluck (1 query)
def pagy_calendar_period(collection)
  minmax = collection.pluck('MIN(created_at)', 'MAX(created_at)').first
  minmax.map { |time| Time.parse(time).in_time_zone }
end

# Fastest version (no queries)
# If you have the starting and ending times in the request (from UI selectors), 
# filter the collection by the param before passing it to `pagy_calendar`. 
# In this example you just use the :starting and :ending params to return the period
def pagy_calendar_period(collection)
  params.fetch_values(:starting, :ending).map { |time| Time.parse(time).in_time_zone }
end

See also Time conversion for details.

Search frameworks storage

If you use ElasticSearchRails, Searchkick, Meilisearch the collection argument is just the Array of the captured search arguments that you passed to the Model.pagy_search method. That array is what pagy uses internally to setup its variables before passing it to the standard Model.search method to do the actual search.

So you should use what you need from the collection array and do your own Model.search(...) in order to get the starting and ending local TimeWithZone objects array to return.

pagy_calendar_filter(collection, from, to)

This method must be implemented by the application.

It receives the main collection and must return a filtered version of it using the from and to local Time objects.

You should filter your collection with a logic equivalent to storage_time >= from && storage_time < to, adapted to the time zone and syntax of your storage.

Depending on the type of storage, the collection argument can contain a different kind of object:

ActiveRecord managed storage

If you use ActiveRecord the collection is going to be an ActiveRecord::Relation object that you can easily filter. Here is an example with the created_at field again (but you can use anything, of course):

def pagy_calendar_filter(collection, from, to)
  collection.where(created_at: from...to)  # 3-dots range excluding the end value
end

See also Time conversion for details.

Search frameworks storage

If you use ElasticSearchRails, Searchkick, Meilisearch the collection argument is just the Array of the captured search arguments that you passed to the Model.pagy_search method. That array is what pagy uses internally to setup its variables before passing it to the standard Model.search method to do the actual search.

So in order to filter the actual search with the from and to local TimeWithZone objects, you should simply return the same array with the filtering added to its relevant item. Pagy will use it to do the actual (filtered) search.

Customization

Order

If you set :order to :desc, you will get the page units in descendent order (e.g. May, then April, then March, …), but keep in mind that you still have to desc-order the records in the collection since pagy has no control over that (indeed it’s your own collection scope).

Beginning of week

If you use the :week time unit, consider that the first day of the week could be different for different locales.

You may want to adjust it by setting the Date.beginning_of_week variable to the symbol of the first day of the week (e.g. Date.beginning_of_week = :sunday). Notice the default is :monday consistently with the ISO-8601 standard (and Rails).

Calendar params

This extra handles the request params of its objects automatically, and you should not need to customize them unless they conflict with other params in your requests. In that case you have a couple of alternatives:

View

You can use the calendar objects with any pagy_*nav and pagy_*nav_js helpers or any templates in your views.

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.

Label format

Each page link in the calendar navs is conveniently labeled with the specific Time period it refers to. You can change the time format to your needs by setting the :format variable to a standard strftime format. (See the Pagy::Calendar variables)

You can also get the current page label with e.g.: @calendar[:month].label, which might be useful to use in your UI.

I18n localization

Pagy implements its own faster version of the i18n translate method (i.e. pagy_t), but does not provide any built-in localize method. If you need localization of calendar labels in other locales, you should delegate it to the I18n gem, so that a change in the global I18n.locale will automatically localize all the time labels accordingly.

You have a couple of options:

Caveats