active-fedora 3.2.0.pre3 → 3.2.0.pre4

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.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active-fedora (3.2.0.pre3)
4
+ active-fedora (3.2.0.pre4)
5
5
  activeresource (>= 3.0.0)
6
6
  activesupport (>= 3.0.0)
7
7
  equivalent-xml
@@ -1,5 +1,8 @@
1
1
  3.2.0
2
2
 
3
+ Added ActiveModel validations.
4
+ Added callbacks on initialize, save, update, create, delete and find
5
+ Added Base.create
3
6
  HYDRA-730 ActiveFedora isn't being initialized unless it is specified in the project Gemfile
4
7
  Don't create pids until save
5
8
  inspect and equality methods
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
6
6
  s.name = "active-fedora"
7
7
  s.version = ActiveFedora::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
- s.authors = ["Matt Zumwalt", "McClain Looney"]
9
+ s.authors = ["Matt Zumwalt", "McClain Looney", "Justin Coyne"]
10
10
  s.email = ["matt.zumwalt@yourmediashelf.com"]
11
11
  s.homepage = %q{http://yourmediashelf.com/activefedora}
12
12
  s.summary = %q{A convenience libary for manipulating MODS (Metadata Object Description Schema) documents.}
@@ -17,6 +17,7 @@ module ActiveFedora #:nodoc:
17
17
  autoload :AttributeMethods
18
18
  autoload :Base
19
19
  autoload :ContentModel
20
+ autoload :Callbacks
20
21
  autoload :Reflection
21
22
  autoload :Relationships
22
23
  autoload :FileManagement
@@ -25,11 +26,13 @@ module ActiveFedora #:nodoc:
25
26
  autoload :Delegating
26
27
  autoload :DigitalObject
27
28
  autoload :UnsavedDigitalObject
29
+ autoload :SolrDigitalObject
28
30
  autoload :Model
29
31
  autoload :MetadataDatastream
30
32
  autoload :MetadataDatastreamHelper
31
33
  autoload :NokogiriDatastream
32
34
  autoload :Property
35
+ autoload :Persistence
33
36
  autoload :QualifiedDublinCoreDatastream
34
37
  autoload :RelsExtDatastream
35
38
  autoload :ServiceDefinitions
@@ -39,6 +42,7 @@ module ActiveFedora #:nodoc:
39
42
  autoload :DatastreamCollections
40
43
  autoload :NamedRelationships
41
44
  autoload :Predicates
45
+ autoload :Validations
42
46
 
43
47
  end
44
48
 
@@ -101,30 +101,40 @@ module ActiveFedora
101
101
  end
102
102
  end
103
103
 
104
- # Constructor. If +attrs+ does not comtain +:pid+, we assume we're making a new one,
105
- # and call off to the Fedora Rest API for the next available Fedora pid, and mark as new object.
104
+ # Constructor. You may supply a custom +:pid+, or we call the Fedora Rest API for the
105
+ # next available Fedora pid, and mark as new object.
106
106
  # Also, if +attrs+ does not contain +:pid+ but does contain +:namespace+ it will pass the
107
107
  # +:namespace+ value to Fedora::Repository.nextid to generate the next pid available within
108
108
  # the given namespace.
109
- #
110
- # If there is a pid, we're re-hydrating an existing object, and new object is false. Once the @inner_object is stored,
111
- # we configure any defined datastreams.
112
109
  def initialize(attrs = nil)
113
110
  attrs = {} if attrs.nil?
114
111
  attributes = attrs.dup
115
- @inner_object = attributes.delete(:inner_object)
116
- unless @inner_object
117
- if attributes[:pid]
118
- @inner_object = DigitalObject.find(self.class, attributes[:pid])
119
- else
120
- @inner_object = UnsavedDigitalObject.new(self.class, attributes.delete(:namespace))
121
- self.relationships_loaded = true
122
- end
123
- end
112
+ @inner_object = UnsavedDigitalObject.new(self.class, attributes.delete(:namespace), attributes.delete(:pid))
113
+ self.relationships_loaded = true
124
114
  load_datastreams
125
115
 
126
- [:pid, :new_object,:create_date, :modified_date].each { |k| attributes.delete(k)}
116
+ [:new_object,:create_date, :modified_date].each { |k| attributes.delete(k)}
127
117
  self.attributes=attributes
118
+ run_callbacks :initialize
119
+ end
120
+
121
+
122
+ # Initialize an empty model object and set the +inner_obj+
123
+ # example:
124
+ #
125
+ # class Post < ActiveFedora::Base
126
+ # has_metadata :name => "properties", :type => ActiveFedora::MetadataDatastream
127
+ # end
128
+ #
129
+ # post = Post.allocate
130
+ # post.init_with(DigitalObject.find(pid))
131
+ # post.properties.title # => 'hello world'
132
+ def init_with(inner_obj)
133
+ @inner_object = inner_obj
134
+ load_datastreams
135
+ run_callbacks :find
136
+ run_callbacks :initialize
137
+ self
128
138
  end
129
139
 
130
140
  def self.datastream_class_for_name(dsid)
@@ -139,6 +149,11 @@ module ActiveFedora
139
149
  ds_specs[args[:name]]= {:type => args[:type], :label => args.fetch(:label,""), :control_group => args.fetch(:control_group,"X"), :disseminator => args.fetch(:disseminator,""), :url => args.fetch(:url,""),:block => block}
140
150
  end
141
151
 
152
+ def self.create(args)
153
+ obj = self.new(args)
154
+ obj.save
155
+ obj
156
+ end
142
157
 
143
158
  ## Given a method name, return the best-guess dsid
144
159
  def corresponding_datastream_name(method_name)
@@ -149,61 +164,7 @@ module ActiveFedora
149
164
  nil
150
165
  end
151
166
 
152
- #Saves a Base object, and any dirty datastreams, then updates
153
- #the Solr index for this object.
154
- def save
155
-
156
- # If it's a new object, set the conformsTo relationship for Fedora CMA
157
- if new_object?
158
- result = create
159
- else
160
- result = update
161
- end
162
- self.update_index if @metadata_is_dirty == true && ENABLE_SOLR_UPDATES
163
- @metadata_is_dirty = false
164
- return result
165
- end
166
-
167
- def save!
168
- save
169
- end
170
167
 
171
- # Refreshes the object's info from Fedora
172
- # Note: Currently just registers any new datastreams that have appeared in fedora
173
- def refresh
174
- # inner_object.load_attributes_from_fedora
175
- end
176
-
177
- #Deletes a Base object, also deletes the info indexed in Solr, and
178
- #the underlying inner_object. If this object is held in any relationships (ie inbound relationships
179
- #outside of this object it will remove it from those items rels-ext as well
180
- def delete
181
- inbound_relationships(:objects).each_pair do |predicate, objects|
182
- objects.each do |obj|
183
- if obj.respond_to?(:remove_relationship)
184
- obj.remove_relationship(predicate,self)
185
- obj.save
186
- end
187
- end
188
- end
189
-
190
- #Fedora::Repository.instance.delete(@inner_object)
191
- pid = self.pid ## cache so it's still available after delete
192
- begin
193
- @inner_object.delete
194
- rescue RestClient::ResourceNotFound =>e
195
- raise ObjectNotFoundError, "Unable to find #{pid} in the repository"
196
- end
197
- if ENABLE_SOLR_UPDATES
198
- ActiveFedora::SolrService.instance.conn.delete(pid)
199
- # if defined?( Solrizer::Solrizer )
200
- # solrizer = Solrizer::Solrizer.new
201
- # solrizer.solrize_delete(pid)
202
- # end
203
- end
204
- end
205
-
206
-
207
168
  #
208
169
  # Datastream Management
209
170
  #
@@ -445,12 +406,6 @@ module ActiveFedora
445
406
  @inner_object.new? ? Time.now : @inner_object.profile["objLastModDate"]
446
407
  end
447
408
 
448
- #return the error list of the inner object (unless it's a new object)
449
- def errors
450
- #@inner_object.errors
451
- []
452
- end
453
-
454
409
  #return the label of the inner object (unless it's a new object)
455
410
  def label
456
411
  @inner_object.label
@@ -546,7 +501,7 @@ module ActiveFedora
546
501
  unless klass.ancestors.include? ActiveFedora::Base
547
502
  raise "Cannot adapt #{self.class.name} to #{klass.name}: Not a ActiveFedora::Base subclass"
548
503
  end
549
- klass.new({:inner_object=>inner_object})
504
+ klass.allocate.init_with(inner_object)
550
505
  end
551
506
  # ** EXPERIMENTAL **
552
507
  #
@@ -574,7 +529,7 @@ module ActiveFedora
574
529
 
575
530
  create_date = solr_doc[ActiveFedora::SolrService.solr_name(:system_create, :date)].nil? ? solr_doc[ActiveFedora::SolrService.solr_name(:system_create, :date).to_s] : solr_doc[ActiveFedora::SolrService.solr_name(:system_create, :date)]
576
531
  modified_date = solr_doc[ActiveFedora::SolrService.solr_name(:system_create, :date)].nil? ? solr_doc[ActiveFedora::SolrService.solr_name(:system_modified, :date).to_s] : solr_doc[ActiveFedora::SolrService.solr_name(:system_modified, :date)]
577
- obj = self.new({:pid=>solr_doc[SOLR_DOCUMENT_ID],:create_date=>create_date,:modified_date=>modified_date})
532
+ obj = self.allocate.init_with(SolrDigitalObject.new(:pid=>solr_doc[SOLR_DOCUMENT_ID],:create_date=>create_date,:modified_date=>modified_date))
578
533
  #set by default to load any dependent relationship objects from solr as well
579
534
  #need to call rels_ext once so it exists when iterating over datastreams
580
535
  obj.rels_ext
@@ -586,24 +541,6 @@ module ActiveFedora
586
541
  obj
587
542
  end
588
543
 
589
- # Updates Solr index with self.
590
- def update_index
591
- if defined?( Solrizer::Fedora::Solrizer )
592
- #logger.info("Trying to solrize pid: #{pid}")
593
- solrizer = Solrizer::Fedora::Solrizer.new
594
- solrizer.solrize( self )
595
- else
596
- #logger.info("Trying to update solr for pid: #{pid}")
597
- SolrService.instance.conn.update(self.to_solr)
598
- end
599
- end
600
-
601
-
602
- def update_attributes(properties)
603
- self.attributes=properties
604
- save
605
- end
606
-
607
544
  # A convenience method for updating indexed attributes. The passed in hash
608
545
  # must look like this :
609
546
  # {{:name=>{"0"=>"a","1"=>"b"}}
@@ -680,11 +617,6 @@ module ActiveFedora
680
617
  end
681
618
  end
682
619
 
683
- # This can be overriden to assert a different model
684
- # It's normally called once in the lifecycle, by #create#
685
- def assert_content_model
686
- add_relationship(:has_model, ActiveFedora::ContentModel.pid_from_ruby_class(self.class))
687
- end
688
620
 
689
621
  private
690
622
  def configure_defined_datastreams
@@ -727,39 +659,16 @@ module ActiveFedora
727
659
  end
728
660
  end
729
661
 
730
-
731
-
732
- # Deals with preparing new object to be saved to Fedora, then pushes it and its datastreams into Fedora.
733
- def create
734
- @inner_object = @inner_object.save #replace the unsaved digital object with a saved digital object
735
- assert_content_model
736
- @metadata_is_dirty = true
737
- update
738
- end
739
-
740
- # Pushes the object and all of its new or dirty datastreams into Fedora
741
- def update
742
- datastreams.each {|k, ds| ds.serialize! }
743
- @metadata_is_dirty = datastreams.any? {|k,ds| ds.changed? && (ds.class.included_modules.include?(ActiveFedora::MetadataDatastreamHelper) || ds.instance_of?(ActiveFedora::RelsExtDatastream))}
744
-
745
- result = @inner_object.save
746
-
747
- ### Rubydora re-inits the datastreams after a save, so ensure our copy stays in synch
748
- @inner_object.datastreams.each do |dsid, ds|
749
- datastreams[dsid] = ds
750
- ds.model = self if ds.kind_of? RelsExtDatastream
751
- end
752
- refresh
753
- return !!result
754
- end
755
-
756
662
  end
757
663
 
758
664
  Base.class_eval do
665
+ include ActiveFedora::Persistence
759
666
  include Model
760
667
  include Solrizer::FieldNameMapper
761
668
  include Loggable
762
669
  include ActiveModel::Conversion
670
+ include Validations
671
+ include Callbacks
763
672
  extend ActiveModel::Naming
764
673
  include Delegating
765
674
  include Associations
@@ -0,0 +1,252 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveFedora
4
+ # = Active Fedora Callbacks, adapted from ActiveRecord
5
+ #
6
+ # Callbacks are hooks into the life cycle of an Active Fedora object that allow you to trigger logic
7
+ # before or after an alteration of the object state. This can be used to make sure that associated and
8
+ # dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
9
+ # before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
10
+ # the <tt>Base#save</tt> call for a new record:
11
+ #
12
+ # * (-) <tt>save</tt>
13
+ # * (-) <tt>valid</tt>
14
+ # * (1) <tt>before_validation</tt>
15
+ # * (-) <tt>validate</tt>
16
+ # * (2) <tt>after_validation</tt>
17
+ # * (3) <tt>before_save</tt>
18
+ # * (4) <tt>before_create</tt>
19
+ # * (-) <tt>create</tt>
20
+ # * (5) <tt>after_create</tt>
21
+ # * (6) <tt>after_save</tt>
22
+ # * (7) <tt>after_commit</tt>
23
+ #
24
+ # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
25
+ # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
26
+ # are instantiated as well.
27
+ #
28
+ # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the
29
+ # Active Fedora life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
30
+ # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
31
+ #
32
+ # Examples:
33
+ # class CreditCard < ActiveFedora::Base
34
+ # # Strip everything but digits, so the user can specify "555 234 34" or
35
+ # # "5552-3434" or both will mean "55523434"
36
+ # before_validation(:on => :create) do
37
+ # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
38
+ # end
39
+ # end
40
+ #
41
+ # class Subscription < ActiveFedora::Base
42
+ # before_create :record_signup
43
+ #
44
+ # private
45
+ # def record_signup
46
+ # self.signed_up_on = Date.today
47
+ # end
48
+ # end
49
+ #
50
+ #
51
+ # == Inheritable callback queues
52
+ #
53
+ # Besides the overwritable callback methods, it's also possible to register callbacks through the
54
+ # use of the callback macros. Their main advantage is that the macros add behavior into a callback
55
+ # queue that is kept intact down through an inheritance hierarchy.
56
+ #
57
+ # class Topic < ActiveFedora::Base
58
+ # before_delete :destroy_author
59
+ # end
60
+ #
61
+ # class Reply < Topic
62
+ # before_delete :destroy_readers
63
+ # end
64
+ #
65
+ # Now, when <tt>Topic#delete</tt> is run only +destroy_author+ is called. When <tt>Reply#delete</tt> is
66
+ # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
67
+ # where the +before_delete+ method is overridden:
68
+ #
69
+ # class Topic < ActiveFedora::Base
70
+ # def before_delete() destroy_author end
71
+ # end
72
+ #
73
+ # class Reply < Topic
74
+ # def before_delete() destroy_readers end
75
+ # end
76
+ #
77
+ # In that case, <tt>Reply#delete</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
78
+ # So, use the callback macros when you want to ensure that a certain callback is called for the entire
79
+ # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant
80
+ # to decide whether they want to call +super+ and trigger the inherited callbacks.
81
+ #
82
+ # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
83
+ # callbacks before specifying the associations. Otherwise, you might trigger the loading of a
84
+ # child before the parent has registered the callbacks and they won't be inherited.
85
+ #
86
+ # == Types of callbacks
87
+ #
88
+ # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
89
+ # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
90
+ # are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
91
+ # creating mix-ins), and inline eval methods are deprecated.
92
+ #
93
+ # The method reference callbacks work by specifying a protected or private method available in the object, like this:
94
+ #
95
+ # class Topic < ActiveFedora::Base
96
+ # before_delete :delete_parents
97
+ #
98
+ # private
99
+ # def delete_parents
100
+ # self.class.delete_all "parent_id = #{id}"
101
+ # end
102
+ # end
103
+ #
104
+ # The callback objects have methods named after the callback called with the record as the only parameter, such as:
105
+ #
106
+ # class BankAccount < ActiveFedora::Base
107
+ # before_save EncryptionWrapper.new
108
+ # after_save EncryptionWrapper.new
109
+ # after_initialize EncryptionWrapper.new
110
+ # end
111
+ #
112
+ # class EncryptionWrapper
113
+ # def before_save(record)
114
+ # record.credit_card_number = encrypt(record.credit_card_number)
115
+ # end
116
+ #
117
+ # def after_save(record)
118
+ # record.credit_card_number = decrypt(record.credit_card_number)
119
+ # end
120
+ #
121
+ # alias_method :after_find, :after_save
122
+ #
123
+ # private
124
+ # def encrypt(value)
125
+ # # Secrecy is committed
126
+ # end
127
+ #
128
+ # def decrypt(value)
129
+ # # Secrecy is unveiled
130
+ # end
131
+ # end
132
+ #
133
+ # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
134
+ # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
135
+ # initialization data such as the name of the attribute to work with:
136
+ #
137
+ # class BankAccount < ActiveFedora::Base
138
+ # before_save EncryptionWrapper.new("credit_card_number")
139
+ # after_save EncryptionWrapper.new("credit_card_number")
140
+ # after_initialize EncryptionWrapper.new("credit_card_number")
141
+ # end
142
+ #
143
+ # class EncryptionWrapper
144
+ # def initialize(attribute)
145
+ # @attribute = attribute
146
+ # end
147
+ #
148
+ # def before_save(record)
149
+ # record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
150
+ # end
151
+ #
152
+ # def after_save(record)
153
+ # record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
154
+ # end
155
+ #
156
+ # alias_method :after_find, :after_save
157
+ #
158
+ # private
159
+ # def encrypt(value)
160
+ # # Secrecy is committed
161
+ # end
162
+ #
163
+ # def decrypt(value)
164
+ # # Secrecy is unveiled
165
+ # end
166
+ # end
167
+ #
168
+ # The callback macros usually accept a symbol for the method they're supposed to run, but you can also
169
+ # pass a "method string", which will then be evaluated within the binding of the callback. Example:
170
+ #
171
+ # class Topic < ActiveFedora::Base
172
+ # before_delete 'self.class.delete_all "parent_id = #{id}"'
173
+ # end
174
+ #
175
+ # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback
176
+ # is triggered. Also note that these inline callbacks can be stacked just like the regular ones:
177
+ #
178
+ # class Topic < ActiveFedora::Base
179
+ # before_delete 'self.class.delete_all "parent_id = #{id}"',
180
+ # 'puts "Evaluated after parents are deleted"'
181
+ # end
182
+ #
183
+ # == <tt>before_validation*</tt> returning statements
184
+ #
185
+ # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be
186
+ # aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
187
+ # ActiveFedora::RecordInvalid exception. Nothing will be appended to the errors object.
188
+ #
189
+ # == Canceling callbacks
190
+ #
191
+ # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are
192
+ # cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled.
193
+ # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
194
+ # methods on the model, which are called last.
195
+ #
196
+ # == Debugging callbacks
197
+ #
198
+ # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
199
+ # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
200
+ # defines what part of the chain the callback runs in.
201
+ #
202
+ # To find all callbacks in the before_save callback chain:
203
+ #
204
+ # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
205
+ #
206
+ # Returns an array of callback objects that form the before_save chain.
207
+ #
208
+ # To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
209
+ #
210
+ # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
211
+ #
212
+ # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
213
+ #
214
+ module Callbacks
215
+ extend ActiveSupport::Concern
216
+
217
+ CALLBACKS = [
218
+ :after_initialize, :after_find, :before_validation, :after_validation,
219
+ :before_save, :around_save, :after_save, :before_create, :around_create,
220
+ :after_create, :before_update, :around_update, :after_update,
221
+ :before_delete, :around_delete, :after_delete
222
+ ]
223
+
224
+ included do
225
+ extend ActiveModel::Callbacks
226
+ include ActiveModel::Validations::Callbacks
227
+
228
+ define_model_callbacks :initialize, :find, :only => :after
229
+ define_model_callbacks :save, :create, :update, :delete
230
+ end
231
+
232
+ def destroy #:nodoc:
233
+ run_callbacks(:destroy) { super }
234
+ end
235
+
236
+ def save #:nodoc:
237
+ run_callbacks(:save) { super }
238
+ end
239
+
240
+ private
241
+
242
+
243
+ def create #:nodoc:
244
+ run_callbacks(:create) { super }
245
+ end
246
+
247
+ def update(*) #:nodoc:
248
+ run_callbacks(:update) { super }
249
+ end
250
+ end
251
+ end
252
+