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.
- 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
|
|