draper 0.11.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +23 -0
- data/Gemfile +4 -0
- data/Readme.markdown +32 -8
- data/draper.gemspec +1 -0
- data/lib/draper.rb +1 -0
- data/lib/draper/active_model_support.rb +24 -0
- data/lib/draper/base.rb +30 -22
- data/lib/draper/decorated_enumerable_proxy.rb +12 -9
- data/lib/draper/helper_support.rb +1 -1
- data/lib/draper/lazy_helpers.rb +1 -1
- data/lib/draper/model_support.rb +2 -4
- data/lib/draper/railtie.rb +31 -0
- data/lib/draper/rspec_integration.rb +22 -11
- data/lib/draper/system.rb +9 -0
- data/lib/draper/version.rb +1 -1
- data/lib/generators/decorator/decorator_generator.rb +28 -0
- data/lib/generators/{draper/decorator → decorator}/templates/decorator.rb +5 -3
- data/lib/generators/resource_override.rb +12 -0
- data/lib/generators/rspec/decorator_generator.rb +9 -0
- data/lib/generators/rspec/templates/decorator_spec.rb +4 -0
- data/lib/generators/test_unit/decorator_generator.rb +9 -0
- data/lib/generators/test_unit/templates/decorator_test.rb +7 -0
- data/performance/active_record.rb +1 -1
- data/performance/models.rb +1 -1
- data/spec/draper/base_spec.rb +74 -15
- data/spec/draper/helper_support_spec.rb +4 -4
- data/spec/draper/model_support_spec.rb +1 -1
- data/spec/draper/view_context_spec.rb +2 -7
- data/spec/generators/decorator/decorator_generator_spec.rb +52 -0
- data/spec/spec_helper.rb +21 -17
- data/spec/support/samples/active_model.rb +9 -0
- data/spec/support/samples/active_record.rb +8 -0
- data/spec/support/samples/application_helper.rb +1 -1
- data/spec/support/samples/decorator.rb +1 -1
- data/spec/support/samples/decorator_with_allows.rb +1 -1
- data/spec/support/samples/non_active_model_product.rb +2 -0
- data/spec/support/samples/specific_product_decorator.rb +1 -1
- metadata +30 -18
- data/lib/generators/draper/decorator/USAGE +0 -7
- data/lib/generators/draper/decorator/decorator_generator.rb +0 -43
- data/lib/generators/draper/decorator/templates/decorator_spec.rb +0 -5
- data/lib/generators/draper/decorator/templates/decorator_test.rb +0 -12
- data/lib/generators/draper/install/install_generator.rb +0 -39
- data/lib/generators/draper/install/templates/application_decorator.rb +0 -28
- data/lib/generators/draper/install/templates/application_decorator_spec.rb +0 -4
- data/lib/generators/draper/install/templates/application_decorator_test.rb +0 -11
- data/lib/generators/rails/decorator_generator.rb +0 -15
- data/spec/generators/draper/decorator/decorator_generator_spec.rb +0 -102
- 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
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.
|
10
|
-
3.
|
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.
|
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
|
-
|
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
|
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
|
-
|
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
data/lib/draper.rb
CHANGED
@@ -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.
|
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
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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(/^
|
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)
|
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
|
10
|
-
@wrapped_collection.
|
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
|
-
|
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
|
data/lib/draper/lazy_helpers.rb
CHANGED
data/lib/draper/model_support.rb
CHANGED
@@ -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
|
data/lib/draper/railtie.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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)
|