active-fedora 3.0.1 → 3.0.3
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.
- data/History.txt +9 -0
- data/config/predicate_mappings.yml +1 -0
- data/lib/active_fedora/associations.rb +73 -0
- data/lib/active_fedora/associations/association_collection.rb +1 -1
- data/lib/active_fedora/associations/has_and_belongs_to_many_association.rb +117 -0
- data/lib/active_fedora/base.rb +4 -0
- data/lib/active_fedora/reflection.rb +1 -1
- data/lib/active_fedora/semantic_node.rb +1 -1
- data/lib/active_fedora/version.rb +1 -1
- data/spec/integration/associations_spec.rb +118 -62
- metadata +4 -3
data/History.txt
CHANGED
@@ -7,6 +7,8 @@ module ActiveFedora
|
|
7
7
|
|
8
8
|
autoload :HasManyAssociation, 'active_fedora/associations/has_many_association'
|
9
9
|
autoload :BelongsToAssociation, 'active_fedora/associations/belongs_to_association'
|
10
|
+
autoload :HasAndBelongsToManyAssociation, 'active_fedora/associations/has_and_belongs_to_many_association'
|
11
|
+
|
10
12
|
|
11
13
|
autoload :AssociationCollection, 'active_fedora/associations/association_collection'
|
12
14
|
autoload :AssociationProxy, 'active_fedora/associations/association_proxy'
|
@@ -50,6 +52,73 @@ module ActiveFedora
|
|
50
52
|
end
|
51
53
|
|
52
54
|
|
55
|
+
# Specifies a many-to-many relationship with another class. The relatioship is written to both classes simultaneously.
|
56
|
+
#
|
57
|
+
# Adds the following methods for retrieval and query:
|
58
|
+
#
|
59
|
+
# [collection(force_reload = false)]
|
60
|
+
# Returns an array of all the associated objects.
|
61
|
+
# An empty array is returned if none are found.
|
62
|
+
# [collection<<(object, ...)]
|
63
|
+
# Adds one or more objects to the collection by creating associations in the join table
|
64
|
+
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
|
65
|
+
# Note that this operation instantly fires update sql without waiting for the save or update call on the
|
66
|
+
# parent object.
|
67
|
+
# [collection.delete(object, ...)]
|
68
|
+
# Removes one or more objects from the collection by removing their associations from the join table.
|
69
|
+
# This does not destroy the objects.
|
70
|
+
# [collection=objects]
|
71
|
+
# Replaces the collection's content by deleting and adding objects as appropriate.
|
72
|
+
# [collection_singular_ids]
|
73
|
+
# Returns an array of the associated objects' ids.
|
74
|
+
# [collection_singular_ids=ids]
|
75
|
+
# Replace the collection by the objects identified by the primary keys in +ids+.
|
76
|
+
# [collection.clear]
|
77
|
+
# Removes every object from the collection. This does not destroy the objects.
|
78
|
+
# [collection.empty?]
|
79
|
+
# Returns +true+ if there are no associated objects.
|
80
|
+
# [collection.size]
|
81
|
+
# Returns the number of associated objects.
|
82
|
+
#
|
83
|
+
# (+collection+ is replaced with the symbol passed as the first argument, so
|
84
|
+
# <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.)
|
85
|
+
#
|
86
|
+
# === Example
|
87
|
+
#
|
88
|
+
# A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
|
89
|
+
# * <tt>Developer#projects</tt>
|
90
|
+
# * <tt>Developer#projects<<</tt>
|
91
|
+
# * <tt>Developer#projects.delete</tt>
|
92
|
+
# * <tt>Developer#projects=</tt>
|
93
|
+
# * <tt>Developer#project_ids</tt>
|
94
|
+
# * <tt>Developer#project_ids=</tt>
|
95
|
+
# * <tt>Developer#projects.clear</tt>
|
96
|
+
# * <tt>Developer#projects.empty?</tt>
|
97
|
+
# * <tt>Developer#projects.size</tt>
|
98
|
+
# * <tt>Developer#projects.find(id)</tt>
|
99
|
+
# * <tt>Developer#projects.exists?(...)</tt>
|
100
|
+
# The declaration may include an options hash to specialize the behavior of the association.
|
101
|
+
#
|
102
|
+
# === Options
|
103
|
+
#
|
104
|
+
# [:class_name]
|
105
|
+
# Specify the class name of the association. Use it only if that name can't be inferred
|
106
|
+
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
|
107
|
+
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
|
108
|
+
# [:property]
|
109
|
+
# <b>REQUIRED</b> Specify the predicate to use when storing the relationship.
|
110
|
+
#
|
111
|
+
# Option examples:
|
112
|
+
# has_and_belongs_to_many :projects, :property=>:works_on
|
113
|
+
# has_and_belongs_to_many :nations, :class_name => "Country", :property=>:is_citizen_of
|
114
|
+
def has_and_belongs_to_many(association_id, options = {}, &extension)
|
115
|
+
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
116
|
+
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
|
117
|
+
#configure_after_destroy_method_for_has_and_belongs_to_many(reflection)
|
118
|
+
#add_association_callbacks(reflection.name, options)
|
119
|
+
end
|
120
|
+
|
121
|
+
|
53
122
|
private
|
54
123
|
|
55
124
|
def create_has_many_reflection(association_id, options)
|
@@ -60,6 +129,10 @@ module ActiveFedora
|
|
60
129
|
create_reflection(:belongs_to, association_id, options, self)
|
61
130
|
end
|
62
131
|
|
132
|
+
def create_has_and_belongs_to_many_reflection(association_id, options)
|
133
|
+
create_reflection(:has_and_belongs_to_many, association_id, options, self)
|
134
|
+
end
|
135
|
+
|
63
136
|
def association_accessor_methods(reflection, association_proxy_class)
|
64
137
|
redefine_method(reflection.name) do |*params|
|
65
138
|
force_reload = params.first unless params.empty?
|
@@ -69,7 +69,7 @@ module ActiveFedora
|
|
69
69
|
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
70
70
|
def <<(*records)
|
71
71
|
result = true
|
72
|
-
load_target
|
72
|
+
load_target unless loaded?
|
73
73
|
|
74
74
|
flatten_deeper(records).each do |record|
|
75
75
|
raise_on_type_mismatch(record)
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
# = Active Fedora Has And Belongs To Many Association
|
3
|
+
module Associations
|
4
|
+
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
|
5
|
+
def initialize(owner, reflection)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def find_target
|
10
|
+
@owner.load_outbound_relationship(@reflection.name.to_s, @reflection.options[:property])
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
# def create(attributes = {})
|
15
|
+
# create_record(attributes) { |record| insert_record(record) }
|
16
|
+
# end
|
17
|
+
|
18
|
+
# def create!(attributes = {})
|
19
|
+
# create_record(attributes) { |record| insert_record(record, true) }
|
20
|
+
# end
|
21
|
+
|
22
|
+
def columns
|
23
|
+
@reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
|
24
|
+
end
|
25
|
+
|
26
|
+
# def reset_column_information
|
27
|
+
# @reflection.reset_column_information
|
28
|
+
# end
|
29
|
+
|
30
|
+
# def has_primary_key?
|
31
|
+
# @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table])
|
32
|
+
# end
|
33
|
+
|
34
|
+
protected
|
35
|
+
# def construct_find_options!(options)
|
36
|
+
# options[:joins] = Arel::SqlLiteral.new @join_sql
|
37
|
+
# options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
|
38
|
+
# options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*'))
|
39
|
+
# end
|
40
|
+
|
41
|
+
def count_records
|
42
|
+
load_target.size
|
43
|
+
end
|
44
|
+
|
45
|
+
def insert_record(record, force = true, validate = true)
|
46
|
+
if record.new_record?
|
47
|
+
if force
|
48
|
+
record.save!
|
49
|
+
else
|
50
|
+
return false unless record.save(:validate => validate)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
### TODO save relationship
|
55
|
+
@owner.add_relationship(@reflection.options[:property], record)
|
56
|
+
record.add_relationship(@reflection.options[:property], @owner)
|
57
|
+
record.save
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete_records(records)
|
62
|
+
records.each do |r|
|
63
|
+
r.remove_relationship(@reflection.options[:property], @owner)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# def construct_sql
|
68
|
+
# if @reflection.options[:finder_sql]
|
69
|
+
# @finder_sql = interpolate_and_sanitize_sql(@reflection.options[:finder_sql])
|
70
|
+
# else
|
71
|
+
# @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
|
72
|
+
# @finder_sql << " AND (#{conditions})" if conditions
|
73
|
+
# end
|
74
|
+
|
75
|
+
# @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
|
76
|
+
|
77
|
+
# construct_counter_sql
|
78
|
+
# end
|
79
|
+
|
80
|
+
def construct_scope
|
81
|
+
{ :find => { :conditions => @finder_sql,
|
82
|
+
:joins => @join_sql,
|
83
|
+
:readonly => false,
|
84
|
+
:order => @reflection.options[:order],
|
85
|
+
:include => @reflection.options[:include],
|
86
|
+
:limit => @reflection.options[:limit] } }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Join tables with additional columns on top of the two foreign keys must be considered
|
90
|
+
# ambiguous unless a select clause has been explicitly defined. Otherwise you can get
|
91
|
+
# broken records back, if, for example, the join column also has an id column. This will
|
92
|
+
# then overwrite the id column of the records coming back.
|
93
|
+
def finding_with_ambiguous_select?(select_clause)
|
94
|
+
!select_clause && columns.size != 2
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
# def create_record(attributes, &block)
|
99
|
+
# # Can't use Base.create because the foreign key may be a protected attribute.
|
100
|
+
# ensure_owner_is_not_new
|
101
|
+
# if attributes.is_a?(Array)
|
102
|
+
# attributes.collect { |attr| create(attr) }
|
103
|
+
# else
|
104
|
+
# build_record(attributes, &block)
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
|
108
|
+
def record_timestamp_columns(record)
|
109
|
+
if record.record_timestamps
|
110
|
+
record.send(:all_timestamp_attributes).map { |x| x.to_s }
|
111
|
+
else
|
112
|
+
[]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/active_fedora/base.rb
CHANGED
@@ -139,6 +139,10 @@ module ActiveFedora
|
|
139
139
|
@metadata_is_dirty == false
|
140
140
|
return result
|
141
141
|
end
|
142
|
+
|
143
|
+
def save!
|
144
|
+
save
|
145
|
+
end
|
142
146
|
|
143
147
|
# Refreshes the object's info from Fedora
|
144
148
|
# Note: Currently just registers any new datastreams that have appeared in fedora
|
@@ -5,7 +5,7 @@ module ActiveFedora
|
|
5
5
|
module ClassMethods
|
6
6
|
def create_reflection(macro, name, options, active_fedora)
|
7
7
|
case macro
|
8
|
-
when :has_many, :belongs_to
|
8
|
+
when :has_many, :belongs_to, :has_and_belongs_to_many
|
9
9
|
klass = AssociationReflection
|
10
10
|
reflection = klass.new(macro, name, options, active_fedora)
|
11
11
|
end
|
@@ -6,80 +6,136 @@ end
|
|
6
6
|
|
7
7
|
class Book < ActiveFedora::Base
|
8
8
|
belongs_to :library, :property=>:has_constituent
|
9
|
+
has_and_belongs_to_many :topics, :property=>:is_topic_of
|
10
|
+
end
|
11
|
+
|
12
|
+
class Topic < ActiveFedora::Base
|
13
|
+
has_and_belongs_to_many :books, :property=>:is_topic_of
|
9
14
|
end
|
10
15
|
|
11
16
|
describe ActiveFedora::Base do
|
12
17
|
describe "an unsaved instance" do
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
40
|
-
it "should let you set an array of object ids" do
|
41
|
-
@library.book_ids = [@book.pid, @book2.pid]
|
42
|
-
@library.books.map(&:pid).should == [@book.pid, @book2.pid]
|
43
|
-
end
|
44
|
-
|
45
|
-
it "setter should wipe out previously saved relations" do
|
46
|
-
@library.book_ids = [@book.pid, @book2.pid]
|
47
|
-
@library.book_ids = [@book2.pid]
|
48
|
-
@library.books.map(&:pid).should == [@book2.pid]
|
18
|
+
describe "of belongs_to" do
|
19
|
+
before do
|
20
|
+
@library = Library.new()
|
21
|
+
@book = Book.new
|
22
|
+
@book.save
|
23
|
+
@book2 = Book.new
|
24
|
+
@book2.save
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should let you shift onto the association" do
|
28
|
+
@library.new_record?.should be_true
|
29
|
+
@library.books.size == 0
|
30
|
+
@library.books.to_ary.should == []
|
31
|
+
@library.book_ids.should ==[]
|
32
|
+
@library.books << @book
|
33
|
+
@library.books.map(&:pid).should == [@book.pid]
|
34
|
+
@library.book_ids.should ==[@book.pid]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should let you set an array of objects" do
|
38
|
+
@library.books = [@book, @book2]
|
39
|
+
@library.books.map(&:pid).should == [@book.pid, @book2.pid]
|
40
|
+
@library.save
|
41
|
+
|
42
|
+
@library.books = [@book]
|
43
|
+
@library.books.map(&:pid).should == [@book.pid]
|
49
44
|
|
45
|
+
end
|
46
|
+
it "should let you set an array of object ids" do
|
47
|
+
@library.book_ids = [@book.pid, @book2.pid]
|
48
|
+
@library.books.map(&:pid).should == [@book.pid, @book2.pid]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "setter should wipe out previously saved relations" do
|
52
|
+
@library.book_ids = [@book.pid, @book2.pid]
|
53
|
+
@library.book_ids = [@book2.pid]
|
54
|
+
@library.books.map(&:pid).should == [@book2.pid]
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
after do
|
60
|
+
@book.delete
|
61
|
+
@book2.delete
|
62
|
+
end
|
50
63
|
end
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
64
|
+
describe "of has_many_and_belongs_to" do
|
65
|
+
before do
|
66
|
+
@topic1 = Topic.new
|
67
|
+
@topic1.save
|
68
|
+
@topic2 = Topic.new
|
69
|
+
@topic2.save
|
70
|
+
end
|
71
|
+
it "habtm should set relationships bidirectionally" do
|
72
|
+
@book = Book.new
|
73
|
+
@book.topics << @topic1
|
74
|
+
@book.topics.map(&:pid).should == [@topic1.pid]
|
75
|
+
Topic.find(@topic1.pid).books.map(&:pid).should == [] #Can't have saved it because @book isn't saved yet.
|
76
|
+
end
|
77
|
+
after do
|
78
|
+
@topic1.delete
|
79
|
+
@topic2.delete
|
80
|
+
end
|
55
81
|
end
|
56
82
|
end
|
57
83
|
|
84
|
+
|
58
85
|
|
59
|
-
describe "a saved instance" do
|
60
|
-
before do
|
61
|
-
@library = Library.new()
|
62
|
-
@library.save()
|
63
|
-
@book = Book.new
|
64
|
-
@book.save
|
65
|
-
end
|
66
|
-
it "should have many books once it has been saved" do
|
67
|
-
@library.save
|
68
|
-
@library.books << @book
|
69
|
-
|
70
|
-
@book.library.pid.should == @library.pid
|
71
|
-
@library.books.reload
|
72
|
-
@library.books.map(&:pid).should == [@book.pid]
|
73
|
-
|
74
|
-
|
75
|
-
@library2 = Library.find(@library.pid)
|
76
|
-
@library2.books.map(&:pid).should == [@book.pid]
|
77
86
|
|
78
|
-
|
87
|
+
describe "a saved instance" do
|
88
|
+
describe "of belongs_to" do
|
89
|
+
before do
|
90
|
+
@library = Library.new()
|
91
|
+
@library.save()
|
92
|
+
@book = Book.new
|
93
|
+
@book.save
|
94
|
+
end
|
95
|
+
it "should have many books once it has been saved" do
|
96
|
+
@library.books << @book
|
97
|
+
|
98
|
+
@book.library.pid.should == @library.pid
|
99
|
+
@library.books.reload
|
100
|
+
@library.books.map(&:pid).should == [@book.pid]
|
101
|
+
|
102
|
+
|
103
|
+
@library2 = Library.find(@library.pid)
|
104
|
+
@library2.books.map(&:pid).should == [@book.pid]
|
105
|
+
end
|
106
|
+
after do
|
107
|
+
@library.delete
|
108
|
+
@book.delete
|
109
|
+
end
|
79
110
|
end
|
80
|
-
|
81
|
-
|
82
|
-
|
111
|
+
describe "of has_many_and_belongs_to" do
|
112
|
+
before do
|
113
|
+
@topic1 = Topic.new
|
114
|
+
@topic1.save
|
115
|
+
@topic2 = Topic.new
|
116
|
+
@topic2.save
|
117
|
+
@book = Book.new
|
118
|
+
@book.save
|
119
|
+
end
|
120
|
+
it "habtm should set relationships bidirectionally" do
|
121
|
+
@book.topics << @topic1
|
122
|
+
@book.topics.map(&:pid).should == [@topic1.pid]
|
123
|
+
Topic.find(@topic1.pid).books.map(&:pid).should == [@book.pid] #Can't have saved it because @book isn't saved yet.
|
124
|
+
end
|
125
|
+
it "should save new child objects" do
|
126
|
+
@book.topics << Topic.new
|
127
|
+
@book.topics.first.pid.should_not be_nil
|
128
|
+
end
|
129
|
+
it "should clear out the old associtions" do
|
130
|
+
@book.topics = [@topic1]
|
131
|
+
@book.topics = [@topic2]
|
132
|
+
@book.topic_ids.should == [@topic2.pid]
|
133
|
+
end
|
134
|
+
after do
|
135
|
+
@book.delete
|
136
|
+
@topic1.delete
|
137
|
+
@topic2.delete
|
138
|
+
end
|
83
139
|
end
|
84
140
|
end
|
85
141
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active-fedora
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 1
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 3
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 3.0.
|
9
|
+
- 3
|
10
|
+
version: 3.0.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Matt Zumwalt
|
@@ -520,6 +520,7 @@ files:
|
|
520
520
|
- lib/active_fedora/associations/association_collection.rb
|
521
521
|
- lib/active_fedora/associations/association_proxy.rb
|
522
522
|
- lib/active_fedora/associations/belongs_to_association.rb
|
523
|
+
- lib/active_fedora/associations/has_and_belongs_to_many_association.rb
|
523
524
|
- lib/active_fedora/associations/has_many_association.rb
|
524
525
|
- lib/active_fedora/attribute_methods.rb
|
525
526
|
- lib/active_fedora/base.rb
|