draper 0.18.0 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +5 -0
  3. data/CHANGELOG.markdown +8 -0
  4. data/Gemfile +4 -0
  5. data/Rakefile +50 -31
  6. data/Readme.markdown +42 -48
  7. data/draper.gemspec +2 -1
  8. data/lib/draper.rb +42 -7
  9. data/lib/draper/collection_decorator.rb +88 -0
  10. data/lib/draper/decoratable.rb +44 -0
  11. data/lib/draper/decorated_association.rb +55 -0
  12. data/lib/draper/decorator.rb +208 -0
  13. data/lib/draper/finders.rb +44 -0
  14. data/lib/draper/helper_proxy.rb +16 -0
  15. data/lib/draper/railtie.rb +17 -21
  16. data/lib/draper/security.rb +48 -0
  17. data/lib/draper/tasks/tu.rake +5 -0
  18. data/lib/draper/test/minitest_integration.rb +1 -1
  19. data/lib/draper/test/test_unit_integration.rb +9 -0
  20. data/lib/draper/version.rb +1 -1
  21. data/lib/draper/view_context.rb +6 -13
  22. data/lib/draper/view_helpers.rb +36 -0
  23. data/lib/generators/decorator/decorator_generator.rb +1 -1
  24. data/lib/generators/decorator/templates/decorator.rb +0 -1
  25. data/spec/draper/collection_decorator_spec.rb +240 -0
  26. data/spec/draper/decoratable_spec.rb +164 -0
  27. data/spec/draper/decorated_association_spec.rb +130 -0
  28. data/spec/draper/decorator_spec.rb +497 -0
  29. data/spec/draper/finders_spec.rb +156 -0
  30. data/spec/draper/helper_proxy_spec.rb +12 -0
  31. data/spec/draper/security_spec.rb +158 -0
  32. data/spec/draper/view_helpers_spec.rb +41 -0
  33. data/spec/dummy/.rspec +2 -0
  34. data/spec/dummy/README.rdoc +261 -0
  35. data/spec/dummy/Rakefile +7 -0
  36. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  37. data/spec/dummy/app/controllers/localized_urls.rb +5 -0
  38. data/spec/dummy/app/controllers/posts_controller.rb +17 -0
  39. data/spec/dummy/app/decorators/post_decorator.rb +25 -0
  40. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  41. data/spec/dummy/app/mailers/application_mailer.rb +3 -0
  42. data/spec/dummy/app/mailers/post_mailer.rb +9 -0
  43. data/spec/dummy/app/models/post.rb +3 -0
  44. data/spec/dummy/app/views/layouts/application.html.erb +11 -0
  45. data/spec/dummy/app/views/post_mailer/decorated_email.html.erb +1 -0
  46. data/spec/dummy/app/views/posts/_post.html.erb +19 -0
  47. data/spec/dummy/app/views/posts/show.html.erb +1 -0
  48. data/spec/dummy/config.ru +4 -0
  49. data/spec/dummy/config/application.rb +64 -0
  50. data/spec/dummy/config/boot.rb +10 -0
  51. data/spec/dummy/config/database.yml +25 -0
  52. data/spec/dummy/config/environment.rb +5 -0
  53. data/spec/dummy/config/environments/development.rb +34 -0
  54. data/spec/dummy/config/environments/production.rb +55 -0
  55. data/spec/dummy/config/environments/test.rb +32 -0
  56. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  57. data/spec/dummy/config/initializers/inflections.rb +15 -0
  58. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  59. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  60. data/spec/dummy/config/initializers/session_store.rb +8 -0
  61. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  62. data/spec/dummy/config/locales/en.yml +5 -0
  63. data/spec/dummy/config/routes.rb +7 -0
  64. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
  65. data/spec/dummy/db/schema.rb +21 -0
  66. data/spec/dummy/db/seeds.rb +2 -0
  67. data/spec/dummy/lib/tasks/spec.rake +5 -0
  68. data/spec/dummy/public/404.html +26 -0
  69. data/spec/dummy/public/422.html +26 -0
  70. data/spec/dummy/public/500.html +25 -0
  71. data/spec/dummy/public/favicon.ico +0 -0
  72. data/spec/dummy/script/rails +6 -0
  73. data/spec/dummy/spec/decorators/post_decorator_spec.rb +23 -0
  74. data/spec/dummy/spec/mailers/post_mailer_spec.rb +29 -0
  75. data/spec/dummy/spec/spec_helper.rb +9 -0
  76. data/spec/generators/decorator/decorator_generator_spec.rb +3 -4
  77. data/spec/integration/integration_spec.rb +33 -0
  78. data/{performance → spec/performance}/active_record.rb +0 -0
  79. data/{performance/bechmark.rb → spec/performance/benchmark.rb} +0 -0
  80. data/{performance → spec/performance}/decorators.rb +2 -4
  81. data/{performance → spec/performance}/models.rb +0 -0
  82. data/spec/spec_helper.rb +20 -41
  83. data/spec/support/action_controller.rb +12 -0
  84. data/spec/support/active_model.rb +7 -0
  85. data/spec/support/{samples/active_record.rb → active_record.rb} +5 -0
  86. data/spec/support/{samples → decorators}/decorator_with_application_helper.rb +1 -1
  87. data/spec/support/decorators/namespaced_product_decorator.rb +5 -0
  88. data/spec/support/decorators/non_active_model_product_decorator.rb +2 -0
  89. data/spec/support/decorators/product_decorator.rb +19 -0
  90. data/spec/support/{samples → decorators}/products_decorator.rb +1 -1
  91. data/spec/support/decorators/some_thing_decorator.rb +2 -0
  92. data/spec/support/{samples → decorators}/specific_product_decorator.rb +0 -2
  93. data/spec/support/{samples → decorators}/widget_decorator.rb +0 -0
  94. data/spec/support/dummy_app.rb +84 -0
  95. data/spec/support/matchers/have_text.rb +50 -0
  96. data/spec/support/{samples → models}/namespaced_product.rb +1 -3
  97. data/spec/support/{samples → models}/non_active_model_product.rb +1 -0
  98. data/spec/support/{samples → models}/product.rb +13 -2
  99. data/spec/support/models/some_thing.rb +5 -0
  100. data/spec/support/models/uninferrable_decorator_model.rb +3 -0
  101. data/spec/support/{samples → models}/widget.rb +0 -0
  102. metadata +185 -68
  103. data/lib/draper/active_model_support.rb +0 -27
  104. data/lib/draper/base.rb +0 -312
  105. data/lib/draper/decorated_enumerable_proxy.rb +0 -90
  106. data/lib/draper/model_support.rb +0 -25
  107. data/lib/draper/rspec_integration.rb +0 -2
  108. data/lib/draper/system.rb +0 -18
  109. data/spec/draper/base_spec.rb +0 -873
  110. data/spec/draper/decorated_enumerable_proxy_spec.rb +0 -45
  111. data/spec/draper/model_support_spec.rb +0 -48
  112. data/spec/support/samples/decorator.rb +0 -5
  113. data/spec/support/samples/decorator_with_allows.rb +0 -3
  114. data/spec/support/samples/decorator_with_denies.rb +0 -3
  115. data/spec/support/samples/decorator_with_denies_all.rb +0 -3
  116. data/spec/support/samples/decorator_with_multiple_allows.rb +0 -4
  117. data/spec/support/samples/decorator_with_special_methods.rb +0 -13
  118. data/spec/support/samples/enumerable_proxy.rb +0 -3
  119. data/spec/support/samples/namespaced_product_decorator.rb +0 -7
  120. data/spec/support/samples/product_decorator.rb +0 -7
  121. data/spec/support/samples/sequel_product.rb +0 -13
  122. data/spec/support/samples/some_thing.rb +0 -2
  123. data/spec/support/samples/some_thing_decorator.rb +0 -3
@@ -0,0 +1,44 @@
1
+ module Draper::Decoratable
2
+ extend ActiveSupport::Concern
3
+
4
+ def decorate(options = {})
5
+ decorator_class.decorate(self, options)
6
+ end
7
+
8
+ def decorator_class
9
+ self.class.decorator_class
10
+ end
11
+
12
+ def applied_decorators
13
+ []
14
+ end
15
+
16
+ def decorated_with?(decorator_class)
17
+ false
18
+ end
19
+
20
+ def decorated?
21
+ false
22
+ end
23
+
24
+ def ===(other)
25
+ super || (other.respond_to?(:source) && super(other.source))
26
+ end
27
+
28
+ module ClassMethods
29
+ def decorate(options = {})
30
+ decorator_class.decorate_collection(self.scoped, options)
31
+ end
32
+
33
+ def decorator_class
34
+ prefix = respond_to?(:model_name) ? model_name : name
35
+ "#{prefix}Decorator".constantize
36
+ rescue NameError
37
+ raise Draper::UninferrableDecoratorError.new(self)
38
+ end
39
+
40
+ def ===(other)
41
+ super || (other.respond_to?(:source) && super(other.source))
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,55 @@
1
+ module Draper
2
+ class DecoratedAssociation
3
+
4
+ attr_reader :source, :association, :options
5
+
6
+ def initialize(source, association, options)
7
+ @source = source
8
+ @association = association
9
+ @options = options
10
+ end
11
+
12
+ def call
13
+ return undecorated if undecorated.nil? || undecorated == []
14
+ decorate
15
+ end
16
+
17
+ private
18
+
19
+ def undecorated
20
+ @undecorated ||= begin
21
+ associated = source.send(association)
22
+ associated = associated.send(options[:scope]) if options[:scope]
23
+ associated
24
+ end
25
+ end
26
+
27
+ def decorate
28
+ @decorated ||= decorator_class.send(decorate_method, undecorated, options)
29
+ end
30
+
31
+ def decorate_method
32
+ if collection? && decorator_class.respond_to?(:decorate_collection)
33
+ :decorate_collection
34
+ else
35
+ :decorate
36
+ end
37
+ end
38
+
39
+ def collection?
40
+ undecorated.respond_to?(:first)
41
+ end
42
+
43
+ def decorator_class
44
+ return options[:with] if options[:with]
45
+
46
+ if collection?
47
+ options[:with] = :infer
48
+ Draper::CollectionDecorator
49
+ else
50
+ undecorated.decorator_class
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,208 @@
1
+ require 'active_support/core_ext/array/extract_options'
2
+
3
+ module Draper
4
+ class Decorator
5
+ include Draper::ViewHelpers
6
+ include ActiveModel::Serialization if defined?(ActiveModel::Serialization)
7
+
8
+ attr_accessor :source, :options
9
+
10
+ alias_method :model, :source
11
+ alias_method :to_source, :source
12
+
13
+ # Initialize a new decorator instance by passing in
14
+ # an instance of the source class. Pass in an optional
15
+ # context inside the options hash is stored for later use.
16
+ #
17
+ # A decorator cannot be applied to other instances of the
18
+ # same decorator and will instead result in a decorator
19
+ # with the same target as the original.
20
+ # You can, however, apply several decorators in a chain but
21
+ # you will get a warning if the same decorator appears at
22
+ # multiple places in the chain.
23
+ #
24
+ # @param [Object] source object to decorate
25
+ # @param [Hash] options (optional)
26
+ def initialize(source, options = {})
27
+ source.to_a if source.respond_to?(:to_a) # forces evaluation of a lazy query from AR
28
+ @source = source
29
+ @options = options
30
+ handle_multiple_decoration if source.is_a?(Draper::Decorator)
31
+ end
32
+
33
+ class << self
34
+ alias_method :decorate, :new
35
+ end
36
+
37
+ # Adds ActiveRecord finder methods to the decorator class. The
38
+ # methods return decorated models, so that you can use
39
+ # `ProductDecorator.find(id)` instead of
40
+ # `ProductDecorator.decorate(Product.find(id))`.
41
+ #
42
+ # If the `:for` option is not supplied, the model class will be
43
+ # inferred from the decorator class.
44
+ #
45
+ # @option options [Class, Symbol] :for The model class to find
46
+ def self.has_finders(options = {})
47
+ extend Draper::Finders
48
+ self.finder_class = options[:for] || name.chomp("Decorator")
49
+ end
50
+
51
+ # Typically called within a decorator definition, this method causes
52
+ # the assocation to be decorated when it is retrieved.
53
+ #
54
+ # @param [Symbol] association name of association to decorate, like `:products`
55
+ # @option options [Class] :with the decorator to apply to the association
56
+ # @option options [Symbol] :scope a scope to apply when fetching the association
57
+ def self.decorates_association(association, options = {})
58
+ define_method(association) do
59
+ decorated_associations[association] ||= Draper::DecoratedAssociation.new(source, association, options)
60
+ decorated_associations[association].call
61
+ end
62
+ end
63
+
64
+ # A convenience method for decorating multiple associations. Calls
65
+ # decorates_association on each of the given symbols.
66
+ #
67
+ # @param [Symbols*] associations name of associations to decorate
68
+ def self.decorates_associations(*associations)
69
+ options = associations.extract_options!
70
+ associations.each do |association|
71
+ decorates_association(association, options)
72
+ end
73
+ end
74
+
75
+ # Specifies a black list of methods which may *not* be proxied to
76
+ # the wrapped object.
77
+ #
78
+ # Do not use both `.allows` and `.denies` together, either write
79
+ # a whitelist with `.allows` or a blacklist with `.denies`
80
+ #
81
+ # @param [Symbols*] methods methods to deny like `:find, :find_by_name`
82
+ def self.denies(*methods)
83
+ security.denies(*methods)
84
+ end
85
+
86
+ # Specifies that all methods may *not* be proxied to the wrapped object.
87
+ #
88
+ # Do not use `.allows` and `.denies` in combination with '.denies_all'
89
+ def self.denies_all
90
+ security.denies_all
91
+ end
92
+
93
+ # Specifies a white list of methods which *may* be proxied to
94
+ # the wrapped object. When `allows` is used, only the listed
95
+ # methods and methods defined in the decorator itself will be
96
+ # available.
97
+ #
98
+ # Do not use both `.allows` and `.denies` together, either write
99
+ # a whitelist with `.allows` or a blacklist with `.denies`
100
+ #
101
+ # @param [Symbols*] methods methods to allow like `:find, :find_by_name`
102
+ def self.allows(*methods)
103
+ security.allows(*methods)
104
+ end
105
+
106
+ # Creates a new CollectionDecorator for the given collection.
107
+ #
108
+ # @param [Object] source collection to decorate
109
+ # @param [Hash] options passed to each item's decorator (except
110
+ # for the keys listed below)
111
+ # @option options [Class,Symbol] :with (self) the class used to decorate
112
+ # items, or `:infer` to call each item's `decorate` method instead
113
+ def self.decorate_collection(source, options = {})
114
+ Draper::CollectionDecorator.new(source, options.reverse_merge(with: self))
115
+ end
116
+
117
+ # Get the chain of decorators applied to the object.
118
+ #
119
+ # @return [Array] list of decorator classes
120
+ def applied_decorators
121
+ chain = source.respond_to?(:applied_decorators) ? source.applied_decorators : []
122
+ chain << self.class
123
+ end
124
+
125
+ # Checks if a given decorator has been applied.
126
+ #
127
+ # @param [Class] decorator_class
128
+ def decorated_with?(decorator_class)
129
+ applied_decorators.include?(decorator_class)
130
+ end
131
+
132
+ def decorated?
133
+ true
134
+ end
135
+
136
+ # Delegates == to the decorated models
137
+ #
138
+ # @return [Boolean] true if other's model == self's model
139
+ def ==(other)
140
+ source == (other.respond_to?(:source) ? other.source : other)
141
+ end
142
+
143
+ def kind_of?(klass)
144
+ super || source.kind_of?(klass)
145
+ end
146
+ alias_method :is_a?, :kind_of?
147
+
148
+ def respond_to?(method, include_private = false)
149
+ super || (allow?(method) && source.respond_to?(method, include_private))
150
+ end
151
+
152
+ # We always want to delegate present, in case we decorate a nil object.
153
+ #
154
+ # I don't like the idea of decorating a nil object, but we'll deal with
155
+ # that later.
156
+ def present?
157
+ source.present?
158
+ end
159
+
160
+ def method_missing(method, *args, &block)
161
+ if allow?(method) && source.respond_to?(method)
162
+ self.class.define_proxy(method)
163
+ send(method, *args, &block)
164
+ else
165
+ super
166
+ end
167
+ end
168
+
169
+ # For ActiveModel compatibilty
170
+ def to_model
171
+ self
172
+ end
173
+
174
+ # For ActiveModel compatibility
175
+ def to_param
176
+ source.to_param
177
+ end
178
+
179
+ private
180
+
181
+ def self.define_proxy(method)
182
+ define_method(method) do |*args, &block|
183
+ source.send(method, *args, &block)
184
+ end
185
+ end
186
+
187
+ def self.security
188
+ @security ||= Security.new
189
+ end
190
+
191
+ def allow?(method)
192
+ self.class.security.allow?(method)
193
+ end
194
+
195
+ def handle_multiple_decoration
196
+ if source.instance_of?(self.class)
197
+ self.options = source.options if options.empty?
198
+ self.source = source.source
199
+ elsif source.decorated_with?(self.class)
200
+ warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
201
+ end
202
+ end
203
+
204
+ def decorated_associations
205
+ @decorated_associations ||= {}
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,44 @@
1
+ module Draper
2
+ module Finders
3
+
4
+ attr_reader :finder_class
5
+ def finder_class=(klass)
6
+ @finder_class = klass.to_s.camelize.constantize
7
+ end
8
+
9
+ def find(id, options = {})
10
+ decorate(finder_class.find(id), options)
11
+ end
12
+
13
+ def all(options = {})
14
+ decorate_collection(finder_class.all, options)
15
+ end
16
+
17
+ def first(options = {})
18
+ decorate(finder_class.first, options)
19
+ end
20
+
21
+ def last(options = {})
22
+ decorate(finder_class.last, options)
23
+ end
24
+
25
+ def method_missing(method, *args, &block)
26
+ result = finder_class.send(method, *args, &block)
27
+ options = args.extract_options!
28
+
29
+ case method.to_s
30
+ when /^find_((last_)?by_|or_(initialize|create)_by_)/
31
+ decorate(result, options)
32
+ when /^find_all_by_/
33
+ decorate_collection(result, options)
34
+ else
35
+ result
36
+ end
37
+ end
38
+
39
+ def respond_to?(method, include_private = false)
40
+ super || finder_class.respond_to?(method)
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ module Draper
2
+ class HelperProxy
3
+ # Some helpers are private, for example html_escape... as a workaround
4
+ # we are wrapping the helpers in a delegator that passes the methods
5
+ # along through a send, which will ignore private/public distinctions
6
+ def method_missing(method, *args, &block)
7
+ view_context.send(method, *args, &block)
8
+ end
9
+
10
+ private
11
+
12
+ def view_context
13
+ Draper::ViewContext.current
14
+ end
15
+ end
16
+ end
@@ -3,8 +3,8 @@ require 'rails/railtie'
3
3
  module ActiveModel
4
4
  class Railtie < Rails::Railtie
5
5
  generators do |app|
6
- Rails::Generators.configure!(app.config.generators)
7
- require "generators/resource_override"
6
+ Rails::Generators.configure! app.config.generators
7
+ require 'generators/resource_override'
8
8
  end
9
9
  end
10
10
  end
@@ -12,40 +12,36 @@ end
12
12
  module Draper
13
13
  class Railtie < Rails::Railtie
14
14
 
15
- ##
16
- # The `app/decorators` path is eager loaded
17
- #
18
- # This is the standard "Rails Way" to add paths from which constants
19
- # can be loaded.
20
- #
21
15
  config.after_initialize do |app|
22
- app.config.paths.add 'app/decorators', :eager_load => true
16
+ app.config.paths.add 'app/decorators', eager_load: true
23
17
  end
24
18
 
25
- initializer "draper.extend_action_controller_base" do |app|
26
- ActiveSupport.on_load(:action_controller) do
27
- Draper::System.setup_action_controller(self)
19
+ initializer "draper.setup_action_controller" do |app|
20
+ ActiveSupport.on_load :action_controller do
21
+ Draper.setup_action_controller self
28
22
  end
29
23
  end
30
24
 
31
- initializer "draper.extend_action_mailer_base" do |app|
32
- ActiveSupport.on_load(:action_mailer) do
33
- Draper::System.setup_action_mailer(self)
25
+ initializer "draper.setup_action_mailer" do |app|
26
+ ActiveSupport.on_load :action_mailer do
27
+ Draper.setup_action_mailer self
34
28
  end
35
29
  end
36
30
 
37
- initializer "draper.extend_active_record_base" do |app|
38
- ActiveSupport.on_load(:active_record) do
39
- self.send(:include, Draper::ModelSupport)
31
+ initializer "draper.setup_active_record" do |app|
32
+ ActiveSupport.on_load :active_record do
33
+ Draper.setup_active_record self
40
34
  end
41
35
  end
42
36
 
43
37
  console do
44
38
  require 'action_controller/test_case'
45
39
  ApplicationController.new.view_context
46
- Draper::ViewContext.current.controller.request ||= ActionController::TestRequest.new
47
- Draper::ViewContext.current.request ||= Draper::ViewContext.current.controller.request
48
- Draper::ViewContext.current.params ||= {}
40
+ Draper::ViewContext.build_view_context
41
+ end
42
+
43
+ rake_tasks do
44
+ Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
49
45
  end
50
46
  end
51
47
  end
@@ -0,0 +1,48 @@
1
+ module Draper
2
+ class Security
3
+ def initialize
4
+ @methods = []
5
+ end
6
+
7
+ def denies(*methods)
8
+ apply_strategy :denies
9
+ add_methods methods
10
+ end
11
+
12
+ def denies_all
13
+ apply_strategy :denies_all
14
+ end
15
+
16
+ def allows(*methods)
17
+ apply_strategy :allows
18
+ add_methods methods
19
+ end
20
+
21
+ def allow?(method)
22
+ case strategy
23
+ when :allows
24
+ methods.include?(method)
25
+ when :denies
26
+ !methods.include?(method)
27
+ when :denies_all
28
+ false
29
+ when nil
30
+ true
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :methods, :strategy
37
+
38
+ def apply_strategy(new_strategy)
39
+ raise ArgumentError, "Use only one of 'allows', 'denies', or 'denies_all'." if strategy && strategy != new_strategy
40
+ @strategy = new_strategy
41
+ end
42
+
43
+ def add_methods(new_methods)
44
+ raise ArgumentError, "Specify at least one method when using #{strategy}" if new_methods.empty?
45
+ @methods += new_methods.map(&:to_sym)
46
+ end
47
+ end
48
+ end