openstax_utilities 4.1.0 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/routines/openstax/utilities/limit_and_paginate_relation.rb +85 -0
- data/app/routines/openstax/utilities/order_relation.rb +105 -0
- data/app/routines/openstax/utilities/search_and_organize_relation.rb +130 -0
- data/app/routines/openstax/utilities/search_relation.rb +89 -0
- data/lib/openstax/utilities/version.rb +1 -1
- data/spec/dummy/{app/routines → config/initializers}/search_users.rb +9 -4
- data/spec/routines/openstax/utilities/limit_and_paginate_relation_spec.rb +72 -0
- data/spec/routines/openstax/utilities/order_relation_spec.rb +57 -0
- data/spec/routines/openstax/utilities/{abstract_keyword_search_routine_spec.rb → search_and_organize_relation_spec.rb} +50 -43
- data/spec/routines/openstax/utilities/search_relation_spec.rb +81 -0
- metadata +16 -10
- data/app/handlers/openstax/utilities/keyword_search_handler.rb +0 -95
- data/app/routines/openstax/utilities/abstract_keyword_search_routine.rb +0 -158
- data/spec/handlers/openstax/utilities/keyword_search_handler_spec.rb +0 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7336e5f9c9952756f9a981da7b79d5dece466022
|
4
|
+
data.tar.gz: e6a8621a11547eae03d63fc0d94e05053cc86664
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b17cb61f65fe32b021ce90b736ff163fedc34ffcb3173f9ba0af024d165a4b53ea586547df15cda7b3d787bcf2afc3cf5a2c39a69ce1bc546ddce1056c6a6cf
|
7
|
+
data.tar.gz: a5a03f359924bc9de55fd70e3b433fd3ef84dbed7c94d1c7ff40e802b1c5cc65d452a6bf09d77198872d94f82f3e3da6ca65a773f1becd7d6b003a122605c16c
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Database-agnostic search result limiting and pagination routine
|
2
|
+
#
|
3
|
+
# Counts the number of items in a relation and empties it
|
4
|
+
# if the number exceeds the specified absolute maximum
|
5
|
+
# Otherwise, applies the specified pagination
|
6
|
+
#
|
7
|
+
# Callers of this routine provide the relation argument and
|
8
|
+
# may provide the max_items, per_page and page arguments
|
9
|
+
#
|
10
|
+
# Required arguments:
|
11
|
+
#
|
12
|
+
# Developer-supplied:
|
13
|
+
#
|
14
|
+
# relation - the ActiveRecord::Relation to be limited or paginated
|
15
|
+
#
|
16
|
+
# Optional arguments:
|
17
|
+
#
|
18
|
+
# Developer-supplied:
|
19
|
+
#
|
20
|
+
# max_items - the maximum allowed number of search results
|
21
|
+
# default: nil (disabled)
|
22
|
+
#
|
23
|
+
# User or developer-supplied:
|
24
|
+
#
|
25
|
+
# per_page - the maximum number of search results per page
|
26
|
+
# default: nil (disabled)
|
27
|
+
# page - the number of the page to return
|
28
|
+
# default: 1
|
29
|
+
#
|
30
|
+
# This routine's outputs contain:
|
31
|
+
#
|
32
|
+
# outputs[:total_count] - the total number of items in the relation
|
33
|
+
# outputs[:items] - the original relation after it has
|
34
|
+
# potentially been emptied or paginated
|
35
|
+
|
36
|
+
require 'lev'
|
37
|
+
|
38
|
+
module OpenStax
|
39
|
+
module Utilities
|
40
|
+
class LimitAndPaginateRelation
|
41
|
+
|
42
|
+
lev_routine transaction: :no_transaction
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def exec(*args)
|
47
|
+
|
48
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
49
|
+
relation = options[:relation] || args[0]
|
50
|
+
max_items = options[:max_items] || nil
|
51
|
+
per_page = Integer(options[:per_page]) rescue nil
|
52
|
+
page = Integer(options[:page]) rescue 1
|
53
|
+
|
54
|
+
raise ArgumentError, 'You must specify a :relation option' \
|
55
|
+
if relation.nil?
|
56
|
+
|
57
|
+
fatal_error(offending_inputs: :per_page,
|
58
|
+
message: 'Invalid page size',
|
59
|
+
code: :invalid_per_page) if !per_page.nil? && per_page < 1
|
60
|
+
fatal_error(offending_inputs: :page,
|
61
|
+
message: 'Invalid page number',
|
62
|
+
code: :invalid_page) if page < 1
|
63
|
+
|
64
|
+
outputs[:total_count] = relation.count
|
65
|
+
|
66
|
+
if !max_items.nil? && outputs[:total_count] > max_items
|
67
|
+
# Limiting
|
68
|
+
relation = relation.none
|
69
|
+
nonfatal_error(code: :too_many_items,
|
70
|
+
message: "The number of matches exceeded the allowed limit of #{
|
71
|
+
max_items} matches. Please refine your query and try again.")
|
72
|
+
elsif per_page.nil?
|
73
|
+
relation = relation.none if page > 1
|
74
|
+
else
|
75
|
+
# Pagination
|
76
|
+
relation = relation.limit(per_page).offset(per_page*(page-1))
|
77
|
+
end
|
78
|
+
|
79
|
+
outputs[:items] = relation
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# Database-agnostic search result ordering routine
|
2
|
+
#
|
3
|
+
# Performs ordering of search results
|
4
|
+
#
|
5
|
+
# Callers of this routine provide the relation,
|
6
|
+
# sortable_fields and order_by arguments
|
7
|
+
#
|
8
|
+
# Required arguments:
|
9
|
+
#
|
10
|
+
# Developer-supplied:
|
11
|
+
#
|
12
|
+
# relation - the ActiveRecord::Relation to be ordered
|
13
|
+
#
|
14
|
+
# sortable_fields - list of fields that can appear in the order_by argument
|
15
|
+
# can be a Hash that maps field names to database columns
|
16
|
+
# or an Array of Strings
|
17
|
+
# invalid fields in order_by will be replaced with
|
18
|
+
# the first field listed here, in :asc order
|
19
|
+
#
|
20
|
+
# Optional arguments:
|
21
|
+
#
|
22
|
+
# User or developer-supplied:
|
23
|
+
#
|
24
|
+
# order_by - list of fields to order by, with optional sort directions
|
25
|
+
# can be (an Array of) Hashes, or Strings
|
26
|
+
# default: {sortable_fields.values.first => :asc}
|
27
|
+
#
|
28
|
+
# This routine's outputs contain:
|
29
|
+
#
|
30
|
+
# outputs[:items] - a relation containing the ordered records
|
31
|
+
|
32
|
+
require 'lev'
|
33
|
+
|
34
|
+
module OpenStax
|
35
|
+
module Utilities
|
36
|
+
class OrderRelation
|
37
|
+
|
38
|
+
lev_routine transaction: :no_transaction
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def exec(*args)
|
43
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
44
|
+
relation = options[:relation] || args[0]
|
45
|
+
sortable_fields = options[:sortable_fields] || args[1]
|
46
|
+
order_by = options[:order_by] || args[2]
|
47
|
+
|
48
|
+
raise ArgumentError, 'You must specify a :relation option' \
|
49
|
+
if relation.nil?
|
50
|
+
raise ArgumentError, 'You must specify a :sortable_fields option' \
|
51
|
+
if sortable_fields.nil?
|
52
|
+
|
53
|
+
# Convert sortable_fields to Hash if it's an Array
|
54
|
+
sortable_fields = Hash[*sortable_fields.collect{|s| [s.to_s, s]}] \
|
55
|
+
if sortable_fields.is_a? Array
|
56
|
+
|
57
|
+
# Ordering
|
58
|
+
order_bys = sanitize_order_bys(sortable_fields, order_by)
|
59
|
+
outputs[:items] = relation.order(order_bys)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns an order_by Object
|
63
|
+
def sanitize_order_by(sortable_fields, field = nil, dir = nil)
|
64
|
+
sanitized_field = sortable_fields[field.to_s.downcase] || \
|
65
|
+
sortable_fields.values.first
|
66
|
+
sanitized_dir = dir.to_s.downcase == 'desc' ? :desc : :asc
|
67
|
+
case sanitized_field
|
68
|
+
when Symbol
|
69
|
+
{sanitized_field => sanitized_dir}
|
70
|
+
when Arel::Attributes::Attribute
|
71
|
+
sanitized_field.send sanitized_dir
|
72
|
+
else
|
73
|
+
"#{sanitized_field.to_s} #{sanitized_dir.to_s.upcase}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns an Array of order_by Objects
|
78
|
+
def sanitize_order_bys(sortable_fields, order_bys = nil)
|
79
|
+
obs = case order_bys
|
80
|
+
when Array
|
81
|
+
order_bys.collect do |ob|
|
82
|
+
case ob
|
83
|
+
when Hash
|
84
|
+
sanitize_order_by(sortable_fields, ob.keys.first, ob.values.first)
|
85
|
+
when Array
|
86
|
+
sanitize_order_by(sortable_fields, ob.first, ob.second)
|
87
|
+
else
|
88
|
+
sanitize_order_by(sortable_fields, ob)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
when Hash
|
92
|
+
order_bys.collect { |k, v| sanitize_order_by(sortable_fields, k, v) }
|
93
|
+
else
|
94
|
+
order_bys.to_s.split(',').collect do |ob|
|
95
|
+
fd = ob.split(' ')
|
96
|
+
sanitize_order_by(sortable_fields, fd.first, fd.second)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
obs.blank? ? sanitize_order_by(sortable_fields) : obs
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# Database-agnostic routine for keyword searching
|
2
|
+
# and ordering, limiting and paginating search results
|
3
|
+
#
|
4
|
+
# Searches, orders, imposes a maximum number or records and paginates
|
5
|
+
# a given relation using the appropriate routines and user-supplied
|
6
|
+
# parameters from the params hash
|
7
|
+
#
|
8
|
+
# See the search_routine.rb, order_routine.rb and
|
9
|
+
# limit_and_paginate_routine.rb files for more information
|
10
|
+
#
|
11
|
+
# Callers must provide the search_relation, search_proc and
|
12
|
+
# sortable_fields arguments and may provide the max_items argument
|
13
|
+
#
|
14
|
+
# Users must provide the q (query) argument and may provide
|
15
|
+
# the order_by, per_page and page arguments in the params hash
|
16
|
+
# Users must also be authorized to search the base class of the search_routine
|
17
|
+
#
|
18
|
+
# Required arguments:
|
19
|
+
#
|
20
|
+
# Developer-supplied:
|
21
|
+
#
|
22
|
+
# relation - the initial ActiveRecord::Relation to start searching on
|
23
|
+
# search_proc - a Proc passed to keyword_search's `search` method
|
24
|
+
# it receives keyword_search's `with` object as argument
|
25
|
+
# this proc must define the `keyword` blocks for keyword_search
|
26
|
+
# the relation to be scoped is contained in the @items instance variable
|
27
|
+
# the `to_string_array` helper can help with
|
28
|
+
# parsing strings from the query
|
29
|
+
#
|
30
|
+
# sortable_fields - list of fields that can appear in the order_by argument
|
31
|
+
# can be a Hash that maps field names to database columns
|
32
|
+
# or an Array of Strings
|
33
|
+
# invalid fields in order_by will be replaced with
|
34
|
+
# the first field listed here, in :asc order
|
35
|
+
#
|
36
|
+
# User or UI-supplied:
|
37
|
+
#
|
38
|
+
# params[:query] - a String that follows the keyword format
|
39
|
+
# Keywords have the format keyword:value
|
40
|
+
# Keywords can also be negated with -, as in -keyword:value
|
41
|
+
# Values are comma-separated; keywords are space-separated
|
42
|
+
#
|
43
|
+
# Optional arguments:
|
44
|
+
#
|
45
|
+
# Developer-supplied (recommended to prevent scraping):
|
46
|
+
#
|
47
|
+
# max_items - the maximum number of matching items allowed to be returned
|
48
|
+
# no results will be returned if this number is exceeded,
|
49
|
+
# but the total result count will still be returned
|
50
|
+
# applies even if pagination is enabled
|
51
|
+
# default: nil (disabled)
|
52
|
+
#
|
53
|
+
# User or UI-supplied:
|
54
|
+
#
|
55
|
+
# params[:order_by] - list of fields to order by, with optional sort directions
|
56
|
+
# can be (an Array of) Hashes, or Strings
|
57
|
+
# default: {sortable_fields.values.first => :asc}
|
58
|
+
#
|
59
|
+
# params[:per_page] - the maximum number of search results per page
|
60
|
+
# default: nil (disabled)
|
61
|
+
# params[:page] - the number of the page to return
|
62
|
+
# default: 1
|
63
|
+
#
|
64
|
+
# This handler's output contains:
|
65
|
+
#
|
66
|
+
# outputs[:total_count] - the total number of items that matched the query
|
67
|
+
# outputs[:items] - the relation returned by the search routines
|
68
|
+
|
69
|
+
require 'lev'
|
70
|
+
|
71
|
+
module OpenStax
|
72
|
+
module Utilities
|
73
|
+
class SearchAndOrganizeRelation
|
74
|
+
|
75
|
+
lev_routine transaction: :no_transaction
|
76
|
+
|
77
|
+
uses_routine SearchRelation,
|
78
|
+
as: :search
|
79
|
+
uses_routine OrderRelation,
|
80
|
+
as: :order
|
81
|
+
uses_routine LimitAndPaginateRelation,
|
82
|
+
as: :limit_and_paginate,
|
83
|
+
errors_are_fatal: false,
|
84
|
+
translations: { inputs: { type: :verbatim },
|
85
|
+
outputs: { type: :verbatim } }
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
def exec(*args, &search_proc)
|
90
|
+
|
91
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
92
|
+
relation = options[:relation] || args[0]
|
93
|
+
sortable_fields = options[:sortable_fields] || args[1]
|
94
|
+
params = options[:params] || args[2]
|
95
|
+
search_proc ||= options[:search_proc] || args[3]
|
96
|
+
max_items = options[:max_items] || nil
|
97
|
+
|
98
|
+
raise ArgumentError, 'You must specify a :relation option' \
|
99
|
+
if relation.nil?
|
100
|
+
raise ArgumentError, 'You must specify a :sortable_fields option' \
|
101
|
+
if sortable_fields.nil?
|
102
|
+
raise ArgumentError, 'You must specify a :params option' if params.nil?
|
103
|
+
raise ArgumentError, 'You must specify a block or :search_proc option' \
|
104
|
+
if search_proc.nil?
|
105
|
+
|
106
|
+
query = params[:query] || params[:q]
|
107
|
+
order_by = params[:order_by] || params[:ob]
|
108
|
+
per_page = params[:per_page] || params[:pp]
|
109
|
+
page = params[:page] || params[:p]
|
110
|
+
|
111
|
+
items = run(:search, relation: relation, search_proc: search_proc,
|
112
|
+
query: query).outputs[:items]
|
113
|
+
|
114
|
+
items = run(:order, relation: items, sortable_fields: sortable_fields,
|
115
|
+
order_by: order_by).outputs[:items]
|
116
|
+
|
117
|
+
if max_items.nil? && per_page.nil? && page.nil?
|
118
|
+
outputs[:items] = items
|
119
|
+
outputs[:total_count] = items.count
|
120
|
+
return
|
121
|
+
end
|
122
|
+
|
123
|
+
run(:limit_and_paginate, relation: items, max_items: max_items,
|
124
|
+
per_page: per_page, page: page)
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# Database-agnostic keyword searching routine
|
2
|
+
#
|
3
|
+
# Filters a relation based on a search Proc and a query String
|
4
|
+
# See https://github.com/bruce/keyword_search for more information
|
5
|
+
# about these arguments
|
6
|
+
#
|
7
|
+
# Callers of this routine provide the search_proc, relation and query arguments
|
8
|
+
#
|
9
|
+
# Required arguments:
|
10
|
+
#
|
11
|
+
# Developer-supplied:
|
12
|
+
#
|
13
|
+
# relation - the initial ActiveRecord::Relation to start searching on
|
14
|
+
# search_proc - a Proc passed to keyword_search's `search` method
|
15
|
+
# it receives keyword_search's `with` object as argument
|
16
|
+
# this proc must define the `keyword` blocks for keyword_search
|
17
|
+
# the relation to be scoped is contained in the @items instance variable
|
18
|
+
# the `to_string_array` helper can help with
|
19
|
+
# parsing strings from the query
|
20
|
+
#
|
21
|
+
# User or developer-supplied:
|
22
|
+
#
|
23
|
+
# query - a String that follows the keyword format
|
24
|
+
# Keywords have the format keyword:value
|
25
|
+
# Keywords can also be negated with -, as in -keyword:value
|
26
|
+
# Values are comma-separated, while keywords are space-separated
|
27
|
+
#
|
28
|
+
# This routine's outputs contain:
|
29
|
+
#
|
30
|
+
# outputs[:items] - a relation with records that match the query terms
|
31
|
+
|
32
|
+
require 'lev'
|
33
|
+
require 'keyword_search'
|
34
|
+
|
35
|
+
module OpenStax
|
36
|
+
module Utilities
|
37
|
+
class SearchRelation
|
38
|
+
|
39
|
+
lev_routine transaction: :no_transaction
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def exec(*args, &search_proc)
|
44
|
+
|
45
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
46
|
+
@items = options[:relation] || args[0]
|
47
|
+
query = options[:query] || args[1]
|
48
|
+
search_proc ||= options[:search_proc] || args[2]
|
49
|
+
|
50
|
+
raise ArgumentError, 'You must specify a :relation option' if @items.nil?
|
51
|
+
raise ArgumentError, 'You must specify a block or :search_proc option' \
|
52
|
+
if search_proc.nil?
|
53
|
+
|
54
|
+
# Scoping
|
55
|
+
|
56
|
+
::KeywordSearch.search(query.to_s) do |with|
|
57
|
+
instance_exec(with, &search_proc)
|
58
|
+
end
|
59
|
+
|
60
|
+
outputs[:items] = @items
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parses a keyword string into an array of strings
|
64
|
+
# User-supplied wildcards are removed and strings are split on commas
|
65
|
+
# Then wildcards are appended or prepended if the append_wildcard or
|
66
|
+
# prepend_wildcard options are specified
|
67
|
+
def to_string_array(str, options = {})
|
68
|
+
sa = case str
|
69
|
+
when Array
|
70
|
+
str.collect{|name| name.gsub('%', '').split(',')}.flatten
|
71
|
+
else
|
72
|
+
str.to_s.gsub('%', '').split(',')
|
73
|
+
end
|
74
|
+
sa = sa.collect{|str| "#{str}%"} if options[:append_wildcard]
|
75
|
+
sa = sa.collect{|str| "%#{str}"} if options[:prepend_wildcard]
|
76
|
+
sa
|
77
|
+
end
|
78
|
+
|
79
|
+
# Parses a keyword string into an array of numbers
|
80
|
+
# User-supplied wildcards are removed and strings are split on commas
|
81
|
+
# Only numbers are returned
|
82
|
+
def to_number_array(str)
|
83
|
+
to_string_array(str).collect{|s| Integer(s) rescue nil}.compact
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
|
-
#
|
1
|
+
# Convenience constants for testing the search routines
|
2
2
|
|
3
|
-
class SearchUsers
|
4
|
-
|
3
|
+
class SearchUsers
|
4
|
+
RELATION = User.unscoped
|
5
|
+
|
6
|
+
SEARCH_PROC = lambda { |with|
|
5
7
|
with.keyword :username do |names|
|
6
8
|
snames = to_string_array(names, append_wildcard: true)
|
7
9
|
@items = @items.where{username.like_any snames}
|
@@ -17,5 +19,8 @@ class SearchUsers < OpenStax::Utilities::AbstractKeywordSearchRoutine
|
|
17
19
|
@items = @items.where{name.like_any snames}
|
18
20
|
end
|
19
21
|
}
|
20
|
-
|
22
|
+
|
23
|
+
SORTABLE_FIELDS = {'id' => :id, 'name' => :name, 'created_at' => :created_at}
|
24
|
+
|
25
|
+
MAX_ITEMS = 50
|
21
26
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module OpenStax
|
4
|
+
module Utilities
|
5
|
+
describe SearchRelation do
|
6
|
+
|
7
|
+
let!(:john_doe) { FactoryGirl.create :user, name: "John Doe",
|
8
|
+
username: "doejohn",
|
9
|
+
email: "john@doe.com" }
|
10
|
+
|
11
|
+
let!(:jane_doe) { FactoryGirl.create :user, name: "Jane Doe",
|
12
|
+
username: "doejane",
|
13
|
+
email: "jane@doe.com" }
|
14
|
+
|
15
|
+
let!(:jack_doe) { FactoryGirl.create :user, name: "Jack Doe",
|
16
|
+
username: "doejack",
|
17
|
+
email: "jack@doe.com" }
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
100.times do
|
21
|
+
FactoryGirl.create(:user)
|
22
|
+
end
|
23
|
+
|
24
|
+
@relation = User.unscoped
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns nothing if too many results" do
|
28
|
+
routine = LimitAndPaginateRelation.call(relation: @relation,
|
29
|
+
max_items: 10)
|
30
|
+
outputs = routine.outputs
|
31
|
+
errors = routine.errors
|
32
|
+
expect(outputs).not_to be_empty
|
33
|
+
expect(outputs[:total_count]).to eq User.count
|
34
|
+
expect(outputs[:items]).to be_empty
|
35
|
+
expect(errors).not_to be_empty
|
36
|
+
expect(errors.first.code).to eq :too_many_items
|
37
|
+
end
|
38
|
+
|
39
|
+
it "paginates results" do
|
40
|
+
all_items = @relation.to_a
|
41
|
+
|
42
|
+
items = LimitAndPaginateRelation.call(relation: @relation,
|
43
|
+
per_page: 20).outputs[:items]
|
44
|
+
expect(items.limit(nil).offset(nil).count).to eq all_items.count
|
45
|
+
expect(items.limit(nil).offset(nil).to_a).to eq all_items
|
46
|
+
expect(items.count).to eq 20
|
47
|
+
expect(items.to_a).to eq all_items[0..19]
|
48
|
+
|
49
|
+
for page in 1..5
|
50
|
+
items = LimitAndPaginateRelation.call(relation: @relation,
|
51
|
+
page: page,
|
52
|
+
per_page: 20)
|
53
|
+
.outputs[:items]
|
54
|
+
expect(items.limit(nil).offset(nil).count).to eq all_items.count
|
55
|
+
expect(items.limit(nil).offset(nil).to_a).to eq all_items
|
56
|
+
expect(items.count).to eq 20
|
57
|
+
expect(items.to_a).to eq all_items.slice(20*(page-1), 20)
|
58
|
+
end
|
59
|
+
|
60
|
+
items = LimitAndPaginateRelation.call(relation: @relation,
|
61
|
+
page: 1000,
|
62
|
+
per_page: 20)
|
63
|
+
.outputs[:items]
|
64
|
+
expect(items.limit(nil).offset(nil).count).to eq all_items.count
|
65
|
+
expect(items.limit(nil).offset(nil).to_a).to eq all_items
|
66
|
+
expect(items.count).to eq 0
|
67
|
+
expect(items.to_a).to be_empty
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module OpenStax
|
4
|
+
module Utilities
|
5
|
+
describe SearchRelation do
|
6
|
+
|
7
|
+
let!(:john_doe) { FactoryGirl.create :user, name: "John Doe",
|
8
|
+
username: "doejohn",
|
9
|
+
email: "john@doe.com" }
|
10
|
+
|
11
|
+
let!(:jane_doe) { FactoryGirl.create :user, name: "Jane Doe",
|
12
|
+
username: "doejane",
|
13
|
+
email: "jane@doe.com" }
|
14
|
+
|
15
|
+
let!(:jack_doe) { FactoryGirl.create :user, name: "Jack Doe",
|
16
|
+
username: "doejack",
|
17
|
+
email: "jack@doe.com" }
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
100.times do
|
21
|
+
FactoryGirl.create(:user)
|
22
|
+
end
|
23
|
+
|
24
|
+
@relation = User.where{username.like 'doe%'}
|
25
|
+
end
|
26
|
+
|
27
|
+
it "orders results by multiple fields in different directions" do
|
28
|
+
items = OrderRelation.call(relation: @relation,
|
29
|
+
sortable_fields: SearchUsers::SORTABLE_FIELDS,
|
30
|
+
order_by: 'cReAtEd_At AsC, iD')
|
31
|
+
.outputs[:items]
|
32
|
+
expect(items).to include(john_doe)
|
33
|
+
expect(items).to include(jane_doe)
|
34
|
+
expect(items).to include(jack_doe)
|
35
|
+
john_index = items.index(john_doe)
|
36
|
+
jane_index = items.index(jane_doe)
|
37
|
+
jack_index = items.index(jack_doe)
|
38
|
+
expect(jane_index).to be > john_index
|
39
|
+
expect(jack_index).to be > jane_index
|
40
|
+
|
41
|
+
items = OrderRelation.call(relation: @relation,
|
42
|
+
sortable_fields: SearchUsers::SORTABLE_FIELDS,
|
43
|
+
order_by: 'CrEaTeD_aT dEsC, Id DeSc')
|
44
|
+
.outputs[:items]
|
45
|
+
expect(items).to include(john_doe)
|
46
|
+
expect(items).to include(jane_doe)
|
47
|
+
expect(items).to include(jack_doe)
|
48
|
+
john_index = items.index(john_doe)
|
49
|
+
jane_index = items.index(jane_doe)
|
50
|
+
jack_index = items.index(jack_doe)
|
51
|
+
expect(jane_index).to be < john_index
|
52
|
+
expect(jack_index).to be < jane_index
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -2,7 +2,14 @@ require 'rails_helper'
|
|
2
2
|
|
3
3
|
module OpenStax
|
4
4
|
module Utilities
|
5
|
-
describe
|
5
|
+
describe SearchAndOrganizeRelation do
|
6
|
+
|
7
|
+
OPTIONS = {
|
8
|
+
relation: SearchUsers::RELATION,
|
9
|
+
search_proc: SearchUsers::SEARCH_PROC,
|
10
|
+
sortable_fields: SearchUsers::SORTABLE_FIELDS,
|
11
|
+
max_items: SearchUsers::MAX_ITEMS
|
12
|
+
}
|
6
13
|
|
7
14
|
let!(:john_doe) { FactoryGirl.create :user, name: "John Doe",
|
8
15
|
username: "doejohn",
|
@@ -22,8 +29,9 @@ module OpenStax
|
|
22
29
|
end
|
23
30
|
end
|
24
31
|
|
25
|
-
it "filters results
|
26
|
-
items =
|
32
|
+
it "filters results" do
|
33
|
+
items = SearchAndOrganizeRelation.call(OPTIONS.merge(params: {
|
34
|
+
q: 'last_name:dOe'})).outputs[:items]
|
27
35
|
|
28
36
|
expect(items).to include(john_doe)
|
29
37
|
expect(items).to include(jane_doe)
|
@@ -31,11 +39,9 @@ module OpenStax
|
|
31
39
|
items.each do |item|
|
32
40
|
expect(item.name.downcase).to match(/\A[\w]* doe[\w]*\z/i)
|
33
41
|
end
|
34
|
-
end
|
35
42
|
|
36
|
-
|
37
|
-
|
38
|
-
.outputs[:items]
|
43
|
+
items = SearchAndOrganizeRelation.call(OPTIONS.merge(params: {
|
44
|
+
q: 'first_name:jOhN last_name:DoE'})).outputs[:items]
|
39
45
|
|
40
46
|
expect(items).to include(john_doe)
|
41
47
|
expect(items).not_to include(jane_doe)
|
@@ -43,11 +49,9 @@ module OpenStax
|
|
43
49
|
items.each do |item|
|
44
50
|
expect(item.name).to match(/\Ajohn[\w]* doe[\w]*\z/i)
|
45
51
|
end
|
46
|
-
end
|
47
52
|
|
48
|
-
|
49
|
-
|
50
|
-
.outputs[:items]
|
53
|
+
items = SearchAndOrganizeRelation.call(OPTIONS.merge(params: {
|
54
|
+
q: 'first_name:JoHn,JaNe last_name:dOe'})).outputs[:items]
|
51
55
|
|
52
56
|
expect(items).to include(john_doe)
|
53
57
|
expect(items).to include(jane_doe)
|
@@ -57,22 +61,10 @@ module OpenStax
|
|
57
61
|
end
|
58
62
|
end
|
59
63
|
|
60
|
-
it "
|
61
|
-
items =
|
62
|
-
|
63
|
-
|
64
|
-
expect(items).to include(john_doe)
|
65
|
-
expect(items).not_to include(jane_doe)
|
66
|
-
expect(items).not_to include(jack_doe)
|
67
|
-
items.each do |item|
|
68
|
-
expect(item.name.downcase).to match(/\Ajohn[\w]* doe[\w]*\z/i)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
it "orders results by multiple fields in different directions" do
|
73
|
-
items = SearchUsers.call(User.unscoped, 'username:DoE',
|
74
|
-
order_by: 'cReAtEd_At AsC, iD')
|
75
|
-
.outputs[:items]
|
64
|
+
it "orders results" do
|
65
|
+
items = SearchAndOrganizeRelation.call(OPTIONS.merge(params: {
|
66
|
+
order_by: 'cReAtEd_At AsC, iD',
|
67
|
+
q: 'username:dOe'})).outputs[:items].to_a
|
76
68
|
expect(items).to include(john_doe)
|
77
69
|
expect(items).to include(jane_doe)
|
78
70
|
expect(items).to include(jack_doe)
|
@@ -81,13 +73,10 @@ module OpenStax
|
|
81
73
|
jack_index = items.index(jack_doe)
|
82
74
|
expect(jane_index).to be > john_index
|
83
75
|
expect(jack_index).to be > jane_index
|
84
|
-
items.each do |item|
|
85
|
-
expect(item.username).to match(/\Adoe[\w]*\z/i)
|
86
|
-
end
|
87
76
|
|
88
|
-
items =
|
89
|
-
|
90
|
-
|
77
|
+
items = SearchAndOrganizeRelation.call(OPTIONS.merge(params: {
|
78
|
+
order_by: 'CrEaTeD_aT dEsC, Id DeSc',
|
79
|
+
q: 'username:dOe'})).outputs[:items].to_a
|
91
80
|
expect(items).to include(john_doe)
|
92
81
|
expect(items).to include(jane_doe)
|
93
82
|
expect(items).to include(jack_doe)
|
@@ -96,31 +85,49 @@ module OpenStax
|
|
96
85
|
jack_index = items.index(jack_doe)
|
97
86
|
expect(jane_index).to be < john_index
|
98
87
|
expect(jack_index).to be < jane_index
|
99
|
-
|
100
|
-
|
101
|
-
|
88
|
+
end
|
89
|
+
|
90
|
+
it "returns nothing if too many results" do
|
91
|
+
routine = SearchAndOrganizeRelation.call(OPTIONS.merge(params: {
|
92
|
+
q: ''}))
|
93
|
+
outputs = routine.outputs
|
94
|
+
errors = routine.errors
|
95
|
+
expect(outputs).not_to be_empty
|
96
|
+
expect(outputs[:total_count]).to eq User.count
|
97
|
+
expect(outputs[:items]).to be_empty
|
98
|
+
expect(errors).not_to be_empty
|
99
|
+
expect(errors.first.code).to eq :too_many_items
|
102
100
|
end
|
103
101
|
|
104
102
|
it "paginates results" do
|
105
|
-
all_items = SearchUsers.
|
103
|
+
all_items = SearchUsers::RELATION.to_a
|
106
104
|
|
107
|
-
items =
|
108
|
-
|
105
|
+
items = SearchAndOrganizeRelation.call(OPTIONS
|
106
|
+
.except(:max_items)
|
107
|
+
.merge(params: {q: '',
|
108
|
+
per_page: 20})).outputs[:items]
|
109
|
+
expect(items.limit(nil).offset(nil).count).to eq all_items.length
|
109
110
|
expect(items.limit(nil).offset(nil).to_a).to eq all_items
|
110
111
|
expect(items.count).to eq 20
|
111
112
|
expect(items.to_a).to eq all_items[0..19]
|
112
113
|
|
113
114
|
for page in 1..5
|
114
|
-
items =
|
115
|
-
|
115
|
+
items = SearchAndOrganizeRelation.call(OPTIONS
|
116
|
+
.except(:max_items)
|
117
|
+
.merge(params: {q: '',
|
118
|
+
page: page,
|
119
|
+
per_page: 20})).outputs[:items]
|
116
120
|
expect(items.limit(nil).offset(nil).count).to eq all_items.count
|
117
121
|
expect(items.limit(nil).offset(nil).to_a).to eq all_items
|
118
122
|
expect(items.count).to eq 20
|
119
123
|
expect(items.to_a).to eq all_items.slice(20*(page-1), 20)
|
120
124
|
end
|
121
125
|
|
122
|
-
items =
|
123
|
-
|
126
|
+
items = SearchAndOrganizeRelation.call(OPTIONS
|
127
|
+
.except(:max_items)
|
128
|
+
.merge(params: {q: '',
|
129
|
+
page: 1000,
|
130
|
+
per_page: 20})).outputs[:items]
|
124
131
|
expect(items.limit(nil).offset(nil).count).to eq all_items.count
|
125
132
|
expect(items.limit(nil).offset(nil).to_a).to eq all_items
|
126
133
|
expect(items.count).to eq 0
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module OpenStax
|
4
|
+
module Utilities
|
5
|
+
describe SearchRelation do
|
6
|
+
|
7
|
+
let!(:john_doe) { FactoryGirl.create :user, name: "John Doe",
|
8
|
+
username: "doejohn",
|
9
|
+
email: "john@doe.com" }
|
10
|
+
|
11
|
+
let!(:jane_doe) { FactoryGirl.create :user, name: "Jane Doe",
|
12
|
+
username: "doejane",
|
13
|
+
email: "jane@doe.com" }
|
14
|
+
|
15
|
+
let!(:jack_doe) { FactoryGirl.create :user, name: "Jack Doe",
|
16
|
+
username: "doejack",
|
17
|
+
email: "jack@doe.com" }
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
100.times do
|
21
|
+
FactoryGirl.create(:user)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "filters results based on one field" do
|
26
|
+
items = SearchRelation.call(relation: SearchUsers::RELATION,
|
27
|
+
search_proc: SearchUsers::SEARCH_PROC,
|
28
|
+
query: 'last_name:dOe').outputs[:items]
|
29
|
+
|
30
|
+
expect(items).to include(john_doe)
|
31
|
+
expect(items).to include(jane_doe)
|
32
|
+
expect(items).to include(jack_doe)
|
33
|
+
items.each do |item|
|
34
|
+
expect(item.name.downcase).to match(/\A[\w]* doe[\w]*\z/i)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "filters results based on multiple fields" do
|
39
|
+
items = SearchRelation.call(relation: SearchUsers::RELATION,
|
40
|
+
search_proc: SearchUsers::SEARCH_PROC,
|
41
|
+
query: 'first_name:jOhN last_name:DoE')
|
42
|
+
.outputs[:items]
|
43
|
+
|
44
|
+
expect(items).to include(john_doe)
|
45
|
+
expect(items).not_to include(jane_doe)
|
46
|
+
expect(items).not_to include(jack_doe)
|
47
|
+
items.each do |item|
|
48
|
+
expect(item.name).to match(/\Ajohn[\w]* doe[\w]*\z/i)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it "filters results based on multiple keywords per field" do
|
53
|
+
items = SearchRelation.call(relation: SearchUsers::RELATION,
|
54
|
+
search_proc: SearchUsers::SEARCH_PROC,
|
55
|
+
query: 'first_name:JoHn,JaNe last_name:dOe')
|
56
|
+
.outputs[:items]
|
57
|
+
|
58
|
+
expect(items).to include(john_doe)
|
59
|
+
expect(items).to include(jane_doe)
|
60
|
+
expect(items).not_to include(jack_doe)
|
61
|
+
items.each do |item|
|
62
|
+
expect(item.name).to match(/\A[john|jane][\w]* doe[\w]*\z/i)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "filters scoped results" do
|
67
|
+
items = SearchRelation.call(relation: User.where{name.like 'jOhN%'},
|
68
|
+
search_proc: SearchUsers::SEARCH_PROC,
|
69
|
+
query: 'last_name:dOe').outputs[:items]
|
70
|
+
|
71
|
+
expect(items).to include(john_doe)
|
72
|
+
expect(items).not_to include(jane_doe)
|
73
|
+
expect(items).not_to include(jack_doe)
|
74
|
+
items.each do |item|
|
75
|
+
expect(item.name.downcase).to match(/\Ajohn[\w]* doe[\w]*\z/i)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openstax_utilities
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- JP Slavinsky
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-12-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -132,8 +132,10 @@ files:
|
|
132
132
|
- MIT-LICENSE
|
133
133
|
- README.md
|
134
134
|
- Rakefile
|
135
|
-
- app/
|
136
|
-
- app/routines/openstax/utilities/
|
135
|
+
- app/routines/openstax/utilities/limit_and_paginate_relation.rb
|
136
|
+
- app/routines/openstax/utilities/order_relation.rb
|
137
|
+
- app/routines/openstax/utilities/search_and_organize_relation.rb
|
138
|
+
- app/routines/openstax/utilities/search_relation.rb
|
137
139
|
- app/views/osu/shared/_action_list.html.erb
|
138
140
|
- lib/openstax/utilities/access.rb
|
139
141
|
- lib/openstax/utilities/access_policy.rb
|
@@ -164,7 +166,6 @@ files:
|
|
164
166
|
- spec/dummy/app/controllers/application_controller.rb
|
165
167
|
- spec/dummy/app/helpers/application_helper.rb
|
166
168
|
- spec/dummy/app/models/user.rb
|
167
|
-
- spec/dummy/app/routines/search_users.rb
|
168
169
|
- spec/dummy/app/views/layouts/application.html.erb
|
169
170
|
- spec/dummy/bin/bundle
|
170
171
|
- spec/dummy/bin/rails
|
@@ -184,6 +185,7 @@ files:
|
|
184
185
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
185
186
|
- spec/dummy/config/initializers/inflections.rb
|
186
187
|
- spec/dummy/config/initializers/mime_types.rb
|
188
|
+
- spec/dummy/config/initializers/search_users.rb
|
187
189
|
- spec/dummy/config/initializers/session_store.rb
|
188
190
|
- spec/dummy/config/initializers/wrap_parameters.rb
|
189
191
|
- spec/dummy/config/locales/en.yml
|
@@ -196,10 +198,12 @@ files:
|
|
196
198
|
- spec/dummy/public/500.html
|
197
199
|
- spec/dummy/public/favicon.ico
|
198
200
|
- spec/factories/user.rb
|
199
|
-
- spec/handlers/openstax/utilities/keyword_search_handler_spec.rb
|
200
201
|
- spec/lib/openstax/utilities/access_policy_spec.rb
|
201
202
|
- spec/rails_helper.rb
|
202
|
-
- spec/routines/openstax/utilities/
|
203
|
+
- spec/routines/openstax/utilities/limit_and_paginate_relation_spec.rb
|
204
|
+
- spec/routines/openstax/utilities/order_relation_spec.rb
|
205
|
+
- spec/routines/openstax/utilities/search_and_organize_relation_spec.rb
|
206
|
+
- spec/routines/openstax/utilities/search_relation_spec.rb
|
203
207
|
- spec/spec_helper.rb
|
204
208
|
homepage: http://github.com/openstax/openstax_utilities
|
205
209
|
licenses:
|
@@ -232,7 +236,6 @@ test_files:
|
|
232
236
|
- spec/dummy/app/controllers/application_controller.rb
|
233
237
|
- spec/dummy/app/helpers/application_helper.rb
|
234
238
|
- spec/dummy/app/models/user.rb
|
235
|
-
- spec/dummy/app/routines/search_users.rb
|
236
239
|
- spec/dummy/app/views/layouts/application.html.erb
|
237
240
|
- spec/dummy/bin/bundle
|
238
241
|
- spec/dummy/bin/rails
|
@@ -251,6 +254,7 @@ test_files:
|
|
251
254
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
252
255
|
- spec/dummy/config/initializers/inflections.rb
|
253
256
|
- spec/dummy/config/initializers/mime_types.rb
|
257
|
+
- spec/dummy/config/initializers/search_users.rb
|
254
258
|
- spec/dummy/config/initializers/session_store.rb
|
255
259
|
- spec/dummy/config/initializers/wrap_parameters.rb
|
256
260
|
- spec/dummy/config/locales/en.yml
|
@@ -266,8 +270,10 @@ test_files:
|
|
266
270
|
- spec/dummy/Rakefile
|
267
271
|
- spec/dummy/README.md
|
268
272
|
- spec/factories/user.rb
|
269
|
-
- spec/handlers/openstax/utilities/keyword_search_handler_spec.rb
|
270
273
|
- spec/lib/openstax/utilities/access_policy_spec.rb
|
271
274
|
- spec/rails_helper.rb
|
272
|
-
- spec/routines/openstax/utilities/
|
275
|
+
- spec/routines/openstax/utilities/limit_and_paginate_relation_spec.rb
|
276
|
+
- spec/routines/openstax/utilities/order_relation_spec.rb
|
277
|
+
- spec/routines/openstax/utilities/search_and_organize_relation_spec.rb
|
278
|
+
- spec/routines/openstax/utilities/search_relation_spec.rb
|
273
279
|
- spec/spec_helper.rb
|
@@ -1,95 +0,0 @@
|
|
1
|
-
# Database-agnostic keyword searching handler
|
2
|
-
#
|
3
|
-
# Keywords have the format keyword:value
|
4
|
-
# Keywords can also be negated with -, as in -keyword:value
|
5
|
-
# Values are comma-separated, while keywords are space-separated
|
6
|
-
# See https://github.com/bruce/keyword_search for more information
|
7
|
-
#
|
8
|
-
# Callers must pass the search_routine and search_relation options:
|
9
|
-
#
|
10
|
-
# Required:
|
11
|
-
#
|
12
|
-
# search_routine - the Lev::Routine that will handle the search
|
13
|
-
# search_relation - the ActiveRecord::Relation that will be searched
|
14
|
-
#
|
15
|
-
# Optional (recommended to prevent scraping):
|
16
|
-
#
|
17
|
-
# min_characters - the minimum number of characters allowed in the query
|
18
|
-
# only an error will be returned if the query has less
|
19
|
-
# than the minimum number of characters allowed
|
20
|
-
# default: nil (disabled)
|
21
|
-
#
|
22
|
-
# max_items - the maximum number of matching items allowed to be returned
|
23
|
-
# no results will be returned if this number is exceeded,
|
24
|
-
# but the total result count will still be returned
|
25
|
-
# applies even if pagination is enabled
|
26
|
-
# default: nil (disabled)
|
27
|
-
#
|
28
|
-
# This handler also expects the following parameters from the user or the UI:
|
29
|
-
#
|
30
|
-
# Required:
|
31
|
-
#
|
32
|
-
# q - the query itself, a String that follows the keyword format above
|
33
|
-
#
|
34
|
-
# Optional:
|
35
|
-
#
|
36
|
-
# order_by - a String used to order the search results - default: 'created_at ASC'
|
37
|
-
# per_page - the number of results returned per page - default: nil (disabled)
|
38
|
-
# page - the current page number - default: 1
|
39
|
-
#
|
40
|
-
# This handler's output contains:
|
41
|
-
#
|
42
|
-
# outputs[:total_count] - the total number of items that matched the query
|
43
|
-
# set even when no results are returned due to
|
44
|
-
# a query that is too short or too generic
|
45
|
-
# outputs[:items] - the array of objects returned by the search routine
|
46
|
-
#
|
47
|
-
# See spec/dummy/app/handlers/users_search.rb for an example search handler
|
48
|
-
|
49
|
-
require 'lev'
|
50
|
-
|
51
|
-
module OpenStax
|
52
|
-
module Utilities
|
53
|
-
class KeywordSearchHandler
|
54
|
-
|
55
|
-
lev_handler
|
56
|
-
|
57
|
-
protected
|
58
|
-
|
59
|
-
def authorized?
|
60
|
-
routine = options[:search_routine]
|
61
|
-
relation = options[:search_relation]
|
62
|
-
raise ArgumentError if routine.nil? || relation.nil?
|
63
|
-
OSU::AccessPolicy.action_allowed?(:search, caller, relation.base_class)
|
64
|
-
end
|
65
|
-
|
66
|
-
def handle
|
67
|
-
query = params[:q]
|
68
|
-
fatal_error(code: :query_blank,
|
69
|
-
message: 'You must provide a query parameter (q or query).') if query.nil?
|
70
|
-
|
71
|
-
min_characters = options[:min_characters]
|
72
|
-
fatal_error(code: :query_too_short,
|
73
|
-
message: "The provided query is too short (minimum #{
|
74
|
-
min_characters} characters).") \
|
75
|
-
if !min_characters.nil? && query.length < min_characters
|
76
|
-
|
77
|
-
routine = options[:search_routine]
|
78
|
-
relation = options[:search_relation]
|
79
|
-
items = run(routine, relation, query, params).outputs[:items]
|
80
|
-
|
81
|
-
outputs[:total_count] = items.limit(nil).offset(nil).count
|
82
|
-
|
83
|
-
max_items = options[:max_items]
|
84
|
-
fatal_error(code: :too_many_items,
|
85
|
-
message: "The number of matches exceeded the allowed limit of #{
|
86
|
-
max_items} matches. Please refine your query and try again.") \
|
87
|
-
if !max_items.nil? && outputs[:total_count] > max_items
|
88
|
-
|
89
|
-
outputs[:items] = items.to_a
|
90
|
-
end
|
91
|
-
|
92
|
-
end
|
93
|
-
|
94
|
-
end
|
95
|
-
end
|
@@ -1,158 +0,0 @@
|
|
1
|
-
# Database-agnostic keyword searching routine
|
2
|
-
#
|
3
|
-
# Keywords have the format keyword:value
|
4
|
-
# Keywords can also be negated with -, as in -keyword:value
|
5
|
-
# Values are comma-separated, while keywords are space-separated
|
6
|
-
# See https://github.com/bruce/keyword_search for more information
|
7
|
-
#
|
8
|
-
# Subclasses must set the search_proc and sortable_fields class variables
|
9
|
-
#
|
10
|
-
# search_proc - a proc passed to keyword_search's `search` method
|
11
|
-
# it receives keyword_search's `with` object as argument
|
12
|
-
# this proc must define the `keyword` blocks for keyword_search
|
13
|
-
# the relation to be scoped is contained in the @items instance variable
|
14
|
-
# the `to_string_array` helper can help with
|
15
|
-
# parsing strings from the query
|
16
|
-
#
|
17
|
-
# sortable_fields_map - a Hash that maps the lowercase names of fields
|
18
|
-
# which can be used to sort the results to symbols
|
19
|
-
# for their respective database columns
|
20
|
-
# keys are lowercase strings that should be allowed
|
21
|
-
# in options[:order_by]
|
22
|
-
# values are the corresponding database column names
|
23
|
-
# that will be passed to the order() method
|
24
|
-
# columns from other tables can be specified either
|
25
|
-
# through Arel attributes (Class.arel_table[:column])
|
26
|
-
# or through literal strings
|
27
|
-
#
|
28
|
-
# Callers of subclass routines provide a relation argument,
|
29
|
-
# a query argument and an options hash
|
30
|
-
#
|
31
|
-
# Required arguments:
|
32
|
-
#
|
33
|
-
# relation - the initial relation to start searching on
|
34
|
-
# query - a string that follows the keyword format above
|
35
|
-
#
|
36
|
-
# Options hash:
|
37
|
-
#
|
38
|
-
# Ordering:
|
39
|
-
#
|
40
|
-
# :order_by - list of fields to sort by, with optional sort directions
|
41
|
-
# can be a String, Array of Strings or Array of Hashes
|
42
|
-
# default: {:created_at => :asc}
|
43
|
-
#
|
44
|
-
# Pagination:
|
45
|
-
#
|
46
|
-
# :per_page - the maximum number of results per page - default: nil (disabled)
|
47
|
-
# :page - the page to return - default: 1
|
48
|
-
#
|
49
|
-
# This routine's output contains:
|
50
|
-
#
|
51
|
-
# outputs[:items] - an ActiveRecord::Relation that matches the query terms and options
|
52
|
-
#
|
53
|
-
# You can use the following expression to obtain the
|
54
|
-
# total count of records that matched the query terms:
|
55
|
-
#
|
56
|
-
# outputs[:items].limit(nil).offset(nil).count
|
57
|
-
#
|
58
|
-
# See spec/dummy/app/routines/search_users.rb for an example search routine
|
59
|
-
|
60
|
-
require 'lev'
|
61
|
-
require 'keyword_search'
|
62
|
-
|
63
|
-
module OpenStax
|
64
|
-
module Utilities
|
65
|
-
class AbstractKeywordSearchRoutine
|
66
|
-
|
67
|
-
lev_routine transaction: :no_transaction
|
68
|
-
|
69
|
-
protected
|
70
|
-
|
71
|
-
class_attribute :search_proc, :sortable_fields_map
|
72
|
-
|
73
|
-
def exec(relation, query, options = {})
|
74
|
-
raise NotImplementedError if search_proc.nil? || sortable_fields_map.nil?
|
75
|
-
|
76
|
-
raise ArgumentError \
|
77
|
-
unless relation.is_a?(ActiveRecord::Relation) && query.is_a?(String)
|
78
|
-
|
79
|
-
@items = relation
|
80
|
-
|
81
|
-
# Scoping
|
82
|
-
|
83
|
-
::KeywordSearch.search(query) do |with|
|
84
|
-
instance_exec(with, &search_proc)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Ordering
|
88
|
-
|
89
|
-
order_bys = sanitize_order_bys(options[:order_by])
|
90
|
-
@items = @items.order(order_bys)
|
91
|
-
|
92
|
-
# Pagination
|
93
|
-
|
94
|
-
per_page = Integer(options[:per_page]) rescue nil
|
95
|
-
unless per_page.nil?
|
96
|
-
page = Integer(options[:page]) rescue 1
|
97
|
-
@items = @items.limit(per_page).offset(per_page*(page-1))
|
98
|
-
end
|
99
|
-
|
100
|
-
outputs[:items] = @items
|
101
|
-
end
|
102
|
-
|
103
|
-
def sanitize_order_by(field, dir = nil)
|
104
|
-
sanitized_field = sortable_fields_map[field.to_s.downcase] || :created_at
|
105
|
-
sanitized_dir = dir.to_s.downcase == 'desc' ? :desc : :asc
|
106
|
-
case sanitized_field
|
107
|
-
when Symbol
|
108
|
-
{sanitized_field => sanitized_dir}
|
109
|
-
when Arel::Attributes::Attribute
|
110
|
-
sanitized_field.send sanitized_dir
|
111
|
-
else
|
112
|
-
"#{sanitized_field.to_s} #{sanitized_dir.to_s.upcase}"
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def sanitize_order_bys(order_bys)
|
117
|
-
case order_bys
|
118
|
-
when Array
|
119
|
-
order_bys.collect do |ob|
|
120
|
-
case ob
|
121
|
-
when Hash
|
122
|
-
sanitize_order_by(ob.keys.first, ob.values.first)
|
123
|
-
when Array
|
124
|
-
sanitize_order_by(ob.first, ob.second)
|
125
|
-
else
|
126
|
-
sanitize_order_by(ob)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
when Hash
|
130
|
-
order_bys.collect { |k, v| sanitize_order_by(k, v) }
|
131
|
-
else
|
132
|
-
order_bys.to_s.split(',').collect do |ob|
|
133
|
-
fd = ob.split(' ')
|
134
|
-
sanitize_order_by(fd.first, fd.second)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
# Parses a keyword string into an array of strings
|
140
|
-
# User-supplied wildcards are removed and strings are split on commas
|
141
|
-
# Then wildcards are appended or prepended if the append_wildcard or
|
142
|
-
# prepend_wildcard options are specified
|
143
|
-
def to_string_array(str, options = {})
|
144
|
-
sa = case str
|
145
|
-
when Array
|
146
|
-
str.collect{|name| name.gsub('%', '').split(',')}.flatten
|
147
|
-
else
|
148
|
-
str.to_s.gsub('%', '').split(',')
|
149
|
-
end
|
150
|
-
sa = sa.collect{|str| "#{str}%"} if options[:append_wildcard]
|
151
|
-
sa = sa.collect{|str| "%#{str}"} if options[:prepend_wildcard]
|
152
|
-
sa
|
153
|
-
end
|
154
|
-
|
155
|
-
end
|
156
|
-
|
157
|
-
end
|
158
|
-
end
|
@@ -1,126 +0,0 @@
|
|
1
|
-
require 'rails_helper'
|
2
|
-
|
3
|
-
module OpenStax
|
4
|
-
module Utilities
|
5
|
-
describe KeywordSearchHandler do
|
6
|
-
|
7
|
-
options = {
|
8
|
-
caller: FactoryGirl.create(:user),
|
9
|
-
search_routine: SearchUsers,
|
10
|
-
search_relation: User.unscoped,
|
11
|
-
max_items: 10,
|
12
|
-
min_characters: 3
|
13
|
-
}
|
14
|
-
|
15
|
-
let!(:john_doe) { FactoryGirl.create :user, name: "John Doe",
|
16
|
-
username: "doejohn",
|
17
|
-
email: "john@doe.com" }
|
18
|
-
|
19
|
-
let!(:jane_doe) { FactoryGirl.create :user, name: "Jane Doe",
|
20
|
-
username: "doejane",
|
21
|
-
email: "jane@doe.com" }
|
22
|
-
|
23
|
-
let!(:jack_doe) { FactoryGirl.create :user, name: "Jack Doe",
|
24
|
-
username: "doejack",
|
25
|
-
email: "jack@doe.com" }
|
26
|
-
|
27
|
-
before(:each) do
|
28
|
-
100.times do
|
29
|
-
FactoryGirl.create(:user)
|
30
|
-
end
|
31
|
-
|
32
|
-
DummyAccessPolicy.last_action = nil
|
33
|
-
DummyAccessPolicy.last_requestor = nil
|
34
|
-
DummyAccessPolicy.last_resource = nil
|
35
|
-
end
|
36
|
-
|
37
|
-
it "passes its params to the search routine and sets the total_count output" do
|
38
|
-
outputs = KeywordSearchHandler.call(options.merge(
|
39
|
-
params: {q: 'username:dOe'})).outputs
|
40
|
-
total_count = outputs[:total_count]
|
41
|
-
items = outputs[:items]
|
42
|
-
expect(DummyAccessPolicy.last_action).to eq :search
|
43
|
-
expect(DummyAccessPolicy.last_requestor).to eq options[:caller]
|
44
|
-
expect(DummyAccessPolicy.last_resource).to eq User
|
45
|
-
expect(total_count).to eq items.count
|
46
|
-
expect(items).to include(john_doe)
|
47
|
-
expect(items).to include(jane_doe)
|
48
|
-
expect(items).to include(jack_doe)
|
49
|
-
john_index = items.index(john_doe)
|
50
|
-
jane_index = items.index(jane_doe)
|
51
|
-
jack_index = items.index(jack_doe)
|
52
|
-
expect(jane_index).to be > john_index
|
53
|
-
expect(jack_index).to be > jane_index
|
54
|
-
items.each do |item|
|
55
|
-
expect(item.username).to match(/\Adoe[\w]*\z/i)
|
56
|
-
end
|
57
|
-
|
58
|
-
DummyAccessPolicy.last_action = nil
|
59
|
-
DummyAccessPolicy.last_requestor = nil
|
60
|
-
DummyAccessPolicy.last_resource = nil
|
61
|
-
outputs = KeywordSearchHandler.call(options.merge(
|
62
|
-
params: {order_by: 'cReAtEd_At DeSc, iD dEsC',
|
63
|
-
q: 'username:DoE'})).outputs
|
64
|
-
total_count = outputs[:total_count]
|
65
|
-
items = outputs[:items]
|
66
|
-
expect(DummyAccessPolicy.last_action).to eq :search
|
67
|
-
expect(DummyAccessPolicy.last_requestor).to eq options[:caller]
|
68
|
-
expect(DummyAccessPolicy.last_resource).to eq User
|
69
|
-
expect(total_count).to eq items.count
|
70
|
-
expect(items).to include(john_doe)
|
71
|
-
expect(items).to include(jane_doe)
|
72
|
-
expect(items).to include(jack_doe)
|
73
|
-
john_index = items.index(john_doe)
|
74
|
-
jane_index = items.index(jane_doe)
|
75
|
-
jack_index = items.index(jack_doe)
|
76
|
-
expect(jane_index).to be < john_index
|
77
|
-
expect(jack_index).to be < jane_index
|
78
|
-
items.each do |item|
|
79
|
-
expect(item.username).to match(/\Adoe[\w]*\z/i)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
it "errors out if no query is provided" do
|
84
|
-
routine = KeywordSearchHandler.call(options.merge(params: {}))
|
85
|
-
outputs = routine.outputs
|
86
|
-
errors = routine.errors
|
87
|
-
expect(DummyAccessPolicy.last_action).to eq :search
|
88
|
-
expect(DummyAccessPolicy.last_requestor).to eq options[:caller]
|
89
|
-
expect(DummyAccessPolicy.last_resource).to eq User
|
90
|
-
expect(outputs).to be_empty
|
91
|
-
expect(errors).not_to be_empty
|
92
|
-
expect(errors.first.code).to eq :query_blank
|
93
|
-
end
|
94
|
-
|
95
|
-
it "errors out if the query is too short" do
|
96
|
-
routine = KeywordSearchHandler.call(options.merge(params: {q: 'a'}))
|
97
|
-
outputs = routine.outputs
|
98
|
-
errors = routine.errors
|
99
|
-
expect(DummyAccessPolicy.last_action).to eq :search
|
100
|
-
expect(DummyAccessPolicy.last_requestor).to eq options[:caller]
|
101
|
-
expect(DummyAccessPolicy.last_resource).to eq User
|
102
|
-
expect(outputs).to be_empty
|
103
|
-
expect(errors).not_to be_empty
|
104
|
-
expect(errors.first.code).to eq :query_too_short
|
105
|
-
end
|
106
|
-
|
107
|
-
it "errors out if too many items match" do
|
108
|
-
routine = KeywordSearchHandler.call(options.merge(
|
109
|
-
params: {
|
110
|
-
q: 'username:a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,0,1,2,3,4,5,6,7,8,9,-,_'
|
111
|
-
}))
|
112
|
-
outputs = routine.outputs
|
113
|
-
errors = routine.errors
|
114
|
-
expect(DummyAccessPolicy.last_action).to eq :search
|
115
|
-
expect(DummyAccessPolicy.last_requestor).to eq options[:caller]
|
116
|
-
expect(DummyAccessPolicy.last_resource).to eq User
|
117
|
-
expect(outputs).not_to be_empty
|
118
|
-
expect(outputs[:total_count]).to eq User.count
|
119
|
-
expect(outputs[:items]).to be_nil
|
120
|
-
expect(errors).not_to be_empty
|
121
|
-
expect(errors.first.code).to eq :too_many_items
|
122
|
-
end
|
123
|
-
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|