active-fedora 3.2.0.pre3 → 3.2.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+