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 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
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+ gem 'cover_me', '>= 1.2.0', :group => :test
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,11 @@
1
+ module TheGrid
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../templates", __FILE__)
5
+
6
+ def copy_initializer
7
+ template "the_grid.rb", "config/initializers/the_grid.rb"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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