Permalink
Browse files

Merge pull request #385 from haines/context

Restore context
  • Loading branch information...
2 parents 059ecbc + 7784564 commit 9609156b997b3a469386eef3a5f043b24d8a2fba @steveklabnik steveklabnik committed Dec 13, 2012
View
@@ -14,6 +14,8 @@
require 'draper/collection_decorator'
require 'draper/railtie' if defined?(Rails)
+require 'active_support/core_ext/hash/keys'
+
module Draper
def self.setup_action_controller(base)
base.class_eval do
@@ -3,20 +3,21 @@ class CollectionDecorator
include Enumerable
include ViewHelpers
- attr_accessor :source, :options, :decorator_class
+ attr_accessor :source, :context, :decorator_class
alias_method :to_source, :source
delegate :as_json, *(Array.instance_methods - Object.instance_methods), to: :decorated_collection
# @param source collection to decorate
- # @param options [Hash] passed to each item's decorator (except
- # for the keys listed below)
- # @option options [Class,Symbol] :with the class used to decorate
+ # @param [Hash] options (optional)
+ # @option options [Class, Symbol] :with the class used to decorate
# items, or `:infer` to call each item's `decorate` method instead
+ # @option options [Hash] :context context available to each item's decorator
def initialize(source, options = {})
+ options.assert_valid_keys(:with, :context)
@source = source
- @decorator_class = options.delete(:with) || self.class.inferred_decorator_class
- @options = options
+ @decorator_class = options.fetch(:with) { self.class.inferred_decorator_class }
+ @context = options.fetch(:context, {})
end
class << self
@@ -56,18 +57,18 @@ def to_s
"#<CollectionDecorator of #{decorator_class} for #{source.inspect}>"
end
- def options=(options)
- each {|item| item.options = options }
- @options = options
+ def context=(value)
+ @context = value
+ each {|item| item.context = value } if @decorated_collection
end
protected
def decorate_item(item)
if decorator_class == :infer
- item.decorate(options)
+ item.decorate(context: context)
else
- decorator_class.decorate(item, options)
+ decorator_class.decorate(item, context: context)
end
end
@@ -1,11 +1,12 @@
module Draper
class DecoratedAssociation
- attr_reader :source, :association, :options
+ attr_reader :base, :association, :options
- def initialize(source, association, options)
- @source = source
+ def initialize(base, association, options)
+ @base = base
@association = association
+ options.assert_valid_keys(:with, :scope, :context)
@options = options
end
@@ -14,6 +15,10 @@ def call
decorate
end
+ def source
+ base.source
+ end
+
private
def undecorated
@@ -25,7 +30,7 @@ def undecorated
end
def decorate
- @decorated ||= decorator_class.send(decorate_method, undecorated, options)
+ @decorated ||= decorator_class.send(decorate_method, undecorated, decorator_options)
end
def decorate_method
@@ -51,5 +56,15 @@ def decorator_class
end
end
+ def decorator_options
+ decorator_class # Ensures options[:with] = :infer for unspecified collections
+
+ dec_options = collection? ? options.slice(:with, :context) : options.slice(:context)
+ dec_options[:context] = base.context unless dec_options.key?(:context)
+ if dec_options[:context].respond_to?(:call)
+ dec_options[:context] = dec_options[:context].call(base.context)
+ end
+ dec_options
+ end
end
end
View
@@ -5,14 +5,15 @@ class Decorator
include Draper::ViewHelpers
include ActiveModel::Serialization if defined?(ActiveModel::Serialization)
- attr_accessor :source, :options
+ attr_accessor :source, :context
alias_method :model, :source
alias_method :to_source, :source
# Initialize a new decorator instance by passing in
# an instance of the source class. Pass in an optional
- # context inside the options hash is stored for later use.
+ # :context inside the options hash which is available
+ # for later use.
#
# A decorator cannot be applied to other instances of the
# same decorator and will instead result in a decorator
@@ -23,11 +24,13 @@ class Decorator
#
# @param [Object] source object to decorate
# @param [Hash] options (optional)
+ # @option options [Hash] :context context available to the decorator
def initialize(source, options = {})
+ options.assert_valid_keys(:context)
source.to_a if source.respond_to?(:to_a) # forces evaluation of a lazy query from AR
@source = source
- @options = options
- handle_multiple_decoration if source.is_a?(Draper::Decorator)
+ @context = options.fetch(:context, {})
+ handle_multiple_decoration(options) if source.is_a?(Draper::Decorator)
end
class << self
@@ -72,17 +75,24 @@ def self.decorates_finders
# @param [Symbol] association name of association to decorate, like `:products`
# @option options [Class] :with the decorator to apply to the association
# @option options [Symbol] :scope a scope to apply when fetching the association
+ # @option options [Hash, #call] :context context available to decorated
+ # objects in collection. Passing a `lambda` or similar will result in that
+ # block being called when the association is evaluated. The block will be
+ # passed the base decorator's `context` Hash and should return the desired
+ # context Hash for the decorated items.
def self.decorates_association(association, options = {})
+ options.assert_valid_keys(:with, :scope, :context)
define_method(association) do
- decorated_associations[association] ||= Draper::DecoratedAssociation.new(source, association, options)
+ decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
decorated_associations[association].call
end
end
# A convenience method for decorating multiple associations. Calls
# decorates_association on each of the given symbols.
#
- # @param [Symbols*] associations name of associations to decorate
+ # @param [Symbols*] associations names of associations to decorate
+ # @param [Hash] options passed to `decorate_association`
def self.decorates_associations(*associations)
options = associations.extract_options!
associations.each do |association|
@@ -128,7 +138,9 @@ def self.allows(*methods)
# for the keys listed below)
# @option options [Class,Symbol] :with (self) the class used to decorate
# items, or `:infer` to call each item's `decorate` method instead
+ # @option options [Hash] :context context available to decorated items
def self.decorate_collection(source, options = {})
+ options.assert_valid_keys(:with, :context)
Draper::CollectionDecorator.new(source, options.reverse_merge(with: self))
end
@@ -244,9 +256,9 @@ def allow?(method)
self.class.security.allow?(method)
end
- def handle_multiple_decoration
+ def handle_multiple_decoration(options)
if source.instance_of?(self.class)
- self.options = source.options if options.empty?
+ self.context = source.context unless options.has_key?(:context)
self.source = source.source
elsif source.decorated_with?(self.class)
warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
@@ -16,33 +16,70 @@
subject.map{|item| item.source}.should == source
end
- context "with options" do
- subject { Draper::CollectionDecorator.new(source, with: ProductDecorator, some: "options") }
+ context "with context" do
+ subject { Draper::CollectionDecorator.new(source, with: ProductDecorator, context: {some: 'context'}) }
- its(:options) { should == {some: "options"} }
+ its(:context) { should == {some: 'context'} }
- it "passes options to the individual decorators" do
+ it "passes context to the individual decorators" do
subject.each do |item|
- item.options.should == {some: "options"}
+ item.context.should == {some: 'context'}
end
end
- describe "#options=" do
- it "updates the options on the collection decorator" do
- subject.options = {other: "options"}
- subject.options.should == {other: "options"}
+ it "does not tie the individual decorators' contexts together" do
+ subject.each do |item|
+ item.context.should == {some: 'context'}
+ item.context = {alt: 'context'}
+ item.context.should == {alt: 'context'}
+ end
+ end
+
+ describe "#context=" do
+ it "updates the collection decorator's context" do
+ subject.context = {other: 'context'}
+ subject.context.should == {other: 'context'}
end
- it "updates the options on the individual decorators" do
- subject.options = {other: "options"}
- subject.each do |item|
- item.options.should == {other: "options"}
+ context "when the collection is already decorated" do
+ it "updates the items' context" do
+ subject.decorated_collection
+ subject.context = {other: 'context'}
+ subject.each do |item|
+ item.context.should == {other: 'context'}
+ end
+ end
+ end
+
+ context "when the collection has not yet been decorated" do
+ it "does not trigger decoration" do
+ subject.should_not_receive(:decorated_collection)
+ subject.context = {other: 'context'}
+ end
+
+ it "sets context after decoration is triggered" do
+ subject.context = {other: 'context'}
+ subject.each do |item|
+ item.context.should == {other: 'context'}
+ end
end
end
end
end
describe "#initialize" do
+ describe "options validation" do
+ let(:valid_options) { {with: ProductDecorator, context: {}} }
+
+ it "does not raise error on valid options" do
+ expect { Draper::CollectionDecorator.new(source, valid_options) }.to_not raise_error
+ end
+
+ it "raises error on invalid options" do
+ expect { Draper::CollectionDecorator.new(source, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
+ end
+ end
+
context "when the :with option is given" do
context "and the decorator can't be inferred from the class" do
subject { Draper::CollectionDecorator.new(source, with: ProductDecorator) }
@@ -123,14 +160,14 @@
end
describe "#localize" do
- before { subject.helpers.should_receive(:localize).with(:an_object, {some: "options"}) }
+ before { subject.helpers.should_receive(:localize).with(:an_object, {some: 'parameter'}) }
it "delegates to helpers" do
- subject.localize(:an_object, some: "options")
+ subject.localize(:an_object, some: 'parameter')
end
it "is aliased to #l" do
- subject.l(:an_object, some: "options")
+ subject.l(:an_object, some: 'parameter')
end
end
@@ -9,9 +9,9 @@
subject.decorate.source.should be subject
end
- it "accepts options" do
- decorator = subject.decorate(some: "options")
- decorator.options.should == {some: "options"}
+ it "accepts context" do
+ decorator = subject.decorate(context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
it "is not memoized" do
@@ -153,9 +153,9 @@
decorator.source.should be Product.scoped
end
- it "accepts options" do
- decorator = Product.decorate(some: "options")
- decorator.options.should == {some: "options"}
+ it "accepts context" do
+ decorator = Product.decorate(context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
it "is not memoized" do
Oops, something went wrong.

0 comments on commit 9609156

Please sign in to comment.