active-fedora 2.3.8 → 3.0.0

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