Skip to content

Commit

Permalink
Implements directly_contains_one association
Browse files Browse the repository at this point in the history
Note: part of this functionality is blocked by #794
  • Loading branch information
flyingzumwalt committed Jun 22, 2015
1 parent d11806b commit befdee5
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 2 deletions.
6 changes: 6 additions & 0 deletions lib/active_fedora/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module Associations
autoload :HasAndBelongsToManyAssociation, 'active_fedora/associations/has_and_belongs_to_many_association'
autoload :BasicContainsAssociation, 'active_fedora/associations/basic_contains_association'
autoload :DirectlyContainsAssociation, 'active_fedora/associations/directly_contains_association'
autoload :DirectlyContainsOneAssociation, 'active_fedora/associations/directly_contains_one_association'
autoload :IndirectlyContainsAssociation, 'active_fedora/associations/indirectly_contains_association'
autoload :ContainsAssociation, 'active_fedora/associations/contains_association'
autoload :DeleteProxy, 'active_fedora/associations/delete_proxy'
Expand All @@ -43,6 +44,7 @@ module Builder
autoload :HasAndBelongsToMany, 'active_fedora/associations/builder/has_and_belongs_to_many'
autoload :Contains, 'active_fedora/associations/builder/contains'
autoload :DirectlyContains, 'active_fedora/associations/builder/directly_contains'
autoload :DirectlyContainsOne, 'active_fedora/associations/builder/directly_contains_one'
autoload :IndirectlyContains, 'active_fedora/associations/builder/indirectly_contains'

autoload :Property, 'active_fedora/associations/builder/property'
Expand Down Expand Up @@ -108,6 +110,10 @@ def directly_contains(name, options={})
Builder::DirectlyContains.build(self, name, { class_name: 'ActiveFedora::File' }.merge(options))
end

def directly_contains_one(name, options={})
Builder::DirectlyContainsOne.build(self, name, { class_name: 'ActiveFedora::File' }.merge(options))
end

# This method is used to declare an ldp:IndirectContainer on a resource
# you must specify an is_member_of_relation or a has_member_relation
#
Expand Down
39 changes: 39 additions & 0 deletions lib/active_fedora/associations/builder/directly_contains_one.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module ActiveFedora::Associations::Builder
class DirectlyContainsOne < SingularAssociation #:nodoc:
self.macro = :directly_contains_one
self.valid_options += [:has_member_relation, :is_member_of_relation, :type, :through]
self.valid_options -= [:predicate]

def validate_options
if options[:through]
inherit_options_from_association(options[:through])
end
super

if !options[:has_member_relation] && !options[:is_member_of_relation]
raise ArgumentError, "You must specify a predicate for #{name}"
elsif !options[:has_member_relation].kind_of?(RDF::URI) && !options[:is_member_of_relation].kind_of?(RDF::URI)
raise ArgumentError, "Predicate must be a kind of RDF::URI"
end

if !options[:type].kind_of?(RDF::URI)
raise ArgumentError, "You must specify a Type and it must be a kind of RDF::URI"
end
end

private

# Inherits :has_member_relation from the association corresponding to association_name
# @param [Symbol] association_name of the association to inherit from
def inherit_options_from_association(association_name)
associated_through_reflection = lookup_reflection(association_name)
raise ArgumentError, "You specified `:through => #{@reflection.options[:through]}` on the #{name} associaiton but #{model} does not actually have a #{@reflection.options[:through]}` association" if associated_through_reflection.nil? || !associated_through_reflection.name
options[:has_member_relation] = associated_through_reflection.options[:has_member_relation] unless options[:has_member_relation]
end

def lookup_reflection(association_name)
model.reflect_on_association(association_name)
end

end
end
118 changes: 118 additions & 0 deletions lib/active_fedora/associations/directly_contains_one_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
module ActiveFedora
module Associations
# Filters a DirectContainer relationship, returning the first item that matches the given :type
class DirectlyContainsOneAssociation < SingularAssociation #:nodoc:

# Finds objects contained by the container predicate (either the configured has_member_relation or ldp:contains)
# TODO: Refactor this to use solr (for efficiency) instead of parsing the RDF graph. Requires indexing ActiveFedora::File objects into solr, including their RDF.type and, if possible, the id of their container
def find_target
# filtered_objects = container_association_proxy.to_a.select { |o| o.metadata_node.type.include?(options[:type]) }
query_node = if container_predicate = options[:has_member_relation]
owner
else
container_predicate = ::RDF::Vocab::LDP.contains
container_association.container # Use the :through association's container
end

contained_uris = query_node.resource.query(predicate: container_predicate).map { |r| r.object.to_s }
contained_objects = contained_uris.map { |object_uri| klass.find(klass.uri_to_id(object_uri)) }
filtered_objects = contained_objects.select {|o| o.metadata_node.type.include?(options[:type]) }

return filtered_objects.first
end

# # Build/Create record and ensure that it's added to the corresponding Container
def new_record(method, attributes={})
# This works, using the container's builder, but then you're stuck with the container's class_name
# record = @owner.send(container_reflection.name).send(method, attributes) do |record|
# add_type_to_record(record, options[:type])
# end

# Build the record using _this_ association's build method (so setting things like class_name on this association will have an effect)
record = @reflection.send("#{method}_association", attributes)
add_to_container(record)
add_type_to_record(record, options[:type])
record
end

# Adds record to the DirectContainer identified by the container_association
# Relies on container_association.initialize_attributes to appropriately set things like record.uri
def add_to_container(record)
container_association.add_to_target(record) # adds record to corresponding Container
container_association.send(:initialize_attributes,record) # Uses the :through association initialize the record with things like the correct URI for a direclty contained object
end

# Replaces association +target+ with +record+
def replace(record, save = true)
if record
raise_on_type_mismatch(record)
remove_existing_target
add_to_container(record)
else
remove_existing_target
end

self.target = record
end

# Sets record as the target of the association
# Ensures that the RDF.type is set on the target
def target=(record)
add_type_to_record(record, options[:type])
@target = record
end

def remove_existing_target
@target ||= find_target
if @target
container_association_proxy.delete @target
@updated = true
end
end

# Overrides build_record to ensure that record is initialized with attributes from the corresponding container
def build_record(attributes)
# TODO make initalize take a block and get rid of the tap
reflection.build_association(attributes).tap do |record|
container_association.initialize_attributes(record) # initializes record with attributes from the corresponding container
initialize_attributes(record)
end
end

# Returns the Reflection corresponding to the direct container association that's being filtered
def container_reflection
@container_reflection ||= @owner.class.reflect_on_association(@reflection.options[:through])
end

# Returns the DirectContainerAssociation corresponding to the direct container that's being filtered
def container_association
container_association_proxy.proxy_association
end

# Returns the ContainerAssociationProxy corresponding to the direct container that's being filtered
def container_association_proxy
@owner.send(@reflection.options[:through])
end

def updated?
@updated
end

private

# Imitates Hydra::PCDM::AddTypeToFile service
def add_type_to_record(record, type_uri)
if record.respond_to?(:metadata_node)
metadata_node = record.metadata_node
else
metadata_node = record
end
types = metadata_node.get_values(:type)
return file if types.include?(type_uri)
types << type_uri
record.metadata_node.set_value(:type, types)
end

end
end
end
6 changes: 5 additions & 1 deletion lib/active_fedora/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,16 @@ def uri
ldp_source.subject
end

# If this file have a parent with ldp#contains, we know it is not new.
# If this file has a parent with ldp#contains, we know it is not new.
# By tracking exists we prevent an unnecessary HEAD request.
def new_record?
!@exists && ldp_source.new?
end

def persisted?
@exists && !ldp_source.new?
end

def destroyed?
false
end
Expand Down
4 changes: 3 additions & 1 deletion lib/active_fedora/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module Reflection # :nodoc:
module ClassMethods
def create_reflection(macro, name, options, active_fedora)
klass = case macro
when :has_many, :belongs_to, :has_and_belongs_to_many, :contains, :directly_contains, :indirectly_contains
when :has_many, :belongs_to, :has_and_belongs_to_many, :contains, :directly_contains, :directly_contains_one, :indirectly_contains
AssociationReflection
when :rdf, :singular_rdf
RDFPropertyReflection
Expand Down Expand Up @@ -260,6 +260,8 @@ def association_class
Associations::RDF
when :directly_contains
Associations::DirectlyContainsAssociation
when :directly_contains_one
Associations::DirectlyContainsOneAssociation
when :indirectly_contains
Associations::IndirectlyContainsAssociation
end
Expand Down
108 changes: 108 additions & 0 deletions spec/integration/directly_contains_one_association_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
require 'spec_helper'

describe ActiveFedora::Base do
before do
class PageImage < ActiveFedora::Base
directly_contains :files, has_member_relation: ::RDF::URI.new("http://example.com/hasFiles"), class_name:"FileWithMetadata"
directly_contains_one :primary_file, through: :files, type: ::RDF::URI.new("http://example.com/primaryFile"), class_name:"FileWithMetadata"
directly_contains_one :special_versioned_file, through: :files, type: ::RDF::URI.new("http://example.com/featuredFile"), class_name:'VersionedFileWithMetadata'
end

class FileWithMetadata < ActiveFedora::File
include ActiveFedora::WithMetadata
end
class VersionedFileWithMetadata < ActiveFedora::File
include ActiveFedora::WithMetadata
has_many_versions
end
end

after do
Object.send(:remove_const, :PageImage)
Object.send(:remove_const, :FileWithMetadata)
Object.send(:remove_const, :VersionedFileWithMetadata)
end

let(:page_image) { PageImage.create }
let(:reloaded_page_image) { PageImage.find(page_image.id) }

let(:a_file) { page_image.files.build }
let(:primary_file) { page_image.build_primary_file }
# let(:primary_file) do
# file = page_image.files.build
# file.metadata_node.set_value(:type, ::RDF::URI.new("http://example.com/primaryFile"))
# file
# end
let(:special_versioned_file) { page_image.build_special_versioned_file }
# let(:special_versioned_file) do
# file = page_image.files.build_special_versioned_file
# file.metadata_node.set_value(:type, ::RDF::URI.new("http://example.com/featuredFile"))
# file
# end

context "#build" do
before do
primary_file.content = "I'm in a container all alone!"
page_image.save!
end
subject { reloaded_page_image.primary_file }
it "initializes an object within the container" do
expect(subject.content).to eq("I'm in a container all alone!")
expect(subject.metadata_node.type).to include( ::RDF::URI.new("http://example.com/primaryFile") )
expect(subject.class).to eq FileWithMetadata
end
it "relies on info from the :through association" do
expect(page_image.files).to include(primary_file)
expect(primary_file.uri).to include("/files/")
end
end
context "finder" do
subject { reloaded_page_image.primary_file }
context "when no matching child is set" do
before { page_image.files.build}
it { is_expected.to be_nil }
end
context "when a matching object is directly contained" do
before do
a_file.content = "I'm a file"
primary_file.content = "I am too"
page_image.save!
end
it "returns the matching object" do
expect(subject).to eq primary_file
end
end
context "if class_name is set" do
before do
a_file.content = "I'm a file"
special_versioned_file.content = "I am too"
page_image.save!
end
subject { reloaded_page_image.special_versioned_file }
it "uses the specified class to load objects" do
expect(subject).to eq special_versioned_file
expect(subject).to be_instance_of VersionedFileWithMetadata
end
end
end

describe "setter" do
before do
a_file.content = "I'm a file"
primary_file.content = "I am too"
page_image.save!
end
subject { reloaded_page_image.files }
it "replaces existing record without disturbing the other contents of the container" do
pending "Blocked by projecthydra/active_fedora#794 Can't remove objects from a ContainerAssociation"
replacement_file = page_image.primary_file = FileWithMetadata.new
replacement_file.content = "I'm a replacement"
page_image.save
expect(subject).to_not include(primary_file)
expect(subject).to eq([a_file, replacement_file])
expect(reloaded_page_image.primary_file).to eq(replacement_file)
end

end

end

0 comments on commit befdee5

Please sign in to comment.