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 +4 -4
- data/.rubocop.yml +5 -7
- data/.travis.yml +6 -2
- data/CHANGELOG +13 -0
- data/README.md +90 -2
- data/actionset.gemspec +4 -3
- data/lib/action_set.rb +33 -15
- data/lib/action_set/attribute_value.rb +3 -1
- data/pagination.png +0 -0
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15622f077374c1ce7a76dea96c6523f1418e0f6d
|
4
|
+
data.tar.gz: a4054979eea205a3ee260f3781372dd9682411b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ddc6b85bc1518198c5528dc983c8e820aafaf8051a609e3b5a8a0c8ee62d42108d821f87d46ac630ed815f0b481105c8bfbc4d2b8699e32fd599aa362fffc334
|
7
|
+
data.tar.gz: 282a1915bfc5868e9ffce1df55425210f530469e54a3f6ace1536b95c652c21376b38db41f98dca593bab6a514fd1157af84afdbf4a9ad928fe3818a9933486f
|
data/.rubocop.yml
CHANGED
@@ -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
|
-
- '
|
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
|
data/.travis.yml
CHANGED
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
|
-
|
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 <strong>2</strong> of <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
|
|
data/actionset.gemspec
CHANGED
@@ -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.
|
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.
|
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 '
|
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
|
data/lib/action_set.rb
CHANGED
@@ -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(
|
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(
|
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(
|
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
|
53
|
-
filter_params.flatten_keys.reject { |_, v| v.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
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
|
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
|
-
|
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
|
data/pagination.png
ADDED
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.
|
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
|
+
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.
|
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.
|
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:
|
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
|