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