actionset 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ee8a50300e9e0918965a3c32ec78d1f740233add
4
- data.tar.gz: 27789ef2fb5e1ff75f1a58ca0bc29c0f4d6fb938
3
+ metadata.gz: 15622f077374c1ce7a76dea96c6523f1418e0f6d
4
+ data.tar.gz: a4054979eea205a3ee260f3781372dd9682411b7
5
5
  SHA512:
6
- metadata.gz: 66d724b335536136cd5ac2efdcaf644cf9586ae7f837a51dbf9acf8da6042b664ad9a1da5307f3b8c9f8e6c8ac3e7d031199dc893ad7baf5c04856302435a554
7
- data.tar.gz: 304690a7e523c824c230349eeced3bf0871adb9bcb98614cc538960982e564612f16a72301440df0ddf45c2ebb5935e31b903677d2ab6f71b6f8d70d32e56b9d
6
+ metadata.gz: ddc6b85bc1518198c5528dc983c8e820aafaf8051a609e3b5a8a0c8ee62d42108d821f87d46ac630ed815f0b481105c8bfbc4d2b8699e32fd599aa362fffc334
7
+ data.tar.gz: 282a1915bfc5868e9ffce1df55425210f530469e54a3f6ace1536b95c652c21376b38db41f98dca593bab6a514fd1157af84afdbf4a9ad928fe3818a9933486f
@@ -1,11 +1,5 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.3
3
- Include:
4
- - Rakefile
5
- Exclude:
6
- - bin/*
7
- - db/**/*
8
- - spec/*_helper.rb
9
3
  Metrics/LineLength:
10
4
  Max: 120
11
5
  # To make it possible to copy or click on URIs in the code, we allow lines
@@ -20,6 +14,10 @@ Style/SignalException:
20
14
  EnforcedStyle: semantic
21
15
  Metrics/BlockLength:
22
16
  Exclude:
23
- - 'actionset.gemspec'
17
+ - 'activeset.gemspec'
24
18
  - 'spec/**/*_spec.rb'
25
19
  - '**/schema.rb'
20
+ Naming/VariableNumber:
21
+ EnforcedStyle: snake_case
22
+ Style/DateTime:
23
+ AllowCoercion: true
@@ -1,5 +1,9 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.1
5
- before_install: gem install bundler -v 1.15.4
4
+ - 2.4.1
5
+ before_install:
6
+ - gem install bundler -v 1.15.4
7
+ - export TZ=America/New_York
8
+ after_success:
9
+ - bash < (curl -s https://codecov.io/bash)
data/CHANGELOG CHANGED
@@ -1,3 +1,16 @@
1
+ v 0.7.0
2
+ - overhaul the specs
3
+ + update the data model to be more extensive and expressive
4
+ + update to the new FactoryBot gem
5
+ + write programmatic specs for extensive coverage
6
+ + ensure factories don't have duplicate attributes
7
+ + add specs for ARel operators in filtering
8
+ - update the README
9
+ - add another exception to the Ruby typcaster (`to_onum`)
10
+ - update dependency on ActiveSet to 0.8.x
11
+ v 0.6.0
12
+ - Refactor to work with versions 0.7.x of ActiveSet
13
+ - Move and rename the typecasting class for attribute values
1
14
  v 0.5.4
2
15
  - Allow the `form_for_object_from_param` helper to accept defaults hash
3
16
  v 0.5.3
data/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # ActionSet
2
2
 
3
+ [![Build Status](https://travis-ci.com/fractaledmind/actionset.svg?branch=master)](https://travis-ci.com/fractaledmind/actionset)
4
+ [![codecov](https://codecov.io/gh/fractaledmind/actionset/branch/master/graph/badge.svg)](https://codecov.io/gh/fractaledmind/actionset)
5
+
3
6
  ## Installation
4
7
 
5
8
  Add this line to your application's Gemfile:
6
9
 
7
10
  ```ruby
8
- gem 'actionset'
11
+ gem 'actionset', require: 'action_set'
9
12
  ```
10
13
 
11
14
  And then execute:
@@ -18,7 +21,92 @@ Or install it yourself as:
18
21
 
19
22
  ## Usage
20
23
 
21
- TODO: Write usage instructions here
24
+ In order to make the **`ActionSet`** helper methods available to your application, you need to `include` the module into your `ApplicationController`.
25
+
26
+ ```ruby
27
+ class ApplicationController < ActionController::Base
28
+ include ActionSet
29
+ end
30
+ ```
31
+
32
+ Or, if you only want or need **`ActionSet`** in certain controllers, you can `include` the module directly into those controllers.
33
+
34
+ The simplest setup is to use the `process_set` helper method in your `index` action. Typically, `index` actions look something like the following:
35
+
36
+ ```ruby
37
+ def index
38
+ @things = Thing.all
39
+ end
40
+ ```
41
+
42
+ In order to wire up the filtering, sorting, and paginating behaviors, we simply need to update our `index` action to:
43
+
44
+ ```ruby
45
+ def index
46
+ @things = process_set(Thing.all)
47
+ end
48
+ ```
49
+
50
+ Now, `@things` will be properly filtered, sorted, and paginated according to the request parameters.
51
+
52
+ > **Note:** `process_set` applies pagination and will paginate your collection regardless of the request parameters. Unless there are request parameters overriding the defaults, your collection will be paginated to 25 items per page showing the first 25 items (page 1). If you want to only filter and sort _without_ paginating, simply use the `filter_set` and `sort_set` helper methods directly, e.g. `sort_set(filter_set(Thing.all))`
53
+
54
+ > **Note:** If you use some authorization library, like [`Pundit`](https://github.com/varvet/pundit), which applies authorization scoping to your `index` action, you can compose that behavior with **`ActionSet`** easily:
55
+ > ```ruby
56
+ > def index
57
+ > @things = process_set(policy_scope(Thing.all))
58
+ > end
59
+ > ```
60
+
61
+ In addition to filtering, sorting, and paginating, **`ActionSet`** provides exporting functionality via the `export_set` helper method. One common use case is to have an `index` action that renders a filtered, sorted, and paginated collection, but allows for a CSV export as well. In such cases, you typically want the HTML collection to be paginated, but the CSV not to be. This behavior is also relatively simple to achieve:
62
+
63
+ ```ruby
64
+ def index
65
+ things = sort_set(filter_set(Thing.all))
66
+
67
+ respond_to do |f|
68
+ f.html { @things = paginate_set(things) }
69
+ f.csv { export_set(things) }
70
+ end
71
+ end
72
+ ```
73
+
74
+ With our controller properly wired up, we now simply need to have our views submitting request parameters in the shape that **`ActionSet`** expects. **`ActionSet`** provides view helpers to simplify such work.
75
+
76
+ Sorting is perhaps the simplest to setup. To create an (ARIA accessible) anchor link to sort by some particular attribute, use the `sort_link_for` view helper. You pass the attribute name (or dot-separated path), and then can add the text for the link (defaults to title-casing your attribute) and/or any HTML attributes you'd like added to the anchor link. A notable feature of the `sort_link_for` helper is that it intelligently infers the sort direction from whatever the current request state is. That is, if no sorting has been applied for that attribute, the link will apply sorting to that attribute in the _ascending_ direction. If sorting is currently being applied for that attribute in the _ascending_ direction, the link will apply sorting to that attribute in the _descending_ direction, and vice versa.
77
+
78
+ Filtering is somewhat more involved. **`ActionSet`** expects filters to be placed under the `filter` request parameter, aside from that one expectation, we leave all other view layer implementation details up to you. You can build your filtering interface however best fits your application. However, if you need a simple default, we suggest the following pattern: a simple form on your `index` action view that simply reloads that action with whatever filter params the user has submitted. In Rails, building such a form is relatively simple:
79
+
80
+ ```erb
81
+ <%= form_for(form_for_object_from_param(:filter),
82
+ method: :get,
83
+ url: things_path) do |form| %>
84
+ <div class="form-group">
85
+ <%= form.label(:attribute, class: 'control-label') %>
86
+ <%= form.text_field(:attribute, class: 'form-control') %>
87
+ </div>
88
+
89
+ <div class="text-right">
90
+ <%= form.submit 'Save', class: 'btn btn-primary' %>
91
+ </div>
92
+ <% end %>
93
+ ```
94
+
95
+ We tell the `form_for` helper to make a `GET` request back to our `index` action (`things_path` in this example). The only odd bit is what we pass as the object to `form_for`; you will note we pass `form_for_object_from_param(:filter)`. This `form_for_object_from_param` view helper is provided by **`ActionSet`** and does precisely what it says—it provides an object (an `OpenStruct` object to be precise) that encodes whatever request parameters are nested under the param name given, where that object will work properly with the `form_for` helper. This view helper allows us to build forms we the user's filter inputs will be retained across searches.
96
+
97
+ For pagination, like filtering, we don't enforce any view-layer specifics. You simply need to pass request parameters under the `paginate` param, specifically the `page` and `size` params. However, **`ActionSet`** does provide a simple default pagination UI component via the `pagination_links_for` view helper. You simply pass your processed set to this view helper, and it will render HTML in this structure:
98
+
99
+ ![alt text](https://raw.githubusercontent.com/fractaledmind/actionset/master/pagination.png)
100
+
101
+ ```html
102
+ <nav class="pagination" aria-label="Page navigation">
103
+ <a class="page-link page-first" href="/foos?paginate%5Bpage%5D=1">« First</a>
104
+ <a rel="prev" class="page-link page-prev" href="/foos?paginate%5Bpage%5D=1">‹ Prev</a>
105
+ <span class="page-current">Page&nbsp;<strong>2</strong>&nbsp;of&nbsp;<strong>3</strong></span>
106
+ <a rel="next" class="page-link page-next" href="/foos?paginate%5Bpage%5D=3">Next ›</a>
107
+ <a class="page-link page-last" href="/foos?paginate%5Bpage%5D=3">Last »</a>
108
+ </nav>
109
+ ```
22
110
 
23
111
  ## Development
24
112
 
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'actionset'
8
- spec.version = '0.6.0'
8
+ spec.version = '0.7.0'
9
9
  spec.authors = ['Stephen Margheim']
10
10
  spec.email = ['stephen.margheim@gmail.com']
11
11
 
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ['lib']
23
23
 
24
- spec.add_dependency 'activeset', '>= 0.7.1'
24
+ spec.add_dependency 'activeset', '>= 0.8.0'
25
25
  spec.add_dependency 'activesupport', '>= 4.0.2'
26
26
  spec.add_dependency 'railties'
27
27
 
@@ -32,8 +32,9 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency 'database_cleaner'
33
33
  spec.add_development_dependency 'capybara'
34
34
  spec.add_development_dependency 'combustion'
35
- spec.add_development_dependency 'factory_girl'
35
+ spec.add_development_dependency 'factory_bot'
36
36
  spec.add_development_dependency 'faker'
37
37
  spec.add_development_dependency 'simplecov'
38
38
  spec.add_development_dependency 'simplecov-console'
39
+ spec.add_development_dependency 'ransack'
39
40
  end
@@ -24,7 +24,7 @@ module ActionSet
24
24
 
25
25
  def filter_set(set)
26
26
  active_set = ensure_active_set(set)
27
- active_set = active_set.filter(filter_structure(set)) if filter_params.any?
27
+ active_set = active_set.filter(filter_instructions_for(set)) if filter_params.any?
28
28
  active_set
29
29
  end
30
30
 
@@ -34,49 +34,67 @@ module ActionSet
34
34
  active_set
35
35
  end
36
36
 
37
+ # TODO: should we move the default value setting to this layer,
38
+ # and have ActiveSet require instructions for pagination?
37
39
  def paginate_set(set)
38
40
  active_set = ensure_active_set(set)
39
- active_set = active_set.paginate(paginate_structure)
41
+ active_set = active_set.paginate(paginate_instructions)
40
42
  active_set
41
43
  end
42
44
 
43
45
  def export_set(set)
44
46
  return send_file(set, export_set_options(request.format)) if set.is_a?(String) && File.file?(set)
47
+
45
48
  active_set = ensure_active_set(set)
46
- exported_data = active_set.export(export_structure)
49
+ exported_data = active_set.export(export_instructions)
47
50
  send_data(exported_data, export_set_options(request.format))
48
51
  end
49
52
 
50
53
  private
51
54
 
52
- def filter_structure(set)
53
- filter_params.flatten_keys.reject { |_, v| v.blank? }.each_with_object({}) do |(keypath, value), memo|
54
- instruction = ActiveSet::AttributeInstruction.new(keypath, value)
55
- item_with_value = set.find { |i| !instruction.value_for(item: i).nil? }
56
- item_value = instruction.value_for(item: item_with_value)
57
- typecast_value = ActionSet::AttributeValue.new(value)
58
- .cast(to: item_value.class)
55
+ def filter_instructions_for(set)
56
+ filter_params.flatten_keys.reject { |_, v| v.try(:empty?) }.each_with_object({}) do |(keypath, value), memo|
57
+ typecast_value = if value.respond_to?(:each)
58
+ value.map { |v| filter_typecasted_value_for(keypath, v, set) }
59
+ else
60
+ filter_typecasted_value_for(keypath, value, set)
61
+ end
59
62
 
60
63
  memo[keypath] = typecast_value
61
64
  end
62
65
  end
63
66
 
64
- def paginate_structure
67
+ def filter_typecasted_value_for(keypath, value, set)
68
+ instruction = ActiveSet::AttributeInstruction.new(keypath, value)
69
+ item_with_value = set.find { |i| !instruction.value_for(item: i).nil? }
70
+ item_value = instruction.value_for(item: item_with_value)
71
+ ActionSet::AttributeValue.new(value)
72
+ .cast(to: item_value.class)
73
+ end
74
+
75
+ def paginate_instructions
65
76
  paginate_params.transform_values(&:to_i)
66
77
  end
67
78
 
68
- def export_structure
79
+ def export_instructions
69
80
  {}.tap do |struct|
70
81
  struct[:format] = export_params[:format] || request.format.symbol
71
82
  columns_params = export_params[:columns]&.map do |column|
72
83
  Hash[column&.map do |k, v|
73
84
  is_literal_value = ->(key) { key.to_s == 'value*' }
74
- key = is_literal_value.(k) ? 'value' : k
75
- val = is_literal_value.(k) ? send(v) : v
85
+ key = is_literal_value.call(k) ? 'value' : k
86
+ val = is_literal_value.call(k) ? send(v) : v
76
87
  [key, val]
77
88
  end]
78
89
  end
79
- struct[:columns] = columns_params || send(:export_set_columns) || []
90
+
91
+ struct[:columns] = if columns_params&.any?
92
+ columns_params
93
+ elsif respond_to?(:export_set_columns, true)
94
+ send(:export_set_columns)
95
+ else
96
+ [{}]
97
+ end
80
98
  end
81
99
  end
82
100
 
@@ -46,7 +46,7 @@ module ActionSet
46
46
  @possible_typecasters ||= String.instance_methods
47
47
  .map(&:to_s)
48
48
  .select { |m| m.start_with? 'to_' }
49
- .reject { |m| %[to_v8].include? m }
49
+ .reject { |m| %w[to_v8 to_onum].include? m }
50
50
  end
51
51
 
52
52
  def typecast(method_name)
@@ -144,10 +144,12 @@ module ActionSet
144
144
  def process
145
145
  return if @raw.is_a? @target
146
146
  return unless @target.eql?(ActiveSupport::TimeWithZone)
147
+
147
148
  time_value = ActiveModelAdapter.new(@raw, Time).process
148
149
 
149
150
  return unless time_value.is_a?(Time)
150
151
  return time_value unless time_value.respond_to?(:in_time_zone)
152
+
151
153
  time_value.in_time_zone
152
154
  end
153
155
  end
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionset
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-08 00:00:00.000000000 Z
11
+ date: 2018-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activeset
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.7.1
19
+ version: 0.8.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.7.1
26
+ version: 0.8.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -151,7 +151,7 @@ dependencies:
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
- name: factory_girl
154
+ name: factory_bot
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="
@@ -206,6 +206,20 @@ dependencies:
206
206
  - - ">="
207
207
  - !ruby/object:Gem::Version
208
208
  version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: ransack
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
209
223
  description: Easily filter, sort, and paginate enumerable sets via web requests.
210
224
  email:
211
225
  - stephen.margheim@gmail.com
@@ -248,6 +262,7 @@ files:
248
262
  - lib/action_set/helpers/sort/link_for_helper.rb
249
263
  - lib/action_set/helpers/sort/next_direction_for_helper.rb
250
264
  - lib/action_set/helpers/sort/path_for_helper.rb
265
+ - pagination.png
251
266
  homepage: https://github.com/fractaledmind/actionset
252
267
  licenses:
253
268
  - MIT