draper 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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)