draper 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/CHANGELOG.txt +23 -0
  2. data/Gemfile +4 -0
  3. data/Readme.markdown +32 -8
  4. data/draper.gemspec +1 -0
  5. data/lib/draper.rb +1 -0
  6. data/lib/draper/active_model_support.rb +24 -0
  7. data/lib/draper/base.rb +30 -22
  8. data/lib/draper/decorated_enumerable_proxy.rb +12 -9
  9. data/lib/draper/helper_support.rb +1 -1
  10. data/lib/draper/lazy_helpers.rb +1 -1
  11. data/lib/draper/model_support.rb +2 -4
  12. data/lib/draper/railtie.rb +31 -0
  13. data/lib/draper/rspec_integration.rb +22 -11
  14. data/lib/draper/system.rb +9 -0
  15. data/lib/draper/version.rb +1 -1
  16. data/lib/generators/decorator/decorator_generator.rb +28 -0
  17. data/lib/generators/{draper/decorator → decorator}/templates/decorator.rb +5 -3
  18. data/lib/generators/resource_override.rb +12 -0
  19. data/lib/generators/rspec/decorator_generator.rb +9 -0
  20. data/lib/generators/rspec/templates/decorator_spec.rb +4 -0
  21. data/lib/generators/test_unit/decorator_generator.rb +9 -0
  22. data/lib/generators/test_unit/templates/decorator_test.rb +7 -0
  23. data/performance/active_record.rb +1 -1
  24. data/performance/models.rb +1 -1
  25. data/spec/draper/base_spec.rb +74 -15
  26. data/spec/draper/helper_support_spec.rb +4 -4
  27. data/spec/draper/model_support_spec.rb +1 -1
  28. data/spec/draper/view_context_spec.rb +2 -7
  29. data/spec/generators/decorator/decorator_generator_spec.rb +52 -0
  30. data/spec/spec_helper.rb +21 -17
  31. data/spec/support/samples/active_model.rb +9 -0
  32. data/spec/support/samples/active_record.rb +8 -0
  33. data/spec/support/samples/application_helper.rb +1 -1
  34. data/spec/support/samples/decorator.rb +1 -1
  35. data/spec/support/samples/decorator_with_allows.rb +1 -1
  36. data/spec/support/samples/non_active_model_product.rb +2 -0
  37. data/spec/support/samples/specific_product_decorator.rb +1 -1
  38. metadata +30 -18
  39. data/lib/generators/draper/decorator/USAGE +0 -7
  40. data/lib/generators/draper/decorator/decorator_generator.rb +0 -43
  41. data/lib/generators/draper/decorator/templates/decorator_spec.rb +0 -5
  42. data/lib/generators/draper/decorator/templates/decorator_test.rb +0 -12
  43. data/lib/generators/draper/install/install_generator.rb +0 -39
  44. data/lib/generators/draper/install/templates/application_decorator.rb +0 -28
  45. data/lib/generators/draper/install/templates/application_decorator_spec.rb +0 -4
  46. data/lib/generators/draper/install/templates/application_decorator_test.rb +0 -11
  47. data/lib/generators/rails/decorator_generator.rb +0 -15
  48. data/spec/generators/draper/decorator/decorator_generator_spec.rb +0 -102
  49. data/spec/generators/draper/install/install_generator_spec.rb +0 -46
data/CHANGELOG.txt ADDED
@@ -0,0 +1,23 @@
1
+ = Draper Changelog
2
+
3
+ = Master
4
+
5
+ == 0.12.1
6
+
7
+ * Added Changelog
8
+
9
+ * Prevented double decoration, see #173
10
+
11
+ * ActiveModel::Errors support, 19496f0c
12
+
13
+ * Fixed autoloading issue, see #188
14
+
15
+ * Re-did generators, see 9155e58f
16
+
17
+ * Added capybara integration, see 57c8678e
18
+
19
+ * Fixed a few bugs with the DecoratedEnumerableProxy
20
+
21
+ == 0.11.1
22
+
23
+ * Fixed regression, we don't want to introduce a hard dependency on Rails. #107
data/Gemfile CHANGED
@@ -10,4 +10,8 @@ gem 'guard-rspec'
10
10
  gem 'launchy'
11
11
  gem 'yard'
12
12
 
13
+ group :development do
14
+ gem 'redcarpet'
15
+ end
16
+
13
17
  gemspec
data/Readme.markdown CHANGED
@@ -6,22 +6,31 @@
6
6
  ## Quick Start
7
7
 
8
8
  1. Add `gem 'draper'` to your `Gemfile` and `bundle`
9
- 2. Run `rails g draper:install` to create the directory and `ApplicationDecorator`
10
- 3. Run `rails g draper:decorator YourModel`
9
+ 2. When you generate a resource with `rails g resource YourModel`, you get a decorator automatically!
10
+ 3. If YourModel exists, run `rails g decorator YourModel`
11
11
  4. Edit `app/decorators/[your_model]_decorator.rb` using:
12
12
  1. `h` to proxy to Rails/application helpers like `h.current_user`
13
13
  2. `[your_model]` to access the wrapped object like `article.created_at`
14
- 5. Put common decorations in `app/decorators/application.rb`
15
- 6. Wrap models in your controller with the decorator using:
14
+ 5. Wrap models in your controller with the decorator using:
16
15
  1. `.find` automatic lookup & wrap
17
16
  ex: `ArticleDecorator.find(1)`
18
17
  2. `.decorate` method with single object or collection,
19
18
  ex: `ArticleDecorator.decorate(Article.all)`
20
19
  3. `.new` method with single object
21
20
  ex: `ArticleDecorator.new(Article.first)`
22
- 7. Output the instance methods in your view templates
21
+ 6. Output the instance methods in your view templates
23
22
  ex: `@article_decorator.created_at`
24
23
 
24
+ If you need common methods in your decorators, create an `app/decorators/application_decorator.rb`:
25
+
26
+ ``` ruby
27
+ class ApplicationDecorator < Draper::Base
28
+ # your methods go here
29
+ end
30
+ ```
31
+
32
+ and make your decorators inherit from it. Newly generated decorators will respect this choice and inherit from `ApplicationDecorator`.
33
+
25
34
  ## Watch the RailsCast
26
35
 
27
36
  Ryan Bates has put together an excellent RailsCast on Draper based on the 0.8.0 release:
@@ -146,12 +155,20 @@ gem "draper"
146
155
 
147
156
  Then run `bundle` from the project directory.
148
157
 
158
+ ### Run the draper:install command
159
+
160
+ This will create the `app/decorators` directory and the `ApplicationDecorator` inside it.
161
+
162
+ ```
163
+ rails generate draper:install
164
+ ```
165
+
149
166
  ### Generate the Decorator
150
167
 
151
168
  To decorate a model named `Article`:
152
169
 
153
170
  ```
154
- rails generate draper:decorator Article
171
+ rails generate draper:decorator article
155
172
  ```
156
173
 
157
174
  ### Writing Methods
@@ -239,7 +256,7 @@ ActionMailer class.
239
256
 
240
257
  ```ruby
241
258
  class ArticleMailer < ActionMailer::Base
242
- defaults 'init-draper' => Proc.new { set_current_view_context }
259
+ default 'init-draper' => Proc.new { set_current_view_context }
243
260
  end
244
261
  ```
245
262
  ### Integration with RSpec
@@ -348,6 +365,14 @@ Now when you call the association it will use a decorator.
348
365
  <%= @article.author.fancy_name %>
349
366
  ```
350
367
 
368
+ ## Contributing
369
+
370
+ 1. Fork it.
371
+ 2. Create a branch (`git checkout -b my_awesome_branch`)
372
+ 3. Commit your changes (`git commit -am "Added some magic"`)
373
+ 4. Push to the branch (`git push origin my_awesome_branch`)
374
+ 5. Send pull request
375
+
351
376
  ## Issues / Pending
352
377
 
353
378
  * Documentation
@@ -355,7 +380,6 @@ Now when you call the association it will use a decorator.
355
380
  * Add information about the `.decorator` method
356
381
  * Make clear the pattern of overriding accessor methods of the wrapped model
357
382
  * Build sample Rails application(s)
358
- * Add a section about contributing
359
383
  * Generators
360
384
  * Implement hook so generating a controller/scaffold generates a decorator
361
385
  * Add generators for...
data/draper.gemspec CHANGED
@@ -16,4 +16,5 @@ Gem::Specification.new do |s|
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
18
  s.add_dependency('activesupport', '>= 2.3.10')
19
+ s.add_development_dependency('redcarpet')
19
20
  end
data/lib/draper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "draper/version"
2
2
  require 'draper/system'
3
+ require 'draper/active_model_support'
3
4
  require 'draper/base'
4
5
  require 'draper/lazy_helpers'
5
6
  require 'draper/model_support'
@@ -0,0 +1,24 @@
1
+ module Draper::ActiveModelSupport
2
+ module Proxies
3
+ def create_proxies
4
+ # These methods (as keys) will be created only if the correspondent
5
+ # model descends from a specific class (as value)
6
+ proxies = {}
7
+ proxies[:to_param] = ActiveModel::Conversion if defined?(ActiveModel::Conversion)
8
+ proxies[:errors] = ActiveModel::Validations if defined?(ActiveModel::Validations)
9
+ proxies[:id] = ActiveRecord::Base if defined?(ActiveRecord::Base)
10
+
11
+ proxies.each do |method_name, dependency|
12
+ if model.kind_of?(dependency) || dependency.nil?
13
+ class << self
14
+ self
15
+ end.class_eval do
16
+ self.send(:define_method, method_name) do |*args, &block|
17
+ model.send(method_name, *args, &block)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/draper/base.rb CHANGED
@@ -1,20 +1,18 @@
1
1
  module Draper
2
2
  class Base
3
3
  require 'active_support/core_ext/class/attribute'
4
+ require 'active_support/core_ext/array/extract_options'
5
+
4
6
  class_attribute :denied, :allowed, :model_class
5
7
  attr_accessor :model, :options
6
8
 
7
- DEFAULT_DENIED = Object.new.methods << :method_missing
9
+ DEFAULT_DENIED = Object.instance_methods << :method_missing
8
10
  DEFAULT_ALLOWED = []
9
- FORCED_PROXY = [:to_param, :id]
10
- FORCED_PROXY.each do |method|
11
- define_method method do |*args, &block|
12
- model.send method, *args, &block
13
- end
14
- end
15
11
  self.denied = DEFAULT_DENIED
16
12
  self.allowed = DEFAULT_ALLOWED
17
13
 
14
+ include Draper::ActiveModelSupport::Proxies
15
+
18
16
  # Initialize a new decorator instance by passing in
19
17
  # an instance of the source class. Pass in an optional
20
18
  # context inside the options hash is stored for later use.
@@ -24,8 +22,9 @@ module Draper
24
22
  def initialize(input, options = {})
25
23
  input.inspect # forces evaluation of a lazy query from AR
26
24
  self.class.model_class = input.class if model_class.nil?
27
- @model = input
25
+ @model = input.kind_of?(Draper::Base) ? input.model : input
28
26
  self.options = options
27
+ create_proxies
29
28
  end
30
29
 
31
30
  # Proxies to the class specified by `decorates` to automatically
@@ -60,7 +59,7 @@ module Draper
60
59
  # the assocation to be decorated when it is retrieved.
61
60
  #
62
61
  # @param [Symbol] name of association to decorate, like `:products`
63
- # @option opts [Class] :with The decorator to decorate the association with
62
+ # @option options [Class] :with The decorator to decorate the association with
64
63
  def self.decorates_association(association_symbol, options = {})
65
64
  define_method(association_symbol) do
66
65
  orig_association = model.send(association_symbol)
@@ -69,15 +68,16 @@ module Draper
69
68
 
70
69
  return options[:with].decorate(orig_association) if options[:with]
71
70
 
72
- if options[:polymorphic]
73
- klass = orig_association.class
74
- elsif model.class.respond_to?(:reflect_on_association) && model.class.reflect_on_association(association_symbol)
75
- klass = model.class.reflect_on_association(association_symbol).klass
76
- elsif orig_association.respond_to?(:first)
77
- klass = orig_association.first.class
78
- else
79
- klass = orig_association.class
80
- end
71
+ klass = if options[:polymorphic]
72
+ orig_association.class
73
+ elsif association_reflection = find_association_reflection(association_symbol)
74
+ association_reflection.klass
75
+ elsif orig_association.respond_to?(:first)
76
+ orig_association.first.class
77
+ else
78
+ orig_association.class
79
+ end
80
+
81
81
  "#{klass}Decorator".constantize.decorate(orig_association, options)
82
82
  end
83
83
  end
@@ -129,6 +129,8 @@ module Draper
129
129
  #
130
130
  # @param [Object] instance(s) to wrap
131
131
  # @param [Hash] options (optional)
132
+ # @option options [Boolean] :infer If true, each model will be
133
+ # wrapped by its inferred decorator.
132
134
  def self.decorate(input, options = {})
133
135
  if input.instance_of?(self)
134
136
  input.options = options unless options.empty?
@@ -206,7 +208,7 @@ module Draper
206
208
  alias :is_a? :kind_of?
207
209
 
208
210
  def respond_to?(method, include_private = false)
209
- super || (allow?(method) && model.respond_to?(method))
211
+ super || (allow?(method) && model.respond_to?(method, include_private))
210
212
  end
211
213
 
212
214
  def method_missing(method, *args, &block)
@@ -228,8 +230,8 @@ module Draper
228
230
  end
229
231
 
230
232
  def self.method_missing(method, *args, &block)
231
- if method.to_s.match(/^find_by.*/)
232
- self.decorate(model_class.send(method, *args, &block))
233
+ if method.to_s.match(/^find_((all_|last_)?by_|or_(initialize|create)_by_).*/)
234
+ self.decorate(model_class.send(method, *args, &block), :context => args.dup.extract_options!)
233
235
  else
234
236
  model_class.send(method, *args, &block)
235
237
  end
@@ -255,7 +257,13 @@ module Draper
255
257
  private
256
258
 
257
259
  def allow?(method)
258
- (allowed.empty? || allowed.include?(method) || FORCED_PROXY.include?(method)) && !denied.include?(method)
260
+ (allowed.empty? || allowed.include?(method)) && !denied.include?(method)
261
+ end
262
+
263
+ def find_association_reflection(association)
264
+ if model.class.respond_to?(:reflect_on_association)
265
+ model.class.reflect_on_association(association)
266
+ end
259
267
  end
260
268
  end
261
269
  end
@@ -2,28 +2,27 @@ module Draper
2
2
  class DecoratedEnumerableProxy
3
3
  include Enumerable
4
4
 
5
+ delegate :as_json, :collect, :map, :each, :[], :all?, :include?, :first, :last, :shift, :to => :decorated_collection
6
+
5
7
  def initialize(collection, klass, options = {})
6
8
  @wrapped_collection, @klass, @options = collection, klass, options
7
9
  end
8
10
 
9
- def each(&block)
10
- @wrapped_collection.each { |member| block.call(@klass.decorate(member, @options)) }
11
- end
12
-
13
- def to_ary
14
- @wrapped_collection.map { |member| @klass.decorate(member, @options) }
11
+ def decorated_collection
12
+ @decorated_collection ||= @wrapped_collection.collect { |member| @klass.decorate(member, @options) }
15
13
  end
14
+ alias_method :to_ary, :decorated_collection
16
15
 
17
16
  def method_missing (method, *args, &block)
18
17
  @wrapped_collection.send(method, *args, &block)
19
18
  end
20
19
 
21
- def respond_to?(method)
22
- super || @wrapped_collection.respond_to?(method)
20
+ def respond_to?(method, include_private = false)
21
+ super || @wrapped_collection.respond_to?(method, include_private)
23
22
  end
24
23
 
25
24
  def kind_of?(klass)
26
- super || @wrapped_collection.kind_of?(klass)
25
+ @wrapped_collection.kind_of?(klass) || super
27
26
  end
28
27
  alias :is_a? :kind_of?
29
28
 
@@ -38,6 +37,10 @@ module Draper
38
37
  def to_s
39
38
  "#<DecoratedEnumerableProxy of #{@klass} for #{@wrapped_collection.inspect}>"
40
39
  end
40
+
41
+ def context=(input)
42
+ self.map { |member| member.context = input }
43
+ end
41
44
 
42
45
  def source
43
46
  @wrapped_collection
@@ -2,4 +2,4 @@ module Draper::HelperSupport
2
2
  def decorate(input, &block)
3
3
  capture { block.call(input.decorate) }
4
4
  end
5
- end
5
+ end
@@ -8,4 +8,4 @@ module Draper
8
8
  end
9
9
  end
10
10
  end
11
- end
11
+ end
@@ -1,4 +1,6 @@
1
1
  module Draper::ModelSupport
2
+ extend ActiveSupport::Concern
3
+
2
4
  def decorator(options = {})
3
5
  @decorator ||= "#{self.class.name}Decorator".constantize.decorate(self, options.merge(:infer => false))
4
6
  block_given? ? yield(@decorator) : @decorator
@@ -12,8 +14,4 @@ module Draper::ModelSupport
12
14
  block_given? ? yield(decorator_proxy) : decorator_proxy
13
15
  end
14
16
  end
15
-
16
- def self.included(base)
17
- base.extend(ClassMethods)
18
- end
19
17
  end
@@ -1,8 +1,39 @@
1
1
  require 'rails/railtie'
2
2
 
3
+ module ActiveModel
4
+ class Railtie < Rails::Railtie
5
+ generators do |app|
6
+ Rails::Generators.configure!(app.config.generators)
7
+ require "generators/resource_override"
8
+ end
9
+ end
10
+ end
11
+
3
12
  module Draper
4
13
  class Railtie < Rails::Railtie
5
14
 
15
+ ##
16
+ # Decorators are loaded in
17
+ # => at app boot in non-development environments
18
+ # => after each request in the development environment
19
+ #
20
+ # This is necessary because we might never explicitly reference
21
+ # Decorator constants.
22
+ #
23
+ config.to_prepare do
24
+ ::Draper::System.load_app_local_decorators
25
+ end
26
+
27
+ ##
28
+ # The `app/decorators` path is eager loaded
29
+ #
30
+ # This is the standard "Rails Way" to add paths from which constants
31
+ # can be loaded.
32
+ #
33
+ config.before_initialize do |app|
34
+ app.config.paths.add 'app/decorators', :eager_load => true
35
+ end
36
+
6
37
  initializer "draper.extend_action_controller_base" do |app|
7
38
  ActiveSupport.on_load(:action_controller) do
8
39
  Draper::System.setup(:action_controller)
@@ -3,20 +3,31 @@ module Draper
3
3
  extend ActiveSupport::Concern
4
4
  included { metadata[:type] = :decorator }
5
5
  end
6
+ end
6
7
 
7
- RSpec.configure do |config|
8
- # Automatically tag specs in specs/decorators as type: :decorator
9
- config.include Draper::DecoratorExampleGroup, :type => :decorator, :example_group => {
10
- :file_path => /spec[\\\/]decorators/
11
- }
8
+ RSpec.configure do |config|
9
+ # Automatically tag specs in specs/decorators as type: :decorator
10
+ config.include Draper::DecoratorExampleGroup, :type => :decorator, :example_group => {
11
+ :file_path => /spec[\\\/]decorators/
12
+ }
12
13
 
13
- # Specs tagged type: :decorator set the Draper view context
14
- config.around do |example|
15
- if :decorator == example.metadata[:type]
16
- ApplicationController.new.set_current_view_context
17
- end
18
- example.call
14
+ # Specs tagged type: :decorator set the Draper view context
15
+ config.around do |example|
16
+ if :decorator == example.metadata[:type]
17
+ ApplicationController.new.set_current_view_context
18
+ Draper::ViewContext.current.controller.request ||= ActionController::TestRequest.new
19
+ Draper::ViewContext.current.request ||= Draper::ViewContext.current.controller.request
20
+ Draper::ViewContext.current.params ||= {}
19
21
  end
22
+ example.call
23
+ end
24
+ end
25
+
26
+ if defined?(Capybara)
27
+ require 'capybara/rspec/matchers'
28
+
29
+ RSpec.configure do |config|
30
+ config.include Capybara::RSpecMatchers, :type => :decorator
20
31
  end
21
32
  end
22
33
 
data/lib/draper/system.rb CHANGED
@@ -1,5 +1,14 @@
1
1
  module Draper
2
2
  class System
3
+ def self.app_local_decorator_glob
4
+ 'app/decorators/**/*_decorator.rb'
5
+ end
6
+
7
+ def self.load_app_local_decorators
8
+ decorator_files = Dir[ "#{ Rails.root }/#{ app_local_decorator_glob }" ]
9
+ decorator_files.each { |d| require_dependency d }
10
+ end
11
+
3
12
  def self.setup(component)
4
13
  if component == :action_controller
5
14
  ActionController::Base.send(:include, Draper::ViewContextFilter)