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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5f74a54eb979770038bb0b7a22dd9ab49c7d0bd7
4
- data.tar.gz: 24be3a1eba292de415db675f55c93fcb127bf693
3
+ metadata.gz: 7336e5f9c9952756f9a981da7b79d5dece466022
4
+ data.tar.gz: e6a8621a11547eae03d63fc0d94e05053cc86664
5
5
  SHA512:
6
- metadata.gz: 3cd0d23114ea4b34234afab28e67ee749af4150a50510fc85c12ec740a10a45518be29405ca8a350ee713eda378850e205394207d3d09ee74ae4c68ab6fac132
7
- data.tar.gz: 77855c34ee526501c39e723164036aa2af493cb473fb4f7c68b481795844641ca6170ed8da1f39a5c6b5a15cdd99bd42ebce9b5d7f379823330f2209bc6598e8
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,5 +1,5 @@
1
1
  module OpenStax
2
2
  module Utilities
3
- VERSION = "4.1.0"
3
+ VERSION = "4.2.0"
4
4
  end
5
5
  end
@@ -1,7 +1,9 @@
1
- # Dummy routine for testing the general keyword search
1
+ # Convenience constants for testing the search routines
2
2
 
3
- class SearchUsers < OpenStax::Utilities::AbstractKeywordSearchRoutine
4
- self.search_proc = lambda { |with|
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
- self.sortable_fields_map = {'name' => :name, 'created_at' => :created_at, 'id' => :id}
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 AbstractKeywordSearchRoutine do
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 based on one field" do
26
- items = SearchUsers.call(User.unscoped, 'last_name:dOe').outputs[: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
- it "filters results based on multiple fields" do
37
- items = SearchUsers.call(User.unscoped, 'first_name:jOhN last_name:DoE')
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
- it "filters results based on multiple keywords per field" do
49
- items = SearchUsers.call(User.unscoped, 'first_name:JoHn,JaNe last_name:dOe')
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 "filters scoped results" do
61
- items = SearchUsers.call(User.where{name.like 'jOhN%'},
62
- 'last_name:dOe').outputs[:items]
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 = SearchUsers.call(User.unscoped, 'username:dOe',
89
- order_by: 'CrEaTeD_aT dEsC, Id DeSc')
90
- .outputs[:items]
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
- items.each do |item|
100
- expect(item.username).to match(/\Adoe[\w]*\z/i)
101
- end
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.call(User.unscoped, '').outputs[:items].to_a
103
+ all_items = SearchUsers::RELATION.to_a
106
104
 
107
- items = SearchUsers.call(User.unscoped, '', per_page: 20).outputs[:items]
108
- expect(items.limit(nil).offset(nil).count).to eq all_items.count
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 = SearchUsers.call(User.unscoped, '', page: page, per_page: 20)
115
- .outputs[:items]
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 = SearchUsers.call(User.unscoped, '', page: 1000, per_page: 20)
123
- .outputs[:items]
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.1.0
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-10-13 00:00:00.000000000 Z
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/handlers/openstax/utilities/keyword_search_handler.rb
136
- - app/routines/openstax/utilities/abstract_keyword_search_routine.rb
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/abstract_keyword_search_routine_spec.rb
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/abstract_keyword_search_routine_spec.rb
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