openstax_utilities 4.1.0 → 4.3.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 +5 -5
- 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 +94 -0
- data/lib/openstax/utilities/access_policy.rb +4 -3
- data/lib/openstax/utilities/assets.rb +29 -0
- data/lib/openstax/utilities/assets/manifest.rb +62 -0
- data/lib/openstax/utilities/version.rb +1 -1
- data/lib/openstax_utilities.rb +5 -5
- data/spec/cassettes/OpenStax_Utilities_Assets/loading_remote_manifest/uses_remote_json.yml +353 -0
- data/spec/dummy/app/access_policies/dummier_access_policy.rb +10 -0
- data/spec/dummy/app/assets/config/manifest.js +3 -0
- data/spec/dummy/config/application.rb +6 -11
- data/spec/dummy/config/environments/test.rb +1 -1
- data/spec/dummy/config/initializers/search_users.rb +26 -0
- data/spec/dummy/config/secrets.yml +2 -5
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +32144 -0
- data/spec/dummy/tmp/cache/C09/760/6da7b2a29da9cb0f80ef102c7effb91fab3374db +0 -0
- data/spec/factories/user.rb +1 -1
- data/spec/lib/openstax/utilities/access_policy_spec.rb +16 -15
- data/spec/lib/openstax/utilities/assets_spec.rb +40 -0
- data/spec/rails_helper.rb +1 -2
- data/spec/routines/openstax/utilities/limit_and_paginate_relation_spec.rb +72 -0
- data/spec/routines/openstax/utilities/order_relation_spec.rb +55 -0
- data/spec/routines/openstax/utilities/{abstract_keyword_search_routine_spec.rb → search_and_organize_relation_spec.rb} +54 -47
- data/spec/routines/openstax/utilities/search_relation_spec.rb +81 -0
- data/spec/vcr_helper.rb +18 -0
- metadata +123 -44
- 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/dummy/app/routines/search_users.rb +0 -21
- data/spec/handlers/openstax/utilities/keyword_search_handler_spec.rb +0 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fc7366a73ada7f334bd42ca06e41c0043f92e0d2aff08969f5ec1759275b1885
|
4
|
+
data.tar.gz: 86fa1a1dbe2f4c8521afb1b6ce635b63c8c2140e6656de3028887fd22d7872d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26555b0e6c39c6cc7c3615d6e948849cce15aa9ff67ebafb5fad260ddb408a8003598b2e588f10ea1c2ed55256ade08a223ad92517b8fcc45adfeb8976583322
|
7
|
+
data.tar.gz: c36765470b6f808e37e2145538b49b56312568ea9ddc73f8329c94e7eb593ecaa8fa5bd09f999db6e7deda74a6792a8c85fdfa4bd2e850a63239de85fbaebd4d
|
@@ -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,94 @@
|
|
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
|
+
begin
|
57
|
+
::KeywordSearch.search(query.to_s) do |with|
|
58
|
+
instance_exec(with, &search_proc)
|
59
|
+
end
|
60
|
+
rescue KeywordSearch::ParseError
|
61
|
+
fatal_error(code: :invalid_query,
|
62
|
+
message: 'The search query string provided is invalid')
|
63
|
+
end
|
64
|
+
|
65
|
+
outputs[:items] = @items
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parses a keyword string into an array of strings
|
69
|
+
# User-supplied wildcards are removed and strings are split on commas
|
70
|
+
# Then wildcards are appended or prepended if the append_wildcard or
|
71
|
+
# prepend_wildcard options are specified
|
72
|
+
def to_string_array(str, options = {})
|
73
|
+
sa = case str
|
74
|
+
when Array
|
75
|
+
str.collect{|name| name.gsub('%', '').split(',')}.flatten
|
76
|
+
else
|
77
|
+
str.to_s.gsub('%', '').split(',')
|
78
|
+
end
|
79
|
+
sa = sa.collect{|str| "#{str}%"} if options[:append_wildcard]
|
80
|
+
sa = sa.collect{|str| "%#{str}"} if options[:prepend_wildcard]
|
81
|
+
sa
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parses a keyword string into an array of numbers
|
85
|
+
# User-supplied wildcards are removed and strings are split on commas
|
86
|
+
# Only numbers are returned
|
87
|
+
def to_number_array(str)
|
88
|
+
to_string_array(str).collect{|s| Integer(s) rescue nil}.compact
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -24,7 +24,8 @@ module OpenStax
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.require_action_allowed!(action, requestor, resource)
|
27
|
-
|
27
|
+
msg = "\"#{requestor.inspect}\" is not allowed to perform \"#{action}\" on \"#{resource.inspect}\""
|
28
|
+
raise(SecurityTransgression, msg) unless action_allowed?(action, requestor, resource)
|
28
29
|
end
|
29
30
|
|
30
31
|
def self.action_allowed?(action, requestor, resource)
|
@@ -38,7 +39,7 @@ module OpenStax
|
|
38
39
|
end
|
39
40
|
|
40
41
|
resource_class = resource.is_a?(Class) ? resource : resource.class
|
41
|
-
policy_class = instance.resource_policy_map[resource_class.to_s]
|
42
|
+
policy_class = instance.resource_policy_map[resource_class.to_s].try(:constantize)
|
42
43
|
|
43
44
|
# If there is no policy registered, we by default deny access
|
44
45
|
return false if policy_class.nil?
|
@@ -47,7 +48,7 @@ module OpenStax
|
|
47
48
|
end
|
48
49
|
|
49
50
|
def self.register(resource_class, policy_class)
|
50
|
-
self.instance.resource_policy_map[resource_class.to_s] = policy_class
|
51
|
+
self.instance.resource_policy_map[resource_class.to_s] = policy_class.to_s
|
51
52
|
end
|
52
53
|
|
53
54
|
end
|