active-fedora 6.8.0 → 7.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +15 -5
- data/CONTRIBUTING.md +2 -0
- data/Gemfile +0 -2
- data/History.txt +2 -32
- data/README.md +143 -0
- data/Rakefile +5 -7
- data/active-fedora.gemspec +9 -9
- data/gemfiles/rails3.gemfile +11 -0
- data/gemfiles/rails4.gemfile +10 -0
- data/lib/active_fedora.rb +31 -4
- data/lib/active_fedora/association_relation.rb +18 -0
- data/lib/active_fedora/associations.rb +38 -171
- data/lib/active_fedora/associations/association.rb +163 -0
- data/lib/active_fedora/associations/association_scope.rb +39 -0
- data/lib/active_fedora/associations/belongs_to_association.rb +47 -25
- data/lib/active_fedora/associations/builder/association.rb +55 -0
- data/lib/active_fedora/associations/builder/belongs_to.rb +100 -0
- data/lib/active_fedora/associations/builder/collection_association.rb +56 -0
- data/lib/active_fedora/associations/builder/has_and_belongs_to_many.rb +30 -0
- data/lib/active_fedora/associations/builder/has_many.rb +63 -0
- data/lib/active_fedora/associations/builder/singular_association.rb +32 -0
- data/lib/active_fedora/associations/{association_collection.rb → collection_association.rb} +203 -53
- data/lib/active_fedora/associations/collection_proxy.rb +862 -0
- data/lib/active_fedora/associations/has_and_belongs_to_many_association.rb +35 -25
- data/lib/active_fedora/associations/has_many_association.rb +36 -11
- data/lib/active_fedora/associations/singular_association.rb +62 -0
- data/lib/active_fedora/attributes.rb +43 -139
- data/lib/active_fedora/autosave_association.rb +317 -0
- data/lib/active_fedora/base.rb +10 -327
- data/lib/active_fedora/callbacks.rb +1 -3
- data/lib/active_fedora/content_model.rb +16 -0
- data/lib/active_fedora/core.rb +151 -0
- data/lib/active_fedora/datastream_attribute.rb +76 -0
- data/lib/active_fedora/datastream_hash.rb +8 -13
- data/lib/active_fedora/datastreams.rb +39 -26
- data/lib/active_fedora/digital_object.rb +2 -2
- data/lib/active_fedora/fedora_attributes.rb +45 -0
- data/lib/active_fedora/fixture_loader.rb +1 -1
- data/lib/active_fedora/indexing.rb +6 -1
- data/lib/active_fedora/model.rb +0 -17
- data/lib/active_fedora/nested_attributes.rb +2 -2
- data/lib/active_fedora/null_relation.rb +7 -0
- data/lib/active_fedora/om_datastream.rb +3 -4
- data/lib/active_fedora/persistence.rb +41 -29
- data/lib/active_fedora/querying.rb +2 -163
- data/lib/active_fedora/rdf.rb +1 -0
- data/lib/active_fedora/rdf/indexing.rb +67 -0
- data/lib/active_fedora/rdf_datastream.rb +2 -50
- data/lib/active_fedora/rdf_node.rb +12 -7
- data/lib/active_fedora/rdf_node/term_proxy.rb +30 -21
- data/lib/active_fedora/rdfxml_rdf_datastream.rb +1 -1
- data/lib/active_fedora/reflection.rb +163 -20
- data/lib/active_fedora/relation.rb +33 -130
- data/lib/active_fedora/relation/calculations.rb +19 -0
- data/lib/active_fedora/relation/delegation.rb +22 -0
- data/lib/active_fedora/relation/finder_methods.rb +247 -0
- data/lib/active_fedora/relation/merger.rb +22 -0
- data/lib/active_fedora/relation/query_methods.rb +58 -0
- data/lib/active_fedora/relation/spawn_methods.rb +46 -0
- data/lib/active_fedora/relationship_graph.rb +0 -2
- data/lib/active_fedora/rels_ext_datastream.rb +1 -4
- data/lib/active_fedora/rubydora_connection.rb +1 -1
- data/lib/active_fedora/scoping.rb +20 -0
- data/lib/active_fedora/scoping/default.rb +38 -0
- data/lib/active_fedora/scoping/named.rb +32 -0
- data/lib/active_fedora/semantic_node.rb +54 -106
- data/lib/active_fedora/serialization.rb +19 -0
- data/lib/active_fedora/sharding.rb +58 -0
- data/lib/active_fedora/solr_digital_object.rb +15 -5
- data/lib/active_fedora/solr_instance_loader.rb +1 -1
- data/lib/active_fedora/solr_service.rb +1 -1
- data/lib/active_fedora/unsaved_digital_object.rb +6 -4
- data/lib/active_fedora/version.rb +1 -1
- data/lib/tasks/active_fedora.rake +3 -0
- data/lib/tasks/active_fedora_dev.rake +6 -5
- data/spec/config_helper.rb +14 -14
- data/spec/integration/associations_spec.rb +168 -455
- data/spec/integration/attributes_spec.rb +12 -11
- data/spec/integration/auditable_spec.rb +11 -11
- data/spec/integration/autosave_association_spec.rb +25 -0
- data/spec/integration/base_spec.rb +163 -163
- data/spec/integration/belongs_to_association_spec.rb +166 -0
- data/spec/integration/bug_spec.rb +7 -7
- data/spec/integration/collection_association_spec.rb +58 -0
- data/spec/integration/complex_rdf_datastream_spec.rb +88 -88
- data/spec/integration/datastream_collections_spec.rb +69 -69
- data/spec/integration/datastream_spec.rb +43 -43
- data/spec/integration/datastreams_spec.rb +63 -63
- data/spec/integration/delete_all_spec.rb +46 -39
- data/spec/integration/fedora_solr_sync_spec.rb +5 -5
- data/spec/integration/field_to_solr_name_spec.rb +34 -0
- data/spec/integration/full_featured_model_spec.rb +100 -101
- data/spec/integration/has_and_belongs_to_many_associations_spec.rb +341 -0
- data/spec/integration/has_many_associations_spec.rb +172 -24
- data/spec/integration/json_serialization_spec.rb +31 -0
- data/spec/integration/load_from_solr_spec.rb +48 -0
- data/spec/integration/model_spec.rb +35 -40
- data/spec/integration/nested_attribute_spec.rb +42 -43
- data/spec/integration/ntriples_datastream_spec.rb +131 -113
- data/spec/integration/om_datastream_spec.rb +67 -67
- data/spec/integration/persistence_spec.rb +7 -7
- data/spec/integration/rdf_nested_attributes_spec.rb +56 -56
- data/spec/integration/relation_delegation_spec.rb +26 -25
- data/spec/integration/relation_spec.rb +42 -0
- data/spec/integration/rels_ext_datastream_spec.rb +20 -20
- data/spec/integration/scoped_query_spec.rb +61 -51
- data/spec/integration/solr_instance_loader_spec.rb +5 -5
- data/spec/integration/solr_service_spec.rb +46 -46
- data/spec/samples/hydra-mods_article_datastream.rb +334 -334
- data/spec/samples/hydra-rights_metadata_datastream.rb +57 -57
- data/spec/samples/marpa-dc_datastream.rb +17 -17
- data/spec/samples/models/audio_record.rb +16 -16
- data/spec/samples/models/image.rb +2 -2
- data/spec/samples/models/mods_article.rb +5 -5
- data/spec/samples/models/oral_history.rb +18 -18
- data/spec/samples/models/seminar.rb +24 -24
- data/spec/samples/models/seminar_audio_file.rb +17 -17
- data/spec/samples/oral_history_sample_model.rb +21 -21
- data/spec/samples/special_thing.rb +14 -14
- data/spec/spec_helper.rb +11 -7
- data/spec/support/an_active_model.rb +2 -8
- data/spec/support/freeze_mocks.rb +12 -0
- data/spec/support/mock_fedora.rb +17 -16
- data/spec/unit/active_fedora_spec.rb +58 -60
- data/spec/unit/attributes_spec.rb +314 -0
- data/spec/unit/base_active_model_spec.rb +28 -27
- data/spec/unit/base_cma_spec.rb +5 -5
- data/spec/unit/base_datastream_management_spec.rb +27 -27
- data/spec/unit/base_extra_spec.rb +76 -48
- data/spec/unit/base_spec.rb +277 -348
- data/spec/unit/callback_spec.rb +18 -19
- data/spec/unit/code_configurator_spec.rb +17 -17
- data/spec/unit/config_spec.rb +8 -16
- data/spec/unit/content_model_spec.rb +79 -60
- data/spec/unit/datastream_collections_spec.rb +229 -229
- data/spec/unit/datastream_spec.rb +51 -63
- data/spec/unit/datastreams_spec.rb +87 -87
- data/spec/unit/file_configurator_spec.rb +217 -217
- data/spec/unit/has_and_belongs_to_many_collection_spec.rb +44 -25
- data/spec/unit/has_many_collection_spec.rb +26 -8
- data/spec/unit/inheritance_spec.rb +13 -12
- data/spec/unit/model_spec.rb +39 -45
- data/spec/unit/nom_datastream_spec.rb +15 -15
- data/spec/unit/ntriples_datastream_spec.rb +123 -118
- data/spec/unit/om_datastream_spec.rb +227 -233
- data/spec/unit/persistence_spec.rb +34 -15
- data/spec/unit/predicates_spec.rb +73 -73
- data/spec/unit/property_spec.rb +17 -9
- data/spec/unit/qualified_dublin_core_datastream_spec.rb +33 -33
- data/spec/unit/query_spec.rb +222 -198
- data/spec/unit/rdf_datastream_spec.rb +21 -28
- data/spec/unit/rdf_list_nested_attributes_spec.rb +34 -34
- data/spec/unit/rdf_list_spec.rb +65 -64
- data/spec/unit/rdf_node_spec.rb +7 -7
- data/spec/unit/rdf_xml_writer_spec.rb +10 -10
- data/spec/unit/rdfxml_rdf_datastream_spec.rb +27 -27
- data/spec/unit/relationship_graph_spec.rb +51 -51
- data/spec/unit/rels_ext_datastream_spec.rb +68 -74
- data/spec/unit/rspec_matchers/belong_to_associated_active_fedora_object_matcher_spec.rb +15 -15
- data/spec/unit/rspec_matchers/have_many_associated_active_fedora_objects_matcher_spec.rb +15 -15
- data/spec/unit/rspec_matchers/have_predicate_matcher_spec.rb +15 -15
- data/spec/unit/rspec_matchers/match_fedora_datastream_matcher_spec.rb +12 -12
- data/spec/unit/rubydora_connection_spec.rb +5 -5
- data/spec/unit/semantic_node_spec.rb +48 -107
- data/spec/unit/serializers_spec.rb +4 -4
- data/spec/unit/service_definitions_spec.rb +26 -26
- data/spec/unit/simple_datastream_spec.rb +17 -17
- data/spec/unit/solr_config_options_spec.rb +29 -28
- data/spec/unit/solr_digital_object_spec.rb +17 -25
- data/spec/unit/solr_service_spec.rb +95 -82
- data/spec/unit/unsaved_digital_object_spec.rb +24 -23
- data/spec/unit/validations_spec.rb +21 -21
- metadata +110 -159
- data/.rspec +0 -1
- data/.rubocop.yml +0 -1
- data/.rubocop_todo.yml +0 -938
- data/CONSOLE_GETTING_STARTED.textile +0 -1
- data/NOKOGIRI_DATASTREAMS.textile +0 -1
- data/README.textile +0 -116
- data/lib/active_fedora/associations/association_proxy.rb +0 -178
- data/lib/active_fedora/delegating.rb +0 -72
- data/lib/active_fedora/nokogiri_datastream.rb +0 -11
- data/spec/integration/delegating_spec.rb +0 -59
- data/spec/rails3_test_app/.gitignore +0 -4
- data/spec/rails3_test_app/.rspec +0 -1
- data/spec/rails3_test_app/Gemfile +0 -40
- data/spec/rails3_test_app/Rakefile +0 -7
- data/spec/rails3_test_app/app/controllers/application_controller.rb +0 -3
- data/spec/rails3_test_app/app/helpers/application_helper.rb +0 -2
- data/spec/rails3_test_app/app/views/layouts/application.html.erb +0 -14
- data/spec/rails3_test_app/config.ru +0 -4
- data/spec/rails3_test_app/config/application.rb +0 -42
- data/spec/rails3_test_app/config/boot.rb +0 -6
- data/spec/rails3_test_app/config/database.yml +0 -22
- data/spec/rails3_test_app/config/environment.rb +0 -5
- data/spec/rails3_test_app/config/environments/development.rb +0 -25
- data/spec/rails3_test_app/config/environments/production.rb +0 -49
- data/spec/rails3_test_app/config/environments/test.rb +0 -35
- data/spec/rails3_test_app/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/rails3_test_app/config/initializers/inflections.rb +0 -10
- data/spec/rails3_test_app/config/initializers/mime_types.rb +0 -5
- data/spec/rails3_test_app/config/initializers/secret_token.rb +0 -7
- data/spec/rails3_test_app/config/initializers/session_store.rb +0 -8
- data/spec/rails3_test_app/config/locales/en.yml +0 -5
- data/spec/rails3_test_app/config/routes.rb +0 -58
- data/spec/rails3_test_app/db/seeds.rb +0 -7
- data/spec/rails3_test_app/run_tests +0 -3
- data/spec/rails3_test_app/script/rails +0 -6
- data/spec/rails3_test_app/spec/spec_helper.rb +0 -27
- data/spec/rails3_test_app/spec/unit/rails_3_init.rb +0 -15
- data/spec/unit/association_proxy_spec.rb +0 -12
- data/spec/unit/base_delegate_spec.rb +0 -197
- data/spec/unit/base_delegate_to_spec.rb +0 -73
@@ -0,0 +1,317 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
# = Active Fedora Autosave Association
|
3
|
+
#
|
4
|
+
# +AutosaveAssociation+ is a module that takes care of automatically saving
|
5
|
+
# associacted records when their parent is saved. In addition to saving, it
|
6
|
+
# also destroys any associated records that were marked for destruction.
|
7
|
+
# (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
|
8
|
+
#
|
9
|
+
# Saving of the parent, its associations, and the destruction of marked
|
10
|
+
# associations, all happen inside a transaction. This should never leave the
|
11
|
+
# database in an inconsistent state.
|
12
|
+
#
|
13
|
+
# If validations for any of the associations fail, their error messages will
|
14
|
+
# be applied to the parent.
|
15
|
+
#
|
16
|
+
# Note that it also means that associations marked for destruction won't
|
17
|
+
# be destroyed directly. They will however still be marked for destruction.
|
18
|
+
#
|
19
|
+
# Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
|
20
|
+
# When the <tt>:autosave</tt> option is not present new associations are saved.
|
21
|
+
#
|
22
|
+
# === One-to-many Example
|
23
|
+
#
|
24
|
+
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
|
25
|
+
#
|
26
|
+
# class Post
|
27
|
+
# has_many :comments # :autosave option is no declared
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# post = Post.new(:title => 'ruby rocks')
|
31
|
+
# post.comments.build(:body => 'hello world')
|
32
|
+
# post.save # => saves both post and comment
|
33
|
+
#
|
34
|
+
# post = Post.create(:title => 'ruby rocks')
|
35
|
+
# post.comments.build(:body => 'hello world')
|
36
|
+
# post.save # => saves both post and comment
|
37
|
+
#
|
38
|
+
# post = Post.create(:title => 'ruby rocks')
|
39
|
+
# post.comments.create(:body => 'hello world')
|
40
|
+
# post.save # => saves both post and comment
|
41
|
+
#
|
42
|
+
# When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records:
|
43
|
+
#
|
44
|
+
# class Post
|
45
|
+
# has_many :comments, :autosave => true
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# post = Post.create(:title => 'ruby rocks')
|
49
|
+
# post.comments.create(:body => 'hello world')
|
50
|
+
# post.comments[0].body = 'hi everyone'
|
51
|
+
# post.save # => saves both post and comment, with 'hi everyone' as body
|
52
|
+
#
|
53
|
+
# Destroying one of the associated models as part of the parent's save action
|
54
|
+
# is as simple as marking it for destruction:
|
55
|
+
#
|
56
|
+
# post.comments.last.mark_for_destruction
|
57
|
+
# post.comments.last.marked_for_destruction? # => true
|
58
|
+
# post.comments.length # => 2
|
59
|
+
#
|
60
|
+
# Note that the model is _not_ yet removed from the database:
|
61
|
+
#
|
62
|
+
# id = post.comments.last.id
|
63
|
+
# Comment.find_by_id(id).nil? # => false
|
64
|
+
#
|
65
|
+
# post.save
|
66
|
+
# post.reload.comments.length # => 1
|
67
|
+
#
|
68
|
+
# Now it _is_ removed from the database:
|
69
|
+
#
|
70
|
+
# Comment.find_by_id(id).nil? # => true
|
71
|
+
#
|
72
|
+
# === Validation
|
73
|
+
#
|
74
|
+
# Children records are validated unless <tt>:validate</tt> is +false+.
|
75
|
+
module AutosaveAssociation
|
76
|
+
extend ActiveSupport::Concern
|
77
|
+
|
78
|
+
ASSOCIATION_TYPES = %w{ HasMany BelongsTo HasAndBelongsToMany }
|
79
|
+
|
80
|
+
module AssociationBuilderExtension #:nodoc:
|
81
|
+
def self.included(base)
|
82
|
+
base.valid_options << :autosave
|
83
|
+
end
|
84
|
+
|
85
|
+
def build
|
86
|
+
reflection = super
|
87
|
+
model.send(:add_autosave_association_callbacks, reflection)
|
88
|
+
reflection
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
included do
|
93
|
+
ASSOCIATION_TYPES.each do |type|
|
94
|
+
Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
module ClassMethods
|
99
|
+
private
|
100
|
+
|
101
|
+
def define_non_cyclic_method(name, reflection, &block)
|
102
|
+
define_method(name) do |*args|
|
103
|
+
result = true; @_already_called ||= {}
|
104
|
+
# Loop prevention for validation of associations
|
105
|
+
unless @_already_called[[name, reflection.name]]
|
106
|
+
begin
|
107
|
+
@_already_called[[name, reflection.name]]=true
|
108
|
+
result = instance_eval(&block)
|
109
|
+
ensure
|
110
|
+
@_already_called[[name, reflection.name]]=false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
result
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Adds validation and save callbacks for the association as specified by
|
119
|
+
# the +reflection+.
|
120
|
+
#
|
121
|
+
# For performance reasons, we don't check whether to validate at runtime.
|
122
|
+
# However the validation and callback methods are lazy and those methods
|
123
|
+
# get created when they are invoked for the very first time. However,
|
124
|
+
# this can change, for instance, when using nested attributes, which is
|
125
|
+
# called _after_ the association has been defined. Since we don't want
|
126
|
+
# the callbacks to get defined multiple times, there are guards that
|
127
|
+
# check if the save or validation methods have already been defined
|
128
|
+
# before actually defining them.
|
129
|
+
def add_autosave_association_callbacks(reflection)
|
130
|
+
save_method = :"autosave_associated_records_for_#{reflection.name}"
|
131
|
+
validation_method = :"validate_associated_records_for_#{reflection.name}"
|
132
|
+
collection = reflection.collection?
|
133
|
+
|
134
|
+
unless method_defined?(save_method)
|
135
|
+
if collection
|
136
|
+
before_save :before_save_collection_association
|
137
|
+
|
138
|
+
define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
|
139
|
+
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
140
|
+
after_create save_method
|
141
|
+
after_update save_method
|
142
|
+
else
|
143
|
+
define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
|
144
|
+
before_save save_method
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
if reflection.validate? && !method_defined?(validation_method)
|
149
|
+
method = (collection ? :validate_collection_association : :validate_single_association)
|
150
|
+
define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
|
151
|
+
validate validation_method
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
|
157
|
+
def reload(options = nil)
|
158
|
+
@marked_for_destruction = false
|
159
|
+
super
|
160
|
+
end
|
161
|
+
|
162
|
+
# Marks this record to be destroyed as part of the parents save transaction.
|
163
|
+
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
|
164
|
+
# when <tt>parent.save</tt> is called.
|
165
|
+
#
|
166
|
+
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
167
|
+
def mark_for_destruction
|
168
|
+
@marked_for_destruction = true
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns whether or not this record will be destroyed as part of the parents save transaction.
|
172
|
+
#
|
173
|
+
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
174
|
+
def marked_for_destruction?
|
175
|
+
@marked_for_destruction
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns whether or not this record has been changed in any way (including whether
|
179
|
+
# any of its nested autosave associations are likewise changed)
|
180
|
+
def changed_for_autosave?
|
181
|
+
new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
# Returns the record for an association collection that should be validated
|
187
|
+
# or saved. If +autosave+ is +false+ only new records will be returned,
|
188
|
+
# unless the parent is/was a new record itself.
|
189
|
+
def associated_records_to_validate_or_save(association, new_record, autosave)
|
190
|
+
if new_record
|
191
|
+
association && association.target
|
192
|
+
elsif autosave
|
193
|
+
association.target.find_all { |record| record.changed_for_autosave? }
|
194
|
+
else
|
195
|
+
association.target.find_all { |record| record.new_record? }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# go through nested autosave associations that are loaded in memory (without loading
|
200
|
+
# any new ones), and return true if is changed for autosave
|
201
|
+
def nested_records_changed_for_autosave?
|
202
|
+
self.class.reflect_on_all_autosave_associations.any? do |reflection|
|
203
|
+
association = association_instance_get(reflection.name)
|
204
|
+
association && Array(association.target).any? { |a| a.changed_for_autosave? }
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
|
209
|
+
# turned on for the association.
|
210
|
+
def validate_single_association(reflection)
|
211
|
+
association = association_instance_get(reflection.name)
|
212
|
+
record = association && association.target
|
213
|
+
association_valid?(reflection, record) if record
|
214
|
+
end
|
215
|
+
|
216
|
+
# Validate the associated records if <tt>:validate</tt> or
|
217
|
+
# <tt>:autosave</tt> is turned on for the association specified by
|
218
|
+
# +reflection+.
|
219
|
+
def validate_collection_association(reflection)
|
220
|
+
if association = association_instance_get(reflection.name)
|
221
|
+
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
|
222
|
+
records.each { |record| association_valid?(reflection, record) }
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns whether or not the association is valid and applies any errors to
|
228
|
+
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
|
229
|
+
# enabled records if they're marked_for_destruction? or destroyed.
|
230
|
+
def association_valid?(reflection, record)
|
231
|
+
return true if record.destroyed? || record.marked_for_destruction?
|
232
|
+
|
233
|
+
unless valid = record.valid?
|
234
|
+
if reflection.options[:autosave]
|
235
|
+
record.errors.each do |attribute, message|
|
236
|
+
attribute = "#{reflection.name}.#{attribute}"
|
237
|
+
errors[attribute] << message
|
238
|
+
errors[attribute].uniq!
|
239
|
+
end
|
240
|
+
else
|
241
|
+
errors.add(reflection.name)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
valid
|
245
|
+
end
|
246
|
+
|
247
|
+
# Is used as a before_save callback to check while saving a collection
|
248
|
+
# association whether or not the parent was a new record before saving.
|
249
|
+
def before_save_collection_association
|
250
|
+
@new_record_before_save = new_record?
|
251
|
+
true
|
252
|
+
end
|
253
|
+
|
254
|
+
# Saves any new associated records, or all loaded autosave associations if
|
255
|
+
# <tt>:autosave</tt> is enabled on the association.
|
256
|
+
#
|
257
|
+
# In addition, it destroys all children that were marked for destruction
|
258
|
+
# with mark_for_destruction.
|
259
|
+
#
|
260
|
+
# This all happens inside a transaction, _if_ the Transactions module is included into
|
261
|
+
# ActiveFedora::Base after the AutosaveAssociation module, which it does by default.
|
262
|
+
def save_collection_association(reflection)
|
263
|
+
if association = association_instance_get(reflection.name)
|
264
|
+
autosave = reflection.options[:autosave]
|
265
|
+
|
266
|
+
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
|
267
|
+
records.each do |record|
|
268
|
+
next if record.destroyed?
|
269
|
+
|
270
|
+
saved = true
|
271
|
+
|
272
|
+
if autosave && record.marked_for_destruction?
|
273
|
+
association.proxy.destroy(record)
|
274
|
+
elsif autosave != false && (@new_record_before_save || record.new_record?)
|
275
|
+
if autosave
|
276
|
+
saved = association.insert_record(record, false)
|
277
|
+
else
|
278
|
+
association.insert_record(record)
|
279
|
+
end
|
280
|
+
elsif autosave
|
281
|
+
saved = record.save(:validate => false)
|
282
|
+
end
|
283
|
+
|
284
|
+
raise ActiveFedora::Rollback unless saved
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# reconstruct the scope now that we know the owner's id
|
289
|
+
association.send(:construct_scope) if association.respond_to?(:construct_scope)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
|
294
|
+
#
|
295
|
+
# In addition, it will destroy the association if it was marked for destruction.
|
296
|
+
def save_belongs_to_association(reflection)
|
297
|
+
association = association_instance_get(reflection.name)
|
298
|
+
record = association && association.load_target
|
299
|
+
if record && !record.destroyed?
|
300
|
+
autosave = reflection.options[:autosave]
|
301
|
+
|
302
|
+
if autosave && record.marked_for_destruction?
|
303
|
+
record.destroy
|
304
|
+
elsif autosave != false
|
305
|
+
saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
|
306
|
+
|
307
|
+
if association.updated?
|
308
|
+
self[reflection.name.to_s + "_id"] = record.id
|
309
|
+
association.loaded!
|
310
|
+
end
|
311
|
+
|
312
|
+
saved if autosave
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
data/lib/active_fedora/base.rb
CHANGED
@@ -12,7 +12,7 @@ module ActiveFedora
|
|
12
12
|
#
|
13
13
|
# =The Basics
|
14
14
|
# class Oralhistory < ActiveFedora::Base
|
15
|
-
# has_metadata
|
15
|
+
# has_metadata "properties", type: ActiveFedora::SimpleDatastream do |m|
|
16
16
|
# m.field "narrator", :string
|
17
17
|
# m.field "narrator", :text
|
18
18
|
# end
|
@@ -24,344 +24,27 @@ module ActiveFedora
|
|
24
24
|
# Datastreams defined with +has_metadata+ are accessed via the +datastreams+ member hash.
|
25
25
|
#
|
26
26
|
class Base
|
27
|
+
extend ActiveModel::Naming
|
28
|
+
extend ActiveSupport::DescendantsTracker
|
27
29
|
include SemanticNode
|
28
|
-
|
29
|
-
class_attribute :fedora_connection, :profile_solr_name
|
30
|
-
self.fedora_connection = {}
|
31
|
-
self.profile_solr_name = ActiveFedora::SolrService.solr_name("object_profile", :displayable)
|
32
|
-
|
33
|
-
delegate :state=,:label=, to: :inner_object
|
34
|
-
|
35
|
-
def method_missing(name, *args)
|
36
|
-
dsid = corresponding_datastream_name(name)
|
37
|
-
if dsid
|
38
|
-
### Create and invoke a proxy method
|
39
|
-
self.class.send :define_method, name do
|
40
|
-
datastreams[dsid]
|
41
|
-
end
|
42
|
-
self.send(name)
|
43
|
-
else
|
44
|
-
super
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def new?
|
49
|
-
new_object?
|
50
|
-
end
|
51
|
-
|
52
|
-
# Has this object been saved?
|
53
|
-
def new_object?
|
54
|
-
inner_object.new?
|
55
|
-
end
|
56
|
-
|
57
|
-
## Required by associations
|
58
|
-
def new_record?
|
59
|
-
self.new_object?
|
60
|
-
end
|
61
|
-
|
62
|
-
def persisted?
|
63
|
-
!new_object?
|
64
|
-
end
|
65
|
-
|
66
|
-
def mark_for_destruction
|
67
|
-
@marked_for_destruction = true
|
68
|
-
end
|
69
|
-
|
70
|
-
def marked_for_destruction?
|
71
|
-
@marked_for_destruction
|
72
|
-
end
|
73
|
-
|
74
|
-
# Constructor. You may supply a custom +:pid+, or we call the Fedora Rest API for the
|
75
|
-
# next available Fedora pid, and mark as new object.
|
76
|
-
# Also, if +attrs+ does not contain +:pid+ but does contain +:namespace+ it will pass the
|
77
|
-
# +:namespace+ value to Fedora::Repository.nextid to generate the next pid available within
|
78
|
-
# the given namespace.
|
79
|
-
def initialize(attrs = nil)
|
80
|
-
attrs = {} if attrs.nil?
|
81
|
-
@association_cache = {}
|
82
|
-
attributes = attrs.dup
|
83
|
-
@inner_object = UnsavedDigitalObject.new(self.class, attributes.delete(:namespace), attributes.delete(:pid))
|
84
|
-
self.relationships_loaded = true
|
85
|
-
load_datastreams
|
86
|
-
|
87
|
-
[:new_object,:create_date, :modified_date].each { |k| attributes.delete(k)}
|
88
|
-
self.attributes=attributes
|
89
|
-
run_callbacks :initialize
|
90
|
-
end
|
91
|
-
|
92
|
-
# Reloads the object from Fedora.
|
93
|
-
def reload
|
94
|
-
raise ActiveFedora::ObjectNotFoundError, "Can't reload an object that hasn't been saved" unless persisted?
|
95
|
-
clear_association_cache
|
96
|
-
init_with_object(self.class.find(self.pid, cast: false).inner_object)
|
97
|
-
end
|
98
|
-
|
99
|
-
# Initialize an empty model object and set the +inner_obj+
|
100
|
-
# example:
|
101
|
-
#
|
102
|
-
# class Post < ActiveFedora::Base
|
103
|
-
# has_metadata :name => "properties", :type => ActiveFedora::SimpleDatastream
|
104
|
-
# end
|
105
|
-
#
|
106
|
-
# post = Post.allocate
|
107
|
-
# post.init_with_object(DigitalObject.find(pid))
|
108
|
-
# post.properties.title # => 'hello world'
|
109
|
-
def init_with_object(inner_obj)
|
110
|
-
@association_cache = {}
|
111
|
-
@inner_object = inner_obj
|
112
|
-
unless @inner_object.is_a? SolrDigitalObject
|
113
|
-
@inner_object.original_class = self.class
|
114
|
-
## Replace existing unchanged datastreams with the definitions found in this class if they have a different type.
|
115
|
-
## Any datastream that is deleted here will cause a reload from fedora, so avoid it whenever possible
|
116
|
-
ds_specs.keys.each do |key|
|
117
|
-
if @inner_object.datastreams[key] != nil && !@inner_object.datastreams[key].content_changed? && @inner_object.datastreams[key].class != self.class.ds_specs[key][:type]
|
118
|
-
@inner_object.datastreams.delete(key)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
load_datastreams
|
123
|
-
run_callbacks :find
|
124
|
-
run_callbacks :initialize
|
125
|
-
self
|
126
|
-
end
|
127
|
-
|
128
|
-
# Uses {shard_index} to find or create the rubydora connection for this pid
|
129
|
-
# @param [String] pid the identifier of the object to get the connection for
|
130
|
-
# @return [Rubydora::Repository] The repository that the identifier exists in.
|
131
|
-
def self.connection_for_pid(pid)
|
132
|
-
idx = shard_index(pid)
|
133
|
-
unless fedora_connection.has_key? idx
|
134
|
-
if ActiveFedora.config.sharded?
|
135
|
-
fedora_connection[idx] = RubydoraConnection.new(ActiveFedora.config.credentials[idx])
|
136
|
-
else
|
137
|
-
fedora_connection[idx] = RubydoraConnection.new(ActiveFedora.config.credentials)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
fedora_connection[idx].connection
|
141
|
-
end
|
142
|
-
|
143
|
-
# This is where your sharding strategy is implemented -- it's how we figure out which shard an object will be (or was) written to.
|
144
|
-
# Given a pid, it decides which shard that pid will be written to (and thus retrieved from).
|
145
|
-
# For a given pid, as long as your shard configuration remains the same it will always return the same value.
|
146
|
-
# If you're not using sharding, this will always return 0, meaning use the first/only Fedora Repository in your configuration.
|
147
|
-
# Default strategy runs a modulo of the md5 of the pid against the number of shards.
|
148
|
-
# If you want to use a different sharding strategy, override this method. Make sure that it will always return the same value for a given pid and shard configuration.
|
149
|
-
#@return [Integer] the index of the shard this object is stored in
|
150
|
-
def self.shard_index(pid)
|
151
|
-
if ActiveFedora.config.sharded?
|
152
|
-
Digest::MD5.hexdigest(pid).hex % ActiveFedora.config.credentials.length
|
153
|
-
else
|
154
|
-
0
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
|
159
|
-
def self.datastream_class_for_name(dsid)
|
160
|
-
ds_specs[dsid] ? ds_specs[dsid].fetch(:type, ActiveFedora::Datastream) : ActiveFedora::Datastream
|
161
|
-
end
|
162
|
-
|
163
|
-
def clone
|
164
|
-
new_object = self.class.create
|
165
|
-
clone_into(new_object)
|
166
|
-
end
|
167
|
-
|
168
|
-
# Clone the datastreams from this object into the provided object, while preserving the pid of the provided object
|
169
|
-
# @param [Base] new_object clone into this object
|
170
|
-
def clone_into(new_object)
|
171
|
-
rels = Nokogiri::XML( rels_ext.content)
|
172
|
-
rels.xpath("//rdf:Description/@rdf:about").first.value = new_object.internal_uri
|
173
|
-
new_object.rels_ext.content = rels.to_xml
|
174
|
-
|
175
|
-
datastreams.each do |k, v|
|
176
|
-
next if k == 'RELS-EXT'
|
177
|
-
new_object.datastreams[k].content = v.content
|
178
|
-
end
|
179
|
-
new_object if new_object.save
|
180
|
-
end
|
181
|
-
|
182
|
-
### if you are doing sharding, override this method to do something other than use a sequence
|
183
|
-
# @return [String] the unique pid for a new object
|
184
|
-
def self.assign_pid(obj)
|
185
|
-
args = {}
|
186
|
-
args[:namespace] = obj.namespace if obj.namespace
|
187
|
-
# TODO: This juggling of Fedora credentials & establishing connections should be handled by
|
188
|
-
# an establish_fedora_connection method,possibly wrap it all into a fedora_connection method - MZ 06-05-2012
|
189
|
-
if ActiveFedora.config.sharded?
|
190
|
-
credentials = ActiveFedora.config.credentials[0]
|
191
|
-
else
|
192
|
-
credentials = ActiveFedora.config.credentials
|
193
|
-
end
|
194
|
-
fedora_connection[0] ||= ActiveFedora::RubydoraConnection.new(credentials)
|
195
|
-
fedora_connection[0].connection.mint(args)
|
196
|
-
end
|
197
|
-
|
198
|
-
def inner_object # :nodoc
|
199
|
-
@inner_object
|
200
|
-
end
|
201
|
-
|
202
|
-
#return the pid of the Fedora Object
|
203
|
-
# if there is no fedora object (loaded from solr) get the instance var
|
204
|
-
# TODO make inner_object a proxy that can hold the pid
|
205
|
-
def pid
|
206
|
-
@inner_object.pid
|
207
|
-
end
|
208
|
-
|
209
|
-
|
210
|
-
def id ### Needed for the nested form helper
|
211
|
-
self.pid
|
212
|
-
end
|
213
|
-
|
214
|
-
def to_param
|
215
|
-
persisted? ? to_key.join('-') : nil
|
216
|
-
end
|
217
|
-
|
218
|
-
def to_key
|
219
|
-
persisted? ? [pid] : nil
|
220
|
-
end
|
221
|
-
|
222
|
-
#return the internal fedora URI
|
223
|
-
def internal_uri
|
224
|
-
"info:fedora/#{pid}"
|
225
|
-
end
|
226
|
-
|
227
|
-
#return the owner id
|
228
|
-
def owner_id
|
229
|
-
Array(@inner_object.ownerId).first
|
230
|
-
end
|
231
|
-
|
232
|
-
def owner_id=(owner_id)
|
233
|
-
@inner_object.ownerId=(owner_id)
|
234
|
-
end
|
235
|
-
|
236
|
-
def label
|
237
|
-
Array(@inner_object.label).first
|
238
|
-
end
|
239
|
-
|
240
|
-
def state
|
241
|
-
Array(@inner_object.state).first
|
242
|
-
end
|
243
|
-
|
244
|
-
#return the create_date of the inner object (unless it's a new object)
|
245
|
-
def create_date
|
246
|
-
if @inner_object.new?
|
247
|
-
Time.now
|
248
|
-
elsif @inner_object.respond_to? :createdDate
|
249
|
-
Array(@inner_object.createdDate).first
|
250
|
-
else
|
251
|
-
@inner_object.profile['objCreateDate']
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
#return the modification date of the inner object (unless it's a new object)
|
256
|
-
def modified_date
|
257
|
-
@inner_object.new? ? Time.now : Array(@inner_object.lastModifiedDate).first
|
258
|
-
end
|
259
|
-
|
260
|
-
def ==(comparison_object)
|
261
|
-
comparison_object.equal?(self) ||
|
262
|
-
(comparison_object.instance_of?(self.class) &&
|
263
|
-
comparison_object.pid == pid &&
|
264
|
-
!comparison_object.new_record?)
|
265
|
-
end
|
266
|
-
|
267
|
-
|
268
|
-
def pretty_pid
|
269
|
-
if self.pid == UnsavedDigitalObject::PLACEHOLDER
|
270
|
-
nil
|
271
|
-
else
|
272
|
-
self.pid
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
# This method adapts the inner_object to a new ActiveFedora::Base implementation
|
277
|
-
# This is intended to minimize redundant interactions with Fedora
|
278
|
-
def adapt_to(klass)
|
279
|
-
unless klass.ancestors.include? ActiveFedora::Base
|
280
|
-
raise "Cannot adapt #{self.class.name} to #{klass.name}: Not a ActiveFedora::Base subclass"
|
281
|
-
end
|
282
|
-
klass.allocate.init_with_object(inner_object)
|
283
|
-
end
|
284
|
-
|
285
|
-
# Examines the :has_model assertions in the RELS-EXT.
|
286
|
-
#
|
287
|
-
# If the object is of type ActiveFedora::Base, then use the first :has_model
|
288
|
-
# in the RELS-EXT for this object. Due to how the RDF writer works, this is
|
289
|
-
# currently just the first alphabetical model.
|
290
|
-
#
|
291
|
-
# If the object was instantiated with any other class, then use that class
|
292
|
-
# as the base of the type the user wants rather than casting to the first
|
293
|
-
# :has_model found on the object.
|
294
|
-
#
|
295
|
-
# In either case, if an extended model of that initial base model of the two
|
296
|
-
# cases above exists in the :has_model, then instantiate as that extended
|
297
|
-
# model type instead.
|
298
|
-
def adapt_to_cmodel
|
299
|
-
best_model_match = self.class unless self.instance_of? ActiveFedora::Base
|
300
|
-
|
301
|
-
ActiveFedora::ContentModel.known_models_for( self ).each do |model_value|
|
302
|
-
# If this is of type ActiveFedora::Base, then set to the first found :has_model.
|
303
|
-
best_model_match ||= model_value
|
304
|
-
|
305
|
-
# If there is an inheritance structure, use the most specific case.
|
306
|
-
if best_model_match > model_value
|
307
|
-
best_model_match = model_value
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
self.instance_of?(best_model_match) ? self : self.adapt_to(best_model_match)
|
312
|
-
end
|
313
|
-
|
314
|
-
# ** EXPERIMENTAL **
|
315
|
-
# This method returns a new object of the same class, with the internal SolrDigitalObject
|
316
|
-
# replaced with an actual DigitalObject.
|
317
|
-
def reify
|
318
|
-
if self.inner_object.is_a? DigitalObject
|
319
|
-
raise "#{self.inspect} is already a full digital object"
|
320
|
-
end
|
321
|
-
self.class.find self.pid
|
322
|
-
end
|
323
|
-
|
324
|
-
# ** EXPERIMENTAL **
|
325
|
-
# This method reinitializes a lightweight, loaded-from-solr object with an actual
|
326
|
-
# DigitalObject inside.
|
327
|
-
def reify!
|
328
|
-
if self.inner_object.is_a? DigitalObject
|
329
|
-
raise "#{self.inspect} is already a full digital object"
|
330
|
-
end
|
331
|
-
self.init_with_object DigitalObject.find(self.class,self.pid)
|
332
|
-
end
|
333
|
-
|
334
|
-
def self.pids_from_uris(uris)
|
335
|
-
if uris.class == String
|
336
|
-
return uris.gsub("info:fedora/", "")
|
337
|
-
elsif uris.class == Array
|
338
|
-
arr = []
|
339
|
-
uris.each do |uri|
|
340
|
-
arr << uri.gsub("info:fedora/", "")
|
341
|
-
end
|
342
|
-
return arr
|
343
|
-
end
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
Base.class_eval do
|
30
|
+
include Sharding
|
348
31
|
include ActiveFedora::Persistence
|
349
|
-
|
350
|
-
extend Model
|
32
|
+
include Scoping
|
351
33
|
include Loggable
|
352
34
|
include Indexing
|
353
35
|
include ActiveModel::Conversion
|
354
36
|
include Validations
|
355
37
|
include Callbacks
|
356
|
-
include Attributes
|
357
38
|
include Datastreams
|
358
|
-
extend ActiveModel::Naming
|
359
|
-
include Delegating
|
360
39
|
extend Querying
|
361
40
|
include Associations
|
41
|
+
include AutosaveAssociation
|
362
42
|
include NestedAttributes
|
363
43
|
include Reflection
|
364
|
-
include
|
44
|
+
include Attributes
|
45
|
+
include Serialization
|
46
|
+
include Core
|
47
|
+
include FedoraAttributes
|
365
48
|
end
|
366
49
|
|
367
50
|
end
|