active-fedora 3.0.1 → 3.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|