oai 1.0.0 → 1.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 +4 -4
- data/README.md +19 -4
- data/Rakefile +7 -0
- data/bin/oai +0 -2
- data/examples/models/file_model.rb +2 -2
- data/lib/oai/client/response.rb +8 -8
- data/lib/oai/client.rb +34 -10
- data/lib/oai/exception.rb +46 -38
- data/lib/oai/harvester/config.rb +1 -1
- data/lib/oai/harvester/harvest.rb +37 -25
- data/lib/oai/harvester/logging.rb +3 -5
- data/lib/oai/harvester.rb +4 -1
- data/lib/oai/provider/model/activerecord_caching_wrapper.rb +5 -8
- data/lib/oai/provider/model/activerecord_wrapper.rb +41 -25
- data/lib/oai/provider/model.rb +1 -1
- data/lib/oai/provider/response/list_records.rb +12 -0
- data/lib/oai/provider/response.rb +7 -4
- data/lib/oai/provider/resumption_token.rb +70 -21
- data/lib/oai/provider.rb +129 -7
- data/test/activerecord_provider/database/0001_oaipmh_tables.rb +7 -1
- data/test/activerecord_provider/helpers/providers.rb +10 -1
- data/test/activerecord_provider/helpers/transactional_test_case.rb +2 -1
- data/test/activerecord_provider/models/dc_field.rb +8 -0
- data/test/activerecord_provider/models/dc_lang.rb +3 -0
- data/test/activerecord_provider/models/exclusive_set_dc_field.rb +6 -0
- data/test/activerecord_provider/tc_activerecord_wrapper.rb +63 -0
- data/test/activerecord_provider/tc_ar_provider.rb +54 -26
- data/test/activerecord_provider/tc_ar_sets_provider.rb +10 -9
- data/test/activerecord_provider/tc_caching_paging_provider.rb +9 -7
- data/test/activerecord_provider/tc_simple_paging_provider.rb +28 -7
- data/test/client/tc_exception.rb +1 -1
- data/test/client/tc_get_record.rb +1 -1
- data/test/client/tc_http_client.rb +2 -2
- data/test/client/tc_libxml.rb +1 -1
- data/test/client/tc_utf8_escaping.rb +8 -1
- data/test/harvester/tc_harvest.rb +42 -0
- data/test/harvester/test_helper_harvester.rb +6 -0
- data/test/provider/models.rb +3 -3
- data/test/provider/tc_functional_tokens.rb +17 -11
- data/test/provider/tc_instance_provider.rb +41 -0
- data/test/provider/tc_provider.rb +26 -0
- data/test/provider/tc_resumption_tokens.rb +6 -0
- data/test/provider/test_helper_provider.rb +17 -0
- metadata +28 -17
@@ -32,12 +32,12 @@ module OAI::Provider
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def earliest
|
35
|
-
earliest_obj = model.order("#{timestamp_field} asc").first
|
35
|
+
earliest_obj = model.order("#{model.base_class.table_name}.#{timestamp_field} asc").first
|
36
36
|
earliest_obj.nil? ? Time.at(0) : earliest_obj.send(timestamp_field)
|
37
37
|
end
|
38
38
|
|
39
39
|
def latest
|
40
|
-
latest_obj = model.order("#{timestamp_field} desc").first
|
40
|
+
latest_obj = model.order("#{model.base_class.table_name}.#{timestamp_field} desc").first
|
41
41
|
latest_obj.nil? ? Time.now : latest_obj.send(timestamp_field)
|
42
42
|
end
|
43
43
|
# A model class is expected to provide a method Model.sets that
|
@@ -61,7 +61,7 @@ module OAI::Provider
|
|
61
61
|
find_scope.where(conditions)
|
62
62
|
end
|
63
63
|
else
|
64
|
-
find_scope.where(conditions).
|
64
|
+
find_scope.where(conditions).where(identifier_field => selector).first
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
@@ -129,15 +129,7 @@ module OAI::Provider
|
|
129
129
|
raise OAI::ResumptionTokenException.new unless @limit
|
130
130
|
|
131
131
|
token = ResumptionToken.parse(token_string)
|
132
|
-
|
133
|
-
|
134
|
-
if @limit < total
|
135
|
-
select_partial(find_scope, token)
|
136
|
-
else # end of result set
|
137
|
-
find_scope.where(token_conditions(token))
|
138
|
-
.limit(@limit)
|
139
|
-
.order("#{identifier_field} asc")
|
140
|
-
end
|
132
|
+
select_partial(find_scope, token)
|
141
133
|
end
|
142
134
|
|
143
135
|
# select a subset of the result set, and return it with a
|
@@ -145,10 +137,12 @@ module OAI::Provider
|
|
145
137
|
def select_partial(find_scope, token)
|
146
138
|
records = find_scope.where(token_conditions(token))
|
147
139
|
.limit(@limit)
|
148
|
-
.order("#{identifier_field} asc")
|
140
|
+
.order("#{model.base_class.table_name}.#{identifier_field} asc")
|
149
141
|
raise OAI::ResumptionTokenException.new unless records
|
150
|
-
offset = records.last.send(identifier_field)
|
151
142
|
|
143
|
+
total = find_scope.where(token_conditions(token)).count
|
144
|
+
# token offset should be nil if this is the last set
|
145
|
+
offset = (@limit >= total) ? nil : records.last.send(identifier_field)
|
152
146
|
PartialResult.new(records, token.next(offset))
|
153
147
|
end
|
154
148
|
|
@@ -159,13 +153,13 @@ module OAI::Provider
|
|
159
153
|
# the last 'id' of the previous set is used as the
|
160
154
|
# filter to the next set.
|
161
155
|
def token_conditions(token)
|
162
|
-
|
156
|
+
last_id = token.last_str
|
163
157
|
sql = sql_conditions token.to_conditions_hash
|
164
158
|
|
165
|
-
return sql if 0 ==
|
159
|
+
return sql if "0" == last_id
|
166
160
|
# Now add last id constraint
|
167
|
-
sql.first << " AND #{identifier_field} > :id"
|
168
|
-
sql.last[:id] =
|
161
|
+
sql.first << " AND #{model.base_class.table_name}.#{identifier_field} > :id"
|
162
|
+
sql.last[:id] = last_id
|
169
163
|
|
170
164
|
return sql
|
171
165
|
end
|
@@ -175,27 +169,49 @@ module OAI::Provider
|
|
175
169
|
sql = []
|
176
170
|
esc_values = {}
|
177
171
|
if opts.has_key?(:from)
|
178
|
-
sql << "#{timestamp_field} >= :from"
|
172
|
+
sql << "#{model.base_class.table_name}.#{timestamp_field} >= :from"
|
179
173
|
esc_values[:from] = parse_to_local(opts[:from])
|
180
174
|
end
|
181
175
|
if opts.has_key?(:until)
|
182
176
|
# Handle databases which store fractions of a second by rounding up
|
183
|
-
sql << "#{timestamp_field} < :until"
|
177
|
+
sql << "#{model.base_class.table_name}.#{timestamp_field} < :until"
|
184
178
|
esc_values[:until] = parse_to_local(opts[:until]) { |t| t + 1 }
|
185
179
|
end
|
180
|
+
|
186
181
|
return [sql.join(" AND "), esc_values]
|
187
182
|
end
|
188
183
|
|
189
184
|
private
|
190
185
|
|
191
186
|
def parse_to_local(time)
|
192
|
-
|
187
|
+
if time.respond_to?(:strftime)
|
188
|
+
time_obj = time
|
189
|
+
else
|
190
|
+
begin
|
191
|
+
if time[-1] == "Z"
|
192
|
+
time_obj = Time.strptime(time, "%Y-%m-%dT%H:%M:%S%Z")
|
193
|
+
else
|
194
|
+
time_obj = Date.strptime(time, "%Y-%m-%d")
|
195
|
+
end
|
196
|
+
rescue
|
197
|
+
raise OAI::ArgumentException.new, "unparsable date: '#{time}'"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
193
201
|
time_obj = yield(time_obj) if block_given?
|
194
|
-
|
195
|
-
|
196
|
-
|
202
|
+
|
203
|
+
if time_obj.kind_of?(Date)
|
204
|
+
time_obj.strftime("%Y-%m-%d")
|
205
|
+
else
|
206
|
+
# Convert to same as DB - :local => :getlocal, :utc => :getutc
|
207
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
208
|
+
tzconv = "get#{ActiveRecord.default_timezone.to_s}".to_sym
|
209
|
+
else
|
210
|
+
tzconv = "get#{model.default_timezone.to_s}".to_sym
|
211
|
+
end
|
212
|
+
time_obj.send(tzconv).strftime("%Y-%m-%d %H:%M:%S")
|
213
|
+
end
|
197
214
|
end
|
198
215
|
|
199
216
|
end
|
200
217
|
end
|
201
|
-
|
data/lib/oai/provider/model.rb
CHANGED
@@ -29,7 +29,7 @@ module OAI::Provider
|
|
29
29
|
# see the ResumptionToken class for more details.
|
30
30
|
#
|
31
31
|
class Model
|
32
|
-
attr_reader :timestamp_field, :identifier_field
|
32
|
+
attr_reader :timestamp_field, :identifier_field, :limit
|
33
33
|
|
34
34
|
def initialize(limit = nil, timestamp_field = 'updated_at', identifier_field = 'id')
|
35
35
|
@limit = limit
|
@@ -2,6 +2,18 @@ module OAI::Provider::Response
|
|
2
2
|
|
3
3
|
class ListRecords < RecordResponse
|
4
4
|
required_parameters :metadata_prefix
|
5
|
+
|
6
|
+
def valid?
|
7
|
+
super && matching_granularity?
|
8
|
+
end
|
9
|
+
|
10
|
+
def matching_granularity?
|
11
|
+
if options[:from].nil? == false && options[:until].nil? == false && options[:from].class.name != options[:until].class.name
|
12
|
+
raise OAI::ArgumentException.new, "The 'from' and 'until' options specified must have the same granularity"
|
13
|
+
else
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
5
17
|
|
6
18
|
def to_xml
|
7
19
|
result = provider.model.find(:all, options)
|
@@ -90,10 +90,13 @@ module OAI
|
|
90
90
|
|
91
91
|
def parse_date(value)
|
92
92
|
return value if value.respond_to?(:strftime)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
93
|
+
|
94
|
+
if value[-1] == "Z"
|
95
|
+
Time.strptime(value, "%Y-%m-%dT%H:%M:%S%Z").utc
|
96
|
+
else
|
97
|
+
Date.strptime(value, "%Y-%m-%d")
|
98
|
+
end
|
99
|
+
rescue ArgumentError => e
|
97
100
|
raise OAI::ArgumentException.new, "unparsable date: '#{value}'"
|
98
101
|
end
|
99
102
|
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'time'
|
2
|
-
require 'enumerator'
|
3
2
|
require File.dirname(__FILE__) + "/partial_result"
|
4
3
|
|
5
4
|
module OAI::Provider
|
@@ -8,16 +7,41 @@ module OAI::Provider
|
|
8
7
|
# The ResumptionToken class forms the basis of paging query results. It
|
9
8
|
# provides several helper methods for dealing with resumption tokens.
|
10
9
|
#
|
10
|
+
# OAI-PMH spec does not specify anything about resumptionToken format, they can
|
11
|
+
# be purely opaque tokens.
|
12
|
+
#
|
13
|
+
# Our implementation however encodes everything needed to construct the next page
|
14
|
+
# inside the resumption token.
|
15
|
+
#
|
16
|
+
# == The 'last' component: offset or ID/pk to resume from
|
17
|
+
#
|
18
|
+
# The `#last` component is an offset or ID to resume from. In the case of it being
|
19
|
+
# an ID to resume from, this assumes that ID's are sortable and results are returned
|
20
|
+
# in ID order, so that the 'last' ID can be used as the place to resume from.
|
21
|
+
#
|
22
|
+
# Originally it was assumed that #last was always an integer, but since existing
|
23
|
+
# implementations (like ActiveRecordWrapper) used it as an ID, and identifiers and
|
24
|
+
# primary keys are _not_ always integers (can be UUID etc), we have expanded to allow
|
25
|
+
# any string value.
|
26
|
+
#
|
27
|
+
# However, for backwards compatibility #last always returns an integer (sometimes 0 if
|
28
|
+
# actual last component is not an integer), and #last_str returns the full string version.
|
29
|
+
# Trying to change #last itself to be string broke a lot of existing code in this gem
|
30
|
+
# in mysterious ways.
|
31
|
+
#
|
32
|
+
# Also beware that in some cases the value 0/"0" seems to be a special value used
|
33
|
+
# to signify some special case. A lot of "code archeology" going on here after significant
|
34
|
+
# period of no maintenance to this gem.
|
11
35
|
class ResumptionToken
|
12
|
-
attr_reader :prefix, :set, :from, :until, :last, :expiration, :total
|
36
|
+
attr_reader :prefix, :set, :from, :until, :last, :last_str, :expiration, :total
|
13
37
|
|
14
38
|
# parses a token string and returns a ResumptionToken
|
15
|
-
def self.parse(token_string)
|
39
|
+
def self.parse(token_string, expiration = nil, total = nil)
|
16
40
|
begin
|
17
41
|
options = {}
|
18
|
-
matches = /(.+):(
|
19
|
-
options[:last] = matches.captures[1]
|
20
|
-
|
42
|
+
matches = /(.+):([^ :]+)$/.match(token_string)
|
43
|
+
options[:last] = matches.captures[1]
|
44
|
+
|
21
45
|
parts = matches.captures[0].split('.')
|
22
46
|
options[:metadata_prefix] = parts.shift
|
23
47
|
parts.each do |part|
|
@@ -30,12 +54,12 @@ module OAI::Provider
|
|
30
54
|
options[:until] = Time.parse(part.sub(/^u\(/, '').sub(/\)$/, '')).localtime
|
31
55
|
end
|
32
56
|
end
|
33
|
-
self.new(options)
|
57
|
+
self.new(options, expiration, total)
|
34
58
|
rescue => err
|
35
59
|
raise OAI::ResumptionTokenException.new
|
36
60
|
end
|
37
61
|
end
|
38
|
-
|
62
|
+
|
39
63
|
# extracts the metadata prefix from a token string
|
40
64
|
def self.extract_format(token_string)
|
41
65
|
return token_string.split('.')[0]
|
@@ -44,32 +68,32 @@ module OAI::Provider
|
|
44
68
|
def initialize(options, expiration = nil, total = nil)
|
45
69
|
@prefix = options[:metadata_prefix]
|
46
70
|
@set = options[:set]
|
47
|
-
|
71
|
+
self.last = options[:last]
|
48
72
|
@from = options[:from] if options[:from]
|
49
73
|
@until = options[:until] if options[:until]
|
50
74
|
@expiration = expiration if expiration
|
51
75
|
@total = total if total
|
52
76
|
end
|
53
|
-
|
77
|
+
|
54
78
|
# convenience method for setting the offset of the next set of results
|
55
79
|
def next(last)
|
56
|
-
|
80
|
+
self.last = last
|
57
81
|
self
|
58
82
|
end
|
59
|
-
|
83
|
+
|
60
84
|
def ==(other)
|
61
85
|
prefix == other.prefix and set == other.set and from == other.from and
|
62
|
-
self.until == other.until and last == other.last and
|
86
|
+
self.until == other.until and last == other.last and
|
63
87
|
expiration == other.expiration and total == other.total
|
64
88
|
end
|
65
|
-
|
89
|
+
|
66
90
|
# output an xml resumption token
|
67
91
|
def to_xml
|
68
92
|
xml = Builder::XmlMarkup.new
|
69
93
|
xml.resumptionToken(encode_conditions, hash_of_attributes)
|
70
94
|
xml.target!
|
71
95
|
end
|
72
|
-
|
96
|
+
|
73
97
|
# return a hash containing just the model selection parameters
|
74
98
|
def to_conditions_hash
|
75
99
|
conditions = {:metadata_prefix => self.prefix }
|
@@ -78,20 +102,45 @@ module OAI::Provider
|
|
78
102
|
conditions[:until] = self.until if self.until
|
79
103
|
conditions
|
80
104
|
end
|
81
|
-
|
82
|
-
# return the a string representation of the token minus the offset
|
105
|
+
|
106
|
+
# return the a string representation of the token minus the offset/ID
|
107
|
+
#
|
108
|
+
# Q: Why does it eliminate the offset/id "last" on the end? Doesn't fully
|
109
|
+
# represent state without it, which is confusing. Not sure, but
|
110
|
+
# other code seems to rely on it, tests break if not.
|
83
111
|
def to_s
|
84
112
|
encode_conditions.gsub(/:\w+?$/, '')
|
85
113
|
end
|
86
114
|
|
87
115
|
private
|
88
|
-
|
116
|
+
|
117
|
+
# take care of our logic to store an integer and a str version, for backwards
|
118
|
+
# compat where it was assumed to be an integer, as well as supporting string.
|
119
|
+
def last=(value)
|
120
|
+
@last = value.to_i
|
121
|
+
@last_str = value.to_s
|
122
|
+
end
|
123
|
+
|
89
124
|
def encode_conditions
|
125
|
+
return "" if last_str.nil? || last_str.to_s.strip.eql?("")
|
126
|
+
|
90
127
|
encoded_token = @prefix.to_s.dup
|
91
128
|
encoded_token << ".s(#{set})" if set
|
92
|
-
|
93
|
-
|
94
|
-
|
129
|
+
if self.from
|
130
|
+
if self.from.respond_to?(:utc)
|
131
|
+
encoded_token << ".f(#{self.from.utc.xmlschema})"
|
132
|
+
else
|
133
|
+
encoded_token << ".f(#{self.from.xmlschema})"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
if self.until
|
137
|
+
if self.until.respond_to?(:utc)
|
138
|
+
encoded_token << ".u(#{self.until.utc.xmlschema})"
|
139
|
+
else
|
140
|
+
encoded_token << ".u(#{self.until.xmlschema})"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
encoded_token << ":#{last_str}"
|
95
144
|
end
|
96
145
|
|
97
146
|
def hash_of_attributes
|
data/lib/oai/provider.rb
CHANGED
@@ -139,6 +139,35 @@ end
|
|
139
139
|
#
|
140
140
|
# Special thanks to Jose Hales-Garcia for this solution.
|
141
141
|
#
|
142
|
+
# ### Leverage the Provider instance
|
143
|
+
#
|
144
|
+
# The traditional implementation of the OAI::Provider would pass the OAI::Provider
|
145
|
+
# class to the different resposnes. This made it hard to inject context into a
|
146
|
+
# common provider. Consider that we might have different request headers that
|
147
|
+
# change the scope of the OAI::Provider queries.
|
148
|
+
#
|
149
|
+
# ```ruby
|
150
|
+
# class InstanceProvider
|
151
|
+
# def initialize(options = {})
|
152
|
+
# super({ :provider_context => :instance_based })
|
153
|
+
# @controller = options.fetch(:controller)
|
154
|
+
# end
|
155
|
+
# attr_reader :controller
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# class OaiController < ApplicationController
|
159
|
+
# def index
|
160
|
+
# provider = InstanceProvider.new({ :controller => self })
|
161
|
+
# request_body = provider.process_request(oai_params.to_h)
|
162
|
+
# render :body => request_body, :content_type => 'text/xml'
|
163
|
+
# end
|
164
|
+
# ```
|
165
|
+
#
|
166
|
+
# In the above example, the underlying response object will now receive an
|
167
|
+
# instance of the InstanceProvider. Without the `super({ :provider_context => :instance_based })`
|
168
|
+
# the response objects would have received the class InstanceProvider as the
|
169
|
+
# given provider.
|
170
|
+
#
|
142
171
|
# ## Supporting custom metadata formats
|
143
172
|
#
|
144
173
|
# See {OAI::MetadataFormat} for details.
|
@@ -292,39 +321,132 @@ module OAI::Provider
|
|
292
321
|
|
293
322
|
Base.register_format(OAI::Provider::Metadata::DublinCore.instance)
|
294
323
|
|
324
|
+
PROVIDER_CONTEXTS = {
|
325
|
+
:class_based => :class_based,
|
326
|
+
:instance_based => :instance_based
|
327
|
+
}
|
328
|
+
|
329
|
+
def initialize(options = {})
|
330
|
+
provider_context = options.fetch(:provider_context) { :class_based }
|
331
|
+
@provider_context = PROVIDER_CONTEXTS.fetch(provider_context)
|
332
|
+
end
|
333
|
+
|
334
|
+
# @note These are the accessor methods on the class. If you need to overwrite
|
335
|
+
# them on the instance level you can do that. However, an instance of this
|
336
|
+
# class won't be used unless you initialize with:
|
337
|
+
# { :provider_context => :instance_based }
|
338
|
+
attr_writer :name, :url, :prefix, :email, :delete_support, :granularity, :model, :identifier, :description
|
339
|
+
|
340
|
+
# The traditional interaction of a Provider has been to:
|
341
|
+
#
|
342
|
+
# 1) Assign attributes to the Provider class
|
343
|
+
# 2) Instantiate the Provider class
|
344
|
+
# 3) Call response instance methods for theProvider which pass
|
345
|
+
# the Provider class and not the instance.
|
346
|
+
#
|
347
|
+
# The above behavior continues unless you initialize the Provider with
|
348
|
+
# { :provider_context => :instance_based }. If you do that, then the
|
349
|
+
# Provider behavior will be:
|
350
|
+
#
|
351
|
+
# 1) Assign attributes to Provider class
|
352
|
+
# 2) Instantiate the Provider class
|
353
|
+
# 3) Call response instance methods for theProvider which pass an
|
354
|
+
# instance of the Provider to those response objects.
|
355
|
+
# a) The instance will mirror all of the assigned Provider class
|
356
|
+
# attributes, but allows for overriding and extending on a
|
357
|
+
# case by case basis.
|
358
|
+
# (Dear reader, please note the second behavior is something most
|
359
|
+
# of us would've assumed to be the case, but for historic now lost
|
360
|
+
# reasons is not the case.)
|
361
|
+
def provider_context
|
362
|
+
if @provider_context == :instance_based
|
363
|
+
self
|
364
|
+
else
|
365
|
+
self.class
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def format_supported?(*args)
|
370
|
+
self.class.format_supported?(*args)
|
371
|
+
end
|
372
|
+
|
373
|
+
def format(*args)
|
374
|
+
self.class.format(*args)
|
375
|
+
end
|
376
|
+
|
377
|
+
def formats
|
378
|
+
self.class.formats
|
379
|
+
end
|
380
|
+
|
381
|
+
def name
|
382
|
+
@name || self.class.name
|
383
|
+
end
|
384
|
+
|
385
|
+
def url
|
386
|
+
@url || self.class.url
|
387
|
+
end
|
388
|
+
|
389
|
+
def prefix
|
390
|
+
@prefix || self.class.prefix
|
391
|
+
end
|
392
|
+
|
393
|
+
def email
|
394
|
+
@email || self.class.email
|
395
|
+
end
|
396
|
+
|
397
|
+
def delete_support
|
398
|
+
@delete_support || self.class.delete_support
|
399
|
+
end
|
400
|
+
|
401
|
+
def granularity
|
402
|
+
@granularity || self.class.granularity
|
403
|
+
end
|
404
|
+
|
405
|
+
def model
|
406
|
+
@model || self.class.model
|
407
|
+
end
|
408
|
+
|
409
|
+
def identifier
|
410
|
+
@identifier || self.class.identifier
|
411
|
+
end
|
412
|
+
|
413
|
+
def description
|
414
|
+
@description || self.class.description
|
415
|
+
end
|
416
|
+
|
295
417
|
# Equivalent to '&verb=Identify', returns information about the repository
|
296
418
|
def identify(options = {})
|
297
|
-
Response::Identify.new(
|
419
|
+
Response::Identify.new(provider_context, options).to_xml
|
298
420
|
end
|
299
421
|
|
300
422
|
# Equivalent to '&verb=ListSets', returns a list of sets that are supported
|
301
423
|
# by the repository or an error if sets are not supported.
|
302
424
|
def list_sets(options = {})
|
303
|
-
Response::ListSets.new(
|
425
|
+
Response::ListSets.new(provider_context, options).to_xml
|
304
426
|
end
|
305
427
|
|
306
428
|
# Equivalent to '&verb=ListMetadataFormats', returns a list of metadata formats
|
307
429
|
# supported by the repository.
|
308
430
|
def list_metadata_formats(options = {})
|
309
|
-
Response::ListMetadataFormats.new(
|
431
|
+
Response::ListMetadataFormats.new(provider_context, options).to_xml
|
310
432
|
end
|
311
433
|
|
312
434
|
# Equivalent to '&verb=ListIdentifiers', returns a list of record headers that
|
313
435
|
# meet the supplied criteria.
|
314
436
|
def list_identifiers(options = {})
|
315
|
-
Response::ListIdentifiers.new(
|
437
|
+
Response::ListIdentifiers.new(provider_context, options).to_xml
|
316
438
|
end
|
317
439
|
|
318
440
|
# Equivalent to '&verb=ListRecords', returns a list of records that meet the
|
319
441
|
# supplied criteria.
|
320
442
|
def list_records(options = {})
|
321
|
-
Response::ListRecords.new(
|
443
|
+
Response::ListRecords.new(provider_context, options).to_xml
|
322
444
|
end
|
323
445
|
|
324
446
|
# Equivalent to '&verb=GetRecord', returns a record matching the required
|
325
447
|
# :identifier option
|
326
448
|
def get_record(options = {})
|
327
|
-
Response::GetRecord.new(
|
449
|
+
Response::GetRecord.new(provider_context, options).to_xml
|
328
450
|
end
|
329
451
|
|
330
452
|
# xml_response = process_verb('ListRecords', :from => 'October 1, 2005',
|
@@ -336,7 +458,7 @@ module OAI::Provider
|
|
336
458
|
begin
|
337
459
|
|
338
460
|
# Allow the request to pass in a url
|
339
|
-
|
461
|
+
provider_context.url = params['url'] ? params.delete('url') : self.url
|
340
462
|
|
341
463
|
verb = params.delete('verb') || params.delete(:verb)
|
342
464
|
|
@@ -10,6 +10,12 @@ class OaipmhTables < ActiveRecord::Migration[5.2]
|
|
10
10
|
t.column :oai_token_id, :integer, :null => false
|
11
11
|
end
|
12
12
|
|
13
|
+
create_table :dc_langs do |t|
|
14
|
+
t.column :name, :string
|
15
|
+
t.column :updated_at, :datetime
|
16
|
+
t.column :created_at, :datetime
|
17
|
+
end
|
18
|
+
|
13
19
|
dc_fields = proc do |t|
|
14
20
|
t.column :title, :string
|
15
21
|
t.column :creator, :string
|
@@ -21,7 +27,7 @@ class OaipmhTables < ActiveRecord::Migration[5.2]
|
|
21
27
|
t.column :type, :string
|
22
28
|
t.column :format, :string
|
23
29
|
t.column :source, :string
|
24
|
-
t.column :
|
30
|
+
t.column :dc_lang_id, :integer
|
25
31
|
t.column :relation, :string
|
26
32
|
t.column :coverage, :string
|
27
33
|
t.column :rights, :string
|
@@ -35,6 +35,13 @@ class SimpleResumptionProvider < OAI::Provider::Base
|
|
35
35
|
source_model ActiveRecordWrapper.new(DCField, :limit => 25)
|
36
36
|
end
|
37
37
|
|
38
|
+
class SimpleResumptionProviderWithNonIntegerID < OAI::Provider::Base
|
39
|
+
repository_name 'ActiveRecord Resumption Provider With Non-Integer ID'
|
40
|
+
repository_url 'http://localhost'
|
41
|
+
record_prefix 'oai:test'
|
42
|
+
source_model ActiveRecordWrapper.new(DCField, :limit => 25, identifier_field: "source")
|
43
|
+
end
|
44
|
+
|
38
45
|
class CachingResumptionProvider < OAI::Provider::Base
|
39
46
|
repository_name 'ActiveRecord Caching Resumption Provider'
|
40
47
|
repository_url 'http://localhost'
|
@@ -49,11 +56,13 @@ class ARLoader
|
|
49
56
|
File.join(File.dirname(__FILE__), '..', 'fixtures', 'dc.yml')
|
50
57
|
)
|
51
58
|
fixtures.keys.sort.each do |key|
|
52
|
-
|
59
|
+
lang = DCLang.create(name: fixtures[key].delete('language'))
|
60
|
+
DCField.create(fixtures[key].merge(dc_lang: lang))
|
53
61
|
end
|
54
62
|
end
|
55
63
|
|
56
64
|
def self.unload
|
57
65
|
DCField.delete_all
|
66
|
+
DCLang.delete_all
|
58
67
|
end
|
59
68
|
end
|
@@ -19,7 +19,8 @@ class TransactionalTestCase < Test::Unit::TestCase
|
|
19
19
|
)
|
20
20
|
disable_logging do
|
21
21
|
fixtures.keys.sort.each do |key|
|
22
|
-
|
22
|
+
lang = DCLang.create(name: fixtures[key].delete('language'))
|
23
|
+
DCField.create(fixtures[key].merge(dc_lang: lang))
|
23
24
|
end
|
24
25
|
end
|
25
26
|
end
|
@@ -4,4 +4,12 @@ class DCField < ActiveRecord::Base
|
|
4
4
|
:join_table => "dc_fields_dc_sets",
|
5
5
|
:foreign_key => "dc_field_id",
|
6
6
|
:class_name => "DCSet"
|
7
|
+
|
8
|
+
belongs_to :dc_lang, class_name: "DCLang", optional: true
|
9
|
+
|
10
|
+
default_scope -> { left_outer_joins(:dc_lang) }
|
11
|
+
|
12
|
+
def language
|
13
|
+
dc_lang&.name
|
14
|
+
end
|
7
15
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'test_helper_ar_provider'
|
2
|
+
|
3
|
+
class ActiveRecordWrapperTest < TransactionalTestCase
|
4
|
+
def test_sql_conditions_from_date
|
5
|
+
input = "2005-12-25"
|
6
|
+
expected = input.dup
|
7
|
+
sql_template, sql_opts = sql_conditions(from: input)
|
8
|
+
assert_equal "dc_fields.updated_at >= :from", sql_template
|
9
|
+
assert_equal expected, sql_opts[:from]
|
10
|
+
sql_template, sql_opts = sql_conditions(from: Date.strptime(input, "%Y-%m-%d"))
|
11
|
+
assert_equal "dc_fields.updated_at >= :from", sql_template
|
12
|
+
assert_equal expected, sql_opts[:from]
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_sql_conditions_from_time
|
16
|
+
input = "2005-12-25T00:00:00Z"
|
17
|
+
expected = "2005-12-25 00:00:00"
|
18
|
+
sql_template, sql_opts = sql_conditions(from: input)
|
19
|
+
assert_equal "dc_fields.updated_at >= :from", sql_template
|
20
|
+
assert_equal expected, sql_opts[:from]
|
21
|
+
sql_template, sql_opts = sql_conditions(from: Time.strptime(input, "%Y-%m-%dT%H:%M:%S%Z"))
|
22
|
+
assert_equal "dc_fields.updated_at >= :from", sql_template
|
23
|
+
assert_equal expected, sql_opts[:from]
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_sql_conditions_until_date
|
27
|
+
input = "2005-12-25"
|
28
|
+
expected = "2005-12-26"
|
29
|
+
sql_template, sql_opts = sql_conditions(until: input)
|
30
|
+
assert_equal "dc_fields.updated_at < :until", sql_template
|
31
|
+
assert_equal expected, sql_opts[:until]
|
32
|
+
sql_template, sql_opts = sql_conditions(until: Date.strptime(input, "%Y-%m-%d"))
|
33
|
+
assert_equal "dc_fields.updated_at < :until", sql_template
|
34
|
+
assert_equal expected, sql_opts[:until]
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_sql_conditions_until_time
|
38
|
+
input = "2005-12-25T00:00:00Z"
|
39
|
+
expected = "2005-12-25 00:00:01"
|
40
|
+
sql_template, sql_opts = sql_conditions(until: input)
|
41
|
+
assert_equal "dc_fields.updated_at < :until", sql_template
|
42
|
+
assert_equal expected, sql_opts[:until]
|
43
|
+
sql_template, sql_opts = sql_conditions(until: Time.strptime(input, "%Y-%m-%dT%H:%M:%S%Z"))
|
44
|
+
assert_equal "dc_fields.updated_at < :until", sql_template
|
45
|
+
assert_equal expected, sql_opts[:until]
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_sql_conditions_both
|
49
|
+
input = "2005-12-25"
|
50
|
+
sql_template, sql_opts = sql_conditions(from: input, until: input)
|
51
|
+
assert_equal "dc_fields.updated_at >= :from AND dc_fields.updated_at < :until", sql_template
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup
|
55
|
+
@wrapper = OAI::Provider::ActiveRecordWrapper.new(DCField)
|
56
|
+
end
|
57
|
+
|
58
|
+
def sql_conditions(opts)
|
59
|
+
@wrapper.send :sql_conditions, opts
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|