openstax_utilities 4.1.0 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|