openstax_utilities 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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