qa 5.5.2 → 5.8.1
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 +4 -4
- data/README.md +8 -2
- data/app/controllers/qa/terms_controller.rb +34 -23
- data/app/services/qa/pagination_service.rb +586 -0
- data/config/initializers/mime_types.rb +4 -0
- data/config/routes.rb +4 -0
- data/lib/qa/authorities/local/file_based_authority.rb +3 -2
- data/lib/qa/version.rb +1 -1
- data/spec/controllers/terms_controller_spec.rb +102 -1
- data/spec/fixtures/authorities/authority_U.yml +13 -0
- data/spec/lib/authorities/{file_based_authority_spec.rb → local/file_based_authority_spec.rb} +11 -0
- data/spec/lib/authorities/{mysql_table_based_authority_spec.rb → local/mysql_table_based_authority_spec.rb} +0 -0
- data/spec/lib/authorities/{table_based_authority_spec.rb → local/table_based_authority_spec.rb} +0 -0
- data/spec/lib/authorities/local_spec.rb +3 -1
- data/spec/services/pagination_service_spec.rb +682 -0
- metadata +20 -14
@@ -0,0 +1,586 @@
|
|
1
|
+
module Qa
|
2
|
+
# Provide pagination processing used to respond to requests for paginated results.
|
3
|
+
#
|
4
|
+
# <b>Defaults for page_offset and page_limit:</b> (see example responses under #build_response)
|
5
|
+
#
|
6
|
+
# format == :json
|
7
|
+
#
|
8
|
+
# * if neither page_offset nor page_limit is passed in, then... (backward compatible)
|
9
|
+
# * returns results as an Array<Hash>
|
10
|
+
# * returns all results
|
11
|
+
# * page_offset is "1"
|
12
|
+
# * page_limit is total_num_found
|
13
|
+
#
|
14
|
+
# * if either page_offset or page_limit is passed in, then...
|
15
|
+
# * returns results as an Array<Hash>
|
16
|
+
# * returns a page of results
|
17
|
+
# * default for page_offset is "1"
|
18
|
+
# * default for page_limit is DEFAULT_PAGE_LIMIT (i.e., 10)
|
19
|
+
#
|
20
|
+
# format == :jsonapi
|
21
|
+
#
|
22
|
+
# * response is always in the jsonapi format
|
23
|
+
# * results are always paginated
|
24
|
+
# * default for page_offset is "1"
|
25
|
+
# * default for page_limit is DEFAULT_PAGE_LIMIT (i.e., 10)
|
26
|
+
#
|
27
|
+
# <b>How page_offset is calculated for pagination links:</b>
|
28
|
+
#
|
29
|
+
# expected page boundaries
|
30
|
+
#
|
31
|
+
# * Expected page boundaries are always calculated starting from page_offset=1
|
32
|
+
# and the current page_limit. The page boundaries will include the page_offsets
|
33
|
+
# that cover all results. For example, page_limit=10 with 36 results will have
|
34
|
+
# page boundaries 1, 11, 21, 31.
|
35
|
+
#
|
36
|
+
# self url
|
37
|
+
#
|
38
|
+
# * The self url always has the page_offset for the current page, which defaults
|
39
|
+
# to 1 if not passed in.
|
40
|
+
#
|
41
|
+
# first page url
|
42
|
+
#
|
43
|
+
# * The first page url always has page_offset=1.
|
44
|
+
#
|
45
|
+
# last page url
|
46
|
+
#
|
47
|
+
# * The last page url always has page_offset equal to the last of the expected page
|
48
|
+
# boundaries regardless of the passed in page_offset. For the example where
|
49
|
+
# page_limit=10 with 36 results, the last page will always have page_offset=31.
|
50
|
+
#
|
51
|
+
# prev page url
|
52
|
+
#
|
53
|
+
# * Previous' page_offset is calculated from the passed in page_offset whether or
|
54
|
+
# not it is on an expected page boundary.
|
55
|
+
#
|
56
|
+
# * For prev, page_offset = passed in page_offset - page_limit || nil if calculated as < 1
|
57
|
+
# * when current page_offset (e.g. 1) is less than page_limit (e.g. 10), then page_offset
|
58
|
+
# for prev will be nil (e.g. 1 - 10 = -9 which is < 1)
|
59
|
+
# * when current page_offset is an expected page boundary (e.g. 21), then
|
60
|
+
# page_offset for prev will also be a page boundary (e.g. 21 - 10 = 11
|
61
|
+
# which is an expected page boundary)
|
62
|
+
# * when current page_offset is not on an expected page boundary (e.g. 13), then
|
63
|
+
# page_offset for prev will not be on an expected page boundary (e.g. 13 - 10 = 3
|
64
|
+
# which is not an expected page boundary)
|
65
|
+
#
|
66
|
+
# next page url
|
67
|
+
#
|
68
|
+
# * Next's page_offset is calculated from the passed in page_offset whether or
|
69
|
+
# not it is on an expected page boundary.
|
70
|
+
#
|
71
|
+
# * For next, page_offset = passed in page_offset + page_limit || nil if calculated > total number of results found
|
72
|
+
# * when current page_offset (e.g. 31) is greater than total number of results (e.g. 36),
|
73
|
+
# then page_offset for next will be nil (e.g. 31 + 10 = 41 which is > 36)
|
74
|
+
# * when current page_offset is an expected page boundary (e.g. 21), then
|
75
|
+
# page_offset for next will also be a page boundary (e.g. 21 + 10 = 31
|
76
|
+
# which is an expected page boundary)
|
77
|
+
# * when current page_offset is not on an expected page boundary (e.g. 13), then
|
78
|
+
# page_offset for next will not be on an expected page boundary (e.g. 13 + 10 = 23
|
79
|
+
# which is not an expected page boundary)
|
80
|
+
#
|
81
|
+
class PaginationService # rubocop:disable Metrics/ClassLength
|
82
|
+
# Default page_limit to use if not passed in with the request.
|
83
|
+
DEFAULT_PAGE_LIMIT = 10
|
84
|
+
|
85
|
+
# Error code for page_limit and page_offset when the value is not an integer.
|
86
|
+
ERROR_NOT_INTEGER = 901
|
87
|
+
# Error code for page_limit and page_offset when the value is below the acceptable range (e.g. < 1).
|
88
|
+
ERROR_OUT_OF_RANGE_TOO_SMALL = 902
|
89
|
+
# Error code for page_offset when the value is above the acceptable range (e.g. > total_num_found).
|
90
|
+
ERROR_OUT_OF_RANGE_TOO_LARGE = 903
|
91
|
+
|
92
|
+
# @param request [ActionDispatch::Request] The request from the controller.
|
93
|
+
# To support pagination, it's params need to respond to:
|
94
|
+
# * #page_offset [Integer] - the offset into the results for the start of the page (counts from 1; default: 1)
|
95
|
+
# * #page_limit [Integer] - the max number of records to return in a page
|
96
|
+
# * if format==:jsonapi, defaults to: DEFAULT_PAGE_LIMIT
|
97
|
+
# * if page_offset is passed in, defaults to: DEFAULT_PAGE_LIMIT
|
98
|
+
# * else when format==:json && page_offset.nil?, defaults to all results (backward compatible)
|
99
|
+
# @param results [Array<Hash>] results of a search query as processed by the authority module
|
100
|
+
# @param format [String] - if present, supported values are [:json | :jsonapi]
|
101
|
+
# * when :json, the response is an array of results (default)
|
102
|
+
# * when :jsonapi, the response follows the JSON API specification
|
103
|
+
#
|
104
|
+
# @see https://jsonapi.org/format/#fetching-pagination Pagination section of JSON API specification
|
105
|
+
# @see https://jsonapi.org/examples/#pagination JSON API example pagination
|
106
|
+
def initialize(request:, results:, format: :json)
|
107
|
+
@request = request
|
108
|
+
@results = results
|
109
|
+
@requested_format = format
|
110
|
+
@page_offset_error = false
|
111
|
+
@page_limit_error = false
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return json results, optionally limited to requested page and optionally
|
115
|
+
# formatted according to the JSON-API standard. The default is to return
|
116
|
+
# just the results for backward compatibility. See examples.
|
117
|
+
#
|
118
|
+
# @example json without pagination (backward compatible) (used only if neither page_offset nor page_limit are passed in)
|
119
|
+
# # request: q=term
|
120
|
+
# # response: format=json, no pagination, all results
|
121
|
+
# [
|
122
|
+
# { "id": "1", "label": "term 1" },
|
123
|
+
# { "id": "2", "label": "term 2" },
|
124
|
+
# ...
|
125
|
+
# { "id": "28", "label": "term 28" }
|
126
|
+
# ]
|
127
|
+
#
|
128
|
+
# @example json with pagination (used if either page_offset or page_limit are passed in)
|
129
|
+
# # request: q=term, page_offset=3, page_limit=2
|
130
|
+
# # response: format=json, paginated, results 3..4
|
131
|
+
# [
|
132
|
+
# { "id": "3", "label": "term 3" },
|
133
|
+
# { "id": "4", "label": "term 4" }
|
134
|
+
# ]
|
135
|
+
#
|
136
|
+
# @example json-api with pagination using default page_offset and page_limit
|
137
|
+
# # request: q=term, format=json-api
|
138
|
+
# # response: format=json-api, paginated, results 1..10
|
139
|
+
# {
|
140
|
+
# "data": [
|
141
|
+
# { "id": "1", "label": "term 1" },
|
142
|
+
# { "id": "2", "label": "term 2" },
|
143
|
+
# ...
|
144
|
+
# { "id": "10", "label": "term 10" }
|
145
|
+
# ],
|
146
|
+
# "meta": {
|
147
|
+
# "page": {
|
148
|
+
# "page_offset": "1",
|
149
|
+
# "page_limit": "10",
|
150
|
+
# "actual_page_size": "10",
|
151
|
+
# "total_num_found": "28",
|
152
|
+
# }
|
153
|
+
# }
|
154
|
+
# "links": {
|
155
|
+
# "self_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=10&page_offset=1",
|
156
|
+
# "first_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=10&page_offset=1",
|
157
|
+
# "prev_url": nil,
|
158
|
+
# "next_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=10&page_offset=11",
|
159
|
+
# "last_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=10&page_offset=21"
|
160
|
+
# }
|
161
|
+
# }
|
162
|
+
#
|
163
|
+
# @example json-api with pagination for page_offset=7 and page_limit=2
|
164
|
+
# # request: q=term, format=json-api, page_offset=7, page_limit=2
|
165
|
+
# # response: format=json, paginated, results 7..8
|
166
|
+
# {
|
167
|
+
# "data": [
|
168
|
+
# { "id": "7", "label": "term 7" },
|
169
|
+
# { "id": "8", "label": "term 8" }
|
170
|
+
# ],
|
171
|
+
# "meta": {
|
172
|
+
# "page": {
|
173
|
+
# "page_offset": "7",
|
174
|
+
# "page_limit": "2",
|
175
|
+
# "actual_page_size": "2",
|
176
|
+
# "total_num_found": "28",
|
177
|
+
# }
|
178
|
+
# }
|
179
|
+
# "links": {
|
180
|
+
# "self_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=2&page_offset=7",
|
181
|
+
# "first_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=2&page_offset=1",
|
182
|
+
# "prev_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=2&page_offset=5",
|
183
|
+
# "next_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=2&page_offset=9",
|
184
|
+
# "last_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=2&page_offset=27"
|
185
|
+
# }
|
186
|
+
# }
|
187
|
+
#
|
188
|
+
# @example json-api with page_offset and page_limit errors
|
189
|
+
# # request: q=term, format=json-api, page_offset=0, page_limit=-1
|
190
|
+
# # response: format=json-api, paginated, no results, errors
|
191
|
+
# {
|
192
|
+
# "data": [],
|
193
|
+
# "meta": {
|
194
|
+
# "page": {
|
195
|
+
# "page_offset": "0",
|
196
|
+
# "page_limit": "-1",
|
197
|
+
# "actual_page_size": nil,
|
198
|
+
# "total_num_found": "28",
|
199
|
+
# }
|
200
|
+
# }
|
201
|
+
# "links": {
|
202
|
+
# "self_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=-1&page_offset=0",
|
203
|
+
# "first_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=10&page_offset=1",
|
204
|
+
# "prev_url": nil,
|
205
|
+
# "next_url": nil,
|
206
|
+
# "last_url": "http://example.com/qa/search/local/states?q=new&format=json-api&page_limit=10&page_offset=21"
|
207
|
+
# }
|
208
|
+
# "errors": [
|
209
|
+
# {
|
210
|
+
# "status" => "200",
|
211
|
+
# "source" => { "page_offset" => "0" },
|
212
|
+
# "title" => "Page Offset Out of Range",
|
213
|
+
# "detail" => "Offset 0 < 1 (first result). Returning empty results."
|
214
|
+
# },
|
215
|
+
# {
|
216
|
+
# "status" => "200",
|
217
|
+
# "source" => { "page_limit" => "-1" },
|
218
|
+
# "title" => "Page Limit Out of Range",
|
219
|
+
# "detail" => "Page limit -1 < 1 (minimum limit). Returning empty results."
|
220
|
+
#
|
221
|
+
# }
|
222
|
+
# ]
|
223
|
+
# }
|
224
|
+
#
|
225
|
+
# @see DEFAULT_PAGE_LIMIT
|
226
|
+
# @see ERROR_NOT_INTEGER
|
227
|
+
# @see ERROR_OUT_OF_RANGE_TOO_SMALL
|
228
|
+
# @see ERROR_OUT_OF_RANGE_TOO_LARGE
|
229
|
+
def build_response
|
230
|
+
json_api? ? build_json_api_response : build_json_response
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def errors?
|
236
|
+
page_offset_error? || page_limit_error?
|
237
|
+
end
|
238
|
+
|
239
|
+
def page_offset_error?
|
240
|
+
page_offset
|
241
|
+
@page_offset_error
|
242
|
+
end
|
243
|
+
|
244
|
+
def page_limit_error?
|
245
|
+
page_limit
|
246
|
+
@page_limit_error
|
247
|
+
end
|
248
|
+
|
249
|
+
# @return just the data as a JSON array
|
250
|
+
def build_json_response
|
251
|
+
errors? ? [] : build_data
|
252
|
+
end
|
253
|
+
|
254
|
+
# @return pages of results following the JSON API standard
|
255
|
+
def build_json_api_response
|
256
|
+
errors? ? build_json_api_response_with_errors : build_json_api_response_without_errors
|
257
|
+
end
|
258
|
+
|
259
|
+
def build_json_api_response_without_errors
|
260
|
+
{
|
261
|
+
"data" => build_data,
|
262
|
+
"meta" => build_meta,
|
263
|
+
"links" => build_links
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
def build_json_api_response_with_errors
|
268
|
+
{
|
269
|
+
"data" => [],
|
270
|
+
"meta" => build_meta,
|
271
|
+
"links" => build_links_when_errors,
|
272
|
+
"errors" => build_errors
|
273
|
+
}
|
274
|
+
end
|
275
|
+
|
276
|
+
def build_data
|
277
|
+
@results[start_index..last_index]
|
278
|
+
end
|
279
|
+
|
280
|
+
def build_meta
|
281
|
+
meta = {}
|
282
|
+
meta['page_offset'] = page_offset_error? ? @requested_page_offset.to_s : page_offset.to_s
|
283
|
+
meta['page_limit'] = page_limit_error? ? @requested_page_limit.to_s : page_limit.to_s
|
284
|
+
meta['actual_page_size'] = errors? ? "0" : actual_page_size.to_s
|
285
|
+
meta['total_num_found'] = total_num_found.to_s
|
286
|
+
{ "page" => meta }
|
287
|
+
end
|
288
|
+
|
289
|
+
def build_links
|
290
|
+
links = {}
|
291
|
+
links['self'] = self_link
|
292
|
+
links['first'] = first_link
|
293
|
+
links['prev'] = prev_link
|
294
|
+
links['next'] = next_link
|
295
|
+
links['last'] = last_link
|
296
|
+
links
|
297
|
+
end
|
298
|
+
|
299
|
+
def build_links_when_errors
|
300
|
+
links = {}
|
301
|
+
links['self'] = "#{request_base_url}#{request_path}?#{request_query_string}"
|
302
|
+
links['first'] = first_link
|
303
|
+
links['prev'] = nil
|
304
|
+
links['next'] = nil
|
305
|
+
links['last'] = last_link
|
306
|
+
links
|
307
|
+
end
|
308
|
+
|
309
|
+
def build_errors
|
310
|
+
errors = []
|
311
|
+
errors << build_page_offset_error if page_offset_error?
|
312
|
+
errors << build_page_limit_error if page_limit_error?
|
313
|
+
errors
|
314
|
+
end
|
315
|
+
|
316
|
+
def build_page_offset_error
|
317
|
+
case @page_offset_error
|
318
|
+
when ERROR_NOT_INTEGER
|
319
|
+
build_page_offset_not_integer
|
320
|
+
when ERROR_OUT_OF_RANGE_TOO_LARGE
|
321
|
+
build_page_offset_too_large
|
322
|
+
when ERROR_OUT_OF_RANGE_TOO_SMALL
|
323
|
+
build_page_offset_too_small
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def build_page_limit_error
|
328
|
+
case @page_limit_error
|
329
|
+
when ERROR_NOT_INTEGER
|
330
|
+
build_page_limit_not_integer
|
331
|
+
when ERROR_OUT_OF_RANGE_TOO_SMALL
|
332
|
+
build_page_limit_too_small
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def build_page_offset_not_integer
|
337
|
+
{
|
338
|
+
"status" => "200",
|
339
|
+
"source" => { "page_offset" => @requested_page_offset },
|
340
|
+
"title" => "Page Offset Invalid",
|
341
|
+
"detail" => "Page offset #{@requested_page_offset} is not an Integer. Returning empty results."
|
342
|
+
}
|
343
|
+
end
|
344
|
+
|
345
|
+
def build_page_offset_too_large
|
346
|
+
{
|
347
|
+
"status" => "200",
|
348
|
+
"source" => { "page_offset" => @requested_page_offset },
|
349
|
+
"title" => "Page Offset Out of Range",
|
350
|
+
"detail" => "Page offset #{@requested_page_offset} > #{total_num_found} (total number of results). Returning empty results."
|
351
|
+
}
|
352
|
+
end
|
353
|
+
|
354
|
+
def build_page_offset_too_small
|
355
|
+
{
|
356
|
+
"status" => "200",
|
357
|
+
"source" => { "page_offset" => page_offset.to_s },
|
358
|
+
"title" => "Page Offset Out of Range",
|
359
|
+
"detail" => "Page offset #{@requested_page_offset} < 1 (first result). Returning empty results."
|
360
|
+
}
|
361
|
+
end
|
362
|
+
|
363
|
+
def build_page_limit_not_integer
|
364
|
+
{
|
365
|
+
"status" => "200",
|
366
|
+
"source" => { "page_limit" => @requested_page_limit },
|
367
|
+
"title" => "Page Limit Invalid",
|
368
|
+
"detail" => "Page limit #{@requested_page_limit} is not an Integer. Returning empty results."
|
369
|
+
}
|
370
|
+
end
|
371
|
+
|
372
|
+
def build_page_limit_too_small
|
373
|
+
{
|
374
|
+
"status" => "200",
|
375
|
+
"source" => { "page_limit" => @requested_page_limit.to_s },
|
376
|
+
"title" => "Page Limit Out of Range",
|
377
|
+
"detail" => "Page limit #{@requested_page_limit} < 1 (minimum limit). Returning empty results."
|
378
|
+
}
|
379
|
+
end
|
380
|
+
|
381
|
+
def request_params
|
382
|
+
@request_params ||= @request.params
|
383
|
+
end
|
384
|
+
|
385
|
+
def request_query_params
|
386
|
+
@request_query_params ||= @request.query_parameters
|
387
|
+
end
|
388
|
+
|
389
|
+
def request_query_string
|
390
|
+
@request_query_string ||= @request.query_string
|
391
|
+
end
|
392
|
+
|
393
|
+
def request_base_url
|
394
|
+
@request_base_url ||= @request.base_url
|
395
|
+
end
|
396
|
+
|
397
|
+
def request_path
|
398
|
+
@request_path ||= @request.path
|
399
|
+
end
|
400
|
+
|
401
|
+
# @return [Boolean] true if results should be formatted according to JSON API standard
|
402
|
+
def json_api?
|
403
|
+
format == :jsonapi
|
404
|
+
end
|
405
|
+
|
406
|
+
# @param [Symbol] The requested format of the response (default=:json)
|
407
|
+
# @note Supported formats include [:json | :json-api]. For backward compatibility,
|
408
|
+
# it defaults to :json.
|
409
|
+
def format
|
410
|
+
return @format if @format.present?
|
411
|
+
return @format = @requested_format if [:json, :jsonapi].include? @requested_format
|
412
|
+
Rails.logger.warn("Format '#{@requested_format}' is not a valid format for search. Supported formats are [:json, :jsonapi]. Defaulting to :json.")
|
413
|
+
@format = :json
|
414
|
+
end
|
415
|
+
|
416
|
+
# @return [Integer] The first record to include in the response data. (default=1).
|
417
|
+
# @note The first record may be out of range (i.e., < 1 or > total_num_of_results),
|
418
|
+
# but it will always be numeric, defaulting to 1 if not specified or not an Integer.
|
419
|
+
def page_offset
|
420
|
+
return @page_offset if @page_offset.present?
|
421
|
+
return @page_offset = 1 unless page_offset_specified?
|
422
|
+
@page_offset = validated_request_page_offset || 1
|
423
|
+
end
|
424
|
+
|
425
|
+
# @return [Boolean] true if the request specifies the page offset; otherwise, false
|
426
|
+
def page_offset_specified?
|
427
|
+
request_params.keys.include?("page_offset") || request_params.keys.include?("startRecord")
|
428
|
+
end
|
429
|
+
|
430
|
+
# @return [Integer] The page offset as specified in the request_params, nil if invalid.
|
431
|
+
# @note The page offset can be specified with page_offset (preferred) or
|
432
|
+
# startRecord (deprecated, supported for backward compatibility with
|
433
|
+
# linked_data module pagination).
|
434
|
+
def requested_page_offset
|
435
|
+
return @requested_page_offset if @requested_page_offset.present?
|
436
|
+
@requested_page_offset = (request_params['page_offset'] || request_params['startRecord'])
|
437
|
+
end
|
438
|
+
|
439
|
+
# @return [Integer] The first record to include in the response data as
|
440
|
+
# requested as long as it is an Integer; otherwise, returns nil.
|
441
|
+
def validated_request_page_offset
|
442
|
+
@page_offset_error = false
|
443
|
+
offset = Integer(requested_page_offset)
|
444
|
+
return offset if offset == 1
|
445
|
+
@page_offset_error = ERROR_OUT_OF_RANGE_TOO_SMALL if offset < 1
|
446
|
+
@page_offset_error = ERROR_OUT_OF_RANGE_TOO_LARGE if offset > total_num_found
|
447
|
+
offset
|
448
|
+
rescue ArgumentError
|
449
|
+
@page_offset_error = ERROR_NOT_INTEGER
|
450
|
+
nil
|
451
|
+
end
|
452
|
+
|
453
|
+
# @return [Integer] The requested maximum number of results to return (default=DEFAULT_PAGE_LIMIT | ALL)
|
454
|
+
# @see #default_page_limit
|
455
|
+
# @see DEFAULT_PAGE_LIMIT
|
456
|
+
def page_limit
|
457
|
+
return @page_limit if @page_limit.present?
|
458
|
+
return @page_limit = default_page_limit unless page_limit_specified?
|
459
|
+
@page_limit = validated_request_page_limit || default_page_limit
|
460
|
+
end
|
461
|
+
|
462
|
+
# @return [Boolean] true if the request specifies the page limit; otherwise, false
|
463
|
+
def page_limit_specified?
|
464
|
+
request_params.keys.include?("page_limit") || request_params.keys.include?("maxRecords")
|
465
|
+
end
|
466
|
+
|
467
|
+
# @return [Integer] The max number of records for a page as specified in the request_params.
|
468
|
+
# @note The page size limit can be specified with page_limit (preferred) or
|
469
|
+
# maxRecords (deprecated, supported for backward compatibility with
|
470
|
+
# linked_data module pagination).
|
471
|
+
def requested_page_limit
|
472
|
+
return @requested_page_limit if @requested_page_limit.present?
|
473
|
+
@requested_page_limit ||= (request_params['page_limit'] || request_params['maxRecords'])
|
474
|
+
end
|
475
|
+
|
476
|
+
# @return [Integer] The max number of records to include in response data as
|
477
|
+
# requested as long as it is a positive Integer; otherwise, returns nil.
|
478
|
+
def validated_request_page_limit
|
479
|
+
@page_limit_error = false
|
480
|
+
limit = Integer(requested_page_limit)
|
481
|
+
@page_limit_error = ERROR_OUT_OF_RANGE_TOO_SMALL if limit < 1
|
482
|
+
limit.positive? ? limit : nil
|
483
|
+
rescue ArgumentError
|
484
|
+
@page_limit_error = ERROR_NOT_INTEGER
|
485
|
+
nil
|
486
|
+
end
|
487
|
+
|
488
|
+
# @return [Integer] The default to use when page_limit isn't specified.
|
489
|
+
# @note To maintain backward compatibility, the limit will be all results
|
490
|
+
# if format is json and neither page_limit nor page_offset were specified.
|
491
|
+
def default_page_limit
|
492
|
+
return total_num_found unless json_api? || page_offset_specified? || page_limit_specified?
|
493
|
+
DEFAULT_PAGE_LIMIT
|
494
|
+
end
|
495
|
+
|
496
|
+
# @return [Integer] the index into the terms Array for the first record to
|
497
|
+
# include in the page data
|
498
|
+
# @note page_offset begins counting at 1 and the Array index begins at 0.
|
499
|
+
def start_index
|
500
|
+
@start_index ||= page_offset - 1
|
501
|
+
end
|
502
|
+
|
503
|
+
# @return [Integer] the index into the terms Array for the last record to
|
504
|
+
# include in the page data
|
505
|
+
def last_index
|
506
|
+
return @last_index if @last_index.present?
|
507
|
+
return @last_index = start_index if start_index >= last_possible_index
|
508
|
+
last_index = start_index + page_limit - 1
|
509
|
+
@last_index = last_index <= last_possible_index ? last_index : last_possible_index
|
510
|
+
end
|
511
|
+
|
512
|
+
# @return [Integer] the index for the last term in the Array
|
513
|
+
def last_possible_index
|
514
|
+
total_num_found - 1
|
515
|
+
end
|
516
|
+
|
517
|
+
# @return [Integer] actual number of results in the returned page of results;
|
518
|
+
# 0 if request is out of range
|
519
|
+
def actual_page_size
|
520
|
+
@actual_page_size ||= start_index <= last_possible_index ? last_index - start_index + 1 : 0
|
521
|
+
end
|
522
|
+
|
523
|
+
# @return [Integer] total number of terms matching the search query
|
524
|
+
def total_num_found
|
525
|
+
@results.length
|
526
|
+
end
|
527
|
+
|
528
|
+
# @return the URL to current page of results
|
529
|
+
def self_link
|
530
|
+
url_with(page_offset: page_offset)
|
531
|
+
end
|
532
|
+
|
533
|
+
# @return the URL to the first page of results
|
534
|
+
def first_link
|
535
|
+
url_with(page_offset: 1)
|
536
|
+
end
|
537
|
+
|
538
|
+
# @return the URL to the last page of results
|
539
|
+
def last_link
|
540
|
+
last_start = (total_num_found / page_limit) * page_limit + 1
|
541
|
+
last_start -= page_limit if last_start > total_num_found
|
542
|
+
last_start = 1 if last_start < 1
|
543
|
+
url_with(page_offset: last_start)
|
544
|
+
end
|
545
|
+
|
546
|
+
# @return the URL to the next page of results; nil if on last page
|
547
|
+
def next_link
|
548
|
+
next_start = page_offset + page_limit
|
549
|
+
next_start <= total_num_found ? url_with(page_offset: next_start) : nil
|
550
|
+
end
|
551
|
+
|
552
|
+
# @return the URL to the previous page of results; nil if on first page
|
553
|
+
def prev_link
|
554
|
+
return if page_offset == 1
|
555
|
+
prev_start = page_offset - page_limit
|
556
|
+
prev_start >= 1 ? url_with(page_offset: prev_start) : url_with(page_offset: 1)
|
557
|
+
end
|
558
|
+
|
559
|
+
# @return the original URL without the parameters
|
560
|
+
def url_without_parameters
|
561
|
+
URI.parse(request_base_url + request_path)
|
562
|
+
end
|
563
|
+
|
564
|
+
# Generate a URL based off the original URL and parameter values with page_offset
|
565
|
+
# updated based on the passed in value.
|
566
|
+
# @param page_offset [Integer] the value to use for page_offset
|
567
|
+
# @return [String] a full URL with the updated page_offset
|
568
|
+
def url_with(page_offset:)
|
569
|
+
updated_params = update_parameters(page_offset)
|
570
|
+
|
571
|
+
uri = url_without_parameters
|
572
|
+
uri.query = URI.encode_www_form(updated_params)
|
573
|
+
uri.to_s
|
574
|
+
end
|
575
|
+
|
576
|
+
# @param page_offset [Integer] the value to use for page_offset
|
577
|
+
# @return [Hash] parameter key-value pairs formatted for the URL using
|
578
|
+
# the preferred parameter name and updated page_offset value
|
579
|
+
def update_parameters(page_offset)
|
580
|
+
updated_params = request_query_params.except('page_offset', 'page_limit')
|
581
|
+
updated_params['page_limit'] = page_limit
|
582
|
+
updated_params['page_offset'] = page_offset
|
583
|
+
updated_params
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
data/config/routes.rb
CHANGED
@@ -9,6 +9,8 @@ Qa::Engine.routes.draw do
|
|
9
9
|
get "/search/:vocab(/:subauthority)", controller: :terms, action: :search
|
10
10
|
get "/show/:vocab/:id", controller: :terms, action: :show
|
11
11
|
get "/show/:vocab/:subauthority/:id", controller: :terms, action: :show
|
12
|
+
get "/fetch/:vocab", controller: :terms, action: :fetch
|
13
|
+
get "/fetch/:vocab/:subauthority", controller: :terms, action: :fetch
|
12
14
|
|
13
15
|
match "/search/linked_data/:vocab(/:subauthority)", to: 'application#options', via: [:options]
|
14
16
|
match "/show/linked_data/:vocab/:id", to: 'application#options', via: [:options]
|
@@ -17,4 +19,6 @@ Qa::Engine.routes.draw do
|
|
17
19
|
match "/search/:vocab(/:subauthority)", to: 'application#options', via: [:options]
|
18
20
|
match "/show/:vocab/:id", to: 'application#options', via: [:options]
|
19
21
|
match "/show/:vocab/:subauthority/:id", to: 'application#options', via: [:options]
|
22
|
+
match "/fetch/:vocab", to: 'application#options', via: [:options]
|
23
|
+
match "/fetch/:vocab/:subauthority", to: 'application#options', via: [:options]
|
20
24
|
end
|
@@ -8,13 +8,14 @@ module Qa::Authorities
|
|
8
8
|
def search(q)
|
9
9
|
r = q.blank? ? [] : terms.select { |term| /\b#{q.downcase}/.match(term[:term].downcase) }
|
10
10
|
r.map do |res|
|
11
|
-
{ id: res[:id], label: res[:term] }.with_indifferent_access
|
11
|
+
{ id: res[:id], label: res[:term], uri: res.fetch(:uri, nil) }.compact.with_indifferent_access
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
def all
|
16
16
|
terms.map do |res|
|
17
|
-
{ id: res[:id], label: res[:term], active: res.fetch(:active, true) }
|
17
|
+
{ id: res[:id], label: res[:term], active: res.fetch(:active, true), uri: res.fetch(:uri, nil) }
|
18
|
+
.compact.with_indifferent_access
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
data/lib/qa/version.rb
CHANGED