diff --git a/lib/active_fedora/associations.rb b/lib/active_fedora/associations.rb index 4dcc56d14..04c236826 100644 --- a/lib/active_fedora/associations.rb +++ b/lib/active_fedora/associations.rb @@ -24,6 +24,7 @@ module Associations autoload :HasManyAssociation autoload :BelongsToAssociation autoload :HasAndBelongsToManyAssociation + autoload :BasicContainsAssociation autoload :HasSubresourceAssociation autoload :DirectlyContainsAssociation autoload :DirectlyContainsOneAssociation @@ -45,6 +46,7 @@ module Builder autoload :BelongsTo, 'active_fedora/associations/builder/belongs_to' autoload :HasMany, 'active_fedora/associations/builder/has_many' autoload :HasAndBelongsToMany, 'active_fedora/associations/builder/has_and_belongs_to_many' + autoload :BasicContains, 'active_fedora/associations/builder/basic_contains' autoload :HasSubresource, 'active_fedora/associations/builder/has_subresource' autoload :DirectlyContains, 'active_fedora/associations/builder/directly_contains' autoload :DirectlyContainsOne, 'active_fedora/associations/builder/directly_contains_one' @@ -104,6 +106,23 @@ def association_instance_set(name, association) end module ClassMethods + # This method is used to declare this resource acts like an LDP BasicContainer + # + # @param [Hash] options + # @option options [String] :class_name ('ActiveFedora::File') The name of the + # class that will represent the contained resources + # + # example: + # class FooHistory < ActiveFedora::Base + # is_a_container class_name: 'Thing' + # end + # + def is_a_container(options = {}) + defaults = { class_name: 'ActiveFedora::File', + predicate: ::RDF::Vocab::LDP.contains } + Builder::BasicContains.build(self, :contains, defaults.merge(options)) + end + # This method is used to declare an ldp:DirectContainer on a resource # you must specify an is_member_of_relation or a has_member_relation # diff --git a/lib/active_fedora/associations/basic_contains_association.rb b/lib/active_fedora/associations/basic_contains_association.rb new file mode 100644 index 000000000..d14c33dcf --- /dev/null +++ b/lib/active_fedora/associations/basic_contains_association.rb @@ -0,0 +1,22 @@ +module ActiveFedora + module Associations + class BasicContainsAssociation < ContainsAssociation #:nodoc: + def find_target + uris = owner.resource.query(predicate: options[:predicate]) + .map { |r| r.object.to_s } + + uris.map { |object_uri| klass.find(klass.uri_to_id(object_uri)) } + end + + def insert_record(record, force = true, validate = true) + record.base_path_for_resource = owner.uri.to_s + super + end + + def add_to_target(record, skip_callbacks = false) + record.base_path_for_resource = owner.uri.to_s + super + end + end + end +end diff --git a/lib/active_fedora/associations/builder/basic_contains.rb b/lib/active_fedora/associations/builder/basic_contains.rb new file mode 100644 index 000000000..828f9b325 --- /dev/null +++ b/lib/active_fedora/associations/builder/basic_contains.rb @@ -0,0 +1,7 @@ +module ActiveFedora::Associations::Builder + class BasicContains < CollectionAssociation #:nodoc: + def self.macro + :is_a_container + end + end +end diff --git a/lib/active_fedora/associations/contains_association.rb b/lib/active_fedora/associations/contains_association.rb index 5bbf571cd..8c0ea2743 100644 --- a/lib/active_fedora/associations/contains_association.rb +++ b/lib/active_fedora/associations/contains_association.rb @@ -1,7 +1,15 @@ -# This is the parent class of DirectlyContainsAssociation and IndirectlyContainsAssociation +# This is the parent class of BasicContainsAssociation, DirectlyContainsAssociation and IndirectlyContainsAssociation module ActiveFedora module Associations class ContainsAssociation < CollectionAssociation #:nodoc: + def insert_record(record, force = true, validate = true) + if force + record.save! + else + record.save(validate: validate) + end + end + def reader @records ||= ContainerProxy.new(self) end @@ -27,6 +35,16 @@ def uri raise "Can't get uri. Owner isn't saved" if @owner.new_record? "#{@owner.uri}/#{@reflection.name}" end + + private + + def delete_records(records, method) + if method == :destroy + records.each(&:destroy) + else + records.each(&:delete) + end + end end end end diff --git a/lib/active_fedora/associations/directly_contains_association.rb b/lib/active_fedora/associations/directly_contains_association.rb index 1e3ebb9ae..57441a31f 100644 --- a/lib/active_fedora/associations/directly_contains_association.rb +++ b/lib/active_fedora/associations/directly_contains_association.rb @@ -3,11 +3,7 @@ module Associations class DirectlyContainsAssociation < ContainsAssociation #:nodoc: def insert_record(record, force = true, validate = true) container.save! - if force - record.save! - else - record.save(validate: validate) - end + super end def find_target @@ -39,16 +35,6 @@ def initialize_attributes(record) #:nodoc: record.uri = ActiveFedora::Base.id_to_uri(container.mint_id) set_inverse_instance(record) end - - private - - def delete_records(records, method) - if method == :destroy - records.each(&:destroy) - else - records.each(&:delete) - end - end end end end diff --git a/lib/active_fedora/autosave_association.rb b/lib/active_fedora/autosave_association.rb index dda08af65..bc5a9d787 100644 --- a/lib/active_fedora/autosave_association.rb +++ b/lib/active_fedora/autosave_association.rb @@ -75,7 +75,7 @@ module ActiveFedora module AutosaveAssociation extend ActiveSupport::Concern - ASSOCIATION_TYPES = [:has_many, :belongs_to, :has_and_belongs_to_many, :directly_contains, :indirectly_contains].freeze + ASSOCIATION_TYPES = [:has_many, :belongs_to, :has_and_belongs_to_many, :directly_contains, :indirectly_contains, :is_a_container].freeze module AssociationBuilderExtension #:nodoc: def self.valid_options diff --git a/lib/active_fedora/base.rb b/lib/active_fedora/base.rb index 1506fd79b..1a57e58bc 100644 --- a/lib/active_fedora/base.rb +++ b/lib/active_fedora/base.rb @@ -50,7 +50,6 @@ class Base include Versionable include LoadableFromJson include Schema - include Pathing include Aggregation::BaseExtension end diff --git a/lib/active_fedora/common.rb b/lib/active_fedora/common.rb index 7d10750f7..8e516d94d 100644 --- a/lib/active_fedora/common.rb +++ b/lib/active_fedora/common.rb @@ -1,6 +1,7 @@ module ActiveFedora module Common extend ActiveSupport::Concern + include Pathing module ClassMethods def initialize_generated_modules # :nodoc: diff --git a/lib/active_fedora/file.rb b/lib/active_fedora/file.rb index 75e29fb5c..9bb569249 100644 --- a/lib/active_fedora/file.rb +++ b/lib/active_fedora/file.rb @@ -203,7 +203,7 @@ def build_ldp_resource(id) end def build_ldp_resource_via_uri(uri = nil, content = '') - Ldp::Resource::BinarySource.new(ldp_connection, uri, content, ActiveFedora.fedora.host + ActiveFedora.fedora.base_path) + Ldp::Resource::BinarySource.new(ldp_connection, uri, content, base_path_for_resource) end def uploaded_file?(payload) diff --git a/lib/active_fedora/file/attributes.rb b/lib/active_fedora/file/attributes.rb index 792fd94d1..5e1deebd3 100644 --- a/lib/active_fedora/file/attributes.rb +++ b/lib/active_fedora/file/attributes.rb @@ -1,6 +1,10 @@ module ActiveFedora::File::Attributes attr_writer :mime_type + def assign_attributes(_) + # nop + end + def mime_type @mime_type ||= fetch_mime_type end diff --git a/lib/active_fedora/file_persistence.rb b/lib/active_fedora/file_persistence.rb index 84192e3b4..efd62bcbc 100644 --- a/lib/active_fedora/file_persistence.rb +++ b/lib/active_fedora/file_persistence.rb @@ -8,7 +8,7 @@ module FilePersistence def _create_record(_options = {}) return false if content.nil? - ldp_source.content = content + @ldp_source = build_ldp_binary_source ldp_source.create do |req| req.headers.merge!(ldp_headers) end @@ -23,5 +23,13 @@ def _update_record(_options = {}) end refresh end + + def build_ldp_binary_source + if id + build_ldp_resource_via_uri(uri, content) + else + build_ldp_resource_via_uri(nil, content) + end + end end end diff --git a/lib/active_fedora/ldp_resource_service.rb b/lib/active_fedora/ldp_resource_service.rb index 136d59686..296ff61d4 100644 --- a/lib/active_fedora/ldp_resource_service.rb +++ b/lib/active_fedora/ldp_resource_service.rb @@ -10,10 +10,16 @@ def build(klass, id) if id LdpResource.new(connection, to_uri(klass, id)) else - LdpResource.new(connection, nil, nil, ActiveFedora.fedora.host + ActiveFedora.fedora.base_path) + parent_uri = ActiveFedora.fedora.host + ActiveFedora.fedora.base_path + LdpResource.new(connection, nil, nil, parent_uri) end end + def build_resource_under_path(graph, parent_uri) + parent_uri ||= ActiveFedora.fedora.host + ActiveFedora.fedora.base_path + LdpResource.new(connection, nil, graph, parent_uri) + end + def update(change_set, klass, id) SparqlInsert.new(change_set.changes).execute(to_uri(klass, id)) end diff --git a/lib/active_fedora/persistence.rb b/lib/active_fedora/persistence.rb index 3ef2793ed..7a2bbeb74 100644 --- a/lib/active_fedora/persistence.rb +++ b/lib/active_fedora/persistence.rb @@ -87,6 +87,11 @@ def eradicate self.class.eradicate(id) end + # Used when setting containment + def base_path_for_resource=(path) + @base_path = path + end + module ClassMethods # Creates an object (or multiple objects) and saves it to the repository, if validations pass. # The resulting object is returned whether the object was saved successfully to the repository or not. @@ -201,11 +206,15 @@ def assign_rdf_subject @ldp_source = if !id && new_id = assign_id LdpResource.new(ActiveFedora.fedora.connection, self.class.id_to_uri(new_id), @resource) else - LdpResource.new(ActiveFedora.fedora.connection, @ldp_source.subject, @resource, ActiveFedora.fedora.host + base_path_for_resource) + LdpResource.new(ActiveFedora.fedora.connection, @ldp_source.subject, @resource, base_path_for_resource) end end def base_path_for_resource + @base_path ||= ActiveFedora.fedora.host + default_base_path_for_resource + end + + def default_base_path_for_resource init_root_path if has_uri_prefix? root_resource_path end diff --git a/lib/active_fedora/reflection.rb b/lib/active_fedora/reflection.rb index f247a36b9..b084442ca 100644 --- a/lib/active_fedora/reflection.rb +++ b/lib/active_fedora/reflection.rb @@ -28,6 +28,8 @@ def create(macro, name, scope, options, active_fedora) DirectlyContainsOneReflection when :indirectly_contains IndirectlyContainsReflection + when :is_a_container + BasicContainsReflection when :rdf RDFPropertyReflection when :singular_rdf @@ -513,6 +515,20 @@ def association_class end end + class BasicContainsReflection < AssociationReflection # :nodoc: + def macro + :is_a_container + end + + def collection? + true + end + + def association_class + Associations::BasicContainsAssociation + end + end + class DirectlyContainsReflection < AssociationReflection # :nodoc: def macro :directly_contains diff --git a/spec/integration/basic_contains_association_spec.rb b/spec/integration/basic_contains_association_spec.rb new file mode 100644 index 000000000..ccc7d7762 --- /dev/null +++ b/spec/integration/basic_contains_association_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe ActiveFedora::Base do + let(:model) { Source.new } + + context "with a file" do + before do + class Source < ActiveFedora::Base + is_a_container + end + end + + after do + Object.send(:remove_const, :Source) + end + + it 'is empty' do + expect(model.contains).to eq [] + end + + it 'can build a child' do + child = model.contains.build + expect(child).to be_kind_of ActiveFedora::File + child.content = "hello" + model.save! + expect(child).to be_persisted + expect(child.uri.to_s).to include model.uri.to_s + end + + it 'can create a child on a persisted parent' do + model.save! + child = model.contains.build + expect(child).to be_kind_of ActiveFedora::File + child.content = "hello" + model.save! + expect(child).to be_persisted + expect(child.uri.to_s).to include model.uri.to_s + end + end + + context "with an AF::Base object" do + before do + class Thing < ActiveFedora::Base + property :title, predicate: ::RDF::Vocab::DC.title + end + class Source < ActiveFedora::Base + is_a_container class_name: 'Thing' + end + end + after do + Object.send(:remove_const, :Source) + Object.send(:remove_const, :Thing) + end + + let(:model) { Source.new } + + it 'is empty' do + expect(model.contains).to eq [] + end + + describe "creating" do + it 'can build a child' do + child = model.contains.build + expect(model.contains.build(title: ['my title'])).to be_kind_of Thing + model.save! + expect(child).to be_persisted + expect(child.uri.to_s).to include model.uri.to_s + end + + it 'can create a child on a persisted parent' do + model.save! + child = model.contains.create(title: ['my title']) + expect(child).to be_kind_of Thing + expect(child).to be_persisted + expect(child.uri.to_s).to include model.uri.to_s + end + end + + describe "loading" do + before do + model.save! + model.contains.create(title: ['title 1']) + model.contains.create(title: ['title 2']) + model.reload + end + + it "has the two contained objects" do + expect(model.contains.size).to eq 2 + expect(model.contains.map(&:title)).to eq [['title 1'], ['title 2']] + end + end + + describe "#destroy_all" do + before do + model.save! + model.contains.create(title: ['title 1']) + model.contains.create(title: ['title 2']) + model.reload + end + + it "destroys the two contained objects" do + expect { model.contains.destroy_all } + .to change { model.contains.size }.by(-2) + .and change { Thing.count }.by(-2) + end + end + end +end