paginated 1.0.8 → 1.0.9
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 +4 -4
- data/README.md +22 -5
- data/lib/{grape_extensions → grape}/dsl.rb +5 -4
- data/lib/grape-paginated/args.rb +63 -0
- data/lib/grape-paginated/collection.rb +58 -0
- data/lib/grape-paginated/configuration.rb +20 -0
- data/lib/grape-paginated/search.rb +99 -0
- data/lib/grape-paginated/sorting.rb +12 -0
- data/lib/grape-paginated/spreadsheet.rb +69 -0
- data/lib/paginated.rb +12 -12
- metadata +25 -23
- data/lib/paginated/args_andling.rb +0 -63
- data/lib/paginated/collection.rb +0 -58
- data/lib/paginated/search.rb +0 -99
- data/lib/paginated/sorting.rb +0 -12
- data/lib/paginated/spreadsheet.rb +0 -61
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9bcce808539f59666e9f47ac429c4fb081f8d1ead5152ceb9cf33ba7bcb1e00
|
4
|
+
data.tar.gz: 665b4534980a20527920130fb5b9593517f82ce0a7d30de05c0bbd756fa6f76a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a680390d0db51559a570dc52d373b6388dd9487bd179a572f75224b9d2b6d036e16cae112ea8d07478890926f6b01b38db3d8ccbcf7e3952939f21abb4546b13
|
7
|
+
data.tar.gz: 0a6e40dd565c4d6b7887bf7174bd6984b087f09785ea049ff67a95c3a36732bfd3b2516309e4b58a47d2647d1f2d8644746ec7939a08911e869940b5a0f3921c
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
# Paginated
|
1
|
+
# Grape Paginated
|
2
|
+
[](https://badge.fury.io/rb/paginated)
|
2
3
|
|
3
|
-
Гем для пагинации и фильтрации
|
4
|
+
Гем для пагинации и фильтрации записей с возможностью формирования электронных таблиц (XLSX) на базе Grape.
|
4
5
|
|
5
6
|
## Установка
|
6
7
|
|
@@ -24,9 +25,12 @@ bundle
|
|
24
25
|
Например:
|
25
26
|
|
26
27
|
```
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
get 'users' do
|
29
|
+
paginated model: User,
|
30
|
+
entity: UserEntity,
|
31
|
+
search: %w[email name|ilike role|custom.for_role]
|
32
|
+
end
|
33
|
+
|
30
34
|
```
|
31
35
|
|
32
36
|
Опции (с примерами):
|
@@ -70,3 +74,16 @@ paginated model: User,
|
|
70
74
|
- `sort_order` направление, по которому должна осуществляться сортировка (`asc/desc`).
|
71
75
|
|
72
76
|
По умолчанию сортировка осуществляется по `id` записей в направлении `DESC`.
|
77
|
+
|
78
|
+
## Конфигурация
|
79
|
+
|
80
|
+
Добавьте файл конфигурации `config/initializers/grape_paginated.rb` с содержимым:
|
81
|
+
|
82
|
+
```
|
83
|
+
GrapePaginated::Configuration.configure do |config|
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
Доступные опции для конфигурирования:
|
88
|
+
|
89
|
+
`spreadsheet_limit` - максимальный размер формируемой электронной таблицы. По умолчанию отстутствует.
|
@@ -39,15 +39,16 @@ module Grape
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def paginated_spreadsheet(opts)
|
42
|
-
Dir.mktmpdir do |
|
43
|
-
|
42
|
+
Dir.mktmpdir do |dir|
|
43
|
+
opts[:tempdir] = dir
|
44
|
+
file_path = Paginated.spreadsheet(**opts)
|
44
45
|
|
45
46
|
spreadsheet_file_headers File.basename(file_path)
|
46
|
-
File.
|
47
|
+
File.read(file_path)
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
|
-
def spreadsheet_file_headers(filename)
|
51
|
+
def spreadsheet_file_headers(filename)
|
51
52
|
header['Content-Type'] =
|
52
53
|
case File.extname(filename)
|
53
54
|
when '.csv'
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module GrapePaginated
|
2
|
+
module Args
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def handle_args(**args)
|
7
|
+
# обрабатываемая Rails модель
|
8
|
+
@model = args[:model]
|
9
|
+
|
10
|
+
# параметры запроса и текущий пользователь
|
11
|
+
@params = args[:params].stringify_keys
|
12
|
+
@current_user = args[:current_user]
|
13
|
+
|
14
|
+
# поля, по которым возможен поиск
|
15
|
+
@search_fields = args[:search]
|
16
|
+
|
17
|
+
# сериализация - через Grape Entity или список полей
|
18
|
+
@grape_entity = args[:entity]
|
19
|
+
@fields = args[:fields]
|
20
|
+
|
21
|
+
# Rails scopes для применения
|
22
|
+
@scopes = args[:scopes] || {}
|
23
|
+
|
24
|
+
# offset / limit
|
25
|
+
@offset = @params['offset'].to_i
|
26
|
+
@limit = @params['limit'] || 20
|
27
|
+
|
28
|
+
# требуемый список полей для коллекции
|
29
|
+
@only_columns = args[:only_columns]
|
30
|
+
|
31
|
+
# список полей для эл. таблицы
|
32
|
+
@spreadsheet_columns = args[:spreadsheet_columns]
|
33
|
+
|
34
|
+
# сортировка
|
35
|
+
@sort_by = @params['sort_by'] || :id
|
36
|
+
@sort_order = @params['sort_order'] || :desc
|
37
|
+
|
38
|
+
# подсчет кол-ва записей
|
39
|
+
@objects_count = records_count
|
40
|
+
|
41
|
+
# временная директория (для генерации файлов)
|
42
|
+
@tempdir = args[:tempdir]
|
43
|
+
end
|
44
|
+
|
45
|
+
def records_count
|
46
|
+
# получение кол-ва записей из кеша
|
47
|
+
cache_key = "#{@model}_records_count"
|
48
|
+
cached = Rails.cache.read(cache_key)
|
49
|
+
return cached if cached
|
50
|
+
|
51
|
+
# получение кол-ва записей из запроса к БД
|
52
|
+
count = @model.merge(@scopes).count(:id)
|
53
|
+
|
54
|
+
# кеширование кол-ва записей при превышении порогового значения
|
55
|
+
if count >= 1_000
|
56
|
+
Rails.cache.write(cache_key, count, expires_in: 30.minutes)
|
57
|
+
end
|
58
|
+
|
59
|
+
count
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module GrapePaginated
|
2
|
+
module Collection
|
3
|
+
|
4
|
+
def collection
|
5
|
+
if @objects_count > 0
|
6
|
+
search
|
7
|
+
serialize
|
8
|
+
else
|
9
|
+
@objects = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# результат с пагинацией
|
13
|
+
{ count: @objects_count, objects: @objects }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def serialize
|
19
|
+
# коллекция записей ActiveRecord для применения аггрегирования
|
20
|
+
list = @objects || @model.unscoped.merge(@scopes)
|
21
|
+
|
22
|
+
# применение сортировки и пагинации к коллекции записей
|
23
|
+
@objects = list.offset(@offset).merge(sort_proc).limit(@limit)
|
24
|
+
|
25
|
+
# сериализация с помощью переданных полей или сериалайзера
|
26
|
+
if @fields
|
27
|
+
serialize_with_fields
|
28
|
+
elsif @grape_entity
|
29
|
+
serialize_with_entity
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def serialize_with_fields
|
34
|
+
# добавление ID в список полей по умолчанию
|
35
|
+
@fields.push(:id)
|
36
|
+
|
37
|
+
# список массивов значений
|
38
|
+
@objects = @objects.map { |i| obtain_fields_values(i) }
|
39
|
+
|
40
|
+
# трансформация массивов значений в объекты (ключ-значение)
|
41
|
+
@objects = @objects.map { |i| @fields.zip(i).to_h }
|
42
|
+
end
|
43
|
+
|
44
|
+
def serialize_with_entity
|
45
|
+
opts = { current_user: @current_user }
|
46
|
+
|
47
|
+
# требуемый список полей (если был передан)
|
48
|
+
opts[:only] = @only_columns if @only_columns
|
49
|
+
|
50
|
+
@objects = @objects.map { |i| @grape_entity.represent(i, opts).as_json }
|
51
|
+
end
|
52
|
+
|
53
|
+
def obtain_fields_values(record)
|
54
|
+
@fields.map { |i| record.send(i) }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GrapePaginated
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
attr_accessor :spreadsheet_limit
|
5
|
+
|
6
|
+
def spreadsheet_limit
|
7
|
+
@spreadsheet_limit || Float::INFINITY
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configure
|
11
|
+
yield(config)
|
12
|
+
config
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.config
|
16
|
+
@config ||= Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module GrapePaginated
|
2
|
+
module Search
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def search
|
7
|
+
@search_fields&.each do |field|
|
8
|
+
# коллекция записей для дальнейшей фильтрации
|
9
|
+
list = @objects || @model.unscoped.merge(@scopes)
|
10
|
+
|
11
|
+
# оператор и значение для поиска
|
12
|
+
operator, value = search_operands(field)
|
13
|
+
|
14
|
+
# пропускаем, если параметр для поиска не передан
|
15
|
+
next if value.blank? || value == 'null'
|
16
|
+
|
17
|
+
# кастомный поиск
|
18
|
+
if operator.include?('custom') && value.present?
|
19
|
+
# название скоупа из модели, который нужно применить
|
20
|
+
scope_name = operator.split('.')[1]
|
21
|
+
# применение скоупа
|
22
|
+
@objects = list.send(scope_name, value)
|
23
|
+
@objects_count = @objects.count
|
24
|
+
next
|
25
|
+
end
|
26
|
+
|
27
|
+
# форматирование кавычек
|
28
|
+
if operator != 'IN' && !@model.defined_enums.key?(field)
|
29
|
+
value = quotations_formatting(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
if field.include?('.')
|
33
|
+
# нужно приджойнить таблицу
|
34
|
+
join_assoc, field = field.split('.')
|
35
|
+
table = join_assoc.tableize
|
36
|
+
list = list.joins(join_assoc.to_sym)
|
37
|
+
else
|
38
|
+
table = @model.table_name
|
39
|
+
field = field.split('|')[0]
|
40
|
+
end
|
41
|
+
|
42
|
+
query = where_query(table, field, operator, value)
|
43
|
+
@objects = list.where(query)
|
44
|
+
@objects_count = @objects.count
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def search_operands(field)
|
49
|
+
value = default_param_value(field)
|
50
|
+
|
51
|
+
# WHERE query operator
|
52
|
+
operator = field.split('|').size > 1 ? field.split('|').last : '='
|
53
|
+
|
54
|
+
# условие для поиска ILIKE
|
55
|
+
value = "%#{value}%" if operator == 'ilike' && value.present?
|
56
|
+
|
57
|
+
# проверка мульти-поиска по параметру с массивом
|
58
|
+
if operator == 'multi' && !value.blank?
|
59
|
+
operator = 'IN'
|
60
|
+
value = "(#{value.join(', ')})"
|
61
|
+
end
|
62
|
+
|
63
|
+
# проверка диапазона (MIN/MAX)
|
64
|
+
if %w[min max].include?(operator)
|
65
|
+
range_field = field.split('.').last.split(':')[0].try { |i| i.split('|')[0] }
|
66
|
+
value = @params["#{range_field}_#{operator}"]
|
67
|
+
operator = operator == 'min' ? '>=' : '<='
|
68
|
+
end
|
69
|
+
|
70
|
+
# check array operator
|
71
|
+
if operator == 'array' && value.present?
|
72
|
+
operator = '='
|
73
|
+
value = "{#{value}}"
|
74
|
+
end
|
75
|
+
|
76
|
+
[operator, value]
|
77
|
+
end
|
78
|
+
|
79
|
+
def default_param_value(field)
|
80
|
+
# название ключа-поля без оператора (напр. ilike)
|
81
|
+
key = field.split('|')[0]
|
82
|
+
|
83
|
+
@params[key]
|
84
|
+
end
|
85
|
+
|
86
|
+
def where_query(table, field, operator, value)
|
87
|
+
"#{table}.#{field} #{operator} #{value}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def quotations_formatting(string)
|
91
|
+
# удаление одинарных кавычек (напр. внутри строки)
|
92
|
+
value = string.to_s.gsub("'", '')
|
93
|
+
|
94
|
+
# добавление одинарных кавычек вокруг строки
|
95
|
+
"'#{value}'"
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'fast_excel'
|
2
|
+
|
3
|
+
module GrapePaginated
|
4
|
+
module Spreadsheet
|
5
|
+
|
6
|
+
def spreadsheet
|
7
|
+
# поиск и фильтрация
|
8
|
+
search
|
9
|
+
|
10
|
+
# коллекция записей ActiveRecord для формирования эл. таблицы
|
11
|
+
records = @objects || @model.unscoped.merge(@scopes)
|
12
|
+
|
13
|
+
# exception в случае превышения лимита записей
|
14
|
+
raise StandardError.new('Кол-во записей превышает заданный лимит') if records.count > spreadsheet_limit
|
15
|
+
|
16
|
+
# формирования файла эл. таблицы
|
17
|
+
generate_spreadsheet(records)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def spreadsheet_limit
|
23
|
+
GrapePaginated::Configuration.config.spreadsheet_limit
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_spreadsheet(records)
|
27
|
+
filepath = "#{@tempdir}/#{Time.now.strftime('%Y%m%d_%H%M%S')}.xlsx"
|
28
|
+
|
29
|
+
# создание таблицы и ее конфигурация
|
30
|
+
workbook = FastExcel.open(filepath, constant_memory: true)
|
31
|
+
workbook.default_format.set(
|
32
|
+
font_size: 0,
|
33
|
+
font_family: 'Arial'
|
34
|
+
)
|
35
|
+
|
36
|
+
# стили
|
37
|
+
bold = workbook.bold_format
|
38
|
+
|
39
|
+
# лист
|
40
|
+
worksheet = workbook.add_worksheet('Основной')
|
41
|
+
worksheet.auto_width = true
|
42
|
+
|
43
|
+
# заголовки
|
44
|
+
worksheet.append_row(spreadsheet_titles, bold)
|
45
|
+
|
46
|
+
# содержимое
|
47
|
+
cols = spreadsheet_columns
|
48
|
+
records.order(:id).each do |record|
|
49
|
+
values = cols.map { |col| record.send(col) }
|
50
|
+
worksheet.append_row(values)
|
51
|
+
end
|
52
|
+
|
53
|
+
# закрытие/сохранение файла
|
54
|
+
workbook.close
|
55
|
+
|
56
|
+
# путь до файла для дальнейшей обработки
|
57
|
+
filepath
|
58
|
+
end
|
59
|
+
|
60
|
+
def spreadsheet_titles
|
61
|
+
@spreadsheet_columns.map { |i| i['title'] }
|
62
|
+
end
|
63
|
+
|
64
|
+
def spreadsheet_columns
|
65
|
+
@spreadsheet_columns.map { |i| i['column'] }
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/paginated.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
|
-
require '
|
2
|
-
require 'paginated/
|
3
|
-
require 'paginated/
|
4
|
-
require 'paginated/
|
5
|
-
require 'paginated/
|
6
|
-
|
7
|
-
require '
|
1
|
+
require 'grape/dsl'
|
2
|
+
require 'grape-paginated/args'
|
3
|
+
require 'grape-paginated/collection'
|
4
|
+
require 'grape-paginated/configuration'
|
5
|
+
require 'grape-paginated/search'
|
6
|
+
require 'grape-paginated/sorting'
|
7
|
+
require 'grape-paginated/spreadsheet'
|
8
8
|
|
9
9
|
class Paginated
|
10
10
|
|
11
|
-
include
|
12
|
-
include Collection
|
13
|
-
include Search
|
14
|
-
include Sorting
|
15
|
-
include Spreadsheet
|
11
|
+
include GrapePaginated::Args
|
12
|
+
include GrapePaginated::Collection
|
13
|
+
include GrapePaginated::Search
|
14
|
+
include GrapePaginated::Sorting
|
15
|
+
include GrapePaginated::Spreadsheet
|
16
16
|
|
17
17
|
def initialize(**args)
|
18
18
|
handle_args(**args)
|
metadata
CHANGED
@@ -1,71 +1,71 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: paginated
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Павел Бабин
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: fast_excel
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.5.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:
|
26
|
+
version: 0.5.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: grape
|
28
|
+
name: grape
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 2.0.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 2.0.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: grape-entity
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 1.0.0
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 1.0.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: rails
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: '7.0'
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: '7.0'
|
69
69
|
description: Простая пагинация записей, поиск и формирование эл. таблиц для API на
|
70
70
|
базе Grape
|
71
71
|
email: babin359@gmail.com
|
@@ -74,17 +74,19 @@ extensions: []
|
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
76
|
- README.md
|
77
|
-
- lib/
|
77
|
+
- lib/grape-paginated/args.rb
|
78
|
+
- lib/grape-paginated/collection.rb
|
79
|
+
- lib/grape-paginated/configuration.rb
|
80
|
+
- lib/grape-paginated/search.rb
|
81
|
+
- lib/grape-paginated/sorting.rb
|
82
|
+
- lib/grape-paginated/spreadsheet.rb
|
83
|
+
- lib/grape/dsl.rb
|
78
84
|
- lib/paginated.rb
|
79
|
-
- lib/paginated/args_andling.rb
|
80
|
-
- lib/paginated/collection.rb
|
81
|
-
- lib/paginated/search.rb
|
82
|
-
- lib/paginated/sorting.rb
|
83
|
-
- lib/paginated/spreadsheet.rb
|
84
85
|
homepage:
|
85
86
|
licenses:
|
86
87
|
- MIT
|
87
|
-
metadata:
|
88
|
+
metadata:
|
89
|
+
rubygems_mfa_required: 'true'
|
88
90
|
post_install_message:
|
89
91
|
rdoc_options: []
|
90
92
|
require_paths:
|
@@ -93,14 +95,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
95
|
requirements:
|
94
96
|
- - ">="
|
95
97
|
- !ruby/object:Gem::Version
|
96
|
-
version: 3.
|
98
|
+
version: 3.1.0
|
97
99
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
100
|
requirements:
|
99
101
|
- - ">="
|
100
102
|
- !ruby/object:Gem::Version
|
101
103
|
version: '0'
|
102
104
|
requirements: []
|
103
|
-
rubygems_version: 3.
|
105
|
+
rubygems_version: 3.4.10
|
104
106
|
signing_key:
|
105
107
|
specification_version: 4
|
106
108
|
summary: Пагинация для API на базе Grape
|
@@ -1,63 +0,0 @@
|
|
1
|
-
module ArgsHandling
|
2
|
-
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
private
|
6
|
-
|
7
|
-
def handle_args(**args)
|
8
|
-
# обрабатываемая Rails модель
|
9
|
-
@model = args[:model]
|
10
|
-
|
11
|
-
# параметры запроса и текущий пользователь
|
12
|
-
@params = args[:params].stringify_keys
|
13
|
-
@current_user = args[:current_user]
|
14
|
-
|
15
|
-
# поля, по которым возможен поиск
|
16
|
-
@search_fields = args[:search]
|
17
|
-
|
18
|
-
# сериализация - через Grape Entity или список полей
|
19
|
-
@grape_entity = args[:entity]
|
20
|
-
@fields = args[:fields]
|
21
|
-
|
22
|
-
# Rails scopes для применения
|
23
|
-
@scopes = args[:scopes] || {}
|
24
|
-
|
25
|
-
# offset / limit
|
26
|
-
@offset = @params['offset'].to_i
|
27
|
-
@limit = @params['limit'] || 20
|
28
|
-
|
29
|
-
# требуемый список полей для коллекции
|
30
|
-
@only_columns = args[:only_columns]
|
31
|
-
|
32
|
-
# список полей для эл. таблицы
|
33
|
-
@spreadsheet_columns = args[:spreadsheet_columns]
|
34
|
-
|
35
|
-
# сортировка
|
36
|
-
@sort_by = @params['sort_by'] || :id
|
37
|
-
@sort_order = @params['sort_order'] || :desc
|
38
|
-
|
39
|
-
# подсчет кол-ва записей
|
40
|
-
@objects_count = records_count
|
41
|
-
|
42
|
-
# временная директория (для генерации файлов)
|
43
|
-
@tempdir = args[:tempdir]
|
44
|
-
end
|
45
|
-
|
46
|
-
def records_count
|
47
|
-
# получение кол-ва записей из кеша
|
48
|
-
cache_key = "#{@model}_records_count"
|
49
|
-
cached = Rails.cache.read(cache_key)
|
50
|
-
return cached if cached
|
51
|
-
|
52
|
-
# получение кол-ва записей из запроса к БД
|
53
|
-
count = @model.merge(@scopes).count(:id)
|
54
|
-
|
55
|
-
# кеширование кол-ва записей при превышении порогового значения
|
56
|
-
if count >= 1_000
|
57
|
-
Rails.cache.write(cache_key, count, expires_in: 30.minutes)
|
58
|
-
end
|
59
|
-
|
60
|
-
count
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|
data/lib/paginated/collection.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
module Collection
|
2
|
-
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
def collection
|
6
|
-
if @objects_count > 0
|
7
|
-
search
|
8
|
-
serialize
|
9
|
-
else
|
10
|
-
@objects = []
|
11
|
-
end
|
12
|
-
|
13
|
-
# результат с пагинацией
|
14
|
-
{ count: @objects_count, objects: @objects }
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def serialize
|
20
|
-
# коллекция записей ActiveRecord для применения аггрегирования
|
21
|
-
list = @objects || @model.unscoped.merge(@scopes)
|
22
|
-
|
23
|
-
# применение сортировки и пагинации к коллекции записей
|
24
|
-
@objects = list.offset(@offset).merge(sort_proc).limit(@limit)
|
25
|
-
|
26
|
-
# сериализация с помощью переданных полей или сериалайзера
|
27
|
-
if @fields
|
28
|
-
serialize_with_fields
|
29
|
-
elsif @grape_entity
|
30
|
-
serialize_with_entity
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def serialize_with_fields
|
35
|
-
# добавление ID в список полей по умолчанию
|
36
|
-
@fields.push(:id)
|
37
|
-
|
38
|
-
# список массивов значений
|
39
|
-
@objects = @objects.map { |i| obtain_fields_values(i) }
|
40
|
-
|
41
|
-
# трансформация массивов значений в объекты (ключ-значение)
|
42
|
-
@objects = @objects.map { |i| @fields.zip(i).to_h }
|
43
|
-
end
|
44
|
-
|
45
|
-
def serialize_with_entity
|
46
|
-
opts = { current_user: @current_user }
|
47
|
-
|
48
|
-
# требуемый список полей (если был передан)
|
49
|
-
opts[:only] = @only_columns if @only_columns
|
50
|
-
|
51
|
-
@objects = @objects.map { |i| @grape_entity.represent(i, opts).as_json }
|
52
|
-
end
|
53
|
-
|
54
|
-
def obtain_fields_values(record)
|
55
|
-
@fields.map { |i| record.send(i) }
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
data/lib/paginated/search.rb
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
module Search
|
2
|
-
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
private
|
6
|
-
|
7
|
-
def search
|
8
|
-
@search_fields&.each do |field|
|
9
|
-
# коллекция записей для дальнейшей фильтрации
|
10
|
-
list = @objects || @model.unscoped.merge(@scopes)
|
11
|
-
|
12
|
-
# оператор и значение для поиска
|
13
|
-
operator, value = search_operands(field)
|
14
|
-
|
15
|
-
# пропускаем, если параметр для поиска не передан
|
16
|
-
next if value.blank? || value == 'null'
|
17
|
-
|
18
|
-
# кастомный поиск
|
19
|
-
if operator.include?('custom') && value.present?
|
20
|
-
# название скоупа из модели, который нужно применить
|
21
|
-
scope_name = operator.split('.')[1]
|
22
|
-
# применение скоупа
|
23
|
-
@objects = list.send(scope_name, value)
|
24
|
-
@objects_count = @objects.count
|
25
|
-
next
|
26
|
-
end
|
27
|
-
|
28
|
-
# форматирование кавычек
|
29
|
-
if operator != 'IN' && !@model.defined_enums.key?(field)
|
30
|
-
value = quotations_formatting(value)
|
31
|
-
end
|
32
|
-
|
33
|
-
if field.include?('.')
|
34
|
-
# нужно приджойнить таблицу
|
35
|
-
join_assoc, field = field.split('.')
|
36
|
-
table = join_assoc.tableize
|
37
|
-
list = list.joins(join_assoc.to_sym)
|
38
|
-
else
|
39
|
-
table = @model.table_name
|
40
|
-
field = field.split('|')[0]
|
41
|
-
end
|
42
|
-
|
43
|
-
query = where_query(table, field, operator, value)
|
44
|
-
@objects = list.where(query)
|
45
|
-
@objects_count = @objects.count
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def search_operands(field)
|
50
|
-
value = default_param_value(field)
|
51
|
-
|
52
|
-
# WHERE query operator
|
53
|
-
operator = field.split('|').size > 1 ? field.split('|').last : '='
|
54
|
-
|
55
|
-
# условие для поиска ILIKE
|
56
|
-
value = "%#{value}%" if operator == 'ilike' && value.present?
|
57
|
-
|
58
|
-
# проверка мульти-поиска по параметру с массивом
|
59
|
-
if operator == 'multi' && !value.blank?
|
60
|
-
operator = 'IN'
|
61
|
-
value = "(#{value.join(', ')})"
|
62
|
-
end
|
63
|
-
|
64
|
-
# проверка диапазона (MIN/MAX)
|
65
|
-
if %w[min max].include?(operator)
|
66
|
-
range_field = field.split('.').last.split(':')[0].try { |i| i.split('|')[0] }
|
67
|
-
value = @params["#{range_field}_#{operator}"]
|
68
|
-
operator = operator == 'min' ? '>=' : '<='
|
69
|
-
end
|
70
|
-
|
71
|
-
# check array operator
|
72
|
-
if operator == 'array' && value.present?
|
73
|
-
operator = '='
|
74
|
-
value = "{#{value}}"
|
75
|
-
end
|
76
|
-
|
77
|
-
[operator, value]
|
78
|
-
end
|
79
|
-
|
80
|
-
def default_param_value(field)
|
81
|
-
# название ключа-поля без оператора (напр. ilike)
|
82
|
-
key = field.split('|')[0]
|
83
|
-
|
84
|
-
@params[key]
|
85
|
-
end
|
86
|
-
|
87
|
-
def where_query(table, field, operator, value)
|
88
|
-
"#{table}.#{field} #{operator} #{value}"
|
89
|
-
end
|
90
|
-
|
91
|
-
def quotations_formatting(string)
|
92
|
-
# удаление одинарных кавычек (напр. внутри строки)
|
93
|
-
value = string.to_s.gsub(/'/, '')
|
94
|
-
|
95
|
-
# добавление одинарных кавычек вокруг строки
|
96
|
-
"'#{value}'"
|
97
|
-
end
|
98
|
-
|
99
|
-
end
|
data/lib/paginated/sorting.rb
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
require 'fast_excel'
|
2
|
-
|
3
|
-
module Spreadsheet
|
4
|
-
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
def spreadsheet
|
8
|
-
search if @objects_count > 0
|
9
|
-
|
10
|
-
# коллекция записей ActiveRecord для формирования эл. таблицы
|
11
|
-
records = @objects || @model.unscoped.merge(@scopes)
|
12
|
-
|
13
|
-
# формирования файла эл. таблицы
|
14
|
-
generate_spreadsheet(records)
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def generate_spreadsheet(records)
|
20
|
-
filepath = "#{@tempdir}/#{Time.now.strftime('%Y%m%d_%H%M%S')}.xlsx"
|
21
|
-
|
22
|
-
# создание таблицы и ее конфигурация
|
23
|
-
workbook = FastExcel.open(filepath, constant_memory: true)
|
24
|
-
workbook.default_format.set(
|
25
|
-
font_size: 0,
|
26
|
-
font_family: 'Arial'
|
27
|
-
)
|
28
|
-
|
29
|
-
# стили
|
30
|
-
bold = workbook.bold_format
|
31
|
-
|
32
|
-
# лист
|
33
|
-
worksheet = workbook.add_worksheet('Основной')
|
34
|
-
worksheet.auto_width = true
|
35
|
-
|
36
|
-
# заголовки
|
37
|
-
worksheet.append_row(spreadsheet_titles, bold)
|
38
|
-
|
39
|
-
# содержимое
|
40
|
-
cols = spreadsheet_columns
|
41
|
-
records.each do |record|
|
42
|
-
values = cols.map { |col| record.send(col) }
|
43
|
-
worksheet.append_row(values)
|
44
|
-
end
|
45
|
-
|
46
|
-
# закрытие/сохранение файла
|
47
|
-
workbook.close
|
48
|
-
|
49
|
-
# путь до файла для дальнейшей обработки
|
50
|
-
filepath
|
51
|
-
end
|
52
|
-
|
53
|
-
def spreadsheet_titles
|
54
|
-
@spreadsheet_columns.map { |i| i['title'] }
|
55
|
-
end
|
56
|
-
|
57
|
-
def spreadsheet_columns
|
58
|
-
@spreadsheet_columns.map { |i| i['column'] }
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|