oai 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|