active-fedora 2.3.8 → 3.0.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 (47) hide show
  1. data/.rvmrc +1 -1
  2. data/Gemfile.lock +16 -10
  3. data/History.txt +4 -5
  4. data/README.textile +1 -1
  5. data/active-fedora.gemspec +2 -2
  6. data/lib/active_fedora.rb +36 -19
  7. data/lib/active_fedora/associations.rb +157 -0
  8. data/lib/active_fedora/associations/association_collection.rb +180 -0
  9. data/lib/active_fedora/associations/association_proxy.rb +177 -0
  10. data/lib/active_fedora/associations/belongs_to_association.rb +36 -0
  11. data/lib/active_fedora/associations/has_many_association.rb +52 -0
  12. data/lib/active_fedora/attribute_methods.rb +8 -0
  13. data/lib/active_fedora/base.rb +76 -80
  14. data/lib/active_fedora/datastream.rb +0 -1
  15. data/lib/active_fedora/delegating.rb +53 -0
  16. data/lib/active_fedora/model.rb +4 -2
  17. data/lib/active_fedora/nested_attributes.rb +153 -0
  18. data/lib/active_fedora/nokogiri_datastream.rb +17 -18
  19. data/lib/active_fedora/reflection.rb +140 -0
  20. data/lib/active_fedora/relationships_helper.rb +10 -5
  21. data/lib/active_fedora/semantic_node.rb +146 -57
  22. data/lib/active_fedora/solr_service.rb +0 -7
  23. data/lib/active_fedora/version.rb +1 -1
  24. data/lib/fedora/connection.rb +75 -111
  25. data/lib/fedora/repository.rb +14 -28
  26. data/lib/ruby-fedora.rb +1 -1
  27. data/spec/integration/associations_spec.rb +139 -0
  28. data/spec/integration/nested_attribute_spec.rb +40 -0
  29. data/spec/integration/repository_spec.rb +9 -14
  30. data/spec/integration/semantic_node_spec.rb +2 -0
  31. data/spec/spec_helper.rb +1 -3
  32. data/spec/unit/active_fedora_spec.rb +2 -1
  33. data/spec/unit/association_proxy_spec.rb +13 -0
  34. data/spec/unit/base_active_model_spec.rb +61 -0
  35. data/spec/unit/base_delegate_spec.rb +59 -0
  36. data/spec/unit/base_spec.rb +45 -58
  37. data/spec/unit/connection_spec.rb +21 -21
  38. data/spec/unit/datastream_spec.rb +0 -11
  39. data/spec/unit/has_many_collection_spec.rb +27 -0
  40. data/spec/unit/model_spec.rb +1 -1
  41. data/spec/unit/nokogiri_datastream_spec.rb +3 -29
  42. data/spec/unit/repository_spec.rb +2 -2
  43. data/spec/unit/semantic_node_spec.rb +2 -0
  44. data/spec/unit/solr_service_spec.rb +0 -7
  45. metadata +36 -15
  46. data/lib/active_fedora/active_fedora_configuration_exception.rb +0 -2
  47. data/lib/util/class_level_inheritable_attributes.rb +0 -23
@@ -21,7 +21,6 @@ module ActiveFedora
21
21
  #set this Datastream's content
22
22
  def content=(content)
23
23
  self.blob = content
24
- self.dirty = true
25
24
  end
26
25
 
27
26
  def self.delete(parent_pid, dsid)
@@ -0,0 +1,53 @@
1
+ module ActiveFedora
2
+ module Delegating
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Provides a delegate class method to expose methods in metadata streams
7
+ # as member of the base object. Pass the target datastream via the
8
+ # <tt>:to</tt> argument. If you want to return a unique result, (e.g. string
9
+ # instead of an array) set the <tt>:unique</tt> argument to true.
10
+ #
11
+ # class Foo < ActiveFedora::Base
12
+ # has_metadata :name => "descMetadata", :type => MyDatastream
13
+ #
14
+ # delegate :field1, :to=>"descMetadata", :unique=>true
15
+ # end
16
+ #
17
+ # foo = Foo.new
18
+ # foo.field1 = "My Value"
19
+ # foo.field1 # => "My Value"
20
+ # foo.field2 # => NoMethodError: undefined method `field2' for #<Foo:0x1af30c>
21
+
22
+ def delegate(field, args ={})
23
+ create_delegate_accessor(field, args)
24
+ create_delegate_setter(field, args)
25
+ end
26
+
27
+ private
28
+ def create_delegate_accessor(field, args)
29
+ define_method field do
30
+ ds = self.send(args[:to])
31
+ val = if ds.kind_of? ActiveFedora::NokogiriDatastream
32
+ ds.send(:term_values, field)
33
+ else
34
+ ds.send(:get_values, field)
35
+ end
36
+ args[:unique] ? val.first : val
37
+
38
+ end
39
+ end
40
+
41
+ def create_delegate_setter(field, args)
42
+ define_method "#{field}=".to_sym do |v|
43
+ ds = self.send(args[:to])
44
+ if ds.kind_of? ActiveFedora::NokogiriDatastream
45
+ ds.send(:update_indexed_attributes, {[field] => v})
46
+ else
47
+ ds.send(:set_value, field, v)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -61,8 +61,10 @@ module ActiveFedora
61
61
  return_multiple = false
62
62
  if args == :all
63
63
  return_multiple = true
64
- escaped_class_name = self.name.gsub(/(:)/, '\\:')
65
- q = "#{ActiveFedora::SolrService.solr_name(:active_fedora_model, :symbol)}:#{escaped_class_name}"
64
+ # escaped_class_name = self.name.gsub(/(:)/, '\\:')
65
+ escaped_class_uri = "info:fedora/afmodel:#{self.name}".gsub(/(:)/, '\\:')
66
+ # q = "#{ActiveFedora::SolrService.solr_name(:active_fedora_model, :symbol)}:#{escaped_class_name}"
67
+ q = "#{ActiveFedora::SolrService.solr_name(:has_model, :symbol)}:#{escaped_class_uri}"
66
68
  elsif args.class == String
67
69
  escaped_id = args.gsub(/(:)/, '\\:')
68
70
  q = "#{SOLR_DOCUMENT_ID}:#{escaped_id}"
@@ -0,0 +1,153 @@
1
+ require 'active_support/core_ext/hash/except'
2
+ require 'active_support/core_ext/object/try'
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+
6
+
7
+ module ActiveFedora
8
+ module NestedAttributes #:nodoc:
9
+ extend ActiveSupport::Concern
10
+ included do
11
+ class_inheritable_accessor :nested_attributes_options, :instance_writer => false
12
+ self.nested_attributes_options = {}
13
+ end
14
+
15
+
16
+ # Defines an attributes writer for the specified association(s). If you
17
+ # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
18
+ # will need to add the attribute writer to the allowed list.
19
+ #
20
+ # Supported options:
21
+ # [:allow_destroy]
22
+ # If true, destroys any members from the attributes hash with a
23
+ # <tt>_destroy</tt> key and a value that evaluates to +true+
24
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
25
+ # [:reject_if]
26
+ # Allows you to specify a Proc or a Symbol pointing to a method
27
+ # that checks whether a record should be built for a certain attribute
28
+ # hash. The hash is passed to the supplied Proc or the method
29
+ # and it should return either +true+ or +false+. When no :reject_if
30
+ # is specified, a record will be built for all attribute hashes that
31
+ # do not have a <tt>_destroy</tt> value that evaluates to true.
32
+ # Passing <tt>:all_blank</tt> instead of a Proc will create a proc
33
+ # that will reject a record where all the attributes are blank.
34
+ # [:limit]
35
+ # Allows you to specify the maximum number of the associated records that
36
+ # can be processed with the nested attributes. If the size of the
37
+ # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
38
+ # exception is raised. If omitted, any number associations can be processed.
39
+ # Note that the :limit option is only applicable to one-to-many associations.
40
+ # [:update_only]
41
+ # Allows you to specify that an existing record may only be updated.
42
+ # A new record may only be created when there is no existing record.
43
+ # This option only works for one-to-one associations and is ignored for
44
+ # collection associations. This option is off by default.
45
+ #
46
+ # Examples:
47
+ # # creates avatar_attributes=
48
+ # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
49
+ # # creates avatar_attributes=
50
+ # accepts_nested_attributes_for :avatar, :reject_if => :all_blank
51
+ # # creates avatar_attributes= and posts_attributes=
52
+ # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
53
+ module ClassMethods
54
+ def accepts_nested_attributes_for(*attr_names)
55
+ options = { :allow_destroy => false, :update_only => false }
56
+ options.update(attr_names.extract_options!)
57
+ # options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
58
+ options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
59
+
60
+ attr_names.each do |association_name|
61
+ if reflection = reflect_on_association(association_name)
62
+ reflection.options[:autosave] = true
63
+ # add_autosave_association_callbacks(reflection)
64
+ ## TODO this ought to work, but doesn't seem to do the class inheitance right
65
+ nested_attributes_options[association_name.to_sym] = options
66
+ type = (reflection.collection? ? :collection : :one_to_one)
67
+
68
+ # def pirate_attributes=(attributes)
69
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
70
+ # end
71
+ class_eval <<-eoruby, __FILE__, __LINE__ + 1
72
+ if method_defined?(:#{association_name}_attributes=)
73
+ remove_method(:#{association_name}_attributes=)
74
+ end
75
+ def #{association_name}_attributes=(attributes)
76
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
77
+ ## in lieu of autosave_association_callbacks just save all of em.
78
+ send(:#{association_name}).each {|obj| obj.save}
79
+ end
80
+ eoruby
81
+ else
82
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ # Attribute hash keys that should not be assigned as normal attributes.
91
+ # These hash keys are nested attributes implementation details.
92
+ UNASSIGNABLE_KEYS = %w( id _destroy )
93
+
94
+
95
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
96
+ options= {}
97
+ #options = nested_attributes_options[association_name]
98
+ if attributes_collection.is_a? Hash
99
+ keys = attributes_collection.keys
100
+ attributes_collection = if keys.include?('id') || keys.include?(:id)
101
+ Array.wrap(attributes_collection)
102
+ else
103
+ attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes }
104
+ end
105
+ end
106
+
107
+ association = send(association_name)
108
+
109
+ existing_records = if association.loaded?
110
+ association.to_a
111
+ else
112
+ attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
113
+ attribute_ids.present? ? association.select{ |x| attribute_ids.include?(x.pid)} : []
114
+ end
115
+
116
+ attributes_collection.each do |attributes|
117
+ attributes = attributes.with_indifferent_access
118
+
119
+ if attributes['id'].blank?
120
+ association.build(attributes.except(*UNASSIGNABLE_KEYS))
121
+
122
+ elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
123
+ association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? && !call_reject_if(association_name, attributes)
124
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
125
+
126
+ else
127
+ raise_nested_attributes_record_not_found(association_name, attributes['id'])
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ # Updates a record with the +attributes+ or marks it for destruction if
134
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
135
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
136
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
137
+ record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
138
+ end
139
+
140
+ # Determines if a hash contains a truthy _destroy key.
141
+ def has_destroy_flag?(hash)
142
+ ["1", "true"].include?(hash['_destroy'].to_s)
143
+ end
144
+
145
+ def raise_nested_attributes_record_not_found(association_name, record_id)
146
+ reflection = self.class.reflect_on_association(association_name)
147
+ raise ObjectNotFoundError, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
148
+ end
149
+
150
+ end
151
+ end
152
+
153
+
@@ -14,8 +14,7 @@ class ActiveFedora::NokogiriDatastream < ActiveFedora::Datastream
14
14
  alias_method(:om_term_values, :term_values) unless method_defined?(:om_term_values)
15
15
  alias_method(:om_update_values, :update_values) unless method_defined?(:om_update_values)
16
16
 
17
- attr_accessor :internal_solr_doc
18
- attr_reader :ng_xml
17
+ attr_accessor :ng_xml, :internal_solr_doc
19
18
 
20
19
  #constructor, calls up to ActiveFedora::Datastream's constructor
21
20
  def initialize(attrs=nil)
@@ -44,22 +43,22 @@ class ActiveFedora::NokogiriDatastream < ActiveFedora::Datastream
44
43
  Nokogiri::XML::Document.parse("<xml/>")
45
44
  end
46
45
 
47
- def ng_xml=(new_xml)
48
- case new_xml
49
- when Nokogiri::XML::Document, Nokogiri::XML::Element, Nokogiri::XML::Node
50
- @ng_xml = new_xml
51
- when String
52
- @ng_xml = Nokogiri::XML::Document.parse(new_xml)
53
- else
54
- raise TypeError, "You passed a #{new_xml.class} into the ng_xml of the #{self.dsid} datastream. NokogiriDatastream.ng_xml= only accepts Nokogiri::XML::Document, Nokogiri::XML::Element, Nokogiri::XML::Node, or raw XML (String) as inputs."
55
- end
56
- self.dirty = true
57
- end
58
-
59
- def content=(content)
60
- super
61
- self.ng_xml = Nokogiri::XML::Document.parse(content)
62
- end
46
+ # class << self
47
+ # from_xml_original = self.instance_method(:from_xml)
48
+ #
49
+ # define_method(:from_xml, xml, tmpl=self.new) do
50
+ # from_xml_original.bind(self).call(xml, tmpl)
51
+ # tmpl.send(:dirty=, false)
52
+ # end
53
+ #
54
+ # # def from_xml_custom(xml, tmpl=self.new)
55
+ # # from_xml_original(xml, tmpl)
56
+ # # tmpl.send(:dirty=, false)
57
+ # # end
58
+ # #
59
+ # # alias_method :from_xml_original, :from_xml
60
+ # # alias_method :from_xml, :from_xml_custom
61
+ # end
63
62
 
64
63
 
65
64
  def to_xml(xml = self.ng_xml)
@@ -0,0 +1,140 @@
1
+ module ActiveFedora
2
+ module Reflection # :nodoc:
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def create_reflection(macro, name, options, active_fedora)
7
+ case macro
8
+ when :has_many, :belongs_to
9
+ klass = AssociationReflection
10
+ reflection = klass.new(macro, name, options, active_fedora)
11
+ end
12
+ write_inheritable_hash :reflections, name => reflection
13
+ reflection
14
+ end
15
+
16
+ # Returns a hash containing all AssociationReflection objects for the current class.
17
+ # Example:
18
+ #
19
+ # Invoice.reflections
20
+ # Account.reflections
21
+ #
22
+ def reflections
23
+ read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
24
+ end
25
+
26
+ # Returns the AssociationReflection object for the +association+ (use the symbol).
27
+ #
28
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
29
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
30
+ #
31
+ def reflect_on_association(association)
32
+ reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
33
+ end
34
+
35
+ class MacroReflection
36
+
37
+ # Returns the target association's class.
38
+ #
39
+ # class Author < ActiveRecord::Base
40
+ # has_many :books
41
+ # end
42
+ #
43
+ # Author.reflect_on_association(:books).klass
44
+ # # => Book
45
+ #
46
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
47
+ # a new association object. Use +build_association+ or +create_association+
48
+ # instead. This allows plugins to hook into association object creation.
49
+ def klass
50
+ #@klass ||= active_record.send(:compute_type, class_name)
51
+ @klass ||= class_name
52
+ end
53
+
54
+
55
+
56
+ def initialize(macro, name, options, active_fedora)
57
+ @macro, @name, @options, @active_fedora = macro, name, options, active_fedora
58
+ end
59
+
60
+ # Returns a new, unsaved instance of the associated class. +options+ will
61
+ # be passed to the class's constructor.
62
+ def build_association(*options)
63
+ klass.new(*options)
64
+ end
65
+
66
+ # Returns the name of the macro.
67
+ #
68
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
69
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
70
+ attr_reader :name
71
+
72
+
73
+ # Returns the hash of options used for the macro.
74
+ #
75
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
76
+ # <tt>has_many :clients</tt> returns +{}+
77
+ attr_reader :options
78
+
79
+
80
+ # Returns the class for the macro.
81
+ #
82
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
83
+ # <tt>has_many :clients</tt> returns the Client class
84
+ def klass
85
+ @klass ||= class_name.constantize
86
+ end
87
+
88
+ # Returns the class name for the macro.
89
+ #
90
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
91
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
92
+ def class_name
93
+ @class_name ||= options[:class_name] || derive_class_name
94
+ end
95
+
96
+
97
+ # Returns whether or not this association reflection is for a collection
98
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
99
+ # +has_and_belongs_to_many+, +false+ otherwise.
100
+ def collection?
101
+ @collection
102
+ end
103
+
104
+
105
+
106
+ private
107
+ def derive_class_name
108
+ class_name = name.to_s.camelize
109
+ class_name = class_name.singularize if collection?
110
+ class_name
111
+ end
112
+
113
+
114
+ end
115
+
116
+ # Holds all the meta-data about an association as it was specified in the
117
+ # Active Record class.
118
+ class AssociationReflection < MacroReflection #:nodoc:
119
+
120
+ def initialize(macro, name, options, active_record)
121
+ super
122
+ @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
123
+ end
124
+
125
+ def primary_key_name
126
+ @primary_key_name ||= options[:foreign_key] || derive_primary_key_name
127
+ end
128
+
129
+ private
130
+
131
+ def derive_primary_key_name
132
+ 'pid'
133
+ end
134
+
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+
@@ -1,3 +1,4 @@
1
+ require 'active_support/core_ext/class/inheritable_attributes'
1
2
  module ActiveFedora
2
3
  # This module is meant to extend semantic node to add functionality based on a relationship's name
3
4
  # It is meant to turn a relationship into just another attribute in a model.
@@ -22,13 +23,17 @@ module ActiveFedora
22
23
  #
23
24
  # Then obj.parents will only return parents where their eyes are blue.
24
25
  module RelationshipsHelper
26
+ extend ActiveSupport::Concern
25
27
 
26
- include MediaShelfClassLevelInheritableAttributes
27
- ms_inheritable_attributes :class_relationships_desc
28
-
29
- def self.included(klass)
30
- klass.extend(ClassMethods)
28
+ # ms_inheritable_attributes :class_relationships_desc
29
+ included do
30
+ class_inheritable_accessor :class_relationships_desc
31
+ # self.class_relationships_desc = {}
31
32
  end
33
+
34
+ # def self.included(klass)
35
+ # klass.extend(ClassMethods)
36
+ # end
32
37
 
33
38
 
34
39
  # ** EXPERIMENTAL **
@@ -1,18 +1,13 @@
1
- require 'active_fedora/relationships_helper'
2
-
3
1
  module ActiveFedora
4
2
  module SemanticNode
5
- include MediaShelfClassLevelInheritableAttributes
6
-
7
- ms_inheritable_attributes :class_relationships, :internal_uri
8
-
9
- attr_accessor :internal_uri, :relationships_are_dirty, :load_from_solr
10
-
11
-
12
- def self.included(klass)
13
- klass.extend(ClassMethods)
14
- klass.send(:include, ActiveFedora::RelationshipsHelper)
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ class_inheritable_accessor :class_relationships, :internal_uri, :class_named_relationships_desc
6
+ self.class_relationships = {}
7
+ self.class_named_relationships_desc = {}
15
8
  end
9
+ attr_accessor :internal_uri, :named_relationship_desc, :relationships_are_dirty, :load_from_solr
10
+ #TODO I think we can remove named_relationship_desc from attr_accessor - jcoyne
16
11
 
17
12
  def assert_kind_of(n, o,t)
18
13
  raise "Assertion failure: #{n}: #{o} is not of type #{t}" unless o.kind_of?(t)
@@ -176,6 +171,56 @@ module ActiveFedora
176
171
  xml.to_s
177
172
  end
178
173
 
174
+ def load_inbound_relationship(name, predicate, opts={})
175
+ opts = {:rows=>25}.merge(opts)
176
+ query = self.class.inbound_relationship_query(self.pid,"#{name}")
177
+ return [] if query.empty?
178
+ solr_result = SolrService.instance.conn.query(query, :rows=>opts[:rows])
179
+ if opts[:response_format] == :solr
180
+ return solr_result
181
+ else
182
+ if opts[:response_format] == :id_array
183
+ id_array = []
184
+ solr_result.hits.each do |hit|
185
+ id_array << hit[SOLR_DOCUMENT_ID]
186
+ end
187
+ return id_array
188
+ elsif opts[:response_format] == :load_from_solr || self.load_from_solr
189
+ return ActiveFedora::SolrService.reify_solr_results(solr_result,{:load_from_solr=>true})
190
+ else
191
+ return ActiveFedora::SolrService.reify_solr_results(solr_result)
192
+ end
193
+ end
194
+ end
195
+
196
+ def load_outbound_relationship(name, predicate, opts={})
197
+ id_array = []
198
+ if !outbound_relationships[predicate].nil?
199
+ outbound_relationships[predicate].each do |rel|
200
+ id_array << rel.gsub("info:fedora/", "")
201
+ end
202
+ end
203
+ if opts[:response_format] == :id_array && !self.class.relationship_has_solr_filter_query?(:self,"#{name}")
204
+ return id_array
205
+ else
206
+ query = self.class.outbound_relationship_query("#{name}",id_array)
207
+ solr_result = SolrService.instance.conn.query(query)
208
+ if opts[:response_format] == :solr
209
+ return solr_result
210
+ elsif opts[:response_format] == :id_array
211
+ id_array = []
212
+ solr_result.hits.each do |hit|
213
+ id_array << hit[SOLR_DOCUMENT_ID]
214
+ end
215
+ return id_array
216
+ elsif opts[:response_format] == :load_from_solr || self.load_from_solr
217
+ return ActiveFedora::SolrService.reify_solr_results(solr_result,{:load_from_solr=>true})
218
+ else
219
+ return ActiveFedora::SolrService.reify_solr_results(solr_result)
220
+ end
221
+ end
222
+ end
223
+
179
224
  module ClassMethods
180
225
  #include ActiveFedora::RelationshipsHelper::ClassMethods
181
226
 
@@ -227,7 +272,7 @@ module ActiveFedora
227
272
  register_predicate(:inbound, predicate)
228
273
  create_inbound_relationship_finders(name, predicate, opts)
229
274
  else
230
- #raise "Duplicate use of predicate for outbound relationship name not allowed" if predicate_exists_with_different_relationship_name?(:self,name,predicate)
275
+ #raise "Duplicate use of predicate for named outbound relationship \"#{predicate.inspect}\" not allowed" if named_predicate_exists_with_different_name?(:self,name,predicate)
231
276
  register_relationship_desc(:self, name, predicate, opts)
232
277
  register_predicate(:self, predicate)
233
278
  create_relationship_name_methods(name)
@@ -253,29 +298,96 @@ module ActiveFedora
253
298
  def has_bidirectional_relationship(name, outbound_predicate, inbound_predicate, opts={})
254
299
  create_bidirectional_relationship_finders(name, outbound_predicate, inbound_predicate, opts)
255
300
  end
301
+
302
+ # ** EXPERIMENTAL **
303
+ #
304
+ # Check to make sure a subject,name, and predicate triple does not already exist
305
+ # with the same subject but different name.
306
+ # This method is used to ensure conflicting has_relationship calls are not made because
307
+ # predicates cannot be reused across relationship names. Otherwise, the mapping of relationship name
308
+ # to predicate in RELS-EXT would be broken.
309
+ def named_predicate_exists_with_different_name?(subject,name,predicate)
310
+ if named_relationships_desc.has_key?(subject)
311
+ named_relationships_desc[subject].each_pair do |existing_name, args|
312
+ return true if !args[:predicate].nil? && args[:predicate] == predicate && existing_name != name
313
+ end
314
+ end
315
+ return false
316
+ end
317
+
318
+ # ** EXPERIMENTAL **
319
+ #
320
+ # Return hash that stores named relationship metadata defined by has_relationship calls
321
+ # ====Example
322
+ # For the following relationship
323
+ #
324
+ # has_relationship "audio_records", :has_part, :type=>AudioRecord
325
+ # Results in the following returned by named_relationships_desc
326
+ # {:self=>{"audio_records"=>{:type=>AudioRecord, :singular=>nil, :predicate=>:has_part, :inbound=>false}}}
327
+ def named_relationships_desc
328
+ @class_named_relationships_desc ||= Hash[:self => {}]
329
+ #class_named_relationships_desc
330
+ end
331
+
332
+ # ** EXPERIMENTAL **
333
+ #
334
+ # Internal method that ensures a relationship subject such as :self and :inbound
335
+ # exist within the named_relationships_desc hash tracking named relationships metadata.
336
+ def register_named_subject(subject)
337
+ unless named_relationships_desc.has_key?(subject)
338
+ named_relationships_desc[subject] = {}
339
+ end
340
+ end
341
+
342
+ # ** EXPERIMENTAL **
343
+ #
344
+ # Internal method that adds relationship name and predicate pair to either an outbound (:self)
345
+ # or inbound (:inbound) relationship types.
346
+ def register_named_relationship(subject, name, predicate, opts)
347
+ register_named_subject(subject)
348
+ opts.merge!({:predicate=>predicate})
349
+ named_relationships_desc[subject][name] = opts
350
+ end
351
+
352
+ # ** EXPERIMENTAL **
353
+ #
354
+ # Used in has_relationship call to create dynamic helper methods to
355
+ # append and remove objects to and from a named relationship
356
+ # ====Example
357
+ # For the following relationship
358
+ #
359
+ # has_relationship "audio_records", :has_part, :type=>AudioRecord
360
+ #
361
+ # Methods audio_records_append and audio_records_remove are created.
362
+ # Boths methods take an object that is kind_of? ActiveFedora::Base as a parameter
363
+ def create_named_relationship_methods(name)
364
+ append_method_name = "#{name.to_s.downcase}_append"
365
+ remove_method_name = "#{name.to_s.downcase}_remove"
366
+ self.send(:define_method,:"#{append_method_name}") {|object| add_named_relationship(name,object)}
367
+ self.send(:define_method,:"#{remove_method_name}") {|object| remove_named_relationship(name,object)}
368
+ end
369
+
370
+ # ** EXPERIMENTAL **
371
+ # Similar to +create_named_relationship_methods+ except we are merely creating an alias for outbound portion of bidirectional
372
+ #
373
+ # ====Example
374
+ # has_bidirectional_relationship "members", :has_collection_member, :is_member_of_collection
375
+ #
376
+ # Method members_outbound_append and members_outbound_remove added
377
+ # This method will create members_append which does same thing as members_outbound_append
378
+ # and will create members_remove which does same thing as members_outbound_remove
379
+ def create_bidirectional_named_relationship_methods(name,outbound_name)
380
+ append_method_name = "#{name.to_s.downcase}_append"
381
+ remove_method_name = "#{name.to_s.downcase}_remove"
382
+ self.send(:define_method,:"#{append_method_name}") {|object| add_named_relationship(outbound_name,object)}
383
+ self.send(:define_method,:"#{remove_method_name}") {|object| remove_named_relationship(outbound_name,object)}
384
+ end
385
+
256
386
 
257
387
  def create_inbound_relationship_finders(name, predicate, opts = {})
258
388
  class_eval <<-END
259
389
  def #{name}(opts={})
260
- opts = {:rows=>25}.merge(opts)
261
- query = self.class.inbound_relationship_query(self.pid,"#{name}")
262
- return [] if query.empty?
263
- solr_result = SolrService.instance.conn.query(query, :rows=>opts[:rows])
264
- if opts[:response_format] == :solr
265
- return solr_result
266
- else
267
- if opts[:response_format] == :id_array
268
- id_array = []
269
- solr_result.hits.each do |hit|
270
- id_array << hit[SOLR_DOCUMENT_ID]
271
- end
272
- return id_array
273
- elsif opts[:response_format] == :load_from_solr || self.load_from_solr
274
- return ActiveFedora::SolrService.reify_solr_results(solr_result,{:load_from_solr=>true})
275
- else
276
- return ActiveFedora::SolrService.reify_solr_results(solr_result)
277
- end
278
- end
390
+ load_inbound_relationship('#{name}', '#{predicate}', opts)
279
391
  end
280
392
  def #{name}_ids
281
393
  #{name}(:response_format => :id_array)
@@ -292,31 +404,7 @@ module ActiveFedora
292
404
  def create_outbound_relationship_finders(name, predicate, opts = {})
293
405
  class_eval <<-END
294
406
  def #{name}(opts={})
295
- id_array = []
296
- if !outbound_relationships[#{predicate.inspect}].nil?
297
- outbound_relationships[#{predicate.inspect}].each do |rel|
298
- id_array << rel.gsub("info:fedora/", "")
299
- end
300
- end
301
- if opts[:response_format] == :id_array && !self.class.relationship_has_solr_filter_query?(:self,"#{name}")
302
- return id_array
303
- else
304
- query = self.class.outbound_relationship_query("#{name}",id_array)
305
- solr_result = SolrService.instance.conn.query(query)
306
- if opts[:response_format] == :solr
307
- return solr_result
308
- elsif opts[:response_format] == :id_array
309
- id_array = []
310
- solr_result.hits.each do |hit|
311
- id_array << hit[SOLR_DOCUMENT_ID]
312
- end
313
- return id_array
314
- elsif opts[:response_format] == :load_from_solr || self.load_from_solr
315
- return ActiveFedora::SolrService.reify_solr_results(solr_result,{:load_from_solr=>true})
316
- else
317
- return ActiveFedora::SolrService.reify_solr_results(solr_result)
318
- end
319
- end
407
+ load_outbound_relationship(#{name.inspect}, #{predicate.inspect}, opts)
320
408
  end
321
409
  def #{name}_ids
322
410
  #{name}(:response_format => :id_array)
@@ -390,6 +478,7 @@ module ActiveFedora
390
478
  # ds.relationships # => {:self=>{:has_model=>["afmodel:SimpleThing"],:has_part=>["demo:20"]},:inbound=>{:is_part_of=>["demo:6"]}
391
479
  def relationships
392
480
  @class_relationships ||= Hash[:self => {}]
481
+ #class_relationships
393
482
  end
394
483
 
395
484