mincer 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/Gemfile +2 -1
- data/README.md +111 -64
- data/lib/mincer/action_view/sort_helper.rb +7 -7
- data/lib/mincer/base.rb +1 -1
- data/lib/mincer/config.rb +29 -0
- data/lib/mincer/core_ext/string.rb +5 -0
- data/lib/mincer/processors/cache_digest/processor.rb +54 -0
- data/lib/mincer/processors/helpers.rb +15 -0
- data/lib/mincer/processors/pagination/processor.rb +63 -0
- data/lib/mincer/processors/pg_json_dumper/processor.rb +69 -0
- data/lib/mincer/processors/pg_search/processor.rb +148 -0
- data/lib/mincer/processors/pg_search/sanitizer.rb +61 -0
- data/lib/mincer/processors/pg_search/search_engines/array.rb +41 -0
- data/lib/mincer/processors/pg_search/search_engines/base.rb +56 -0
- data/lib/mincer/processors/pg_search/search_engines/fulltext.rb +58 -0
- data/lib/mincer/processors/pg_search/search_engines/trigram.rb +41 -0
- data/lib/mincer/processors/pg_search/search_statement.rb +31 -0
- data/lib/mincer/processors/sorting/processor.rb +113 -0
- data/lib/mincer/version.rb +1 -1
- data/lib/mincer.rb +31 -31
- data/mincer.gemspec +0 -2
- data/spec/lib/mincer/action_view/sort_helper_spec.rb +11 -0
- data/spec/lib/mincer/base_spec.rb +15 -0
- data/spec/lib/mincer/config_spec.rb +7 -0
- data/spec/lib/{processors/cache_digest_spec.rb → mincer/processors/cache_digest/processor_spec.rb} +2 -9
- data/spec/lib/{processors/paginate_spec.rb → mincer/processors/pagination/processor_spec.rb} +43 -17
- data/spec/lib/{processors/pg_json_dumper_spec.rb → mincer/processors/pg_json_dumper/processor_spec.rb} +2 -6
- data/spec/lib/mincer/processors/pg_search/processor_spec.rb +268 -0
- data/spec/lib/mincer/processors/pg_search/sanitizer_spec.rb +38 -0
- data/spec/lib/mincer/processors/pg_search/search_engines/array_spec.rb +83 -0
- data/spec/lib/mincer/processors/pg_search/search_engines/fulltext_spec.rb +101 -0
- data/spec/lib/mincer/processors/pg_search/search_engines/trigram_spec.rb +91 -0
- data/spec/lib/mincer/processors/sorting/processor_spec.rb +181 -0
- data/spec/mincer_config.rb +38 -0
- data/spec/spec_helper.rb +40 -4
- data/spec/support/postgres_adapter.rb +12 -3
- data/spec/support/sqlite3_adapter.rb +3 -0
- metadata +42 -45
- data/lib/mincer/processors/cache_digest.rb +0 -50
- data/lib/mincer/processors/paginate.rb +0 -41
- data/lib/mincer/processors/pg_json_dumper.rb +0 -51
- data/lib/mincer/processors/search.rb +0 -34
- data/lib/mincer/processors/sort.rb +0 -59
- data/spec/lib/processors/search_spec.rb +0 -77
- data/spec/lib/processors/sort_spec.rb +0 -77
@@ -0,0 +1,148 @@
|
|
1
|
+
# This and all processors are heavily influenced by pg_search(https://github.com/Casecommons/pg_search)
|
2
|
+
module Mincer
|
3
|
+
module Processors
|
4
|
+
module PgSearch
|
5
|
+
class Processor
|
6
|
+
include ::Mincer::Processors::Helpers
|
7
|
+
|
8
|
+
def initialize(mincer)
|
9
|
+
@mincer, @args, @relation = mincer, mincer.args, mincer.relation
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply
|
13
|
+
if Mincer.postgres?
|
14
|
+
@relation = apply_pg_search(@relation, @args)
|
15
|
+
else
|
16
|
+
@relation
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def apply_pg_search(relation, args)
|
21
|
+
relation.where(conditions(args)).reorder(rank(args))
|
22
|
+
end
|
23
|
+
|
24
|
+
def conditions(args)
|
25
|
+
search_statements_conditions = search_statements.map do |search_statement|
|
26
|
+
conditions = pg_search_engines(args, search_statement).map do |pg_search_engine|
|
27
|
+
pg_search_engine.conditions
|
28
|
+
end.compact
|
29
|
+
join_expressions(conditions, options[:join_with] || :or)
|
30
|
+
end.compact
|
31
|
+
join_expressions(search_statements_conditions, options[:join_with] || :or).try(:to_sql)
|
32
|
+
end
|
33
|
+
|
34
|
+
def rank(args)
|
35
|
+
search_statements_conditions = search_statements.map do |search_statement|
|
36
|
+
conditions = pg_search_engines(args, search_statement).map do |pg_search_engine|
|
37
|
+
pg_search_engine.rank
|
38
|
+
end.compact
|
39
|
+
join_expressions(conditions, :+)
|
40
|
+
end.compact
|
41
|
+
rank = join_expressions(search_statements_conditions, :+).try(:to_sql)
|
42
|
+
"#{rank} DESC" if rank.present?
|
43
|
+
end
|
44
|
+
|
45
|
+
def pg_search_engines(args, search_statement)
|
46
|
+
Mincer.config.pg_search.engines.map do |engine_class|
|
47
|
+
engine_class.new(args, [search_statement])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def search_statements
|
52
|
+
@search_statements ||= params.any? { |param| param[:columns] } ? search_statements_from_params : default_search_statements
|
53
|
+
end
|
54
|
+
|
55
|
+
def search_statements_from_params
|
56
|
+
params.map do |param|
|
57
|
+
par = param.dup
|
58
|
+
SearchStatement.new(par.delete(:columns), search_statement_default_options(param[:engines]).merge(par))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# We use only text/string columns and avoid array
|
63
|
+
def default_search_statements
|
64
|
+
column_names = @relation.columns.reject do |column|
|
65
|
+
![:string, :text].include?(column.type) || column.array
|
66
|
+
end.map do |column|
|
67
|
+
"#{@relation.table_name}.#{column.name}"
|
68
|
+
end
|
69
|
+
[SearchStatement.new(column_names, search_statement_default_options([:fulltext]).merge(engines: [:fulltext]))]
|
70
|
+
end
|
71
|
+
|
72
|
+
def params
|
73
|
+
Array.wrap(@mincer.send(:pg_search_params))
|
74
|
+
end
|
75
|
+
|
76
|
+
def options
|
77
|
+
@mincer.send(:pg_search_options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def search_statement_default_options(engines)
|
81
|
+
(engines & [:fulltext, :trigram, :array]).inject({}) do |options, engine|
|
82
|
+
options = Mincer.config.pg_search.send("#{engine}_engine").merge(options)
|
83
|
+
options
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
module Options
|
90
|
+
extend ActiveSupport::Concern
|
91
|
+
|
92
|
+
def sort_by_rank
|
93
|
+
end
|
94
|
+
|
95
|
+
module ClassMethods
|
96
|
+
def skip_pg_search!
|
97
|
+
active_processors.delete(Mincer::Processors::PgSearch::Processor)
|
98
|
+
end
|
99
|
+
|
100
|
+
def skip_search!
|
101
|
+
skip_pg_search!
|
102
|
+
end
|
103
|
+
|
104
|
+
def pg_search(params, options = {})
|
105
|
+
class_eval <<-OPTIONS, __FILE__, __LINE__
|
106
|
+
def pg_search_params
|
107
|
+
@pg_search_params ||= #{params.inspect}
|
108
|
+
end
|
109
|
+
def pg_search_options
|
110
|
+
@pg_search_options ||= #{options.inspect}
|
111
|
+
end
|
112
|
+
OPTIONS
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def pg_search_params
|
117
|
+
@pg_search_params ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
def pg_search_options
|
121
|
+
@pg_search_options ||= {}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Configuration
|
126
|
+
include ActiveSupport::Configurable
|
127
|
+
config_accessor :param_name do
|
128
|
+
'pattern'
|
129
|
+
end
|
130
|
+
config_accessor :fulltext_engine do
|
131
|
+
{ ignore_accent: true, any_word: false, dictionary: :simple, ignore_case: false }
|
132
|
+
end
|
133
|
+
config_accessor :trigram_engine do
|
134
|
+
{ ignore_accent: true, threshold: 0.3 }
|
135
|
+
end
|
136
|
+
config_accessor :array_engine do
|
137
|
+
{ ignore_accent: true, any_word: true }
|
138
|
+
end
|
139
|
+
config_accessor :engines do
|
140
|
+
[Mincer::PgSearch::SearchEngines::Fulltext, Mincer::PgSearch::SearchEngines::Array, Mincer::PgSearch::SearchEngines::Trigram]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
::Mincer.add_processor(:pg_search)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Mincer
|
2
|
+
module Processors
|
3
|
+
module PgSearch
|
4
|
+
class Sanitizer
|
5
|
+
AVAILABLE_SANITIZERS = [:coalesce, :ignore_case, :ignore_accent]
|
6
|
+
attr_accessor :term, :sanitizers
|
7
|
+
|
8
|
+
def initialize(term, *sanitizers)
|
9
|
+
@term, @sanitizers = term, AVAILABLE_SANITIZERS & Array.wrap(sanitizers).flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
def sanitize_column
|
13
|
+
@sanitized_column ||= sanitize(Arel.sql(@term))
|
14
|
+
end
|
15
|
+
|
16
|
+
def sanitize_string(options = {})
|
17
|
+
if sanitizers.empty?
|
18
|
+
return options[:quote] ? Mincer.connection.quote(@term) : @term
|
19
|
+
end
|
20
|
+
@sanitized_string ||= sanitize(@term)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sanitize(node)
|
24
|
+
sanitizers.inject(node) do |query, sanitizer|
|
25
|
+
query = self.class.send(sanitizer, query)
|
26
|
+
query
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.sanitize_column(term, *sanitizers)
|
31
|
+
new(term, *sanitizers).sanitize_column
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.sanitize_string(term, *sanitizers)
|
35
|
+
new(term, *sanitizers).sanitize_string
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.sanitize_string_quoted(term, *sanitizers)
|
39
|
+
new(term, *sanitizers).sanitize_string(quote: true)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.ignore_case(term)
|
43
|
+
Arel::Nodes::NamedFunction.new('lower', [term])
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.ignore_accent(term)
|
47
|
+
Arel::Nodes::NamedFunction.new('unaccent', [term])
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.coalesce(term, val = '')
|
51
|
+
if Mincer.pg_extension_installed?(:unaccent)
|
52
|
+
Arel::Nodes::NamedFunction.new('coalesce', [term, val])
|
53
|
+
else
|
54
|
+
term
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Array < Base
|
5
|
+
|
6
|
+
def conditions
|
7
|
+
return nil unless prepared_search_statements.any?
|
8
|
+
arel_group do
|
9
|
+
conditions = prepared_search_statements.map do |search_statement|
|
10
|
+
if search_statement.pattern = args[search_statement.param_name]
|
11
|
+
terms_delimiter = search_statement.options[:any_word] ? '&&' : '@>'
|
12
|
+
arel_group(Arel::Nodes::InfixOperation.new(terms_delimiter, document_for(search_statement), query_for(search_statement)))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
join_expressions(conditions, :or)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def document_for(search_statement)
|
22
|
+
arel_group do
|
23
|
+
documents = search_statement.columns.map do |search_column|
|
24
|
+
Arel.sql(search_column + '::text[]')
|
25
|
+
end
|
26
|
+
join_expressions(documents, '||')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def query_for(search_statement)
|
31
|
+
normalized_pattern = search_statement.pattern.split(%r{\s|,}).uniq.reject(&:empty?).map do |item|
|
32
|
+
sanitize_string_quoted(item, search_statement.sanitizers).to_sql
|
33
|
+
end.join(',')
|
34
|
+
Arel.sql("ARRAY[#{normalized_pattern}]")
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Base
|
5
|
+
include ::Mincer::Processors::Helpers
|
6
|
+
attr_reader :args, :search_statements
|
7
|
+
|
8
|
+
def initialize(args, search_statements)
|
9
|
+
@args, @search_statements = ::ActiveSupport::HashWithIndifferentAccess.new(args), search_statements
|
10
|
+
end
|
11
|
+
|
12
|
+
def arel_group(sql_string = nil)
|
13
|
+
sql_string = yield if block_given?
|
14
|
+
arel_query = sql_string.is_a?(String) ? Arel.sql(sql_string) : sql_string
|
15
|
+
Arel::Nodes::Grouping.new(arel_query)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sanitize_column(term, sanitizers)
|
19
|
+
::Mincer::Processors::PgSearch::Sanitizer.sanitize_column(term, sanitizers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sanitize_string(term, sanitizers)
|
23
|
+
::Mincer::Processors::PgSearch::Sanitizer.sanitize_string(term, sanitizers)
|
24
|
+
end
|
25
|
+
|
26
|
+
def sanitize_string_quoted(term, sanitizers)
|
27
|
+
::Mincer::Processors::PgSearch::Sanitizer.sanitize_string_quoted(term, sanitizers)
|
28
|
+
end
|
29
|
+
|
30
|
+
def search_engine_statements
|
31
|
+
@search_engine_statements ||= self.search_statements.select do |search_statement|
|
32
|
+
search_statement.options[:engines].try(:include?, engine_sym)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def prepared_search_statements
|
37
|
+
@prepared_search_statements ||= search_engine_statements.map do |search_statement|
|
38
|
+
search_statement.pattern = args[search_statement.param_name]
|
39
|
+
search_statement.pattern.present? ? search_statement : nil
|
40
|
+
end.compact
|
41
|
+
end
|
42
|
+
|
43
|
+
# Redefine this method in subclass if your engine name does not match class
|
44
|
+
def engine_sym
|
45
|
+
@engine_sym ||= self.class.name.to_s.demodulize.underscore.to_sym
|
46
|
+
end
|
47
|
+
|
48
|
+
def rank
|
49
|
+
#Must be implemented in subclasses
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Fulltext < Base
|
5
|
+
DISALLOWED_TSQUERY_CHARACTERS = /[!(:&|)'?\\]/
|
6
|
+
|
7
|
+
def conditions
|
8
|
+
return nil unless prepared_search_statements.any?
|
9
|
+
arel_group do
|
10
|
+
conditions = prepared_search_statements.map do |search_statement|
|
11
|
+
arel_group(Arel::Nodes::InfixOperation.new('@@', document_for(search_statement), query_for(search_statement)))
|
12
|
+
end
|
13
|
+
join_expressions(conditions, :or)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def rank
|
18
|
+
return nil unless prepared_search_statements.any?
|
19
|
+
arel_group do
|
20
|
+
ranks = prepared_search_statements.map do |search_statement|
|
21
|
+
Arel::Nodes::NamedFunction.new('ts_rank', [document_for(search_statement), query_for(search_statement)]).to_sql
|
22
|
+
end
|
23
|
+
join_expressions(ranks, '+')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def prepared_search_statements
|
30
|
+
@prepared_search_statements ||= search_engine_statements.map do |search_statement|
|
31
|
+
pattern = args[search_statement.param_name]
|
32
|
+
search_statement.pattern = pattern && pattern.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ').split(' ').compact
|
33
|
+
search_statement.pattern.present? && search_statement.pattern.any? ? search_statement : nil
|
34
|
+
end.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def document_for(search_statement)
|
38
|
+
arel_group do
|
39
|
+
search_statement.columns.map do |search_column|
|
40
|
+
sanitized_term = sanitize_column(search_column, search_statement.sanitizers + [:coalesce])
|
41
|
+
Arel::Nodes::NamedFunction.new('to_tsvector', [search_statement.dictionary, sanitized_term]).to_sql
|
42
|
+
end.join(' || ')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def query_for(search_statement)
|
47
|
+
terms_delimiter = search_statement.options[:any_word] ? '|' : '&'
|
48
|
+
tsquery_sql = Arel.sql(search_statement.terms.map { |term| sanitize_string_quoted(term, search_statement.sanitizers).to_sql }.join(" || ' #{terms_delimiter} ' || "))
|
49
|
+
arel_group do
|
50
|
+
Arel::Nodes::NamedFunction.new('to_tsquery', [search_statement.dictionary, tsquery_sql])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mincer
|
2
|
+
module PgSearch
|
3
|
+
module SearchEngines
|
4
|
+
class Trigram < Base
|
5
|
+
|
6
|
+
def conditions
|
7
|
+
return nil unless prepared_search_statements.any?
|
8
|
+
arel_group do
|
9
|
+
join_expressions(prepared_search_statements.map { |search_statement| document_for(search_statement) }, :or)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def rank
|
14
|
+
return nil unless prepared_search_statements.any?
|
15
|
+
arel_group do
|
16
|
+
join_expressions(prepared_search_statements.map { |search_statement| rank_for(search_statement) }, :+)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
|
23
|
+
def document_for(search_statement)
|
24
|
+
documents = search_statement.columns.map do |search_column|
|
25
|
+
similarity = Arel::Nodes::NamedFunction.new('similarity', [sanitize_column(search_column, search_statement.sanitizers), sanitize_string(search_statement.pattern, search_statement.sanitizers)])
|
26
|
+
arel_group(similarity.gteq(search_statement.threshold))
|
27
|
+
end
|
28
|
+
join_expressions(documents, :or)
|
29
|
+
end
|
30
|
+
|
31
|
+
def rank_for(search_statement)
|
32
|
+
ranks = search_statement.columns.map do |search_column|
|
33
|
+
Arel::Nodes::NamedFunction.new('similarity', [sanitize_column(search_column, search_statement.sanitizers), sanitize_string(search_statement.pattern, search_statement.sanitizers)])
|
34
|
+
end
|
35
|
+
join_expressions(ranks, :+)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Mincer
|
2
|
+
module Processors
|
3
|
+
module PgSearch
|
4
|
+
class SearchStatement
|
5
|
+
attr_accessor :columns, :options, :pattern
|
6
|
+
alias_method :terms, :pattern
|
7
|
+
|
8
|
+
def initialize(columns, options = {})
|
9
|
+
@columns, @options = columns, ::ActiveSupport::HashWithIndifferentAccess.new(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sanitizers
|
13
|
+
@sanitizers ||= Sanitizer::AVAILABLE_SANITIZERS.select { |sanitizer| options[sanitizer] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def dictionary
|
17
|
+
options[:dictionary] || Mincer.config.pg_search.fulltext_engine[:dictionary]
|
18
|
+
end
|
19
|
+
|
20
|
+
def threshold
|
21
|
+
options[:threshold] || Mincer.config.pg_search.trigram_engine[:threshold]
|
22
|
+
end
|
23
|
+
|
24
|
+
def param_name
|
25
|
+
options[:param_name] || Mincer.config.pg_search.param_name
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Mincer
|
2
|
+
module Processors
|
3
|
+
module Sorting
|
4
|
+
class Processor
|
5
|
+
def initialize(mincer)
|
6
|
+
@mincer, @args, @relation = mincer, mincer.args, mincer.relation
|
7
|
+
end
|
8
|
+
|
9
|
+
def apply
|
10
|
+
relation = @relation.order(sort_string)
|
11
|
+
@mincer.sort_attribute, @mincer.sort_order = sort_attr, order_attr
|
12
|
+
relation
|
13
|
+
end
|
14
|
+
|
15
|
+
def sort_string
|
16
|
+
if sort_attr
|
17
|
+
"#{sort_attr} #{order_attr || default_order}"
|
18
|
+
else
|
19
|
+
"#{default_sort} #{order_attr || default_order}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def sort_attr
|
24
|
+
@mincer.send(:allowed_sort_attributes).include?(sort) && sort
|
25
|
+
end
|
26
|
+
|
27
|
+
def order_attr
|
28
|
+
%w{asc desc}.include?(order.try(:downcase)) && order
|
29
|
+
end
|
30
|
+
|
31
|
+
def sort
|
32
|
+
@args[::Mincer.config.sorting.sort_param_name]
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_sort
|
36
|
+
@mincer.try(:default_sort_attribute)
|
37
|
+
end
|
38
|
+
|
39
|
+
def order
|
40
|
+
@args[::Mincer.config.sorting.order_param_name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_order
|
44
|
+
@mincer.try(:default_sort_order)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
module Options
|
51
|
+
extend ActiveSupport::Concern
|
52
|
+
|
53
|
+
included do
|
54
|
+
# Used in view helpers
|
55
|
+
attr_accessor :sort_attribute, :sort_order
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
def skip_sorting!
|
60
|
+
active_processors.delete(Mincer::Processors::Sorting::Processor)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Default sort attribute. You must override this method if you want something else
|
65
|
+
def default_sort_attribute
|
66
|
+
::Mincer.config.sorting.sort_attribute
|
67
|
+
end
|
68
|
+
|
69
|
+
# Default order attribute. You must override this method if you want something else
|
70
|
+
def default_sort_order
|
71
|
+
::Mincer.config.sorting.order_attribute
|
72
|
+
end
|
73
|
+
|
74
|
+
# Allowed sort attributes, should return array of strings
|
75
|
+
def allowed_sort_attributes
|
76
|
+
@scope.attribute_names
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Configuration
|
81
|
+
include ActiveSupport::Configurable
|
82
|
+
|
83
|
+
config_accessor :sort_param_name do
|
84
|
+
:sort
|
85
|
+
end
|
86
|
+
|
87
|
+
config_accessor :sort_attribute do
|
88
|
+
:id
|
89
|
+
end
|
90
|
+
|
91
|
+
config_accessor :order_param_name do
|
92
|
+
:order
|
93
|
+
end
|
94
|
+
|
95
|
+
config_accessor :order_attribute do
|
96
|
+
:asc
|
97
|
+
end
|
98
|
+
|
99
|
+
config_accessor :asc_class do
|
100
|
+
'sorted order_down'
|
101
|
+
end
|
102
|
+
|
103
|
+
config_accessor :desc_class do
|
104
|
+
'sorted order_up'
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
::Mincer.add_processor(:sorting)
|
data/lib/mincer/version.rb
CHANGED
data/lib/mincer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
require 'ostruct'
|
1
2
|
require 'mincer/version'
|
2
|
-
|
3
|
-
|
3
|
+
require 'mincer/core_ext/string'
|
4
|
+
require 'mincer/config'
|
4
5
|
require 'mincer/base'
|
5
6
|
|
6
7
|
module Mincer
|
@@ -15,24 +16,39 @@ module Mincer
|
|
15
16
|
def self.connection
|
16
17
|
::ActiveRecord::Base.connection()
|
17
18
|
end
|
18
|
-
end
|
19
19
|
|
20
|
+
def self.add_processor(processor)
|
21
|
+
processor_scope = ::Mincer::Processors.const_get(ActiveSupport::Inflector.camelize(processor.to_s, true))
|
22
|
+
::Mincer.processors << processor_scope.const_get('Processor')
|
23
|
+
::Mincer::Base.send(:include, processor_scope.const_get('Options')) if processor_scope.const_defined?('Options')
|
24
|
+
::Mincer.config.add(processor, processor_scope.const_get('Configuration')) if processor_scope.const_defined?('Configuration') if processor_scope.const_defined?('Configuration')
|
25
|
+
end
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
::Mincer::Processors.constants.each do |k|
|
28
|
-
klass = ::Mincer::Processors.const_get(k)
|
29
|
-
if klass.is_a?(Class)
|
30
|
-
::Mincer.processors << klass
|
31
|
-
elsif klass.is_a?(Module)
|
32
|
-
::Mincer::Base.send(:include, klass)
|
27
|
+
def self.pg_extension_installed?(extension)
|
28
|
+
@installed_extensions ||= {}
|
29
|
+
if @installed_extensions[extension.to_sym].nil?
|
30
|
+
@installed_extensions[extension.to_sym] = ::Mincer.connection.execute("SELECT DISTINCT p.proname FROM pg_proc p WHERE p.proname = '#{extension}'").count > 0
|
31
|
+
end
|
32
|
+
@installed_extensions[extension.to_sym]
|
33
33
|
end
|
34
|
+
|
34
35
|
end
|
35
36
|
|
37
|
+
# Loading helpers
|
38
|
+
require 'mincer/processors/helpers'
|
39
|
+
|
40
|
+
# Loading processors
|
41
|
+
require 'mincer/processors/sorting/processor'
|
42
|
+
require 'mincer/processors/pagination/processor'
|
43
|
+
require 'mincer/processors/pg_search/search_statement'
|
44
|
+
require 'mincer/processors/pg_search/sanitizer'
|
45
|
+
require 'mincer/processors/pg_search/search_engines/base'
|
46
|
+
require 'mincer/processors/pg_search/search_engines/array'
|
47
|
+
require 'mincer/processors/pg_search/search_engines/fulltext'
|
48
|
+
require 'mincer/processors/pg_search/search_engines/trigram'
|
49
|
+
require 'mincer/processors/pg_search/processor'
|
50
|
+
require 'mincer/processors/cache_digest/processor'
|
51
|
+
require 'mincer/processors/pg_json_dumper/processor'
|
36
52
|
|
37
53
|
# Loading ActionView helpers
|
38
54
|
if defined?(ActionView)
|
@@ -42,19 +58,3 @@ if defined?(ActionView)
|
|
42
58
|
ActionView::Base.send(:include, klass) if klass.is_a?(Module)
|
43
59
|
end
|
44
60
|
end
|
45
|
-
|
46
|
-
|
47
|
-
#if defined?(::Rails)
|
48
|
-
# module Mincer
|
49
|
-
# class Railtie < ::Rails::Railtie
|
50
|
-
# initializer 'mincer.setup_paths' do
|
51
|
-
# end
|
52
|
-
#
|
53
|
-
# initializer 'carrierwave.active_record' do
|
54
|
-
# #ActiveSupport.on_load :active_record do
|
55
|
-
# # require 'carrierwave/orm/activerecord'
|
56
|
-
# #end
|
57
|
-
# end
|
58
|
-
# end
|
59
|
-
# end
|
60
|
-
#end
|
data/mincer.gemspec
CHANGED
@@ -22,10 +22,8 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
24
24
|
spec.add_development_dependency 'rake'
|
25
|
-
spec.add_development_dependency 'rspec', '~> 2.14.1'
|
26
25
|
spec.add_development_dependency 'pg'
|
27
26
|
spec.add_development_dependency 'sqlite3'
|
28
27
|
spec.add_development_dependency 'kaminari'
|
29
28
|
spec.add_development_dependency 'will_paginate'
|
30
|
-
spec.add_development_dependency 'textacular'
|
31
29
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe ::Mincer::Base do
|
5
|
+
|
6
|
+
it 'passes all missing methods to relation' do
|
7
|
+
setup_basic_sqlite3_table
|
8
|
+
subject = Class.new(Mincer::Base) do
|
9
|
+
pg_search [{ columns: %w{"active_record_models"."tags" }, engines: [:array] }]
|
10
|
+
end
|
11
|
+
query = subject.new(ActiveRecordModel)
|
12
|
+
expect { query.attribute_names }.not_to raise_exception
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|