Skip to content

Commit 538dc89

Browse files
committed
Merge pull request #18 from tjdett/smarter-set-membership
Simpler provider set handling through reflection
2 parents 04ad0da + 8c2efbf commit 538dc89

File tree

7 files changed

+152
-41
lines changed

7 files changed

+152
-41
lines changed

lib/oai/provider/model/activerecord_wrapper.rb

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,20 @@ def sets
4343
end
4444

4545
def find(selector, options={})
46-
return next_set(options[:resumption_token]) if options[:resumption_token]
46+
find_scope = find_scope(options)
47+
return next_set(find_scope,
48+
options[:resumption_token]) if options[:resumption_token]
4749
conditions = sql_conditions(options)
4850
if :all == selector
49-
total = model.count(:id, :conditions => conditions)
51+
total = find_scope.count(:id, :conditions => conditions)
5052
if @limit && total > @limit
51-
select_partial(ResumptionToken.new(options.merge({:last => 0})))
53+
select_partial(find_scope,
54+
ResumptionToken.new(options.merge({:last => 0})))
5255
else
53-
model.find(:all, :conditions => conditions)
56+
find_scope.find(:all, :conditions => conditions)
5457
end
5558
else
56-
model.find(selector, :conditions => conditions)
59+
find_scope.find(selector, :conditions => conditions)
5760
end
5861
end
5962

@@ -84,26 +87,58 @@ def method_missing(m, *args, &block)
8487

8588
protected
8689

90+
def find_scope(options)
91+
return model unless options.key?(:set)
92+
93+
# Find the set or return an empty scope
94+
set = find_set_by_spec(options[:set])
95+
return model.scoped(:limit => 0) if set.nil?
96+
97+
# If the set has a backward relationship, we'll use it
98+
if set.class.respond_to?(:reflect_on_all_associations)
99+
set.class.reflect_on_all_associations.each do |assoc|
100+
return set.send(assoc.name).scoped if assoc.klass == model
101+
end
102+
end
103+
104+
# Search the attributes for 'set'
105+
if model.column_names.include?('set')
106+
# Scope using the set attribute as the spec
107+
model.scoped(:conditions => {:set => options[:set]})
108+
else
109+
# Default to empty set, as we've tried everything else
110+
model.scoped(:limit => 0)
111+
end
112+
end
113+
114+
def find_set_by_spec(spec)
115+
if sets.class == ActiveRecord::Relation
116+
sets.find_by_spec(spec)
117+
else
118+
sets.detect {|set| set.spec == spec}
119+
end
120+
end
121+
87122
# Request the next set in this sequence.
88-
def next_set(token_string)
123+
def next_set(find_scope, token_string)
89124
raise OAI::ResumptionTokenException.new unless @limit
90125

91126
token = ResumptionToken.parse(token_string)
92-
total = model.count(:id, :conditions => token_conditions(token))
127+
total = find_scope.count(:id, :conditions => token_conditions(token))
93128

94129
if @limit < total
95-
select_partial(token)
130+
select_partial(find_scope, token)
96131
else # end of result set
97-
model.find(:all,
132+
find_scope.find(:all,
98133
:conditions => token_conditions(token),
99134
:limit => @limit, :order => "#{model.primary_key} asc")
100135
end
101136
end
102137

103138
# select a subset of the result set, and return it with a
104139
# resumption token to get the next subset
105-
def select_partial(token)
106-
records = model.find(:all,
140+
def select_partial(find_scope, token)
141+
records = find_scope.find(:all,
107142
:conditions => token_conditions(token),
108143
:limit => @limit,
109144
:order => "#{model.primary_key} asc")
@@ -144,10 +179,6 @@ def sql_conditions(opts)
144179
sql << "#{timestamp_field} < :until"
145180
esc_values[:until] = parse_to_local(opts[:until]) { |t| t + 1 }
146181
end
147-
if opts.has_key?(:set)
148-
sql << "set = :set"
149-
esc_values[:set] = opts[:set]
150-
end
151182
return [sql.join(" AND "), esc_values]
152183
end
153184

test/activerecord_provider/database/0001_oaipmh_tables.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def self.up
1010
t.column :oai_token_id, :integer, :null => false
1111
end
1212

13-
create_table :dc_fields do |t|
13+
dc_fields = proc do |t|
1414
t.column :title, :string
1515
t.column :creator, :string
1616
t.column :subject, :string
@@ -30,6 +30,13 @@ def self.up
3030
t.column :deleted, :boolean, :default => false
3131
end
3232

33+
create_table :exclusive_set_dc_fields do |t|
34+
dc_fields.call(t)
35+
t.column :set, :string
36+
end
37+
38+
create_table :dc_fields, &dc_fields
39+
3340
create_table :dc_fields_dc_sets, :id => false do |t|
3441
t.column :dc_field_id, :integer
3542
t.column :dc_set_id, :integer
Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,24 @@
11
# Extend ActiveRecordModel to support sets
22
class SetModel < OAI::Provider::ActiveRecordWrapper
3-
3+
44
# Return all available sets
55
def sets
6-
DCSet.find(:all)
6+
DCSet.scoped
77
end
88

9-
# Scope the find to a set relation if we get a set in the options
10-
def find(selector, opts={})
11-
if opts[:set]
12-
set = DCSet.find_by_spec(opts.delete(:set))
13-
conditions = sql_conditions(opts)
14-
15-
if :all == selector
16-
set.dc_fields.find(selector, :conditions => conditions)
17-
else
18-
set.dc_fields.find(selector, :conditions => conditions)
19-
end
20-
else
21-
if :all == selector
22-
model.find(selector, :conditions => sql_conditions(opts))
23-
else
24-
model.find(selector, :conditions => sql_conditions(opts))
25-
end
26-
end
27-
end
28-
299
end
3010

3111
class ARSetProvider < OAI::Provider::Base
3212
repository_name 'ActiveRecord Set Based Provider'
3313
repository_url 'http://localhost'
3414
record_prefix = 'oai:test'
3515
source_model SetModel.new(DCField, :timestamp_field => 'date')
16+
end
17+
18+
class ARExclusiveSetProvider < OAI::Provider::Base
19+
repository_name 'ActiveRecord Set Based Provider'
20+
repository_url 'http://localhost'
21+
record_prefix = 'oai:test'
22+
source_model OAI::Provider::ActiveRecordWrapper.new(
23+
ExclusiveSetDCField, :timestamp_field => 'date')
3624
end

test/activerecord_provider/helpers/transactional_test_case.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def run(result, &block)
1111
result
1212
end
1313

14-
private
14+
protected
1515

1616
def load_fixtures
1717
fixtures = YAML.load_file(
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
class DCSet < ActiveRecord::Base
2-
has_and_belongs_to_many :dc_fields,
3-
:join_table => "dc_fields_dc_sets",
2+
has_and_belongs_to_many :dc_fields,
3+
:join_table => "dc_fields_dc_sets",
44
:foreign_key => "dc_set_id",
55
:class_name => "DCField"
6+
67
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class ExclusiveSetDCField < ActiveRecord::Base
2+
inheritance_column = 'DONOTINHERIT'
3+
4+
def self.sets
5+
klass = Struct.new(:name, :spec)
6+
self.uniq.pluck('`set`').compact.map do |spec|
7+
klass.new("Set #{spec}", spec)
8+
end
9+
end
10+
11+
end

test/activerecord_provider/tc_ar_sets_provider.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,77 @@ def define_sets
6262
next_id = record.id
6363
end
6464
end
65+
end
66+
67+
68+
class ActiveRecordExclusiveSetsProviderTest < TransactionalTestCase
69+
70+
def test_list_sets
71+
doc = REXML::Document.new(@provider.list_sets)
72+
sets = doc.elements["/OAI-PMH/ListSets"]
73+
assert_equal 3, sets.size
74+
assert_equal "Set A", sets[0].elements["//setName"].text
75+
end
76+
77+
def test_set_a
78+
doc = REXML::Document.new(@provider.list_records(:set => "A"))
79+
assert_equal 20, doc.elements['OAI-PMH/ListRecords'].to_a.size
80+
end
81+
82+
def test_set_b
83+
doc = REXML::Document.new(@provider.list_records(:set => "B"))
84+
assert_equal 10, doc.elements['OAI-PMH/ListRecords'].to_a.size
85+
end
86+
87+
def test_set_ab
88+
doc = REXML::Document.new(@provider.list_records(:set => "A:B"))
89+
assert_equal 10, doc.elements['OAI-PMH/ListRecords'].to_a.size
90+
end
91+
92+
def setup
93+
@provider = ARExclusiveSetProvider.new
94+
define_sets
95+
end
96+
97+
def define_sets
98+
next_id = 0
99+
100+
ExclusiveSetDCField.find(:all, :limit => 10, :order => "id asc").each do |record|
101+
record.set = "A"
102+
record.save!
103+
next_id = record.id
104+
end
105+
106+
ExclusiveSetDCField.find(:all, :limit => 10, :order => "id asc", :conditions => "id > #{next_id}").each do |record|
107+
record.set = "B"
108+
record.save!
109+
next_id = record.id
110+
end
111+
112+
ExclusiveSetDCField.find(:all, :limit => 10, :order => "id asc", :conditions => "id > #{next_id}").each do |record|
113+
record.set = "A:B"
114+
record.save!
115+
next_id = record.id
116+
end
117+
118+
ExclusiveSetDCField.find(:all, :limit => 10, :order => "id asc", :conditions => "id > #{next_id}").each do |record|
119+
record.set = "A"
120+
record.save!
121+
next_id = record.id
122+
end
123+
end
124+
125+
protected
126+
127+
def load_fixtures
128+
fixtures = YAML.load_file(
129+
File.join(File.dirname(__FILE__), 'fixtures', 'dc.yml')
130+
)
131+
disable_logging do
132+
fixtures.keys.sort.each do |key|
133+
ExclusiveSetDCField.create(fixtures[key])
134+
end
135+
end
136+
end
137+
65138
end

0 commit comments

Comments
 (0)