the_grid 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
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