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.
Files changed (34) hide show
  1. checksums.yaml +5 -5
  2. data/app/routines/openstax/utilities/limit_and_paginate_relation.rb +85 -0
  3. data/app/routines/openstax/utilities/order_relation.rb +105 -0
  4. data/app/routines/openstax/utilities/search_and_organize_relation.rb +130 -0
  5. data/app/routines/openstax/utilities/search_relation.rb +94 -0
  6. data/lib/openstax/utilities/access_policy.rb +4 -3
  7. data/lib/openstax/utilities/assets.rb +29 -0
  8. data/lib/openstax/utilities/assets/manifest.rb +62 -0
  9. data/lib/openstax/utilities/version.rb +1 -1
  10. data/lib/openstax_utilities.rb +5 -5
  11. data/spec/cassettes/OpenStax_Utilities_Assets/loading_remote_manifest/uses_remote_json.yml +353 -0
  12. data/spec/dummy/app/access_policies/dummier_access_policy.rb +10 -0
  13. data/spec/dummy/app/assets/config/manifest.js +3 -0
  14. data/spec/dummy/config/application.rb +6 -11
  15. data/spec/dummy/config/environments/test.rb +1 -1
  16. data/spec/dummy/config/initializers/search_users.rb +26 -0
  17. data/spec/dummy/config/secrets.yml +2 -5
  18. data/spec/dummy/db/test.sqlite3 +0 -0
  19. data/spec/dummy/log/test.log +32144 -0
  20. data/spec/dummy/tmp/cache/C09/760/6da7b2a29da9cb0f80ef102c7effb91fab3374db +0 -0
  21. data/spec/factories/user.rb +1 -1
  22. data/spec/lib/openstax/utilities/access_policy_spec.rb +16 -15
  23. data/spec/lib/openstax/utilities/assets_spec.rb +40 -0
  24. data/spec/rails_helper.rb +1 -2
  25. data/spec/routines/openstax/utilities/limit_and_paginate_relation_spec.rb +72 -0
  26. data/spec/routines/openstax/utilities/order_relation_spec.rb +55 -0
  27. data/spec/routines/openstax/utilities/{abstract_keyword_search_routine_spec.rb → search_and_organize_relation_spec.rb} +54 -47
  28. data/spec/routines/openstax/utilities/search_relation_spec.rb +81 -0
  29. data/spec/vcr_helper.rb +18 -0
  30. metadata +123 -44
  31. data/app/handlers/openstax/utilities/keyword_search_handler.rb +0 -95
  32. data/app/routines/openstax/utilities/abstract_keyword_search_routine.rb +0 -158
  33. data/spec/dummy/app/routines/search_users.rb +0 -21
  34. data/spec/handlers/openstax/utilities/keyword_search_handler_spec.rb +0 -126
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5f74a54eb979770038bb0b7a22dd9ab49c7d0bd7
4
- data.tar.gz: 24be3a1eba292de415db675f55c93fcb127bf693
2
+ SHA256:
3
+ metadata.gz: fc7366a73ada7f334bd42ca06e41c0043f92e0d2aff08969f5ec1759275b1885
4
+ data.tar.gz: 86fa1a1dbe2f4c8521afb1b6ce635b63c8c2140e6656de3028887fd22d7872d1
5
5
  SHA512:
6
- metadata.gz: 3cd0d23114ea4b34234afab28e67ee749af4150a50510fc85c12ec740a10a45518be29405ca8a350ee713eda378850e205394207d3d09ee74ae4c68ab6fac132
7
- data.tar.gz: 77855c34ee526501c39e723164036aa2af493cb473fb4f7c68b481795844641ca6170ed8da1f39a5c6b5a15cdd99bd42ebce9b5d7f379823330f2209bc6598e8
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
- raise SecurityTransgression unless action_allowed?(action, requestor, resource)
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