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
@@ -0,0 +1,177 @@
1
+ module ActiveFedora
2
+ module Associations
3
+ # This is the root class of all association proxies:
4
+ #
5
+ # AssociationProxy
6
+ # BelongsToAssociation
7
+ # AssociationCollection
8
+ # HasManyAssociation
9
+ #
10
+ # Association proxies in Active Fedora are middlemen between the object that
11
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
12
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
13
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
14
+ # ActiveFedora::Reflection::AssociationReflection.
15
+ #
16
+ # For example, given
17
+ #
18
+ # class Blog < ActiveFedora::Base
19
+ # has_many :posts
20
+ # end
21
+ #
22
+ # blog = Blog.find('changeme:123')
23
+ #
24
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
25
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
26
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
27
+ #
28
+ # This class has most of the basic instance methods removed, and delegates
29
+ # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
30
+ # corner case, it even removes the +class+ method and that's why you get
31
+ #
32
+ # blog.posts.class # => Array
33
+ #
34
+ # though the object behind <tt>blog.posts</tt> is not an Array, but an
35
+ # ActiveFedora::Associations::HasManyAssociation.
36
+
37
+ class AssociationProxy
38
+ delegate :to_param, :to=>:target
39
+
40
+ def initialize(owner, reflection)
41
+ @owner, @reflection = owner, reflection
42
+ @updated = false
43
+ # reflection.check_validity!
44
+ # Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
45
+ reset
46
+ end
47
+
48
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
49
+ def reset
50
+ @loaded = false
51
+ @target = nil
52
+ end
53
+
54
+ # Reloads the \target and returns +self+ on success.
55
+ def reload
56
+ reset
57
+ load_target
58
+ self unless @target.nil?
59
+ end
60
+
61
+ # Has the \target been already \loaded?
62
+ def loaded?
63
+ @loaded
64
+ end
65
+
66
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
67
+ def loaded
68
+ @loaded = true
69
+ end
70
+
71
+ # Returns the target of this proxy, same as +proxy_target+.
72
+ def target
73
+ @target
74
+ end
75
+
76
+ # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
77
+ def target=(target)
78
+ @target = target
79
+ loaded
80
+ end
81
+
82
+ # # Forwards the call to the target. Loads the \target if needed.
83
+ # def inspect
84
+ # load_target
85
+ # @target.inspect
86
+ # end
87
+
88
+ protected
89
+
90
+
91
+ # Assigns the ID of the owner to the corresponding foreign key in +record+.
92
+ # If the association is polymorphic the type of the owner is also set.
93
+ def set_belongs_to_association_for(record)
94
+ unless @owner.new_record?
95
+ record.add_relationship(@reflection.options[:property], @owner)
96
+ end
97
+ end
98
+
99
+
100
+ private
101
+ def method_missing(method, *args)
102
+ if load_target
103
+ unless @target.respond_to?(method)
104
+ message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
105
+ raise NoMethodError, message
106
+ end
107
+
108
+ if block_given?
109
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
110
+ else
111
+ @target.send(method, *args)
112
+ end
113
+ end
114
+ end
115
+
116
+
117
+ # Loads the \target if needed and returns it.
118
+ #
119
+ # This method is abstract in the sense that it relies on +find_target+,
120
+ # which is expected to be provided by descendants.
121
+ #
122
+ # If the \target is already \loaded it is just returned. Thus, you can call
123
+ # +load_target+ unconditionally to get the \target.
124
+ #
125
+ # ActiveFedora::RecordNotFound is rescued within the method, and it is
126
+ # not reraised. The proxy is \reset and +nil+ is the return value.
127
+ def load_target
128
+ return nil unless defined?(@loaded)
129
+
130
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
131
+ @target = find_target
132
+ end
133
+
134
+ if @target.nil?
135
+ reset
136
+ else
137
+ @loaded = true
138
+ @target
139
+ end
140
+ end
141
+
142
+ # Can be overwritten by associations that might have the foreign key
143
+ # available for an association without having the object itself (and
144
+ # still being a new record). Currently, only +belongs_to+ presents
145
+ # this scenario.
146
+ def foreign_key_present
147
+ false
148
+ end
149
+
150
+
151
+
152
+ # Raises ActiveFedora::AssociationTypeMismatch unless +record+ is of
153
+ # the kind of the class of the associated objects. Meant to be used as
154
+ # a sanity check when you are about to assign an associated record.
155
+ def raise_on_type_mismatch(record)
156
+ unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
157
+ message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
158
+ raise ActiveFedora::AssociationTypeMismatch, message
159
+ end
160
+ end
161
+
162
+
163
+ if RUBY_VERSION < '1.9.2'
164
+ # Array#flatten has problems with recursive arrays before Ruby 1.9.2.
165
+ # Going one level deeper solves the majority of the problems.
166
+ def flatten_deeper(array)
167
+ array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
168
+ end
169
+ else
170
+ def flatten_deeper(array)
171
+ array.flatten
172
+ end
173
+ end
174
+
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,36 @@
1
+ module ActiveFedora
2
+ module Associations
3
+ class BelongsToAssociation < AssociationProxy #:nodoc:
4
+ def replace(record)
5
+ if record.nil?
6
+ ### TODO a more efficient way of doing this would be to write a clear_relationship method
7
+ old_record = find_target
8
+ @owner.remove_relationship(@reflection.options[:property], old_record) unless old_record.nil?
9
+ else
10
+ raise_on_type_mismatch(record)
11
+
12
+ ### TODO a more efficient way of doing this would be to write a clear_relationship method
13
+ old_record = find_target
14
+ @owner.remove_relationship(@reflection.options[:property], old_record) unless old_record.nil?
15
+
16
+ @target = (AssociationProxy === record ? record.target : record)
17
+ @owner.add_relationship(@reflection.options[:property], record) unless record.new_record?
18
+ @updated = true
19
+ end
20
+
21
+ loaded
22
+ record
23
+ end
24
+
25
+ private
26
+ def find_target
27
+ @owner.load_outbound_relationship(@reflection.name.to_s, @reflection.options[:property]).first
28
+ end
29
+
30
+ def foreign_key_present
31
+ !@owner.send(@reflection.primary_key_name).nil?
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveFedora
2
+ module Associations
3
+ class HasManyAssociation < AssociationCollection #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ end
7
+
8
+ # Returns the number of records in this collection.
9
+ #
10
+ # That does not depend on whether the collection has already been loaded
11
+ # or not. The +size+ method is the one that takes the loaded flag into
12
+ # account and delegates to +count_records+ if needed.
13
+ #
14
+ # If the collection is empty the target is set to an empty array and
15
+ # the loaded flag is set to true as well.
16
+ def count_records
17
+ count = if @target
18
+ @target.size
19
+ else
20
+ 0
21
+ end
22
+
23
+ # If there's nothing in the database and @target has no new records
24
+ # we are certain the current target is an empty array. This is a
25
+ # documented side-effect of the method that may avoid an extra SELECT.
26
+ @target ||= [] and loaded if count == 0
27
+
28
+ return count
29
+ end
30
+
31
+ def insert_record(record, force = false, validate = true)
32
+ set_belongs_to_association_for(record)
33
+ #force ? record.save! : record.save(:validate => validate)
34
+ record.save
35
+ end
36
+
37
+ protected
38
+
39
+ # Deletes the records according to the <tt>:dependent</tt> option.
40
+ def delete_records(records)
41
+ records.each do |r|
42
+ r.remove_relationship(@reflection.options[:property], @owner)
43
+ end
44
+ end
45
+
46
+
47
+
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,8 @@
1
+ module ActiveFedora
2
+ module AttributeMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ end
7
+ end
8
+ end
@@ -1,10 +1,11 @@
1
- require 'util/class_level_inheritable_attributes'
2
- require 'active_fedora/model'
3
- require 'active_fedora/semantic_node'
4
1
  require "solrizer"
5
2
  require 'nokogiri'
6
3
  require "loggable"
7
- require 'benchmark'
4
+
5
+ #require 'active_support/core_ext/kernel/singleton_class'
6
+ #require 'active_support/core_ext/class/attribute'
7
+ require 'active_support/core_ext/class/inheritable_attributes'
8
+ #require 'active_support/inflector'
8
9
 
9
10
  SOLR_DOCUMENT_ID = "id" unless (defined?(SOLR_DOCUMENT_ID) && !SOLR_DOCUMENT_ID.nil?)
10
11
  ENABLE_SOLR_UPDATES = true unless defined?(ENABLE_SOLR_UPDATES)
@@ -32,13 +33,11 @@ module ActiveFedora
32
33
  # =Implementation
33
34
  # This class is really a facade for a basic Fedora::FedoraObject, which is stored internally.
34
35
  class Base
35
- include MediaShelfClassLevelInheritableAttributes
36
- ms_inheritable_attributes :ds_specs, :class_named_datastreams_desc
37
- include Model
36
+ include RelationshipsHelper
38
37
  include SemanticNode
39
- include Solrizer::FieldNameMapper
40
- include Loggable
41
-
38
+ class_inheritable_accessor :ds_specs, :class_named_datastreams_desc
39
+ self.class_named_datastreams_desc = {}
40
+ self.ds_specs = {}
42
41
  attr_accessor :named_datastreams_desc
43
42
 
44
43
 
@@ -56,7 +55,22 @@ module ActiveFedora
56
55
  @new_object = bool
57
56
  inner_object.new_object = bool
58
57
  end
59
-
58
+
59
+ ## Required by associations
60
+ def new_record?
61
+ self.new_object?
62
+ end
63
+
64
+ def persisted?
65
+ !new_object?
66
+ end
67
+
68
+ def attributes=(properties)
69
+ properties.each do |k, v|
70
+ respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
71
+ end
72
+ end
73
+
60
74
  # Constructor. If +attrs+ does not comtain +:pid+, we assume we're making a new one,
61
75
  # and call off to the Fedora Rest API for the next available Fedora pid, and mark as new object.
62
76
  # Also, if +attrs+ does not contain +:pid+ but does contain +:namespace+ it will pass the
@@ -65,7 +79,8 @@ module ActiveFedora
65
79
  #
66
80
  # If there is a pid, we're re-hydrating an existing object, and new object is false. Once the @inner_object is stored,
67
81
  # we configure any defined datastreams.
68
- def initialize(attrs = {})
82
+ def initialize(attrs = nil)
83
+ attrs = {} if attrs.nil?
69
84
  unless attrs[:pid]
70
85
  if attrs[:namespace]
71
86
  attrs = attrs.merge!({:pid=>Fedora::Repository.instance.nextid({:namespace=>attrs[:namespace]})})
@@ -79,6 +94,10 @@ module ActiveFedora
79
94
  @inner_object = Fedora::FedoraObject.new(attrs)
80
95
  @datastreams = {}
81
96
  configure_defined_datastreams
97
+
98
+ attributes = attrs.dup
99
+ [:pid, :namespace, :new_object,:create_date, :modified_date].each { |k| attributes.delete(k)}
100
+ self.attributes=attributes
82
101
  end
83
102
 
84
103
  #This method is used to specify the details of a datastream.
@@ -86,18 +105,19 @@ module ActiveFedora
86
105
  #execute the block, but stores it at the class level, to be executed
87
106
  #by any future instantiations.
88
107
  def self.has_metadata(args, &block)
89
- @ds_specs ||= Hash.new
90
- @ds_specs[args[:name]]= [args[:type], args.fetch(:label,""), block]
108
+ #@ds_specs ||= Hash.new
109
+ ds_specs[args[:name]]= [args[:type], args.fetch(:label,""), block]
91
110
  end
92
111
 
93
112
  def method_missing(name, *args)
94
113
  if datastreams.has_key? name.to_s
95
114
  ### Create and invoke a proxy method
96
115
  self.class.class_eval <<-end_eval
97
- def #{name}()
116
+ def #{name.to_s}()
98
117
  datastreams["#{name.to_s}"]
99
118
  end
100
119
  end_eval
120
+
101
121
  self.send(name)
102
122
  else
103
123
  super
@@ -123,12 +143,8 @@ module ActiveFedora
123
143
  # Refreshes the object's info from Fedora
124
144
  # Note: Currently just registers any new datastreams that have appeared in fedora
125
145
  def refresh
126
- ms = 1000 * Benchmark.realtime do
127
- inner_object.load_attributes_from_fedora
128
- @datastreams = datastreams_in_fedora.merge(datastreams_in_memory)
129
- end
130
- logger.debug "refreshing #{pid} took #{ms} ms"
131
- @datastreams
146
+ inner_object.load_attributes_from_fedora
147
+ @datastreams = datastreams_in_fedora.merge(datastreams_in_memory)
132
148
  end
133
149
 
134
150
  #Deletes a Base object, also deletes the info indexed in Solr, and
@@ -713,7 +729,7 @@ module ActiveFedora
713
729
  #
714
730
  # This hash is later used when adding a named datastream such as an "audio_file" as defined above.
715
731
  def self.named_datastreams_desc
716
- @class_named_datastreams_desc ||= {}
732
+ self.class_named_datastreams_desc ||= {}
717
733
  end
718
734
 
719
735
  #
@@ -762,11 +778,16 @@ module ActiveFedora
762
778
  def pid
763
779
  @inner_object.pid
764
780
  end
765
-
766
- #For Rails compatibility with url generators.
767
- def to_param
781
+
782
+
783
+ def id ### Needed for the nested form helper
768
784
  self.pid
769
785
  end
786
+
787
+ def to_key
788
+ persisted? ? [pid] : nil
789
+ end
790
+
770
791
  #return the internal fedora URI
771
792
  def internal_uri
772
793
  "info:fedora/#{pid}"
@@ -950,50 +971,18 @@ module ActiveFedora
950
971
  def update_index
951
972
  if defined?( Solrizer::Fedora::Solrizer )
952
973
  #logger.info("Trying to solrize pid: #{pid}")
953
- ms = 1000 * Benchmark.realtime do
954
- solrizer = Solrizer::Fedora::Solrizer.new
955
- solrizer.solrize( self )
956
- end
957
- logger.debug "solrize for #{pid} took #{ms} ms"
974
+ solrizer = Solrizer::Fedora::Solrizer.new
975
+ solrizer.solrize( self )
958
976
  else
959
977
  #logger.info("Trying to update solr for pid: #{pid}")
960
- ms = 1000 * Benchmark.realtime do
961
- SolrService.instance.conn.update(self.to_solr)
962
- end
963
- logger.debug "solr update for #{pid} took #{ms} ms"
978
+ SolrService.instance.conn.update(self.to_solr)
964
979
  end
965
980
  end
966
981
 
967
- # An ActiveRecord-ism to udpate metadata values.
968
- #
969
- # Example Usage:
970
- #
971
- # m.update_attributes(:fubar=>'baz')
972
- #
973
- # This will attempt to set the values for any fields named fubar in any of
974
- # the object's datastreams. This means DS1.fubar_values and DS2.fubar_values
975
- # are _both_ overwritten.
976
- #
977
- # If you want to specify which datastream(s) to update,
978
- # use the :datastreams argument like so:
979
- # m.update_attributes({:fubar=>'baz'}, :datastreams=>"my_ds")
980
- # or
981
- # m.update_attributes({:fubar=>'baz'}, :datastreams=>["my_ds", "my_other_ds"])
982
- def update_attributes(params={}, opts={})
983
- result = {}
984
- if opts[:datastreams]
985
- ds_array = []
986
- opts[:datastreams].each do |dsname|
987
- ds_array << datastreams[dsname]
988
- end
989
- else
990
- ds_array = metadata_streams
991
- end
992
- ds_array.each do |d|
993
- ds_result = d.update_attributes(params,opts)
994
- result[d.dsid] = ds_result
995
- end
996
- return result
982
+
983
+ def update_attributes(properties)
984
+ self.attributes=properties
985
+ save
997
986
  end
998
987
 
999
988
  # A convenience method for updating indexed attributes. The passed in hash
@@ -1101,25 +1090,32 @@ module ActiveFedora
1101
1090
 
1102
1091
  # Pushes the object and all of its new or dirty datastreams into Fedora
1103
1092
  def update
1104
- result = nil
1105
- ms = 1000 * Benchmark.realtime do
1106
- result = Fedora::Repository.instance.save(@inner_object)
1107
- end
1108
- logger.debug "instance save for #{pid} took #{ms} ms"
1109
- ms = 1000 * Benchmark.realtime do
1110
- datastreams_in_memory.each do |k,ds|
1111
- if ds.dirty? || ds.new_object?
1112
- if ds.class.included_modules.include?(ActiveFedora::MetadataDatastreamHelper) || ds.instance_of?(ActiveFedora::RelsExtDatastream)
1113
- @metadata_is_dirty = true
1114
- end
1115
- result = ds.save
1116
- end
1117
- end
1118
- refresh
1093
+ result = Fedora::Repository.instance.save(@inner_object)
1094
+ datastreams_in_memory.each do |k,ds|
1095
+ if ds.dirty? || ds.new_object?
1096
+ if ds.class.included_modules.include?(ActiveFedora::MetadataDatastreamHelper) || ds.instance_of?(ActiveFedora::RelsExtDatastream)
1097
+ # if ds.kind_of?(ActiveFedora::MetadataDatastream) || ds.kind_of?(ActiveFedora::NokogiriDatastream) || ds.instance_of?(ActiveFedora::RelsExtDatastream)
1098
+ @metadata_is_dirty = true
1099
+ end
1100
+ result = ds.save
1101
+ end
1119
1102
  end
1120
- logger.debug "datastream save for #{pid} took #{ms} ms"
1103
+ refresh
1121
1104
  return result
1122
1105
  end
1123
1106
 
1124
1107
  end
1108
+
1109
+ Base.class_eval do
1110
+ include Model
1111
+ include Solrizer::FieldNameMapper
1112
+ include Loggable
1113
+ include ActiveModel::Conversion
1114
+ extend ActiveModel::Naming
1115
+ include Delegating
1116
+ include Associations
1117
+ include NestedAttributes
1118
+ include Reflection
1119
+ end
1120
+
1125
1121
  end