autoforme 1.13.0 → 1.14.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bff68076c7cdf213c0142df9ab3809d33ede301c99f12e5cfbe7d1e111df30d
4
- data.tar.gz: d2446bc814e6a19ebb70dfd46717ac596bb0aa4915abf02dd83aded973ba8d65
3
+ metadata.gz: 4a736d797ecc713bf84bca96606007e8eb02c19e83b698365417afcc44fe20f6
4
+ data.tar.gz: bb6237775d2413f4d57e6d6c30e458f8b0037d65ea3b87db83bbfa63affae2e5
5
5
  SHA512:
6
- metadata.gz: '048c064a080ae50bd872be8e17a96f06e6f9eece5fb095c31121810edbe2f03d1f5db7a6562fb0d33c7229b1e1a49641246186f6567435664be646da836ce251'
7
- data.tar.gz: 596b1caa6bfc6a91cf8745019df9c299e9315ace558388c9c5ba4fb44b7df18fdfd0aff0c9e5a7213c71e26ffb3fa12d3afed34e91c962e1ae258254413cded4
6
+ metadata.gz: 21db83b2b59a9b71679b21f5d1fffae43f7c73809e5ee3bfe7af70cb14faf768208c794b2adf27c20777f322ef2dfc15b352e88f34b32dc6ca91b036ad88d785
7
+ data.tar.gz: 8b0f1295ff5b59334658c34e4d90d80aff5c9778bf3717f7c43eabede1accc7cb95aafca9043ac2fb81eaf667b215bb7f40f45368cdf6c88ce44b8a15a98e202
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ === 1.14.0 (2025-10-02)
2
+
3
+ * Support customizing the searching done for specific columns via column_search_filter (jeremyevans)
4
+
5
+ * Support autoforme_framework Roda class method in Roda plugin, to allow for reflection (jeremyevans)
6
+
7
+ * Omit invalid Next/Previous links instead of including them in a tag with a disabled attribute (jeremyevans)
8
+
9
+ * Support pagination via filtering instead of offset using pagination_strategy :filter (requires unambiguous order) (jeremyevans)
10
+
1
11
  === 1.13.0 (2024-07-10)
2
12
 
3
13
  * Typecast primary key values before using them for lookup (jeremyevans)
data/README.rdoc CHANGED
@@ -132,6 +132,9 @@ autocomplete_options :: Enable autocompletion for this model, with the given
132
132
  :limit :: The number of results to return
133
133
  :filter :: Similar to callback, but overriding the default filter
134
134
  (a case insensitive substring search on display)
135
+ column_search_filter :: This should be a Proc that accepts a dataset, column, value, and request,
136
+ and returns a filtered dataset. This allows for customizing the search filtering,
137
+ so you can use non-columns/non-associations on the search form.
135
138
  eager :: Array of associations to eagerly load in separate queries
136
139
  eager_graph :: Array of associations to eager load in the same query
137
140
  (necessary if order or filter refers to them)
@@ -144,6 +147,8 @@ inline_mtm_associations :: Array of many to many association symbols to allow ed
144
147
  lazy_load_association_links :: Whether to show the association links directly on the show/edit pages,
145
148
  or to load them via ajax on request
146
149
  mtm_associations :: Array of many to many association symbols to support editing on a separate page
150
+ pagination_strategy :: Set to :filter to paginate using a filter instead of using offsets. This requires
151
+ the column have an unambiguous order.
147
152
  per_page :: Number of records to show per page on the browse and search pages
148
153
  session_value :: Sets up a filter and before_create hook that makes it so access is limited
149
154
  to objects where the object's column value is the same as the session value
@@ -139,7 +139,6 @@ module AutoForme
139
139
  end
140
140
 
141
141
  request.redirect(path)
142
- nil
143
142
  end
144
143
 
145
144
  # Handle the current action, returning an HTML string containing the page content,
@@ -415,20 +414,41 @@ module AutoForme
415
414
  def table_pager(type, next_page)
416
415
  html = String.new
417
416
  html << '<ul class="pager">'
418
- page = request.id.to_i
419
- if page > 1
420
- html << "<li><a href=\"#{url_for("#{type}/#{page-1}?#{h request.query_string}")}\">Previous</a></li>"
417
+ qs = next_qs = h request.query_string
418
+
419
+ if next_page.is_a?(Array)
420
+ # Filter pagination, next_page contains the values for the last record of current page
421
+ no_previous = true
422
+ if next_page.empty?
423
+ next_page = nil
424
+ else
425
+ next_qs = qs.dup
426
+ params = request.params.dup
427
+ next_page = next_page.map(&:to_s)
428
+ next_page = next_page[0] if next_page.length == 1
429
+ params["_after"] = next_page
430
+ next_qs = h Rack::Utils.build_nested_query(params)
431
+ following = prev = "0"
432
+ end
421
433
  else
422
- html << '<li class="disabled"><a href="#">Previous</a></li>'
434
+ # Offset pagination, request id contains current page
435
+ page = request.id.to_i
436
+ page = 1 if page < 1
437
+ no_previous = page <= 1
438
+ prev = page-1
439
+ following = page+1
440
+ end
441
+
442
+ unless no_previous
443
+ html << "<li><a href=\"#{url_for("#{type}/#{prev}?#{qs}")}\">Previous</a></li>"
423
444
  end
445
+
424
446
  if next_page
425
- page = 1 if page < 1
426
- html << "<li><a href=\"#{url_for("#{type}/#{page+1}?#{h request.query_string}")}\">Next</a></li>"
427
- else
428
- html << '<li class="disabled"><a href="#">Next</a></li>'
447
+ html << "<li><a href=\"#{url_for("#{type}/#{following}?#{next_qs}")}\">Next</a></li>"
429
448
  end
449
+
430
450
  html << "</ul>"
431
- html << "<p><a href=\"#{url_for("#{type}/csv?#{h request.query_string}")}\">CSV Format</a></p>"
451
+ html << "<p><a href=\"#{url_for("#{type}/csv?#{qs}")}\">CSV Format</a></p>"
432
452
  end
433
453
 
434
454
  # Show page used for browse/search pages.
@@ -36,9 +36,9 @@ module AutoForme
36
36
 
37
37
  opts_attribute :after_create, :after_destroy, :after_update, :association_links,
38
38
  :autocomplete_options, :before_action, :before_create, :before_destroy,
39
- :before_edit, :before_new, :before_update, :column_options,
39
+ :before_edit, :before_new, :before_update, :column_options, :column_search_filter,
40
40
  :columns, :display_name, :filter, :form_attributes, :form_options,
41
- :inline_mtm_associations, :lazy_load_association_links,
41
+ :inline_mtm_associations, :lazy_load_association_links, :pagination_strategy,
42
42
  :model_type, :mtm_associations, :order, :page_footer, :page_header, :per_page,
43
43
  :redirect, :supported_actions, :table_class, :show_html, :edit_html
44
44
 
@@ -118,6 +118,14 @@ module AutoForme
118
118
  handle_proc(association_links, model, type, request)
119
119
  end
120
120
 
121
+ def column_search_filter_for(model, dataset, column, value, request)
122
+ handle_proc(column_search_filter, model, dataset, column, value, request)
123
+ end
124
+
125
+ def pagination_strategy_for(model, type, request)
126
+ handle_proc(pagination_strategy, model, type, request)
127
+ end
128
+
121
129
  def show_html_for(obj, column, type, request)
122
130
  handle_proc(show_html, obj, column, type, request)
123
131
  end
@@ -6,7 +6,10 @@ module AutoForme
6
6
  class Request < AutoForme::Request
7
7
  def initialize(request)
8
8
  @controller = request
9
- @params = request.params
9
+ @params = {}
10
+ request.params.each do |k, v|
11
+ @params[k] = v
12
+ end
10
13
  @session = request.session
11
14
  @env = request.request.env
12
15
  @method = @env['REQUEST_METHOD']
@@ -18,6 +18,11 @@ module AutoForme
18
18
  # Regexp for valid constant names, to prevent code execution.
19
19
  VALID_CONSTANT_NAME_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze
20
20
 
21
+ # The default pagination strategy to use. Offset is used by default,
22
+ # as a filter strategy requires an unambiguous order, which the library
23
+ # cannot guarantee.
24
+ DEFAULT_PAGINATION_STRATEGY = :offset
25
+
21
26
  extend OptsAttributes
22
27
 
23
28
  # Create a new instance for the given model type and underlying model class
@@ -38,9 +43,9 @@ module AutoForme
38
43
  :autocomplete_options, :before_action, :before_create, :before_destroy,
39
44
  :before_edit, :before_new, :before_update, :class_display_name,
40
45
  :column_options, :columns, :display_name, :eager, :eager_graph,
41
- :filter, :form_attributes, :form_options,
46
+ :filter, :form_attributes, :form_options, :column_search_filter,
42
47
  :inline_mtm_associations, :lazy_load_association_links, :link_name, :mtm_associations,
43
- :order, :page_footer, :page_header, :per_page,
48
+ :order, :page_footer, :page_header, :per_page, :pagination_strategy,
44
49
  :redirect, :supported_actions, :table_class, :show_html, :edit_html
45
50
 
46
51
  def initialize(model, framework)
@@ -95,6 +100,14 @@ module AutoForme
95
100
  handle_proc(columns || framework.columns_for(model, type, request), type, request) || default_columns
96
101
  end
97
102
 
103
+ def pagination_strategy_for(type, request)
104
+ handle_proc(pagination_strategy || framework.pagination_strategy_for(model, type, request), type, request) || DEFAULT_PAGINATION_STRATEGY
105
+ end
106
+
107
+ def column_search_filter_for(dataset, column, value, request)
108
+ handle_proc(column_search_filter || framework.column_search_filter_for(model, dataset, column, value, request), dataset, column, value, request)
109
+ end
110
+
98
111
  # The options to use for the given column and request. Instead of the model options overriding the framework
99
112
  # options, they are merged together.
100
113
  def column_options_for(type, request, column)
@@ -177,7 +177,9 @@ module AutoForme
177
177
  ds = apply_associated_eager(:search, request, all_dataset_for(type, request))
178
178
  columns_for(:search_form, request).each do |c|
179
179
  if (v = params[c.to_s]) && !(v = v.to_s).empty?
180
- if association?(c)
180
+ if filtered_ds = column_search_filter_for(ds, c, v, request)
181
+ ds = filtered_ds
182
+ elsif association?(c)
181
183
  ref = model.association_reflection(c)
182
184
  ads = ref.associated_dataset
183
185
  if model_class = associated_model_class(c)
@@ -212,13 +214,32 @@ module AutoForme
212
214
  def paginate(type, request, ds, opts={})
213
215
  return ds.all if opts[:all_results]
214
216
  limit = limit_for(type, request)
215
- %r{\/(\d+)\z} =~ request.env['PATH_INFO']
216
- offset = (($1||1).to_i - 1) * limit
217
- objs = ds.limit(limit+1, (offset if offset > 0)).all
218
- next_page = false
217
+ ds = ds.limit(limit+1)
218
+
219
+ if pagination_strategy_for(type, request) == :filter
220
+ order_cols = ds.send(:hash_key_symbols, Array(order_for(type, request)).map{|c| c.is_a?(S::SQL::OrderedExpression) ? c.expression : c})
221
+ after = Array(request.params["_after"])
222
+ if order_cols.length == after.length
223
+ begin
224
+ after = order_cols.zip(after).map{|c, v| typecast_value(c, v)}
225
+ rescue S::InvalidValue
226
+ # ignore pagination, assume first page
227
+ else
228
+ ds = ds.where(ds.send(:ignore_values_preceding, {}){after})
229
+ end
230
+ end
231
+ else # offset strategy, the default
232
+ %r{\/(\d+)\z} =~ request.env['PATH_INFO']
233
+ offset = (($1||1).to_i - 1) * limit
234
+ ds = ds.offset(offset) if offset > 0
235
+ end
236
+
237
+ objs = ds.all
238
+ next_page = order_cols && []
219
239
  if objs.length > limit
220
- next_page = true
221
240
  objs.pop
241
+ last_obj = objs[-1]
242
+ next_page = after ? order_cols.map{|c| last_obj.send(c)} : true
222
243
  end
223
244
  [next_page, objs]
224
245
  end
@@ -6,7 +6,7 @@ module AutoForme
6
6
  MAJOR = 1
7
7
 
8
8
  # The minor version of AutoForme, updated for new feature releases of AutoForme.
9
- MINOR = 13
9
+ MINOR = 14
10
10
 
11
11
  # The patch version of AutoForme, updated only for bug fixes from the last
12
12
  # feature release.
@@ -14,6 +14,7 @@ class Roda
14
14
  # the options and block.
15
15
  def self.configure(app, opts={}, &block)
16
16
  app.instance_exec do
17
+ @autoforme_frameworks ||= {}
17
18
  @autoforme_routes ||= {}
18
19
  if block
19
20
  autoforme(opts, &block)
@@ -26,7 +27,13 @@ class Roda
26
27
  # options and block. If the :name option is given, store
27
28
  # this configuration for the given name.
28
29
  def autoforme(opts={}, &block)
29
- @autoforme_routes[opts[:name]] = ::AutoForme.for(:roda, self, opts, &block).route_proc
30
+ framework = @autoforme_frameworks[opts[:name]] = ::AutoForme.for(:roda, self, opts, &block)
31
+ @autoforme_routes[opts[:name]] = framework.route_proc
32
+ end
33
+
34
+ # Retrieve the framework the named or default AutoForme.
35
+ def autoforme_framework(name=nil)
36
+ @autoforme_frameworks[name]
30
37
  end
31
38
 
32
39
  # Retrieve the route proc for the named or default AutoForme.
@@ -37,6 +44,7 @@ class Roda
37
44
  # Copy the autoforme configurations into the subclass.
38
45
  def inherited(subclass)
39
46
  super
47
+ subclass.instance_variable_set(:@autoforme_frameworks, @autoforme_frameworks.dup)
40
48
  subclass.instance_variable_set(:@autoforme_routes, @autoforme_routes.dup)
41
49
  end
42
50
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: autoforme
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-07-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: forme
@@ -221,9 +220,9 @@ email: code@jeremyevans.net
221
220
  executables: []
222
221
  extensions: []
223
222
  extra_rdoc_files:
224
- - README.rdoc
225
223
  - CHANGELOG
226
224
  - MIT-LICENSE
225
+ - README.rdoc
227
226
  files:
228
227
  - CHANGELOG
229
228
  - MIT-LICENSE
@@ -251,7 +250,6 @@ metadata:
251
250
  documentation_uri: http://autoforme.jeremyevans.net
252
251
  mailing_list_uri: https://github.com/jeremyevans/autoforme/discussions
253
252
  source_code_uri: https://github.com/jeremyevans/autoforme
254
- post_install_message:
255
253
  rdoc_options:
256
254
  - "--quiet"
257
255
  - "--line-numbers"
@@ -273,8 +271,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
273
271
  - !ruby/object:Gem::Version
274
272
  version: '0'
275
273
  requirements: []
276
- rubygems_version: 3.5.11
277
- signing_key:
274
+ rubygems_version: 3.6.9
278
275
  specification_version: 4
279
276
  summary: Web Administrative Console for Roda/Sinatra/Rails and Sequel::Model
280
277
  test_files: []