Permalink
Browse files

Added accepts_nested_attributes_for on RDFNodes

  • Loading branch information...
jcoyne committed Jun 19, 2013
1 parent a2516fb commit 13b55ba4d3d98f849280a940573b9e6e44e2bace
View
@@ -6,6 +6,7 @@
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object'
require 'active_support/core_ext/hash/indifferent_access'
require "active_support/core_ext/hash/except"
require 'rdf'
SOLR_DOCUMENT_ID = Solrizer.default_field_mapper.id_field unless defined?(SOLR_DOCUMENT_ID)
@@ -48,6 +49,7 @@ class RecordNotSaved < RuntimeError; end # :nodoc:
autoload :Persistence
autoload :QualifiedDublinCoreDatastream
autoload :Querying
autoload :Rdf
autoload :RDFDatastream
autoload :RdfList
autoload :RdfNode
View
@@ -0,0 +1,6 @@
module ActiveFedora
module Rdf
extend ActiveSupport::Autoload
autoload :NestedAttributes
end
end
@@ -0,0 +1,73 @@
module ActiveFedora
module Rdf
module NestedAttributes
extend ActiveSupport::Concern
included do
class_attribute :nested_attributes_options, :instance_writer => false
self.nested_attributes_options = {}
end
private
UNASSIGNABLE_KEYS = %w( id _destroy )
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = self.nested_attributes_options[association_name]
# TODO
#check_record_limit!(options[:limit], attributes_collection)
if attributes_collection.is_a?(Hash) || attributes_collection.is_a?(String)
attributes_collection = [attributes_collection]
end
association = self.send(association_name)
attributes_collection.each do |attributes|
if attributes.instance_of? Hash
attributes = attributes.with_indifferent_access
association.build(attributes.except(*UNASSIGNABLE_KEYS))
else
association.build(attributes)
end
end
end
module ClassMethods
def accepts_nested_attributes_for *relationships
relationships.each do |association_name|
nested_attributes_options[association_name] = {}
generate_association_writer(association_name)
end
end
private
# Generates a writer method for this association. Serves as a point for
# accessing the objects in the association. For example, this method
# could generate the following:
#
# def pirate_attributes=(attributes)
# assign_nested_attributes_for_collection_association(:pirate, attributes)
# end
#
# This redirects the attempts to write objects in an association through
# the helper methods defined below. Makes it seem like the nested
# associations are just regular associations.
def generate_association_writer(association_name)
class_eval <<-eoruby, __FILE__, __LINE__ + 1
if method_defined?(:#{association_name}_attributes=)
remove_method(:#{association_name}_attributes=)
end
def #{association_name}_attributes=(attributes)
unless attributes.nil?
assign_nested_attributes_for_collection_association(:#{association_name}, attributes)
end
end
eoruby
end
end
end
end
end
@@ -2,6 +2,7 @@ module ActiveFedora
module RdfNode
extend ActiveSupport::Concern
extend ActiveSupport::Autoload
include ActiveFedora::Rdf::NestedAttributes
autoload :TermProxy
@@ -10,7 +11,17 @@ def self.rdf_registry
@@rdf_registry ||= {}
end
# Comparison Operator
# Checks that
# * RDF subject id (URI) is same
# * Class is the same
# * Both objects reference the same RDF graph in memory
def ==(other_object)
self.class == other_object.class &&
self.rdf_subject.id == other_object.rdf_subject.id &&
self.graph.object_id == other_object.graph.object_id
end
##
# Get the subject for this rdf object
def rdf_subject
@@ -62,6 +73,19 @@ def set_value(subject, predicate, values)
TermProxy.new(self, subject, predicate, options)
end
# @option [Hash] values the values to assign to this rdf node.
def attributes=(values)
raise ArgumentError, "values must be a Hash, you provided #{values.class}" unless values.kind_of? Hash
self.class.config.keys.each do |key|
if values.has_key?(key)
set_value(rdf_subject, key, values[key])
end
end
nested_attributes_options.keys.each do |key|
send("#{key}_attributes=".to_sym, values["#{key}_attributes".to_sym])
end
end
def delete_predicate(subject, predicate, values = nil)
predicate = find_predicate(predicate) unless predicate.kind_of? RDF::URI
@@ -164,6 +188,7 @@ def find_values_with_class(subject, predicate, rdf_type)
end
matching
end
class Builder
def initialize(parent)
@parent = parent
@@ -18,10 +18,12 @@ def initialize(graph, subject, predicate, options)
@options = options
end
def build
def build(attributes=nil)
new_subject = RDF::Node.new
graph.graph.insert([subject, predicate, new_subject])
graph.target_class(predicate).new(graph.graph, new_subject)
graph.target_class(predicate).new(graph.graph, new_subject).tap do |node|
node.attributes = attributes if attributes
end
end
def <<(*values)
@@ -49,26 +51,30 @@ def values
# If the user provided options[:class_name], we should query to make sure this
# potential solution is of the right RDF.type
if options[:class_name]
klass = class_from_rdf_type(v, predicate)
klass = class_from_rdf_type(v)
values << v if klass == ActiveFedora.class_from_string(options[:class_name], graph.class)
else
values << v
end
end
if options[:class_name]
values = values.map{ |found_subject| class_from_rdf_type(found_subject, predicate).new(graph.graph, found_subject)}
values = values.map{ |found_subject| class_from_rdf_type(found_subject).new(graph.graph, found_subject)}
end
values
end
private
def target_class
graph.target_class(predicate)
end
# Look for a RDF.type assertion on this node to see if an RDF class is specified.
# Two classes may be valid for the same predicate (e.g. hasMember)
# If no RDF.type assertion is found, fall back to using target_class
def class_from_rdf_type(subject, predicate)
def class_from_rdf_type(subject)
q = RDF::Query.new do
pattern [subject, RDF.type, :value]
end
@@ -79,7 +85,7 @@ def class_from_rdf_type(subject, predicate)
end
klass = ActiveFedora::RdfNode.rdf_registry[type_uri.first]
klass ||= graph.target_class(predicate)
klass ||= target_class
klass
end
@@ -0,0 +1,118 @@
require 'spec_helper'
describe ActiveFedora::RDFDatastream do
before do
class DummyMADS < RDF::Vocabulary("http://www.loc.gov/mads/rdf/v1#")
# componentList and Types of components
property :componentList
property :Topic
property :Temporal
property :PersonalName
property :CorporateName
property :ComplexSubject
# elementList and elementList values
property :elementList
property :elementValue
property :TopicElement
property :TemporalElement
property :NameElement
property :FullNameElement
property :DateNameElement
end
class ComplexRDFDatastream < ActiveFedora::NtriplesRDFDatastream
map_predicates do |map|
map.topic(in: DummyMADS, to: "Topic", class_name:"Topic")
map.personalName(in: DummyMADS, to: "PersonalName", class_name:"PersonalName")
map.title(in: RDF::DC)
end
accepts_nested_attributes_for :topic, :personalName
class Topic
include ActiveFedora::RdfObject
map_predicates do |map|
map.elementList(in: DummyMADS, class_name:"ComplexRDFDatastream::ElementList")
end
accepts_nested_attributes_for :elementList
end
class PersonalName
include ActiveFedora::RdfObject
map_predicates do |map|
map.elementList(in: DummyMADS, to: "elementList", class_name:"ComplexRDFDatastream::ElementList")
map.extraProperty(in: DummyMADS, to: "elementValue", class_name:"ComplexRDFDatastream::Topic")
end
accepts_nested_attributes_for :elementList, :extraProperty
end
class ElementList
include ActiveFedora::RdfObject
rdf_type DummyMADS.elementList
map_predicates do |map|
map.topicElement(in: DummyMADS, to: "TopicElement")
map.temporalElement(in: DummyMADS, to: "TemporalElement")
map.fullNameElement(in: DummyMADS, to: "FullNameElement")
map.dateNameElement(in: DummyMADS, to: "DateNameElement")
map.nameElement(in: DummyMADS, to: "NameElement")
map.elementValue(in: DummyMADS)
end
end
end
end
after do
Object.send(:remove_const, :ComplexRDFDatastream)
Object.send(:remove_const, :DummyMADS)
end
subject { ComplexRDFDatastream.new(stub('inner object', :pid=>'foo', :new? =>true), 'descMetadata') }
describe ".attributes=" do
describe "complex properties" do
let(:params) do
{ myResource:
{
topic_attributes: [
{
elementList_attributes: {
topicElement:"Cosmology"
}
},
{
elementList_attributes: {
topicElement:"Quantum Behavior"
}
}
],
personalName_attributes: [
{
elementList_attributes: {
fullNameElement: "Jefferson, Thomas",
dateNameElement: "1743-1826"
}
}
#, "Hemings, Sally"
],
}
}
end
it "should support mass-assignment" do
# Replace the graph's contents with the Hash
subject.attributes = params[:myResource]
# Here's how this would happen if we didn't have attributes=
# personal_name = subject.personalName.build
# elem_list = personal_name.elementList.build
# elem_list.fullNameElement = "Jefferson, Thomas"
# elem_list.dateNameElement = "1743-1826"
# topic = subject.topic.build
# elem_list = topic.elementList.build
# elem_list.fullNameElement = 'Cosmology'
subject.topic.first.elementList.first.topicElement.should == ["Cosmology"]
subject.topic[1].elementList.first.topicElement.should == ["Quantum Behavior"]
subject.personalName.first.elementList.first.fullNameElement.should == ["Jefferson, Thomas"]
subject.personalName.first.elementList.first.dateNameElement.should == ["1743-1826"]
end
end
end
end

0 comments on commit 13b55ba

Please sign in to comment.