-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implements directly_contains_one association
Note: part of this functionality is blocked by #794
- Loading branch information
1 parent
d11806b
commit befdee5
Showing
6 changed files
with
279 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
lib/active_fedora/associations/builder/directly_contains_one.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
118
lib/active_fedora/associations/directly_contains_one_association.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
spec/integration/directly_contains_one_association_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |