the_grid 1.0.7
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 +15 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +424 -0
- data/Rakefile +1 -0
- data/lib/generators/the_grid/install/install_generator.rb +11 -0
- data/lib/generators/the_grid/install/templates/the_grid.rb +10 -0
- data/lib/the_grid/api/command/batch_remove.rb +14 -0
- data/lib/the_grid/api/command/batch_update.rb +21 -0
- data/lib/the_grid/api/command/filter.rb +43 -0
- data/lib/the_grid/api/command/paginate.rb +29 -0
- data/lib/the_grid/api/command/search.rb +86 -0
- data/lib/the_grid/api/command/sort.rb +17 -0
- data/lib/the_grid/api/command.rb +48 -0
- data/lib/the_grid/api.rb +52 -0
- data/lib/the_grid/builder/context.rb +96 -0
- data/lib/the_grid/builder/json.rb +37 -0
- data/lib/the_grid/builder.rb +61 -0
- data/lib/the_grid/config.rb +18 -0
- data/lib/the_grid/version.rb +3 -0
- data/lib/the_grid.rb +18 -0
- data/spec/api/command/batch_remove_spec.rb +41 -0
- data/spec/api/command/batch_update_spec.rb +44 -0
- data/spec/api/command/filter_spec.rb +89 -0
- data/spec/api/command/paginate_spec.rb +44 -0
- data/spec/api/command/search_spec.rb +64 -0
- data/spec/api/command/sort_spec.rb +23 -0
- data/spec/api/command_spec.rb +44 -0
- data/spec/api_spec.rb +58 -0
- data/spec/builder/context_spec.rb +182 -0
- data/spec/builder/json_spec.rb +46 -0
- data/spec/config_spec.rb +35 -0
- data/spec/spec_helper.rb +2 -0
- data/the_grid.gemspec +34 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YWI3ODZiNmIxNzlkYzkxYzIxNjY5ZWMxNGFmMWZlZWMyNjQ1NGRiNw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YmVkOGMwNjRjYmJjYTI0MWM3ZmM5ODZmNTkyOTAxZjFjMjdiMDcwMA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDA1ZmJmZjIwYjAzNGQyNTFjOWQwYWRkM2UxZjkzZjYzZDg4YjdiOGZkZDVm
|
10
|
+
ZjUzZTc2YWIzNzA1MTlmMGM2MTI5NmEzOGU1MjE0ZjFjNTM1NjI5Y2U2OGU5
|
11
|
+
NDVkZmMxYWUxMWRiNmIzYzQwYTIwNzljMTkyYzg2MmFjNzdjN2Q=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
OWQzNGE2YzlhOGM4NzkxMWViOGNhMzcxMjg1NWFhZjhhZTc0ZTFmM2M5OWFl
|
14
|
+
NGQ3MTBiMzk1ZGZlZDllNDQwMThiNWQ3ZTFmZTA2ZWZhNzA1NzIyNjhkYTRh
|
15
|
+
MjY1OWFlNGM5YTIzNjNlNGRlM2RlOTRlZGIxMWJhODFlYWQ4OGE=
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create 1.9.3-p392@grid
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,424 @@
|
|
1
|
+
Yet Another Grid
|
2
|
+
=========
|
3
|
+
|
4
|
+
This plugin is designed to provide API for building json response based on `ActiveRecord::Relation` objects.
|
5
|
+
It makes much easier to fetch information from database for displaying it using JavaScript MV* based frameworks such as Knockout, Backbone, Angular, etc.
|
6
|
+
|
7
|
+
## Getting started
|
8
|
+
|
9
|
+
First of all specify grid in your Gemfile and run `bundle install`.
|
10
|
+
After gem is installed you need to run `rails generate grid:install`. This will generate grid initializer file with basic configuration.
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
|
14
|
+
Controller:
|
15
|
+
```ruby
|
16
|
+
# app/controllers/articles_controller.rb
|
17
|
+
class ArticlesController < ApplicationController
|
18
|
+
respond_to :json
|
19
|
+
|
20
|
+
def index
|
21
|
+
@articles = Article.published
|
22
|
+
respond_with @articles
|
23
|
+
end
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
View:
|
28
|
+
```ruby
|
29
|
+
# app/views/articles/index.json.grid_builder
|
30
|
+
grid_for @articles, :per_page => 25 do
|
31
|
+
searchable_columns :title
|
32
|
+
|
33
|
+
column :title
|
34
|
+
column(:url) { |a| article_url(a) }
|
35
|
+
column(:created_at) { |a| a.created_at.to_s(:date) }
|
36
|
+
column(:author) { |a| a.author.full_name }
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
## API
|
41
|
+
|
42
|
+
The API is based on commands. Term *command* describes client's action which can be simple or complicated as well.
|
43
|
+
The general request looks like:
|
44
|
+
|
45
|
+
http://your.domain.com/route.json? with_meta=1 &
|
46
|
+
page=1 &
|
47
|
+
cmd[]=sort & field=title & order=desc &
|
48
|
+
cmd[]=search & query=test &
|
49
|
+
cmd[]=filter & filters[created_at][from]=1363513288 & filters[created_at][to]=1363513288
|
50
|
+
|
51
|
+
Each parameter relates to options of a command. The only exception is **with_meta** parameter which is used to retrieve extra meta information of grid.
|
52
|
+
|
53
|
+
### Commands
|
54
|
+
|
55
|
+
There are 2 types of commands: batch commands (e.g. *update, remove*) and select commands (e.g. *search*, *paginate*, *sort*, *filter*).
|
56
|
+
Select commands can be processed per one request (i.e. stacked) by **TheGrid::Builder** (method `execute_on` of such commands always returns `ActiveRecord::Relation`).
|
57
|
+
Batch commands can't be processed by **TheGrid::Builder** even more they are ignored (method `execute_on` returns array of processed records or boolean value).
|
58
|
+
There are few predefined commands: `paginate`, `search`, `sort`, `filter`, `batch_update`, `batch_remove`.
|
59
|
+
|
60
|
+
#### Paginate
|
61
|
+
|
62
|
+
This command has 2 non-required parameters:
|
63
|
+
|
64
|
+
- **page** specifies page of data (integer number, starts with 1)
|
65
|
+
- **per_page** specifies how much records should be selected per one page (integer number)
|
66
|
+
|
67
|
+
#### Sort
|
68
|
+
|
69
|
+
This command has also 2 paramters:
|
70
|
+
|
71
|
+
- **field** specifies sort column (string)
|
72
|
+
- **order** specifies sort order (*asc* or *desc*)
|
73
|
+
|
74
|
+
#### Filter
|
75
|
+
|
76
|
+
This command requires only one hash parameter **filters** but it can be in 3 different forms:
|
77
|
+
|
78
|
+
- `{ :title => "test" }` => `name = "test"`
|
79
|
+
- `{ :created_at => { :from => ... , :to => ..., :type => "time|date|nil" } }` => `created_at >= :from AND created_at <= :to`
|
80
|
+
|
81
|
+
- *type* specifies type of from/to parameters (optional, can be *date* or *time*). If `:type` is *date* from/to fields will be parsed into dates using `to_time` method. If `:type` is *time* from/to fields should be timestamps.
|
82
|
+
- *from/to* specifies top and bottom limits (one of them can be omitted)
|
83
|
+
|
84
|
+
- `{ :id => [1, 2, 3] }` => `id IN (1,2,3)`
|
85
|
+
|
86
|
+
#### Search
|
87
|
+
|
88
|
+
This command requires only one parameter **query** which specifies search string.
|
89
|
+
|
90
|
+
#### Batch Update
|
91
|
+
|
92
|
+
This command requires one parameter **items** - an array of hashes (with stringified keys).
|
93
|
+
Each hash should contain integer value with key *id*. Hash row is ignored if *id* is omitted or non-integer.
|
94
|
+
|
95
|
+
#### Batch Remove
|
96
|
+
|
97
|
+
This command also requires one parameter **item_ids** - array of integer ids.
|
98
|
+
Value of array is ignored if it's non-integer.
|
99
|
+
|
100
|
+
### Run batch commands
|
101
|
+
|
102
|
+
It's impossible to run batch commands using `TheGrid::Builder`.
|
103
|
+
By default command is considered as batch one if its class name starts with **Batch**. If you want to override this behavior you need to implement instance method `batch?`.
|
104
|
+
So, client has to manually build grid instance and call `run_command!` method:
|
105
|
+
```ruby
|
106
|
+
TheGrid.build_for(Article).run_command!('batch_update', params)
|
107
|
+
```
|
108
|
+
Actually it's possible to run any command as shown in line above.
|
109
|
+
Example of controller's batch action:
|
110
|
+
```ruby
|
111
|
+
class ArticlesController < ApplicationController
|
112
|
+
def batch_update
|
113
|
+
articles = TheGrid.build_for(Article).run_command!('batch_update', :items => params[:articles])
|
114
|
+
render :json => build_grid_response_for(articles, :success => "Articles has been successfully updated")
|
115
|
+
rescue ArgumentError => e
|
116
|
+
render :json => { :message => e.message, :status => :error }
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
def build_grid_response_for(records, options = {})
|
121
|
+
error_message = records.select(&:invalid?).map{ |r| r.errors.full_messages }.join('. ')
|
122
|
+
if error_message.blank?
|
123
|
+
{:status => :success, :message => options[:success]}
|
124
|
+
else
|
125
|
+
{:status => :error, :message => error_message}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
### Create/override commands
|
131
|
+
|
132
|
+
It's a normal situation when client needs a custom command or a custom version of existing command.
|
133
|
+
Suppose there is a need in `suspend` command which change status of records into *suspended*.
|
134
|
+
Command class should implement at least 2 methods: `configure` and `run_on` (if command is a batch one it should implement `batch?` method which returs `true`).
|
135
|
+
`configure` method should return validated parameters or raise an error if one of the required options is missed. Example:
|
136
|
+
```ruby
|
137
|
+
module GridCommands
|
138
|
+
class BatchSuspend < TheGrid::Api::Command
|
139
|
+
def configure(relation, params)
|
140
|
+
ids = params[:item_ids].reject{ |id| id.to_i <= 0 }
|
141
|
+
raise ArgumentError, "There is nothing to update" if ids.blank?
|
142
|
+
{ :item_ids => ids }
|
143
|
+
end
|
144
|
+
|
145
|
+
def run_on(relation, params)
|
146
|
+
relation.where(relation.table.primary_key.in(params[:item_ids])).update_all(:status => 'suspended')
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
151
|
+
For running this command it's also necessarely to update `commands_lookup_scopes`. It can be done inside grid intializer file:
|
152
|
+
```ruby
|
153
|
+
# config/initializers/grid.rb
|
154
|
+
TheGrid.configure do |config|
|
155
|
+
# Specifies scopes for custom commands
|
156
|
+
config.commands_lookup_scopes += %w{ grid_commands }
|
157
|
+
# ....
|
158
|
+
end
|
159
|
+
```
|
160
|
+
Then it will be possible to run:
|
161
|
+
```ruby
|
162
|
+
TheGrid.build_for(Article).run_command!('batch_suspend', :item_ids => params[:id])
|
163
|
+
```
|
164
|
+
Using lookup technique it's possible to override existing commands. Suppose there is a need to customize `batch_update` command to allow non-integer ids:
|
165
|
+
```ruby
|
166
|
+
module GridCommands
|
167
|
+
class BatchUpdate < TheGrid::Api::Command::BatchUpdate
|
168
|
+
def configure(relation, params)
|
169
|
+
items = params[:items].reject{ |item| item['id'].to_s.strip.blank? }
|
170
|
+
raise ArgumentError, "There is nothing to update" if items.blank?
|
171
|
+
{ :items => items }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
## Template Builder
|
178
|
+
|
179
|
+
For Rails based application there is a template builder which does all the stuff under the hood.
|
180
|
+
```ruby
|
181
|
+
# app/views/articles/index.json.grid_builder
|
182
|
+
grid_for @articles, :per_page => 2 do
|
183
|
+
column :title
|
184
|
+
column :description
|
185
|
+
end
|
186
|
+
```
|
187
|
+
Such view is converted into the next json response:
|
188
|
+
```json
|
189
|
+
{
|
190
|
+
"max_page": 3,
|
191
|
+
"items": [
|
192
|
+
{
|
193
|
+
"id": 1,
|
194
|
+
"title": "My test article",
|
195
|
+
"description": "Something interesting"
|
196
|
+
},
|
197
|
+
{
|
198
|
+
"id": 2,
|
199
|
+
"title": "My hidden article",
|
200
|
+
"description": "Something not interesting"
|
201
|
+
}
|
202
|
+
]
|
203
|
+
}
|
204
|
+
```
|
205
|
+
It's possible to format column output by passing block into column declaration:
|
206
|
+
```ruby
|
207
|
+
# app/views/articles/index.json.grid_builder
|
208
|
+
grid_for @articles, :per_page => 2 do
|
209
|
+
column :title
|
210
|
+
column :description
|
211
|
+
column(:created_at){ |article| article.created_at.to_s(:date) }
|
212
|
+
end
|
213
|
+
```
|
214
|
+
Also it's possible to specify extra information for each column (e.g. *editable*, *searchable*, etc):
|
215
|
+
```ruby
|
216
|
+
# app/views/articles/index.json.grid_builder
|
217
|
+
grid_for @articles, :per_page => 2 do
|
218
|
+
column :title, :editable => true, :sortable => true, :an_option => "any extra information"
|
219
|
+
column :description, :editable => true
|
220
|
+
column(:created_at, :editable => true){ |article| article.created_at.to_s(:date) }
|
221
|
+
end
|
222
|
+
```
|
223
|
+
Looks like a mess, don't it? However there are helper's methods which helps to clean up this view:
|
224
|
+
```ruby
|
225
|
+
# app/views/articles/index.json.grid_builder
|
226
|
+
grid_for @articles, :per_page => 2 do
|
227
|
+
editable_columns :title, :description, :created_at
|
228
|
+
sortable_columns :title
|
229
|
+
|
230
|
+
column :title, :an_option => "any extra information"
|
231
|
+
column :description
|
232
|
+
column(:created_at){ |article| article.created_at.to_s(:date) }
|
233
|
+
end
|
234
|
+
```
|
235
|
+
It's possible to specify any features for columns using the next DSL method template: `"#{feature}ble_columns"` (e.g. `visible_columns *columns_list`).
|
236
|
+
`searchable_columns` method is a bit special. It not only marks column with searchable flag but also specifies which columns will be searched when `search` command is run.
|
237
|
+
|
238
|
+
Sometimes it's reasonable to add extra meta information into response:
|
239
|
+
```ruby
|
240
|
+
grid_for @articles, :per_page => 2 do
|
241
|
+
searchable_columns :title, :created_at
|
242
|
+
|
243
|
+
# specify any kind of meta parameter
|
244
|
+
server_time Time.now
|
245
|
+
my_option "Something important for Frontend side"
|
246
|
+
|
247
|
+
column :title
|
248
|
+
column(:created_at){ |r| r.created_at.to_s(:date) }
|
249
|
+
end
|
250
|
+
```
|
251
|
+
Columns meta and extra meta information will be accessible in response only if client specifies non-empty **with_meta** parameter in request.
|
252
|
+
The previous example is converted into:
|
253
|
+
```json
|
254
|
+
{
|
255
|
+
"meta": {
|
256
|
+
"server_time": "2013-03-17 02:11:05 +0200",
|
257
|
+
"my_option": "Something important for Frontend side"
|
258
|
+
},
|
259
|
+
"columns": {
|
260
|
+
"title": {
|
261
|
+
"searchable": true,
|
262
|
+
"editable": true
|
263
|
+
},
|
264
|
+
"created_at": {
|
265
|
+
"searchable": true
|
266
|
+
}
|
267
|
+
},
|
268
|
+
"max_page": 3,
|
269
|
+
"items": [
|
270
|
+
{
|
271
|
+
"id": 1,
|
272
|
+
"title": "My test article",
|
273
|
+
"created_at": "03/17/2013"
|
274
|
+
},
|
275
|
+
{
|
276
|
+
"id": 2,
|
277
|
+
"title": "My hidden article",
|
278
|
+
"created_at": "03/16/2013"
|
279
|
+
}
|
280
|
+
]
|
281
|
+
}
|
282
|
+
```
|
283
|
+
`per_page` option can be omitted. In such cases will be used `params[:per_page]` or default per page value specified inside grid initializer.
|
284
|
+
Sometimes client need to retrieve all records without pagination. So, for disabling pagination just set `per_page` option to `false`. In such cases `max_page` will be omitted in response.
|
285
|
+
|
286
|
+
#### Nested scopes and tree-like structures
|
287
|
+
|
288
|
+
If you need to create tree-like stucture for custom grid view (e.g. complex navigation) you can use `scope_for` declaration:
|
289
|
+
```ruby
|
290
|
+
grid_for @groups, :per_page => 2 do
|
291
|
+
column :name
|
292
|
+
column :is_active do |p|
|
293
|
+
params[:current_id].to_i == p.id
|
294
|
+
end
|
295
|
+
|
296
|
+
scope_for :articles do
|
297
|
+
column :title
|
298
|
+
column :created_at do |a|
|
299
|
+
a.created_at.to_s(:date)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
```
|
304
|
+
This example builds the next response:
|
305
|
+
```json
|
306
|
+
{
|
307
|
+
"max_page": 2,
|
308
|
+
"items": [
|
309
|
+
{
|
310
|
+
"id": 1,
|
311
|
+
"name": "test",
|
312
|
+
"is_active": true,
|
313
|
+
"articles": [
|
314
|
+
{
|
315
|
+
"id": 2,
|
316
|
+
"title": "Something inetresting",
|
317
|
+
"created_at": "03/17/2013"
|
318
|
+
},
|
319
|
+
{
|
320
|
+
"id": 4,
|
321
|
+
"title": "test article",
|
322
|
+
"created_at": "03/14/2013"
|
323
|
+
}
|
324
|
+
]
|
325
|
+
},
|
326
|
+
{
|
327
|
+
"id": 3,
|
328
|
+
"name": "test2",
|
329
|
+
"is_active": false,
|
330
|
+
"articles": [
|
331
|
+
{
|
332
|
+
"id": 3,
|
333
|
+
"title": "test article 2",
|
334
|
+
"created_at": "03/13/2013"
|
335
|
+
}
|
336
|
+
]
|
337
|
+
}
|
338
|
+
]
|
339
|
+
}
|
340
|
+
```
|
341
|
+
If you need to standardize output you can specify `:as` option - the column name for nested grid (e.g. if you specify `:as => :children` then *articles* key will be substituted with *children* key).
|
342
|
+
Also there are 2 conditional options `:unless` and `:if` which accepts lambda or symbol.
|
343
|
+
If you specify symbol as condition will be used column value with such name (in this case it's important that column is defined before scope).
|
344
|
+
If you need some custom logic to detect if scope should be created for such row or not you can pass lambda.
|
345
|
+
|
346
|
+
For example we want to get articles only of active/current group:
|
347
|
+
```ruby
|
348
|
+
grid_for @groups, :per_page => 2 do
|
349
|
+
column :name
|
350
|
+
column :is_active do |p|
|
351
|
+
params[:current_id].to_i == p.id
|
352
|
+
end
|
353
|
+
|
354
|
+
scope_for :articles, :as => :children, :if => :is_active do
|
355
|
+
column :title
|
356
|
+
end
|
357
|
+
end
|
358
|
+
```
|
359
|
+
Or the same with lambda:
|
360
|
+
```ruby
|
361
|
+
grid_for @groups, :per_page => 2 do
|
362
|
+
column :name
|
363
|
+
column :is_active do |p|
|
364
|
+
params[:current_id].to_i == p.id
|
365
|
+
end
|
366
|
+
|
367
|
+
scope_for :articles, :as => :children, :if => lambda{ |group| group.id == params[:current_id].to_i } do
|
368
|
+
column :title
|
369
|
+
end
|
370
|
+
end
|
371
|
+
```
|
372
|
+
This produces the response:
|
373
|
+
```json
|
374
|
+
{
|
375
|
+
"max_page": 2,
|
376
|
+
"items": [
|
377
|
+
{
|
378
|
+
"id": 1,
|
379
|
+
"name": "test",
|
380
|
+
"is_active": true,
|
381
|
+
"children": [
|
382
|
+
{
|
383
|
+
"id": 2,
|
384
|
+
"title": "Something inetresting",
|
385
|
+
"created_at": "03/17/2013"
|
386
|
+
},
|
387
|
+
{
|
388
|
+
"id": 4,
|
389
|
+
"title": "test article",
|
390
|
+
"created_at": "03/14/2013"
|
391
|
+
}
|
392
|
+
]
|
393
|
+
},
|
394
|
+
{
|
395
|
+
"id": 3,
|
396
|
+
"name": "test2",
|
397
|
+
"is_active": false,
|
398
|
+
"children": null
|
399
|
+
}
|
400
|
+
]
|
401
|
+
}
|
402
|
+
```
|
403
|
+
#### Command delegation
|
404
|
+
|
405
|
+
Sometimes there is a need to delegate command processing to nested nested grid. For example, there are groups and articles.
|
406
|
+
You need to display groups sorted by name asc and provide ability to sort articles inside each group by any columns.
|
407
|
+
For such purposes you can use `delegate` declaration:
|
408
|
+
```ruby
|
409
|
+
grid_for @groups, :per_page => 2 do
|
410
|
+
delegate :sort => :articles, :filter => :articles
|
411
|
+
|
412
|
+
column :name
|
413
|
+
column :is_active do |p|
|
414
|
+
params[:current_id].to_i == p.id
|
415
|
+
end
|
416
|
+
|
417
|
+
scope_for :articles, :as => :children, :if => :is_active do
|
418
|
+
column :title
|
419
|
+
end
|
420
|
+
end
|
421
|
+
```
|
422
|
+
## License
|
423
|
+
|
424
|
+
Released under the [MIT License](http://www.opensource.org/licenses/MIT)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,10 @@
|
|
1
|
+
TheGrid.configure do |config|
|
2
|
+
# Specifies scopes for custom commands
|
3
|
+
# config.commands_lookup_scopes += %w{ command_scope_1 command_scope_2 }
|
4
|
+
|
5
|
+
# Default number of items per page for pagination
|
6
|
+
config.default_max_per_page = 25
|
7
|
+
|
8
|
+
# Print json response with new lines and tabs
|
9
|
+
config.prettify_json = ActionView::Base.pretty_print_json
|
10
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api::Command::BatchRemove < Api::Command
|
3
|
+
def configure(relation, params)
|
4
|
+
{}.tap do |o|
|
5
|
+
o[:item_ids] = params.fetch(:item_ids, []).reject{ |id| id.to_i <= 0 }
|
6
|
+
raise ArgumentError, "There is nothing to remove" if o[:item_ids].blank?
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def run_on(relation, params)
|
11
|
+
relation.where(relation.scoped.table.primary_key.in(params[:item_ids])).destroy_all
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api::Command::BatchUpdate < Api::Command
|
3
|
+
def configure(relation, params)
|
4
|
+
{}.tap do |o|
|
5
|
+
o[:items] = params.fetch(:items, []).reject{ |item| item['id'].to_i <= 0 }
|
6
|
+
raise ArgumentError, "There is nothing to update" if o[:items].blank?
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def run_on(relation, params)
|
11
|
+
record_ids = params[:items].map{ |row| row['id'] }
|
12
|
+
primary_key = relation.scoped.table.primary_key
|
13
|
+
records = relation.where(primary_key.in(record_ids)).index_by(&primary_key.name.to_sym)
|
14
|
+
|
15
|
+
params[:items].map do |row|
|
16
|
+
record = records[row['id'].to_i]
|
17
|
+
record.tap{ |r| r.update_attributes(row.except('id')) } unless record.nil?
|
18
|
+
end.compact
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api::Command::Filter < Api::Command
|
3
|
+
def configure(relation, params)
|
4
|
+
params.fetch(:filters, {}).dup
|
5
|
+
end
|
6
|
+
|
7
|
+
def run_on(relation, filters)
|
8
|
+
conditions = build_conditions_for(relation, filters)
|
9
|
+
relation = relation.where(conditions) unless conditions.blank?
|
10
|
+
relation
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def build_conditions_for(relation, filters)
|
16
|
+
conditions = filters.map do |name, filter|
|
17
|
+
if filter.kind_of?(Array)
|
18
|
+
relation.table[name].in(filter)
|
19
|
+
elsif filter.kind_of?(Hash)
|
20
|
+
expr = []
|
21
|
+
expr << relation.table[name].gteq(prepare_value filter, :from) if filter.has_key?(:from)
|
22
|
+
expr << relation.table[name].lteq(prepare_value filter, :to) if filter.has_key?(:to)
|
23
|
+
expr.inject(:and)
|
24
|
+
else
|
25
|
+
relation.table[name].eq(filter)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
conditions.compact.inject(:and)
|
29
|
+
end
|
30
|
+
|
31
|
+
def prepare_value(filter, name)
|
32
|
+
case filter[:type].to_s
|
33
|
+
when 'time'
|
34
|
+
Time.at(Float filter[name])
|
35
|
+
when 'date'
|
36
|
+
filter[name].to_time
|
37
|
+
else
|
38
|
+
filter[name]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api::Command::Paginate < Api::Command
|
3
|
+
cattr_accessor(:default_per_page){ 10 }
|
4
|
+
|
5
|
+
def configure(relation, params)
|
6
|
+
{}.tap do |o|
|
7
|
+
o[:page] = params[:page].to_i
|
8
|
+
o[:page] = 1 if o[:page] <= 0
|
9
|
+
|
10
|
+
o[:per_page] = params[:per_page].to_i
|
11
|
+
o[:per_page] = self.class.default_per_page if o[:per_page] <= 0
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def run_on(relation, params)
|
16
|
+
relation.offset((params[:page] - 1) * params[:per_page]).limit(params[:per_page])
|
17
|
+
end
|
18
|
+
|
19
|
+
def calculate_max_page_for(relation, params)
|
20
|
+
params = configure(relation, params)
|
21
|
+
(relation.except(:limit, :offset).count / params[:per_page].to_f).ceil
|
22
|
+
end
|
23
|
+
|
24
|
+
def contextualize(relation, params)
|
25
|
+
{:max_page => calculate_max_page_for(relation, params)}
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api::Command::Search < Api::Command
|
3
|
+
def configure(relation, params)
|
4
|
+
{}.tap do |o|
|
5
|
+
o[:query] = params.fetch(:query, '').strip
|
6
|
+
o[:searchable_columns] = params[:searchable_columns]
|
7
|
+
o[:search_over] = params[:search_over]
|
8
|
+
o[:search_over] = Hash[o[:search_over].zip] if o[:search_over].kind_of?(Array)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_on(relation, params)
|
13
|
+
if params[:query].blank?
|
14
|
+
relation
|
15
|
+
elsif params[:search_over].present?
|
16
|
+
search_over(relation, params)
|
17
|
+
else
|
18
|
+
relation.where build_conditions_for(relation, params)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def searchable_columns_of(relation)
|
25
|
+
relation.column_names.select do |column_name|
|
26
|
+
relation.columns_hash[column_name].type == :string
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_conditions_for(relation, params)
|
31
|
+
query = "%#{params[:query]}%"
|
32
|
+
(params[:searchable_columns] || searchable_columns_of(relation)).map do |column|
|
33
|
+
relation.table[column].matches(query)
|
34
|
+
end.inject(:or)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_conditions_for_associations_of(relation, params)
|
38
|
+
params[:search_over].each_with_object({}) do |options, conditions|
|
39
|
+
assoc_name, assoc_fields = options
|
40
|
+
assoc = relation.reflections[assoc_name.to_sym]
|
41
|
+
assoc_condition = build_conditions_for(assoc.klass.scoped, params.merge(:searchable_columns => assoc_fields))
|
42
|
+
conditions[assoc] = assoc_condition unless assoc_condition.blank?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def search_over(relation, params)
|
47
|
+
search_relation = relation.where(build_conditions_for(relation, params))
|
48
|
+
assoc_conditions = build_conditions_for_associations_of(relation, params.except(:searchable_columns))
|
49
|
+
matched_row_ids = row_ids_matched_for(search_relation, assoc_conditions)
|
50
|
+
|
51
|
+
conditions = assoc_conditions.values
|
52
|
+
conditions << relation.table.primary_key.in(matched_row_ids) unless matched_row_ids.blank?
|
53
|
+
relation.where(conditions.inject(:or))
|
54
|
+
end
|
55
|
+
|
56
|
+
def row_ids_matched_for(relation, conditions)
|
57
|
+
conditions.flat_map do |assoc, condition|
|
58
|
+
query = join_relations_with(condition, relation, assoc).to_sql
|
59
|
+
relation.connection.select_all(query).map(&:values)
|
60
|
+
end.uniq
|
61
|
+
end
|
62
|
+
|
63
|
+
def join_relations_with(condition, relation, assoc)
|
64
|
+
primary_key, foreign_key = relationship_between(relation, assoc)
|
65
|
+
|
66
|
+
relation.select(relation.table.primary_key).
|
67
|
+
where(assoc.klass.arel_table.primary_key.eq(nil)).
|
68
|
+
joins("LEFT OUTER JOIN #{assoc.table_name} ON #{primary_key.eq(foreign_key).and(condition).to_sql}")
|
69
|
+
end
|
70
|
+
|
71
|
+
def relationship_between(relation, assoc)
|
72
|
+
case assoc.macro
|
73
|
+
when :belongs_to, :has_one
|
74
|
+
primary_key = assoc.klass.arel_table.primary_key
|
75
|
+
foreign_key = relation.table[assoc.foreign_key]
|
76
|
+
when :has_many
|
77
|
+
primary_key = relation.table.primary_key
|
78
|
+
foreign_key = assoc.klass.arel_table[assoc.foreign_key]
|
79
|
+
else
|
80
|
+
raise ArgumentError, "Unable to search over #{assoc.macro}"
|
81
|
+
end
|
82
|
+
[ primary_key, foreign_key ]
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TheGrid
|
2
|
+
class Api::Command::Sort < Api::Command
|
3
|
+
def configure(relation, params)
|
4
|
+
{}.tap do |o|
|
5
|
+
o[:field] = params[:field]
|
6
|
+
o[:field] = "#{relation.table_name}.#{o[:field]}" if relation.table[o[:field]].present?
|
7
|
+
|
8
|
+
o[:order] = params[:order]
|
9
|
+
o[:order] = 'asc' unless %w{ asc desc }.include?(o[:order])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def run_on(relation, params)
|
14
|
+
relation.order("#{params[:field]} #{params[:order]}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|