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.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/apps/calendar.ru +741 -0
  4. data/apps/demo.ru +513 -0
  5. data/apps/enable_rails_page_segment.rb +54 -0
  6. data/apps/index.rb +9 -0
  7. data/apps/keynav+root_key.ru +316 -0
  8. data/apps/keynav.ru +255 -0
  9. data/apps/keyset.ru +219 -0
  10. data/apps/keyset_sequel.ru +212 -0
  11. data/apps/rails.ru +216 -0
  12. data/apps/repro.ru +185 -0
  13. data/bin/pagy +5 -0
  14. data/config/pagy.rb +46 -0
  15. data/javascripts/ai_widget.js +90 -0
  16. data/javascripts/pagy.js +168 -0
  17. data/javascripts/pagy.min.js +2 -0
  18. data/javascripts/pagy.mjs +161 -0
  19. data/javascripts/wand.js +1172 -0
  20. data/lib/pagy/classes/calendar/calendar.rb +101 -0
  21. data/lib/pagy/{calendar → classes/calendar}/day.rb +9 -12
  22. data/lib/pagy/{calendar → classes/calendar}/month.rb +7 -11
  23. data/lib/pagy/{calendar → classes/calendar}/quarter.rb +12 -16
  24. data/lib/pagy/classes/calendar/unit.rb +93 -0
  25. data/lib/pagy/{calendar → classes/calendar}/week.rb +7 -11
  26. data/lib/pagy/{calendar → classes/calendar}/year.rb +9 -9
  27. data/lib/pagy/classes/exceptions.rb +26 -0
  28. data/lib/pagy/classes/keyset/adapters/active_record.rb +50 -0
  29. data/lib/pagy/classes/keyset/adapters/sequel.rb +62 -0
  30. data/lib/pagy/classes/keyset/keynav.rb +85 -0
  31. data/lib/pagy/classes/keyset/keyset.rb +150 -0
  32. data/lib/pagy/classes/offset/countish.rb +17 -0
  33. data/lib/pagy/classes/offset/countless.rb +63 -0
  34. data/lib/pagy/classes/offset/offset.rb +63 -0
  35. data/lib/pagy/classes/offset/search.rb +34 -0
  36. data/lib/pagy/classes/request.rb +48 -0
  37. data/lib/pagy/cli.rb +122 -0
  38. data/lib/pagy/console.rb +5 -20
  39. data/lib/pagy/deprecated.rb +84 -0
  40. data/lib/pagy/modules/abilities/configurable.rb +37 -0
  41. data/lib/pagy/modules/abilities/countable.rb +23 -0
  42. data/lib/pagy/modules/abilities/linkable.rb +72 -0
  43. data/lib/pagy/modules/abilities/rangeable.rb +14 -0
  44. data/lib/pagy/modules/abilities/shiftable.rb +12 -0
  45. data/lib/pagy/modules/b64.rb +35 -0
  46. data/lib/pagy/modules/console.rb +33 -0
  47. data/lib/pagy/modules/i18n/i18n.rb +72 -0
  48. data/lib/pagy/modules/i18n/p11n/arabic.rb +30 -0
  49. data/lib/pagy/modules/i18n/p11n/east_slavic.rb +27 -0
  50. data/lib/pagy/modules/i18n/p11n/one_other.rb +15 -0
  51. data/lib/pagy/modules/i18n/p11n/one_upto_two_other.rb +15 -0
  52. data/lib/pagy/modules/i18n/p11n/other.rb +13 -0
  53. data/lib/pagy/modules/i18n/p11n/polish.rb +27 -0
  54. data/lib/pagy/modules/i18n/p11n/west_slavic.rb +22 -0
  55. data/lib/pagy/modules/i18n/p11n.rb +16 -0
  56. data/lib/pagy/modules/searcher.rb +20 -0
  57. data/lib/pagy/next.rb +25 -0
  58. data/lib/pagy/tasks/sync.rb +20 -0
  59. data/lib/pagy/toolbox/helpers/anchor_tags.rb +21 -0
  60. data/lib/pagy/toolbox/helpers/bootstrap/input_nav_js.rb +28 -0
  61. data/lib/pagy/toolbox/helpers/bootstrap/previous_next_html.rb +19 -0
  62. data/lib/pagy/toolbox/helpers/bootstrap/series_nav.rb +32 -0
  63. data/lib/pagy/toolbox/helpers/bootstrap/series_nav_js.rb +24 -0
  64. data/lib/pagy/toolbox/helpers/bulma/input_nav_js.rb +25 -0
  65. data/lib/pagy/toolbox/helpers/bulma/previous_next_html.rb +20 -0
  66. data/lib/pagy/toolbox/helpers/bulma/series_nav.rb +31 -0
  67. data/lib/pagy/toolbox/helpers/bulma/series_nav_js.rb +23 -0
  68. data/lib/pagy/toolbox/helpers/data_hash.rb +29 -0
  69. data/lib/pagy/toolbox/helpers/headers_hash.rb +30 -0
  70. data/lib/pagy/toolbox/helpers/info_tag.rb +30 -0
  71. data/lib/pagy/toolbox/helpers/input_nav_js.rb +22 -0
  72. data/lib/pagy/toolbox/helpers/limit_tag_js.rb +25 -0
  73. data/lib/pagy/toolbox/helpers/loaders.rb +55 -0
  74. data/lib/pagy/toolbox/helpers/page_url.rb +16 -0
  75. data/lib/pagy/toolbox/helpers/series_nav.rb +30 -0
  76. data/lib/pagy/toolbox/helpers/series_nav_js.rb +20 -0
  77. data/lib/pagy/toolbox/helpers/support/a_lambda.rb +36 -0
  78. data/lib/pagy/toolbox/helpers/support/data_pagy_attribute.rb +19 -0
  79. data/lib/pagy/toolbox/helpers/support/nav_aria_label_attribute.rb +10 -0
  80. data/lib/pagy/toolbox/helpers/support/series.rb +37 -0
  81. data/lib/pagy/toolbox/helpers/support/wrap_input_nav_js.rb +19 -0
  82. data/lib/pagy/toolbox/helpers/support/wrap_series_nav.rb +17 -0
  83. data/lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb +42 -0
  84. data/lib/pagy/toolbox/helpers/urls_hash.rb +12 -0
  85. data/lib/pagy/toolbox/paginators/calendar.rb +34 -0
  86. data/lib/pagy/toolbox/paginators/countish.rb +38 -0
  87. data/lib/pagy/toolbox/paginators/countless.rb +22 -0
  88. data/lib/pagy/toolbox/paginators/elasticsearch_rails.rb +56 -0
  89. data/lib/pagy/toolbox/paginators/keynav_js.rb +26 -0
  90. data/lib/pagy/toolbox/paginators/keyset.rb +15 -0
  91. data/lib/pagy/toolbox/paginators/meilisearch.rb +34 -0
  92. data/lib/pagy/toolbox/paginators/method.rb +38 -0
  93. data/lib/pagy/toolbox/paginators/offset.rb +24 -0
  94. data/lib/pagy/toolbox/paginators/searchkick.rb +34 -0
  95. data/lib/pagy/toolbox/paginators/typesense_rails.rb +34 -0
  96. data/lib/pagy.rb +67 -131
  97. data/locales/ar.yml +32 -0
  98. data/locales/be.yml +28 -0
  99. data/locales/bg.yml +24 -0
  100. data/locales/bs.yml +28 -0
  101. data/locales/ca.yml +24 -0
  102. data/locales/ckb.yml +20 -0
  103. data/locales/cs.yml +26 -0
  104. data/locales/da.yml +24 -0
  105. data/locales/de.yml +24 -0
  106. data/locales/dz.yml +20 -0
  107. data/locales/en.yml +24 -0
  108. data/{lib/locales → locales}/es.yml +9 -6
  109. data/locales/fr.yml +24 -0
  110. data/locales/hr.yml +28 -0
  111. data/locales/id.yml +20 -0
  112. data/locales/it.yml +24 -0
  113. data/locales/ja.yml +20 -0
  114. data/locales/km.yml +20 -0
  115. data/locales/ko.yml +20 -0
  116. data/locales/nb.yml +24 -0
  117. data/locales/nl.yml +24 -0
  118. data/locales/nn.yml +24 -0
  119. data/locales/pl.yml +28 -0
  120. data/{lib/locales → locales}/pt-BR.yml +10 -7
  121. data/{lib/locales → locales}/pt.yml +10 -7
  122. data/locales/ru.yml +28 -0
  123. data/locales/sk.yml +26 -0
  124. data/locales/sr.yml +28 -0
  125. data/locales/sv-SE.yml +24 -0
  126. data/locales/sv.yml +24 -0
  127. data/locales/sw.yml +28 -0
  128. data/locales/ta.yml +24 -0
  129. data/locales/tr.yml +24 -0
  130. data/locales/uk.yml +28 -0
  131. data/locales/vi.yml +20 -0
  132. data/locales/zh-CN.yml +20 -0
  133. data/locales/zh-HK.yml +20 -0
  134. data/locales/zh-TW.yml +20 -0
  135. data/stylesheets/pagy-tailwind.css +68 -0
  136. data/stylesheets/pagy.css +83 -0
  137. metadata +185 -94
  138. data/lib/config/pagy.rb +0 -258
  139. data/lib/javascripts/pagy-dev.js +0 -112
  140. data/lib/javascripts/pagy-module.js +0 -111
  141. data/lib/javascripts/pagy.js +0 -1
  142. data/lib/locales/ar.yml +0 -30
  143. data/lib/locales/be.yml +0 -25
  144. data/lib/locales/bg.yml +0 -21
  145. data/lib/locales/bs.yml +0 -25
  146. data/lib/locales/ca.yml +0 -23
  147. data/lib/locales/ckb.yml +0 -18
  148. data/lib/locales/cs.yml +0 -23
  149. data/lib/locales/da.yml +0 -23
  150. data/lib/locales/de.yml +0 -21
  151. data/lib/locales/en.yml +0 -21
  152. data/lib/locales/fr.yml +0 -21
  153. data/lib/locales/hr.yml +0 -25
  154. data/lib/locales/id.yml +0 -19
  155. data/lib/locales/it.yml +0 -21
  156. data/lib/locales/ja.yml +0 -19
  157. data/lib/locales/km.yml +0 -19
  158. data/lib/locales/ko.yml +0 -19
  159. data/lib/locales/nb.yml +0 -21
  160. data/lib/locales/nl.yml +0 -21
  161. data/lib/locales/nn.yml +0 -21
  162. data/lib/locales/pl.yml +0 -25
  163. data/lib/locales/ru.yml +0 -27
  164. data/lib/locales/sr.yml +0 -25
  165. data/lib/locales/sv-SE.yml +0 -21
  166. data/lib/locales/sv.yml +0 -21
  167. data/lib/locales/sw.yml +0 -23
  168. data/lib/locales/ta.yml +0 -23
  169. data/lib/locales/tr.yml +0 -19
  170. data/lib/locales/uk.yml +0 -25
  171. data/lib/locales/vi.yml +0 -17
  172. data/lib/locales/zh-CN.yml +0 -19
  173. data/lib/locales/zh-HK.yml +0 -19
  174. data/lib/locales/zh-TW.yml +0 -19
  175. data/lib/pagy/backend.rb +0 -39
  176. data/lib/pagy/calendar/helper.rb +0 -65
  177. data/lib/pagy/calendar.rb +0 -126
  178. data/lib/pagy/countless.rb +0 -37
  179. data/lib/pagy/exceptions.rb +0 -25
  180. data/lib/pagy/extras/arel.rb +0 -36
  181. data/lib/pagy/extras/array.rb +0 -24
  182. data/lib/pagy/extras/bootstrap.rb +0 -108
  183. data/lib/pagy/extras/bulma.rb +0 -105
  184. data/lib/pagy/extras/calendar.rb +0 -53
  185. data/lib/pagy/extras/countless.rb +0 -37
  186. data/lib/pagy/extras/elasticsearch_rails.rb +0 -80
  187. data/lib/pagy/extras/foundation.rb +0 -105
  188. data/lib/pagy/extras/frontend_helpers.rb +0 -67
  189. data/lib/pagy/extras/gearbox.rb +0 -54
  190. data/lib/pagy/extras/headers.rb +0 -53
  191. data/lib/pagy/extras/i18n.rb +0 -26
  192. data/lib/pagy/extras/items.rb +0 -61
  193. data/lib/pagy/extras/jsonapi.rb +0 -79
  194. data/lib/pagy/extras/materialize.rb +0 -96
  195. data/lib/pagy/extras/meilisearch.rb +0 -65
  196. data/lib/pagy/extras/metadata.rb +0 -38
  197. data/lib/pagy/extras/navs.rb +0 -51
  198. data/lib/pagy/extras/overflow.rb +0 -80
  199. data/lib/pagy/extras/searchkick.rb +0 -67
  200. data/lib/pagy/extras/semantic.rb +0 -95
  201. data/lib/pagy/extras/standalone.rb +0 -60
  202. data/lib/pagy/extras/support.rb +0 -40
  203. data/lib/pagy/extras/trim.rb +0 -29
  204. data/lib/pagy/extras/uikit.rb +0 -97
  205. data/lib/pagy/frontend.rb +0 -114
  206. data/lib/pagy/i18n.rb +0 -165
  207. data/lib/pagy/url_helpers.rb +0 -27
  208. data/lib/stylesheets/pagy.css +0 -61
  209. data/lib/stylesheets/pagy.scss +0 -50
  210. data/lib/stylesheets/pagy.tailwind.scss +0 -24
  211. /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
- require 'pagy' # so you can require just the extra in the console
5
- require 'pagy/extras/standalone'
3
+ # Console loader
6
4
 
7
- class Pagy
8
- # Provide a ready to use pagy environment when included in irb/rails console
9
- module Console
10
- # Include Backend, Frontend and set the default URL
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