trk_datatables 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +132 -5
- data/lib/trk_datatables/active_record.rb +5 -1
- data/lib/trk_datatables/column_key_options.rb +31 -10
- data/lib/trk_datatables/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ccb77ab8bb5b29b22c346044bdb06171bed27a50721873cd1d5ec619c8ef1f1
|
4
|
+
data.tar.gz: 0d747920ac2dd280c0274dd46af82c59afd7c55151910e598d97d64ac2447a6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2ce8344fdb8912d0116b5c21695b62cc83292f91a2987917ac78b3ff30c45793be8634b46354d9bb50e5181fa30469d4dcfddf4ed8de64deaba938eaaac29d4
|
7
|
+
data.tar.gz: a4e65f172f9916df7b79fcfc9d084975564623f915dacbfef017521dd8c7a66a5b54a885fab14362b049a6f43b9f30caa1f7e2e4d13e4b16aaed0e145a40dc86
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -245,11 +245,13 @@ For specific columns you can use following keys
|
|
245
245
|
* `title: 'My Title'` set up column header
|
246
246
|
* `search: false` disable searching for this column
|
247
247
|
* `order: false` disable ordering for this column
|
248
|
-
* `predefined_ranges: {}` for datetime fiels add ranges to pick up from
|
249
248
|
* `select_options: Post.statuses` generate select box instead of text input
|
249
|
+
* `predefined_ranges: {}` for datetime fiels add ranges to pick up from
|
250
250
|
* `hide: true` hide column with display none
|
251
251
|
* `class_name: 'Admin::User'` use different class name than
|
252
252
|
`table_name.classify` (in this case of `admin_users` will be `AdminUser`)
|
253
|
+
* `column_type_in_db` one of the: `:string`, `:integer`, `:date`, `:datetime`,
|
254
|
+
`:boolean`
|
253
255
|
|
254
256
|
### Column 'BETWEEN' search with js daterangepicker
|
255
257
|
|
@@ -372,9 +374,10 @@ use empty column_key
|
|
372
374
|
|
373
375
|
If you have more columns that are not actually columns in database (for example
|
374
376
|
links or Ruby calculated values) than you can not use empty column_key since
|
375
|
-
there could be only one in hash. When you disable
|
376
|
-
can use any column name since that column will not
|
377
|
-
example
|
377
|
+
there could be only one (keys in the hash should be unique). When you disable
|
378
|
+
`order` and `search` than you can use any column name since that column will not
|
379
|
+
be used in queries. For example column key `posts.body_size` is not in database
|
380
|
+
nor in Ruby code.
|
378
381
|
|
379
382
|
```
|
380
383
|
def columns
|
@@ -396,7 +399,131 @@ example this column key `posts.body_size` is not in database nor in Ruby code.
|
|
396
399
|
|
397
400
|
### Values calculated in database
|
398
401
|
|
399
|
-
|
402
|
+
There are three types of calculated values (new custom fields that are
|
403
|
+
generated based on other columns):
|
404
|
+
* simple calculations like `SELECT *, quantity * price as full_price`
|
405
|
+
* subqueries like `SELECT *, (SELECT COUNT(*) FROM posts WHERE users.id =
|
406
|
+
posts.user_id) AS posts_count`
|
407
|
+
* aggregate functions in joins/group_by `SELECT users.*, COUNT(posts.id) AS
|
408
|
+
posts_count FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
|
409
|
+
"users"."id" GROUP BY users.id`
|
410
|
+
|
411
|
+
Since in SQL you can not use aggregate functions in WHERE (we can repeat
|
412
|
+
calculation and subqueries), currently TrkDatatables does not support using
|
413
|
+
aggregate functions since it requires implementation of `HAVING` (unless you
|
414
|
+
disable search and order `'users.posts_count': { search: false, order: false }`).
|
415
|
+
You can use concatenation aggregate function: in postgres `STRING_AGG`, in mysql
|
416
|
+
`GROUP_CONCAT` since we search in real columns and show concatenation (we do not
|
417
|
+
search concatenation like we need to search new field `post_count`).
|
418
|
+
|
419
|
+
Simple calculations and subqueries works fine, just you have to use public
|
420
|
+
method to define calculation (that method is also used in filtering). Name of
|
421
|
+
method is the same as column name `title_and_body` or `comments_count`. For table
|
422
|
+
name you should use one of:
|
423
|
+
`:string_calculated_in_db`, `:integer_calculated_in_db`,
|
424
|
+
`:date_calculated_in_db`, `:datetime_calculated_in_db` or
|
425
|
+
`:boolean_calculated_in_db`.
|
426
|
+
|
427
|
+
Before we give an example there is an issue in calling `all.count` if you are
|
428
|
+
using subquery so we need to patch ActiveRecord
|
429
|
+
```
|
430
|
+
# config/initializers/active_record_group_count.rb
|
431
|
+
# When you are using subquery or joins/group_by than all.count does not work
|
432
|
+
# so we need to wrap sql in returns_count_sum
|
433
|
+
# all.returns_count_sum.count # => 123
|
434
|
+
# https://stackoverflow.com/a/21031792/287166
|
435
|
+
# https://github.com/mrbrdo/active_record_group_count/blob/master/lib/active_record_group_count/scope.rb
|
436
|
+
module ActiveRecordGroupCount
|
437
|
+
module Scope
|
438
|
+
extend ActiveSupport::Concern
|
439
|
+
|
440
|
+
module ExtensionMethods
|
441
|
+
def count(*_args)
|
442
|
+
scope = except(:select).select('1')
|
443
|
+
scope_sql = if scope.klass.connection.respond_to?(:unprepared_statement)
|
444
|
+
scope.klass.connection.unprepared_statement { scope.to_sql }
|
445
|
+
else
|
446
|
+
scope.to_sql
|
447
|
+
end
|
448
|
+
query = "SELECT count(*) AS count_all FROM (#{scope_sql}) x"
|
449
|
+
first_result = ActiveRecord::Base.connection.execute(query).first
|
450
|
+
if first_result.is_a? Array
|
451
|
+
first_result.first
|
452
|
+
else
|
453
|
+
first_result['count_all']
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
module ClassMethods
|
459
|
+
def returns_count_sum
|
460
|
+
all.extending(ExtensionMethods)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
# https://github.com/mrbrdo/active_record_group_count/blob/master/lib/active_record_group_count/railtie.rb
|
466
|
+
ActiveSupport.on_load :active_record do
|
467
|
+
include ActiveRecordGroupCount::Scope
|
468
|
+
end
|
469
|
+
```
|
470
|
+
|
471
|
+
So now we can use `all_items.returns_count_sum.count`. Here is example of simple
|
472
|
+
calulation and subquery
|
473
|
+
|
474
|
+
```
|
475
|
+
class MostLikedPostsDatatable < TrkDatatables::ActiveRecord
|
476
|
+
def columns
|
477
|
+
{
|
478
|
+
'posts.id': {},
|
479
|
+
'string_calculated_in_db.title_and_body': {},
|
480
|
+
'integer_calculated_in_db.comments_count': {},
|
481
|
+
}
|
482
|
+
end
|
483
|
+
|
484
|
+
def all_items
|
485
|
+
Post.select(%(
|
486
|
+
posts.*,
|
487
|
+
#{title_and_body} AS title_and_body,
|
488
|
+
(#{comments_count}) AS comments_count
|
489
|
+
))
|
490
|
+
end
|
491
|
+
|
492
|
+
def title_and_body
|
493
|
+
"concat(posts.title, ' ', posts.body)"
|
494
|
+
end
|
495
|
+
|
496
|
+
def comments_count
|
497
|
+
<<~SQL
|
498
|
+
(SELECT COUNT(*) FROM comments
|
499
|
+
WHERE comments.post_id = posts.id)
|
500
|
+
SQL
|
501
|
+
end
|
502
|
+
|
503
|
+
def all_items_count
|
504
|
+
all_items.returns_count_sum.count
|
505
|
+
end
|
506
|
+
|
507
|
+
def filtered_items_count
|
508
|
+
filtered_items.returns_count_sum.count
|
509
|
+
end
|
510
|
+
|
511
|
+
def rows(filtered)
|
512
|
+
# you can use @view.link_to and other helpers
|
513
|
+
filtered.map do |post|
|
514
|
+
[
|
515
|
+
@view.link_to(post.id, post),
|
516
|
+
post.title_and_body,
|
517
|
+
post.comments_count,
|
518
|
+
]
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def default_order
|
523
|
+
[[2, :desc]]
|
524
|
+
end
|
525
|
+
end
|
526
|
+
```
|
400
527
|
|
401
528
|
### Default order and page length
|
402
529
|
|
@@ -121,7 +121,11 @@ module TrkDatatables
|
|
121
121
|
end
|
122
122
|
|
123
123
|
def _arel_column(column_key_option)
|
124
|
-
column_key_option[:table_class]
|
124
|
+
if column_key_option[:table_class] < TrkDatatables::CalculatedInDb
|
125
|
+
Arel.sql send(column_key_option[:column_key])
|
126
|
+
else
|
127
|
+
column_key_option[:table_class].arel_table[column_key_option[:column_name]]
|
128
|
+
end
|
125
129
|
end
|
126
130
|
end
|
127
131
|
end
|
@@ -1,5 +1,18 @@
|
|
1
1
|
module TrkDatatables
|
2
2
|
# rubocop:disable Metrics/ClassLength
|
3
|
+
class CalculatedInDb
|
4
|
+
def self.human_attribute_name(column_name)
|
5
|
+
column_name
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.determine_db_type_for_column
|
9
|
+
# converts TrkDatatables::IntegerCalculatedInDb to :integer
|
10
|
+
name.sub('TrkDatatables::', '').sub('CalculatedInDb', '').downcase.to_sym
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class StringCalculatedInDb < CalculatedInDb; end
|
14
|
+
class IntegerCalculatedInDb < CalculatedInDb; end
|
15
|
+
|
3
16
|
class ColumnKeyOptions
|
4
17
|
include Enumerable
|
5
18
|
|
@@ -12,11 +25,10 @@ module TrkDatatables
|
|
12
25
|
# {
|
13
26
|
# 'users.name': { search: false }
|
14
27
|
# }
|
28
|
+
TITLE_OPTION = :title
|
15
29
|
SEARCH_OPTION = :search
|
16
30
|
ORDER_OPTION = :order
|
17
|
-
TITLE_OPTION = :title
|
18
31
|
SELECT_OPTIONS = :select_options
|
19
|
-
CHECKBOX_OPTION = :checkbox_option
|
20
32
|
PREDEFINED_RANGES = :predefined_ranges
|
21
33
|
HIDE_OPTION = :hide
|
22
34
|
CLASS_NAME = :class_name
|
@@ -26,15 +38,16 @@ module TrkDatatables
|
|
26
38
|
# SEARCH_OPTION_DATETIME_VALUE = :datetime
|
27
39
|
COLUMN_OPTIONS = [
|
28
40
|
SEARCH_OPTION, ORDER_OPTION, TITLE_OPTION, SELECT_OPTIONS,
|
29
|
-
|
41
|
+
PREDEFINED_RANGES, HIDE_OPTION, CLASS_NAME,
|
30
42
|
COLUMN_TYPE_IN_DB
|
31
43
|
].freeze
|
32
44
|
|
33
45
|
# for columns that as calculated in db query
|
34
|
-
STRING_CALCULATED_IN_DB = :
|
46
|
+
STRING_CALCULATED_IN_DB = :string_calculated_in_db
|
35
47
|
INTEGER_CALCULATED_IN_DB = :integer_calculated_in_db
|
36
48
|
DATE_CALCULATED_IN_DB = :date_calculated_in_db
|
37
|
-
DATETIME_CALCULATED_IN_DB = :
|
49
|
+
DATETIME_CALCULATED_IN_DB = :datetime_calculated_in_db
|
50
|
+
BOOLEAN_CALCULATED_IN_DB = :boolean_calculated_in_db
|
38
51
|
|
39
52
|
# for 'columns' that are calculated in Ruby you need to disable search and
|
40
53
|
# order and than it will not be used in queries
|
@@ -90,7 +103,12 @@ module TrkDatatables
|
|
90
103
|
|
91
104
|
column_options.assert_valid_keys(*COLUMN_OPTIONS)
|
92
105
|
table_name, column_name = column_key.to_s.split '.'
|
93
|
-
|
106
|
+
if table_name.present? && table_name.ends_with?('_calculated_in_db')
|
107
|
+
# in calculated columns table_name is used only to determine type
|
108
|
+
column_key = column_name
|
109
|
+
elsif table_name.present? && column_name.nil?
|
110
|
+
raise Error, 'Unless table name ends with _calculad_in_db, column key needs to have one dot for example: posts.name'
|
111
|
+
end
|
94
112
|
|
95
113
|
if table_name.blank?
|
96
114
|
column_name = column_options[TITLE_OPTION] || 'actions' # some default name for a title
|
@@ -101,7 +119,6 @@ module TrkDatatables
|
|
101
119
|
|
102
120
|
unless column_options[SEARCH_OPTION] == false && column_options[ORDER_OPTION] == false
|
103
121
|
column_type_in_db = column_options[COLUMN_TYPE_IN_DB] || _determine_db_type_for_column(table_class, column_name)
|
104
|
-
column_options[CHECKBOX_OPTION] = true if column_type_in_db == :boolean && column_options[CHECKBOX_OPTION].nil?
|
105
122
|
end
|
106
123
|
end
|
107
124
|
arr << {
|
@@ -120,11 +137,13 @@ module TrkDatatables
|
|
120
137
|
def _determine_table_class(table_name, class_name = nil)
|
121
138
|
# post_users -> PostUser
|
122
139
|
# but also check if admin_posts -> Admin::Post so user can defined
|
123
|
-
# class_name: 'Admin::
|
140
|
+
# class_name: 'Admin::Post'
|
124
141
|
# https://github.com/duleorlovic/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L289
|
125
142
|
# note that when class is not eager loaded than const_defined? returns false
|
126
143
|
if class_name.present?
|
127
144
|
class_name.constantize
|
145
|
+
elsif table_name.ends_with? '_calculated_in_db'
|
146
|
+
"TrkDatatables::#{table_name.classify}".constantize
|
128
147
|
else
|
129
148
|
table_name.classify.constantize
|
130
149
|
end
|
@@ -157,7 +176,9 @@ module TrkDatatables
|
|
157
176
|
# @return
|
158
177
|
# :string, :integer, :date, :datetime
|
159
178
|
def _determine_db_type_for_column(table_class, column_name)
|
160
|
-
if
|
179
|
+
if table_class < TrkDatatables::CalculatedInDb
|
180
|
+
table_class.determine_db_type_for_column
|
181
|
+
elsif defined?(::ActiveRecord::Base)
|
161
182
|
ar_column = table_class.columns_hash[column_name]
|
162
183
|
raise Error, "Can't find column #{column_name} in #{table_class.name}" unless ar_column
|
163
184
|
|
@@ -214,7 +235,7 @@ module TrkDatatables
|
|
214
235
|
res['data-searchable'] = false if column_options[SEARCH_OPTION] == false
|
215
236
|
res['data-orderable'] = false if column_options[ORDER_OPTION] == false
|
216
237
|
res['data-datatable-hidden-column'] = true if column_options[HIDE_OPTION] == true
|
217
|
-
res['data-datatable-checkbox'] = true if
|
238
|
+
res['data-datatable-checkbox'] = true if column_type_in_db == :boolean
|
218
239
|
if %i[date datetime].include? column_type_in_db
|
219
240
|
res['data-datatable-range'] = column_type_in_db == :datetime ? :datetime : true
|
220
241
|
if column_options[PREDEFINED_RANGES].present? ||
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trk_datatables
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dusan Orlovic
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|