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.
- data/.rvmrc +1 -1
- data/Gemfile.lock +16 -10
- data/History.txt +4 -5
- data/README.textile +1 -1
- data/active-fedora.gemspec +2 -2
- data/lib/active_fedora.rb +36 -19
- data/lib/active_fedora/associations.rb +157 -0
- data/lib/active_fedora/associations/association_collection.rb +180 -0
- data/lib/active_fedora/associations/association_proxy.rb +177 -0
- data/lib/active_fedora/associations/belongs_to_association.rb +36 -0
- data/lib/active_fedora/associations/has_many_association.rb +52 -0
- data/lib/active_fedora/attribute_methods.rb +8 -0
- data/lib/active_fedora/base.rb +76 -80
- data/lib/active_fedora/datastream.rb +0 -1
- data/lib/active_fedora/delegating.rb +53 -0
- data/lib/active_fedora/model.rb +4 -2
- data/lib/active_fedora/nested_attributes.rb +153 -0
- data/lib/active_fedora/nokogiri_datastream.rb +17 -18
- data/lib/active_fedora/reflection.rb +140 -0
- data/lib/active_fedora/relationships_helper.rb +10 -5
- data/lib/active_fedora/semantic_node.rb +146 -57
- data/lib/active_fedora/solr_service.rb +0 -7
- data/lib/active_fedora/version.rb +1 -1
- data/lib/fedora/connection.rb +75 -111
- data/lib/fedora/repository.rb +14 -28
- data/lib/ruby-fedora.rb +1 -1
- data/spec/integration/associations_spec.rb +139 -0
- data/spec/integration/nested_attribute_spec.rb +40 -0
- data/spec/integration/repository_spec.rb +9 -14
- data/spec/integration/semantic_node_spec.rb +2 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/unit/active_fedora_spec.rb +2 -1
- data/spec/unit/association_proxy_spec.rb +13 -0
- data/spec/unit/base_active_model_spec.rb +61 -0
- data/spec/unit/base_delegate_spec.rb +59 -0
- data/spec/unit/base_spec.rb +45 -58
- data/spec/unit/connection_spec.rb +21 -21
- data/spec/unit/datastream_spec.rb +0 -11
- data/spec/unit/has_many_collection_spec.rb +27 -0
- data/spec/unit/model_spec.rb +1 -1
- data/spec/unit/nokogiri_datastream_spec.rb +3 -29
- data/spec/unit/repository_spec.rb +2 -2
- data/spec/unit/semantic_node_spec.rb +2 -0
- data/spec/unit/solr_service_spec.rb +0 -7
- metadata +36 -15
- data/lib/active_fedora/active_fedora_configuration_exception.rb +0 -2
- data/lib/util/class_level_inheritable_attributes.rb +0 -23
@@ -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
|
data/lib/active_fedora/model.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
|