pagy 7.0.11 → 43.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/apps/calendar.ru +741 -0
- data/apps/demo.ru +513 -0
- data/apps/enable_rails_page_segment.rb +54 -0
- data/apps/index.rb +9 -0
- data/apps/keynav+root_key.ru +316 -0
- data/apps/keynav.ru +255 -0
- data/apps/keyset.ru +219 -0
- data/apps/keyset_sequel.ru +212 -0
- data/apps/rails.ru +216 -0
- data/apps/repro.ru +185 -0
- data/bin/pagy +5 -0
- data/config/pagy.rb +46 -0
- data/javascripts/ai_widget.js +90 -0
- data/javascripts/pagy.js +168 -0
- data/javascripts/pagy.min.js +2 -0
- data/javascripts/pagy.mjs +161 -0
- data/javascripts/wand.js +1172 -0
- data/lib/pagy/classes/calendar/calendar.rb +101 -0
- data/lib/pagy/{calendar → classes/calendar}/day.rb +9 -12
- data/lib/pagy/{calendar → classes/calendar}/month.rb +7 -11
- data/lib/pagy/{calendar → classes/calendar}/quarter.rb +12 -16
- data/lib/pagy/classes/calendar/unit.rb +93 -0
- data/lib/pagy/{calendar → classes/calendar}/week.rb +7 -11
- data/lib/pagy/{calendar → classes/calendar}/year.rb +9 -9
- data/lib/pagy/classes/exceptions.rb +26 -0
- data/lib/pagy/classes/keyset/adapters/active_record.rb +50 -0
- data/lib/pagy/classes/keyset/adapters/sequel.rb +62 -0
- data/lib/pagy/classes/keyset/keynav.rb +85 -0
- data/lib/pagy/classes/keyset/keyset.rb +150 -0
- data/lib/pagy/classes/offset/countish.rb +17 -0
- data/lib/pagy/classes/offset/countless.rb +63 -0
- data/lib/pagy/classes/offset/offset.rb +63 -0
- data/lib/pagy/classes/offset/search.rb +34 -0
- data/lib/pagy/classes/request.rb +48 -0
- data/lib/pagy/cli.rb +122 -0
- data/lib/pagy/console.rb +5 -20
- data/lib/pagy/deprecated.rb +84 -0
- data/lib/pagy/modules/abilities/configurable.rb +37 -0
- data/lib/pagy/modules/abilities/countable.rb +23 -0
- data/lib/pagy/modules/abilities/linkable.rb +72 -0
- data/lib/pagy/modules/abilities/rangeable.rb +14 -0
- data/lib/pagy/modules/abilities/shiftable.rb +12 -0
- data/lib/pagy/modules/b64.rb +35 -0
- data/lib/pagy/modules/console.rb +33 -0
- data/lib/pagy/modules/i18n/i18n.rb +72 -0
- data/lib/pagy/modules/i18n/p11n/arabic.rb +30 -0
- data/lib/pagy/modules/i18n/p11n/east_slavic.rb +27 -0
- data/lib/pagy/modules/i18n/p11n/one_other.rb +15 -0
- data/lib/pagy/modules/i18n/p11n/one_upto_two_other.rb +15 -0
- data/lib/pagy/modules/i18n/p11n/other.rb +13 -0
- data/lib/pagy/modules/i18n/p11n/polish.rb +27 -0
- data/lib/pagy/modules/i18n/p11n/west_slavic.rb +22 -0
- data/lib/pagy/modules/i18n/p11n.rb +16 -0
- data/lib/pagy/modules/searcher.rb +20 -0
- data/lib/pagy/next.rb +25 -0
- data/lib/pagy/tasks/sync.rb +20 -0
- data/lib/pagy/toolbox/helpers/anchor_tags.rb +21 -0
- data/lib/pagy/toolbox/helpers/bootstrap/input_nav_js.rb +28 -0
- data/lib/pagy/toolbox/helpers/bootstrap/previous_next_html.rb +19 -0
- data/lib/pagy/toolbox/helpers/bootstrap/series_nav.rb +32 -0
- data/lib/pagy/toolbox/helpers/bootstrap/series_nav_js.rb +24 -0
- data/lib/pagy/toolbox/helpers/bulma/input_nav_js.rb +25 -0
- data/lib/pagy/toolbox/helpers/bulma/previous_next_html.rb +20 -0
- data/lib/pagy/toolbox/helpers/bulma/series_nav.rb +31 -0
- data/lib/pagy/toolbox/helpers/bulma/series_nav_js.rb +23 -0
- data/lib/pagy/toolbox/helpers/data_hash.rb +29 -0
- data/lib/pagy/toolbox/helpers/headers_hash.rb +30 -0
- data/lib/pagy/toolbox/helpers/info_tag.rb +30 -0
- data/lib/pagy/toolbox/helpers/input_nav_js.rb +22 -0
- data/lib/pagy/toolbox/helpers/limit_tag_js.rb +25 -0
- data/lib/pagy/toolbox/helpers/loaders.rb +55 -0
- data/lib/pagy/toolbox/helpers/page_url.rb +16 -0
- data/lib/pagy/toolbox/helpers/series_nav.rb +30 -0
- data/lib/pagy/toolbox/helpers/series_nav_js.rb +20 -0
- data/lib/pagy/toolbox/helpers/support/a_lambda.rb +36 -0
- data/lib/pagy/toolbox/helpers/support/data_pagy_attribute.rb +19 -0
- data/lib/pagy/toolbox/helpers/support/nav_aria_label_attribute.rb +10 -0
- data/lib/pagy/toolbox/helpers/support/series.rb +37 -0
- data/lib/pagy/toolbox/helpers/support/wrap_input_nav_js.rb +19 -0
- data/lib/pagy/toolbox/helpers/support/wrap_series_nav.rb +17 -0
- data/lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb +42 -0
- data/lib/pagy/toolbox/helpers/urls_hash.rb +12 -0
- data/lib/pagy/toolbox/paginators/calendar.rb +34 -0
- data/lib/pagy/toolbox/paginators/countish.rb +38 -0
- data/lib/pagy/toolbox/paginators/countless.rb +22 -0
- data/lib/pagy/toolbox/paginators/elasticsearch_rails.rb +56 -0
- data/lib/pagy/toolbox/paginators/keynav_js.rb +26 -0
- data/lib/pagy/toolbox/paginators/keyset.rb +15 -0
- data/lib/pagy/toolbox/paginators/meilisearch.rb +34 -0
- data/lib/pagy/toolbox/paginators/method.rb +38 -0
- data/lib/pagy/toolbox/paginators/offset.rb +24 -0
- data/lib/pagy/toolbox/paginators/searchkick.rb +34 -0
- data/lib/pagy/toolbox/paginators/typesense_rails.rb +34 -0
- data/lib/pagy.rb +67 -131
- data/locales/ar.yml +32 -0
- data/locales/be.yml +28 -0
- data/locales/bg.yml +24 -0
- data/locales/bs.yml +28 -0
- data/locales/ca.yml +24 -0
- data/locales/ckb.yml +20 -0
- data/locales/cs.yml +26 -0
- data/locales/da.yml +24 -0
- data/locales/de.yml +24 -0
- data/locales/dz.yml +20 -0
- data/locales/en.yml +24 -0
- data/{lib/locales → locales}/es.yml +9 -6
- data/locales/fr.yml +24 -0
- data/locales/hr.yml +28 -0
- data/locales/id.yml +20 -0
- data/locales/it.yml +24 -0
- data/locales/ja.yml +20 -0
- data/locales/km.yml +20 -0
- data/locales/ko.yml +20 -0
- data/locales/nb.yml +24 -0
- data/locales/nl.yml +24 -0
- data/locales/nn.yml +24 -0
- data/locales/pl.yml +28 -0
- data/{lib/locales → locales}/pt-BR.yml +10 -7
- data/{lib/locales → locales}/pt.yml +10 -7
- data/locales/ru.yml +28 -0
- data/locales/sk.yml +26 -0
- data/locales/sr.yml +28 -0
- data/locales/sv-SE.yml +24 -0
- data/locales/sv.yml +24 -0
- data/locales/sw.yml +28 -0
- data/locales/ta.yml +24 -0
- data/locales/tr.yml +24 -0
- data/locales/uk.yml +28 -0
- data/locales/vi.yml +20 -0
- data/locales/zh-CN.yml +20 -0
- data/locales/zh-HK.yml +20 -0
- data/locales/zh-TW.yml +20 -0
- data/stylesheets/pagy-tailwind.css +68 -0
- data/stylesheets/pagy.css +83 -0
- metadata +185 -94
- data/lib/config/pagy.rb +0 -258
- data/lib/javascripts/pagy-dev.js +0 -112
- data/lib/javascripts/pagy-module.js +0 -111
- data/lib/javascripts/pagy.js +0 -1
- data/lib/locales/ar.yml +0 -30
- data/lib/locales/be.yml +0 -25
- data/lib/locales/bg.yml +0 -21
- data/lib/locales/bs.yml +0 -25
- data/lib/locales/ca.yml +0 -23
- data/lib/locales/ckb.yml +0 -18
- data/lib/locales/cs.yml +0 -23
- data/lib/locales/da.yml +0 -23
- data/lib/locales/de.yml +0 -21
- data/lib/locales/en.yml +0 -21
- data/lib/locales/fr.yml +0 -21
- data/lib/locales/hr.yml +0 -25
- data/lib/locales/id.yml +0 -19
- data/lib/locales/it.yml +0 -21
- data/lib/locales/ja.yml +0 -19
- data/lib/locales/km.yml +0 -19
- data/lib/locales/ko.yml +0 -19
- data/lib/locales/nb.yml +0 -21
- data/lib/locales/nl.yml +0 -21
- data/lib/locales/nn.yml +0 -21
- data/lib/locales/pl.yml +0 -25
- data/lib/locales/ru.yml +0 -27
- data/lib/locales/sr.yml +0 -25
- data/lib/locales/sv-SE.yml +0 -21
- data/lib/locales/sv.yml +0 -21
- data/lib/locales/sw.yml +0 -23
- data/lib/locales/ta.yml +0 -23
- data/lib/locales/tr.yml +0 -19
- data/lib/locales/uk.yml +0 -25
- data/lib/locales/vi.yml +0 -17
- data/lib/locales/zh-CN.yml +0 -19
- data/lib/locales/zh-HK.yml +0 -19
- data/lib/locales/zh-TW.yml +0 -19
- data/lib/pagy/backend.rb +0 -39
- data/lib/pagy/calendar/helper.rb +0 -65
- data/lib/pagy/calendar.rb +0 -126
- data/lib/pagy/countless.rb +0 -37
- data/lib/pagy/exceptions.rb +0 -25
- data/lib/pagy/extras/arel.rb +0 -36
- data/lib/pagy/extras/array.rb +0 -24
- data/lib/pagy/extras/bootstrap.rb +0 -108
- data/lib/pagy/extras/bulma.rb +0 -105
- data/lib/pagy/extras/calendar.rb +0 -53
- data/lib/pagy/extras/countless.rb +0 -37
- data/lib/pagy/extras/elasticsearch_rails.rb +0 -80
- data/lib/pagy/extras/foundation.rb +0 -105
- data/lib/pagy/extras/frontend_helpers.rb +0 -67
- data/lib/pagy/extras/gearbox.rb +0 -54
- data/lib/pagy/extras/headers.rb +0 -53
- data/lib/pagy/extras/i18n.rb +0 -26
- data/lib/pagy/extras/items.rb +0 -61
- data/lib/pagy/extras/jsonapi.rb +0 -79
- data/lib/pagy/extras/materialize.rb +0 -96
- data/lib/pagy/extras/meilisearch.rb +0 -65
- data/lib/pagy/extras/metadata.rb +0 -38
- data/lib/pagy/extras/navs.rb +0 -51
- data/lib/pagy/extras/overflow.rb +0 -80
- data/lib/pagy/extras/searchkick.rb +0 -67
- data/lib/pagy/extras/semantic.rb +0 -95
- data/lib/pagy/extras/standalone.rb +0 -60
- data/lib/pagy/extras/support.rb +0 -40
- data/lib/pagy/extras/trim.rb +0 -29
- data/lib/pagy/extras/uikit.rb +0 -97
- data/lib/pagy/frontend.rb +0 -114
- data/lib/pagy/i18n.rb +0 -165
- data/lib/pagy/url_helpers.rb +0 -27
- data/lib/stylesheets/pagy.css +0 -61
- data/lib/stylesheets/pagy.scss +0 -50
- data/lib/stylesheets/pagy.tailwind.scss +0 -24
- /data/{lib/javascripts/pagy-module.d.ts → javascripts/pagy.d.ts} +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../../modules/b64'
|
|
5
|
+
|
|
6
|
+
class Pagy
|
|
7
|
+
# Fast keyset pagination for big data
|
|
8
|
+
class Keyset < Pagy
|
|
9
|
+
# Autoload adapters only when const_get accesses them
|
|
10
|
+
module Adapters
|
|
11
|
+
path = Pathname.new(__dir__)
|
|
12
|
+
autoload :ActiveRecord, path.join('adapters/active_record')
|
|
13
|
+
autoload :Sequel, path.join('adapters/sequel')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
autoload :Keynav, Pathname.new(__dir__).join('keynav')
|
|
17
|
+
|
|
18
|
+
# Define empty subclasses to allow specific typing without triggering autoload
|
|
19
|
+
class ActiveRecord < self; end
|
|
20
|
+
class Sequel < self; end
|
|
21
|
+
|
|
22
|
+
class TypeError < ::TypeError; end
|
|
23
|
+
|
|
24
|
+
# Factory method: detect the set type, configure the subclass, and instantiate.
|
|
25
|
+
def self.new(set, **)
|
|
26
|
+
# 1. Handle direct subclass usage (e.g. Pagy::Keyset::ActiveRecord.new)
|
|
27
|
+
if /::(?:ActiveRecord|Sequel)$/.match?(name)
|
|
28
|
+
# Ensure the adapter is mixed in (lazy load)
|
|
29
|
+
mix_in_adapter(name.split('::').last)
|
|
30
|
+
return allocate.tap { _1.send(:initialize, set, **) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 2. Handle Factory usage (Pagy::Keyset.new)
|
|
34
|
+
adapter = if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
|
|
35
|
+
:ActiveRecord
|
|
36
|
+
elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
|
|
37
|
+
:Sequel
|
|
38
|
+
else
|
|
39
|
+
raise TypeError, "expected an ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
const_get(adapter).tap { _1.mix_in_adapter(adapter) }.new(set, **)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Lazy-include the adapter module
|
|
46
|
+
def self.mix_in_adapter(adapter)
|
|
47
|
+
adapter_module = Adapters.const_get(adapter)
|
|
48
|
+
include(adapter_module) unless self < adapter_module
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(set, **)
|
|
52
|
+
assign_options(**)
|
|
53
|
+
assign_and_check(limit: 1)
|
|
54
|
+
@set = set
|
|
55
|
+
@keyset = @options[:keyset] || extract_keyset
|
|
56
|
+
raise InternalError, 'the set must be ordered' if @keyset.empty?
|
|
57
|
+
|
|
58
|
+
@identifiers = quoted_identifiers(@set.model.table_name)
|
|
59
|
+
|
|
60
|
+
assign_page
|
|
61
|
+
self.next
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The array of records for the current page
|
|
65
|
+
def records
|
|
66
|
+
@records ||= begin
|
|
67
|
+
ensure_select
|
|
68
|
+
fetch_records
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The next page (i.e., the cutoff of the current page)
|
|
73
|
+
def next
|
|
74
|
+
records
|
|
75
|
+
return unless @more
|
|
76
|
+
|
|
77
|
+
@next ||= B64.urlsafe_encode(extract_cutoff.to_json)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
protected
|
|
81
|
+
|
|
82
|
+
def keyset? = true
|
|
83
|
+
|
|
84
|
+
def assign_page
|
|
85
|
+
return unless (@page = @options[:page])
|
|
86
|
+
|
|
87
|
+
@prior_cutoff = JSON.parse(B64.urlsafe_decode(@page))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fetch_records
|
|
91
|
+
apply_where(compose_predicate, arguments_from(@prior_cutoff)) if @prior_cutoff
|
|
92
|
+
@set.limit(@limit + 1).to_a.tap do |records|
|
|
93
|
+
@more = records.size > @limit && !records.pop.nil?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Compose the parameterized predicate used to extract the page records.
|
|
98
|
+
#
|
|
99
|
+
# For example, with a set like Pet.order(animal: :asc, name: :desc, id: :asc)
|
|
100
|
+
# it returns a union of intersections:
|
|
101
|
+
#
|
|
102
|
+
# ("pets"."animal" = :animal AND "pets"."name" = :name AND "pets"."id" > :id) OR
|
|
103
|
+
# ("pets"."animal" = :animal AND "pets"."name" < :name) OR
|
|
104
|
+
# ("pets"."animal" > :animal)
|
|
105
|
+
#
|
|
106
|
+
# When :tuple_comparison is enabled, and if the order is all :asc or all :desc,
|
|
107
|
+
# with a set like Pet.order(:animal, :name, :id) it returns the following string:
|
|
108
|
+
#
|
|
109
|
+
# ("pets"."animal", "pets"."name", "pets"."id") > (:animal, :name, :id)
|
|
110
|
+
#
|
|
111
|
+
def compose_predicate(prefix = nil)
|
|
112
|
+
operator = { asc: '>', desc: '<' }
|
|
113
|
+
directions = @keyset.values
|
|
114
|
+
identifier = @identifiers
|
|
115
|
+
placeholder = @keyset.to_h { |column| [column, ":#{prefix}#{column}"] }
|
|
116
|
+
|
|
117
|
+
return "(#{identifier.values.join(', ')}) #{operator[directions.first]} (#{placeholder.values.join(', ')})" \
|
|
118
|
+
if @options[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc))
|
|
119
|
+
|
|
120
|
+
keyset = @keyset.to_a
|
|
121
|
+
ors = []
|
|
122
|
+
until keyset.empty?
|
|
123
|
+
column, direction = keyset.pop
|
|
124
|
+
ands = keyset.map { |k, _| "#{identifier[k]} = #{placeholder[k]}" }
|
|
125
|
+
ands << "#{identifier[column]} #{operator[direction]} #{placeholder[column]}"
|
|
126
|
+
ors << "(#{ands.join(' AND ')})"
|
|
127
|
+
end
|
|
128
|
+
query = ors.join(' OR ')
|
|
129
|
+
|
|
130
|
+
return query unless @keyset.size > 1
|
|
131
|
+
|
|
132
|
+
# Add hint predicate for DB optimizers that struggle with ORs
|
|
133
|
+
column, direction = @keyset.first
|
|
134
|
+
hint = "#{identifier[column]} #{operator[direction]}= #{placeholder[column]}"
|
|
135
|
+
"#{hint} AND (#{query})"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The prefixed arguments from a cutoff
|
|
139
|
+
def arguments_from(cutoff, prefix = nil)
|
|
140
|
+
attributes = typecast(@keyset.keys.zip(cutoff).to_h)
|
|
141
|
+
prefix ? attributes.transform_keys { |key| :"#{prefix}#{key}" } : attributes
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def extract_cutoff
|
|
145
|
+
attributes = keyset_attributes_from(@records.last)
|
|
146
|
+
@options[:pre_serialize]&.(attributes)
|
|
147
|
+
attributes.values
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
class Offset
|
|
5
|
+
# Offset pagination with memoized count
|
|
6
|
+
class Countish < Offset
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
# Return page+count or page+count+epoch
|
|
10
|
+
def compose_page_param(page)
|
|
11
|
+
value = "#{page || 1}+#{@count}"
|
|
12
|
+
value << "+#{@options[:epoch]}" if @options[:epoch]
|
|
13
|
+
EscapedValue.new(value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
class Offset
|
|
5
|
+
# Offset pagination without any COUNT query
|
|
6
|
+
class Countless < Offset
|
|
7
|
+
def initialize(**)
|
|
8
|
+
assign_options(**)
|
|
9
|
+
assign_and_check(limit: 1, page: 1)
|
|
10
|
+
@last = @options[:last] unless @options[:headless]
|
|
11
|
+
assign_offset
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def records(collection)
|
|
15
|
+
return super if @options[:headless]
|
|
16
|
+
|
|
17
|
+
fetched = collection.offset(@offset).limit(@limit + 1).to_a # eager load limit + 1
|
|
18
|
+
finalize(fetched.size) # finalize the pagy object
|
|
19
|
+
fetched[0, @limit] # ignore the extra item
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def countless? = true
|
|
25
|
+
|
|
26
|
+
# Finalize the instance variables based on the fetched size
|
|
27
|
+
def finalize(fetched_size)
|
|
28
|
+
# empty records (trigger the right info message for known 0 count)
|
|
29
|
+
@count = 0 if fetched_size.zero? && @page == 1
|
|
30
|
+
|
|
31
|
+
unless in_range? { fetched_size.positive? || @page == 1 }
|
|
32
|
+
assign_empty_page_variables
|
|
33
|
+
return self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
past = @last && @page < @last # current page is before the known last page
|
|
37
|
+
more = fetched_size > @limit # more pages after this one
|
|
38
|
+
@last = (more ? @page + 1 : @page) unless past && more
|
|
39
|
+
@in = [fetched_size, @limit].min
|
|
40
|
+
@from = @in.zero? ? 0 : @offset + 1
|
|
41
|
+
@to = @offset + @in
|
|
42
|
+
assign_previous_and_next
|
|
43
|
+
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Called by false in_range?
|
|
48
|
+
def assign_empty_page_variables
|
|
49
|
+
@in = @from = @to = 0
|
|
50
|
+
target_last = [@page - 1, 1].max
|
|
51
|
+
@last = [@last || target_last, target_last].min
|
|
52
|
+
@previous = @last
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Support easy countless page param overriding (for legacy param and behavior)
|
|
56
|
+
def compose_page_param(page)
|
|
57
|
+
EscapedValue.new("#{page || 1}+#{@last}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
prepend Deprecated::Countless if defined?(Deprecated::Countless)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../modules/abilities/shiftable'
|
|
4
|
+
require_relative '../../modules/abilities/rangeable'
|
|
5
|
+
|
|
6
|
+
class Pagy
|
|
7
|
+
# Implements Offset Pagination
|
|
8
|
+
class Offset < Pagy
|
|
9
|
+
DEFAULT = { page: 1 }.freeze
|
|
10
|
+
|
|
11
|
+
autoload :Countless, Pathname.new(__dir__).join('countless')
|
|
12
|
+
autoload :Countish, Pathname.new(__dir__).join('countish')
|
|
13
|
+
|
|
14
|
+
include Rangeable
|
|
15
|
+
include Shiftable
|
|
16
|
+
include NumericHelpers
|
|
17
|
+
|
|
18
|
+
def initialize(**)
|
|
19
|
+
assign_options(**)
|
|
20
|
+
assign_and_check(limit: 1, count: 0, page: 1)
|
|
21
|
+
assign_last
|
|
22
|
+
assign_offset
|
|
23
|
+
|
|
24
|
+
unless in_range? { @page <= @last }
|
|
25
|
+
assign_empty_page_variables
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@from = [@offset + 1, @count].min
|
|
30
|
+
@to = [@offset + @limit, @count].min
|
|
31
|
+
@in = [@to - @from + 1, @count].min
|
|
32
|
+
|
|
33
|
+
assign_previous_and_next
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :offset, :count, :from, :to, :in, :previous, :next, :last
|
|
37
|
+
alias pages last
|
|
38
|
+
|
|
39
|
+
def records(collection)
|
|
40
|
+
collection.offset(@offset).limit(@limit)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def offset? = true
|
|
46
|
+
|
|
47
|
+
def assign_last
|
|
48
|
+
@last = [(@count.to_f / @limit).ceil, 1].max
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def assign_offset
|
|
52
|
+
@offset = (@limit * (@page - 1))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Called by false in_range?
|
|
56
|
+
def assign_empty_page_variables
|
|
57
|
+
@in = @from = @to = 0 # options relative to the actual page
|
|
58
|
+
@previous = @last # @previous relative to the actual page
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
prepend Deprecated::Offset if defined?(Deprecated::Offset)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
module Search
|
|
5
|
+
class Arguments < Array
|
|
6
|
+
def respond_to_missing?(*) = true
|
|
7
|
+
|
|
8
|
+
def method_missing(*) = push(*)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Collect the search arguments to pass to the actual search
|
|
12
|
+
def pagy_search(*arguments, **options, &block)
|
|
13
|
+
Arguments.new([self, arguments, options, block])
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Search classes do not use OFFSET for querying a DB;
|
|
18
|
+
# however, they use the same positional technique used by Offset.
|
|
19
|
+
class SearchBase < Offset
|
|
20
|
+
DEFAULT = { search_method: :search }.freeze
|
|
21
|
+
|
|
22
|
+
def search? = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ElasticsearchRails < SearchBase; end
|
|
26
|
+
|
|
27
|
+
class Meilisearch < SearchBase
|
|
28
|
+
DEFAULT = { search_method: :ms_search }.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Searchkick < SearchBase; end
|
|
32
|
+
|
|
33
|
+
class TypesenseRails < SearchBase; end
|
|
34
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
# Decouple the request from the env, allowing non-rack apps to use pagy by passing a hash.
|
|
5
|
+
# Resolve the :page and :limit options from params.
|
|
6
|
+
class Request
|
|
7
|
+
def initialize(options)
|
|
8
|
+
@options = options
|
|
9
|
+
request = @options[:request]
|
|
10
|
+
@base_url, @path, @params, @cookie =
|
|
11
|
+
if request.is_a?(Hash)
|
|
12
|
+
request.values_at(:base_url, :path, :params, :cookie)
|
|
13
|
+
else
|
|
14
|
+
[request.base_url, request.path, get_params(request), request.cookies['pagy']]
|
|
15
|
+
end
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :base_url, :path, :params, :cookie
|
|
20
|
+
|
|
21
|
+
def resolve_page(force_integer: true)
|
|
22
|
+
page_key = @options[:page_key] || DEFAULT[:page_key]
|
|
23
|
+
page = @params.dig(@options[:root_key], page_key) || @params[page_key]
|
|
24
|
+
return [page.to_s.to_i, 1].max if force_integer
|
|
25
|
+
|
|
26
|
+
page if page.is_a?(String) && !page.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_limit
|
|
30
|
+
default = @options[:limit] || DEFAULT[:limit]
|
|
31
|
+
max_limit = @options[:max_limit]
|
|
32
|
+
return default unless max_limit
|
|
33
|
+
|
|
34
|
+
limit_key = @options[:limit_key] || DEFAULT[:limit_key]
|
|
35
|
+
limit = (@params.dig(@options[:root_key], limit_key) || @params[limit_key]).to_s.to_i
|
|
36
|
+
limit.zero? ? default : [limit, max_limit].min
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Overriding support
|
|
42
|
+
def get_params(request)
|
|
43
|
+
request.GET.merge(request.POST).to_h.freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
prepend Deprecated::Request if defined?(Deprecated::Request)
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/pagy/cli.rb
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require_relative '../pagy'
|
|
7
|
+
require_relative '../../apps/index'
|
|
8
|
+
|
|
9
|
+
class Pagy
|
|
10
|
+
class CLI
|
|
11
|
+
HOST = 'localhost'
|
|
12
|
+
PORT = '8000'
|
|
13
|
+
|
|
14
|
+
def start(args = ARGV)
|
|
15
|
+
options = parse_options(args)
|
|
16
|
+
run_command(args, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def parse_options(args)
|
|
22
|
+
options = { env: 'development', host: HOST, port: PORT, quiet: false }
|
|
23
|
+
|
|
24
|
+
parser = OptionParser.new do |opts|
|
|
25
|
+
opts.banner = <<~BANNER
|
|
26
|
+
Pagy #{VERSION} (https://ddnexus.github.io/pagy/playground)
|
|
27
|
+
Playground to showcase, clone and develop Pagy APPs
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
pagy APP [opts] Showcase APP from the installed gem
|
|
31
|
+
pagy clone APP Clone APP to the current dir
|
|
32
|
+
pagy FILE [opts] Develop app FILE from local path
|
|
33
|
+
BANNER
|
|
34
|
+
|
|
35
|
+
opts.summary_indent = ' '
|
|
36
|
+
opts.summary_width = 18
|
|
37
|
+
|
|
38
|
+
opts.separator "\nAPPs"
|
|
39
|
+
PagyApps::INDEX.each do |name, path|
|
|
40
|
+
desc = File.readlines(path)[3].sub('# ', '').strip
|
|
41
|
+
opts.separator " #{name.ljust(19)}#{desc}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
opts.separator "\nRackup Options"
|
|
45
|
+
opts.on('-e', '--env ENV', 'Environment') { |v| options[:env] = v }
|
|
46
|
+
opts.on('-o', '--host HOST', 'Host') { |v| options[:host] = v }
|
|
47
|
+
opts.on('-p', '--port PORT', 'Port') { |v| options[:port] = v }
|
|
48
|
+
opts.on('-t', '--threads THREADS', 'Threads') { |v| options[:threads] = v }
|
|
49
|
+
|
|
50
|
+
opts.separator "\nOther Options"
|
|
51
|
+
opts.on('-q', '--quiet', 'Quiet mode for development') { options[:quiet] = true }
|
|
52
|
+
opts.on('-v', '--version', 'Show version') do
|
|
53
|
+
puts VERSION
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
57
|
+
puts opts
|
|
58
|
+
exit
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
opts.separator "\nExamples"
|
|
62
|
+
opts.separator " pagy demo Showcase demo at http://#{HOST}:#{PORT}"
|
|
63
|
+
opts.separator ' pagy clone repro Clone repro to ./repro.ru (rename it)'
|
|
64
|
+
opts.separator " pagy ~/myapp.ru Develop ~/myapp.ru at #{HOST}:#{PORT}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
parser.parse!(args)
|
|
69
|
+
rescue OptionParser::InvalidOption => e
|
|
70
|
+
abort e.message
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if args.empty?
|
|
74
|
+
puts parser
|
|
75
|
+
exit
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
options
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def run_command(args, options)
|
|
82
|
+
arg = args.shift
|
|
83
|
+
|
|
84
|
+
if arg.eql?('clone')
|
|
85
|
+
clone_app(args.shift)
|
|
86
|
+
else
|
|
87
|
+
serve_app(arg, options)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def clone_app(name)
|
|
92
|
+
abort "Expected APP to be in [#{PagyApps::INDEX.keys.join(', ')}]; got #{name.inspect}" unless PagyApps::INDEX.key?(name)
|
|
93
|
+
|
|
94
|
+
if File.exist?(name)
|
|
95
|
+
print "Do you want to overwrite the #{name.inspect} file? (y/n)> "
|
|
96
|
+
answer = gets.chomp
|
|
97
|
+
abort "#{name.inspect} file already present" unless answer.start_with?(/y/i)
|
|
98
|
+
end
|
|
99
|
+
FileUtils.cp(PagyApps::INDEX[name], '.', verbose: true)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def serve_app(arg, options)
|
|
103
|
+
if PagyApps::INDEX.key?(arg)
|
|
104
|
+
options[:env] = 'showcase'
|
|
105
|
+
options[:quiet] = true
|
|
106
|
+
# Avoid the creation of './tmp/local_secret.txt' for showcase env
|
|
107
|
+
ENV['SECRET_KEY_BASE'] = 'absolute secret!' if arg.eql?('rails')
|
|
108
|
+
file = PagyApps::INDEX[arg]
|
|
109
|
+
else
|
|
110
|
+
file = arg
|
|
111
|
+
end
|
|
112
|
+
abort "#{file.inspect} app not found" unless File.exist?(file)
|
|
113
|
+
|
|
114
|
+
gem_dir = File.expand_path('../..', __dir__)
|
|
115
|
+
rackup = "rackup -I #{gem_dir}/lib -r pagy -o #{options[:host]} -p #{options[:port]} -E #{options[:env]} #{file}"
|
|
116
|
+
rackup << " -O Threads=#{options[:threads]}" if options[:threads]
|
|
117
|
+
rackup << ' -q' if options[:quiet]
|
|
118
|
+
|
|
119
|
+
exec(rackup)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/pagy/console.rb
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
# See Pagy::Console API documentation: https://ddnexus.github.io/pagy/docs/api/console
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
require 'pagy/extras/standalone'
|
|
3
|
+
# Console loader
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def self.included(main)
|
|
12
|
-
main.include(Backend)
|
|
13
|
-
main.include(Frontend)
|
|
14
|
-
DEFAULT[:url] = 'http://www.example.com/subdir'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Require the extras passed as arguments
|
|
18
|
-
def pagy_extras(*extras)
|
|
19
|
-
extras.each { |extra| require "pagy/extras/#{extra}" }
|
|
20
|
-
puts "Required extras: #{extras.map(&:inspect).join(', ')}"
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
5
|
+
# :nocov:
|
|
6
|
+
require 'pagy'
|
|
7
|
+
include Pagy::Console # rubocop:disable Style/MixinUsage
|
|
8
|
+
# :nocov:
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file relegates all the deprecation warnings and code.
|
|
4
|
+
# Pagy already implements the next code and this file works as a compatibility layer
|
|
5
|
+
# to avoid breaking changes in the current version, respecting the Semantic Version contract.
|
|
6
|
+
class Pagy
|
|
7
|
+
module Deprecated
|
|
8
|
+
def self.client_max_limit(options)
|
|
9
|
+
if (max_limit = options.delete(:client_max_limit))
|
|
10
|
+
options[:max_limit] ||= max_limit
|
|
11
|
+
warn '[PAGY] the :client_max_limit option is deprecated: use :max_limit instead.'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Pagy
|
|
16
|
+
def assign_options(**options)
|
|
17
|
+
if options.key?(:max_pages)
|
|
18
|
+
warn '[PAGY] the :max_pages option is deprecated: ' \
|
|
19
|
+
'use https://ddnexus.github.io/pagy/guides/how-to/#paginate-only-max-records instead.'
|
|
20
|
+
end
|
|
21
|
+
Deprecated.client_max_limit(options) # if used without Request#resolve_limit
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
##### Enabled from the autoloaded class #####
|
|
27
|
+
|
|
28
|
+
module Request # :client_max_limit option
|
|
29
|
+
def resolve_limit
|
|
30
|
+
Deprecated.client_max_limit(@options)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module Offset
|
|
36
|
+
def assign_last
|
|
37
|
+
super
|
|
38
|
+
@last = @options[:max_pages] if @options[:max_pages] && @last > @options[:max_pages]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module Countless
|
|
43
|
+
def initialize(**)
|
|
44
|
+
super
|
|
45
|
+
@page = upto_max_pages(@page)
|
|
46
|
+
@last = upto_max_pages(@last) unless @options[:headless]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def finalize(fetched_size)
|
|
50
|
+
super
|
|
51
|
+
@last = upto_max_pages(@last)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def upto_max_pages(value)
|
|
55
|
+
return value unless value && @options[:max_pages]
|
|
56
|
+
|
|
57
|
+
[value, @options[:max_pages]].min
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
module Keynav
|
|
62
|
+
def next
|
|
63
|
+
records
|
|
64
|
+
super unless @options[:max_pages] && @page >= @options[:max_pages]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
prepend Deprecated::Pagy
|
|
70
|
+
|
|
71
|
+
# Reopen the module and add the deprecated methods
|
|
72
|
+
module Configurable
|
|
73
|
+
def options
|
|
74
|
+
OPTIONS.tap do
|
|
75
|
+
warn "[PAGY] 'Pagy.options' is deprecated: use 'Pagy::OPTIONS directly'"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sync_javascript(...)
|
|
80
|
+
warn "[PAGY] 'Pagy.sync_javascript(...)' is deprecated: use 'Pagy.sync(:javascript, ...)' instead."
|
|
81
|
+
sync(:javascript, ...)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
# Configuration methods
|
|
5
|
+
module Configurable
|
|
6
|
+
# Sync the pagy resource targets.
|
|
7
|
+
def sync(resource, destination, *targets)
|
|
8
|
+
files = ROOT.join("#{resource}s").glob("{#{targets.join(',')}}")
|
|
9
|
+
unknownn = targets - files.map { |f| f.basename.to_s }
|
|
10
|
+
raise InternalError, "Resource not known: #{unknownn.join(', ')}" if unknownn.any?
|
|
11
|
+
|
|
12
|
+
FileUtils.cp(files, destination)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generate the script and style tags to help development
|
|
16
|
+
def dev_tools(wand_scale: 1)
|
|
17
|
+
<<~HTML
|
|
18
|
+
<script id="pagy-ai-widget">
|
|
19
|
+
#{ROOT.join('javascripts/ai_widget.js').read}
|
|
20
|
+
</script>
|
|
21
|
+
<script id="pagy-wand" data-scale="#{wand_scale}">
|
|
22
|
+
#{ROOT.join('javascripts/wand.js').read}
|
|
23
|
+
</script>
|
|
24
|
+
<style id="pagy-wand-default">
|
|
25
|
+
#{ROOT.join('stylesheets/pagy.css').read}
|
|
26
|
+
</style>
|
|
27
|
+
HTML
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Setup pagy for using the i18n gem
|
|
31
|
+
def translate_with_the_slower_i18n_gem!
|
|
32
|
+
send(:remove_const, :I18n)
|
|
33
|
+
send(:const_set, :I18n, ::I18n)
|
|
34
|
+
::I18n.load_path += Dir[ROOT.join('locales/*.yml')]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
# Count a collection
|
|
5
|
+
module Countable
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def get_count(collection, options)
|
|
9
|
+
return collection.size if collection.instance_of?(Array)
|
|
10
|
+
return collection.count unless defined?(::ActiveRecord) && collection.is_a?(::ActiveRecord::Relation)
|
|
11
|
+
|
|
12
|
+
count = if options[:count_over] && !collection.group_values.empty?
|
|
13
|
+
# COUNT(*) OVER ()
|
|
14
|
+
sql = Arel.star.count.over(Arel::Nodes::Grouping.new([]))
|
|
15
|
+
collection.unscope(:order).pick(sql).to_i
|
|
16
|
+
else
|
|
17
|
+
collection.count(:all)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
count.is_a?(Hash) ? count.size : count
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|