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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -4
  3. data/Rakefile +7 -0
  4. data/bin/oai +0 -2
  5. data/examples/models/file_model.rb +2 -2
  6. data/lib/oai/client/response.rb +8 -8
  7. data/lib/oai/client.rb +34 -10
  8. data/lib/oai/exception.rb +46 -38
  9. data/lib/oai/harvester/config.rb +1 -1
  10. data/lib/oai/harvester/harvest.rb +37 -25
  11. data/lib/oai/harvester/logging.rb +3 -5
  12. data/lib/oai/harvester.rb +4 -1
  13. data/lib/oai/provider/model/activerecord_caching_wrapper.rb +5 -8
  14. data/lib/oai/provider/model/activerecord_wrapper.rb +41 -25
  15. data/lib/oai/provider/model.rb +1 -1
  16. data/lib/oai/provider/response/list_records.rb +12 -0
  17. data/lib/oai/provider/response.rb +7 -4
  18. data/lib/oai/provider/resumption_token.rb +70 -21
  19. data/lib/oai/provider.rb +129 -7
  20. data/test/activerecord_provider/database/0001_oaipmh_tables.rb +7 -1
  21. data/test/activerecord_provider/helpers/providers.rb +10 -1
  22. data/test/activerecord_provider/helpers/transactional_test_case.rb +2 -1
  23. data/test/activerecord_provider/models/dc_field.rb +8 -0
  24. data/test/activerecord_provider/models/dc_lang.rb +3 -0
  25. data/test/activerecord_provider/models/exclusive_set_dc_field.rb +6 -0
  26. data/test/activerecord_provider/tc_activerecord_wrapper.rb +63 -0
  27. data/test/activerecord_provider/tc_ar_provider.rb +54 -26
  28. data/test/activerecord_provider/tc_ar_sets_provider.rb +10 -9
  29. data/test/activerecord_provider/tc_caching_paging_provider.rb +9 -7
  30. data/test/activerecord_provider/tc_simple_paging_provider.rb +28 -7
  31. data/test/client/tc_exception.rb +1 -1
  32. data/test/client/tc_get_record.rb +1 -1
  33. data/test/client/tc_http_client.rb +2 -2
  34. data/test/client/tc_libxml.rb +1 -1
  35. data/test/client/tc_utf8_escaping.rb +8 -1
  36. data/test/harvester/tc_harvest.rb +42 -0
  37. data/test/harvester/test_helper_harvester.rb +6 -0
  38. data/test/provider/models.rb +3 -3
  39. data/test/provider/tc_functional_tokens.rb +17 -11
  40. data/test/provider/tc_instance_provider.rb +41 -0
  41. data/test/provider/tc_provider.rb +26 -0
  42. data/test/provider/tc_resumption_tokens.rb +6 -0
  43. data/test/provider/test_helper_provider.rb +17 -0
  44. 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).find_by!(identifier_field => selector)
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
- total = find_scope.where(token_conditions(token)).count
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
- last = token.last
156
+ last_id = token.last_str
163
157
  sql = sql_conditions token.to_conditions_hash
164
158
 
165
- return sql if 0 == last
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] = last
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
- time_obj = Time.parse(time.to_s)
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
- # Convert to same as DB - :local => :getlocal, :utc => :getutc
195
- tzconv = "get#{model.default_timezone.to_s}".to_sym
196
- time_obj.send(tzconv).strftime("%Y-%m-%d %H:%M:%S")
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
-
@@ -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
- Date.parse(value) # This will raise an exception for badly formatted dates
95
- Time.parse(value).utc # -- UTC Bug fix hack 8/08 not in core
96
- rescue
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 = /(.+):(\d+)$/.match(token_string)
19
- options[:last] = matches.captures[1].to_i
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
- @last = options[:last]
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
- @last = last
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
- encoded_token << ".f(#{self.from.utc.xmlschema})" if self.from
93
- encoded_token << ".u(#{self.until.utc.xmlschema})" if self.until
94
- encoded_token << ":#{last}"
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(self.class, options).to_xml
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(self.class, options).to_xml
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(self.class, options).to_xml
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(self.class, options).to_xml
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(self.class, options).to_xml
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(self.class, options).to_xml
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
- self.class.url = params['url'] ? params.delete('url') : self.class.url
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 :language, :string
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
- DCField.create(fixtures[key])
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
- DCField.create(fixtures[key])
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,3 @@
1
+ class DCLang < ActiveRecord::Base
2
+ has_many :dc_fields
3
+ end
@@ -8,4 +8,10 @@ class ExclusiveSetDCField < ActiveRecord::Base
8
8
  end
9
9
  end
10
10
 
11
+ belongs_to :dc_lang, class_name: "DCLang", optional: true
12
+
13
+ def language
14
+ dc_lang&.name
15
+ end
16
+
11
17
  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
+