draper 0.18.0 → 1.0.0.beta1

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 (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
@@ -1,27 +0,0 @@
1
- module Draper::ActiveModelSupport
2
- module Proxies
3
- def self.extended(base)
4
- # These methods (as keys) will be created only if the correspondent
5
- # model responds to the method
6
- proxies = [:to_param, :errors, :id]
7
-
8
- proxies.each do |method_name|
9
- if base.model.respond_to?(method_name)
10
- base.singleton_class.class_eval do
11
- if !base.class.instance_methods.include?(method_name) || base.class.instance_method(method_name).owner === Draper::Base
12
- define_method(method_name) do |*args, &block|
13
- model.send(method_name, *args, &block)
14
- end
15
- end
16
- end
17
- end
18
- end
19
-
20
- base.class_eval do
21
- def to_model
22
- self
23
- end
24
- end
25
- end
26
- end
27
- end
data/lib/draper/base.rb DELETED
@@ -1,312 +0,0 @@
1
- module Draper
2
- class Base
3
- require 'active_support/core_ext/class/attribute'
4
- require 'active_support/core_ext/array/extract_options'
5
-
6
- class_attribute :denied, :allowed, :model_class
7
- attr_accessor :model, :options
8
-
9
- DEFAULT_DENIED = Object.instance_methods << :method_missing
10
- DEFAULT_ALLOWED = []
11
- self.denied = DEFAULT_DENIED
12
- self.allowed = DEFAULT_ALLOWED
13
-
14
- # Initialize a new decorator instance by passing in
15
- # an instance of the source class. Pass in an optional
16
- # context inside the options hash is stored for later use.
17
- #
18
- # @param [Object] instance to wrap
19
- # @param [Hash] options (optional)
20
- def initialize(input, options = {})
21
- input.to_a if input.respond_to?(:to_a) # forces evaluation of a lazy query from AR
22
- self.class.model_class = input.class if model_class.nil?
23
- @model = input.kind_of?(Draper::Base) ? input.model : input
24
- self.options = options
25
- self.extend Draper::ActiveModelSupport::Proxies
26
- end
27
-
28
- # Proxies to the class specified by `decorates` to automatically
29
- # lookup an object in the database and decorate it.
30
- #
31
- # @param [Symbol or String] id to lookup
32
- # @return [Object] instance of this decorator class
33
- def self.find(input, options = {})
34
- self.new(model_class.find(input), options)
35
- end
36
-
37
- # Typically called within a decorator definition, this method
38
- # specifies the name of the wrapped object class.
39
- #
40
- # For instance, a `ProductDecorator` class might call `decorates :product`
41
- #
42
- # But they don't have to match in name, so a `EmployeeDecorator`
43
- # class could call `decorates :person` to wrap instances of `Person`
44
- #
45
- # This is primarilly set so the `.find` method knows which class
46
- # to query.
47
- #
48
- # @param [Symbol] class_name snakecase name of the decorated class, like `:product`
49
- def self.decorates(input, options = {})
50
- self.model_class = options[:class] || options[:class_name] || input.to_s.camelize
51
- self.model_class = model_class.constantize if model_class.respond_to?(:constantize)
52
- model_class.send :include, Draper::ModelSupport
53
- define_method(input){ @model }
54
- end
55
-
56
- # Typically called within a decorator definition, this method causes
57
- # the assocation to be decorated when it is retrieved.
58
- #
59
- # @param [Symbol] name of association to decorate, like `:products`
60
- # @option options [Hash] :with The decorator to decorate the association with
61
- # :scope The scope to apply to the association
62
- def self.decorates_association(association_symbol, options = {})
63
- define_method(association_symbol) do
64
- orig_association = model.send(association_symbol)
65
-
66
- return orig_association if orig_association.nil?
67
- return decorated_associations[association_symbol] if decorated_associations[association_symbol]
68
-
69
- orig_association = orig_association.send(options[:scope]) if options[:scope]
70
-
71
- return options[:with].decorate(orig_association) if options[:with]
72
-
73
- klass = if options[:polymorphic]
74
- orig_association.class
75
- elsif association_reflection = find_association_reflection(association_symbol)
76
- association_reflection.klass
77
- elsif orig_association.respond_to?(:first)
78
- orig_association.first.class
79
- else
80
- orig_association.class
81
- end
82
-
83
- decorated_associations[association_symbol] = "#{klass}Decorator".constantize.decorate(orig_association, options)
84
- end
85
- end
86
-
87
- # A convenience method for decorating multiple associations. Calls
88
- # decorates_association on each of the given symbols.
89
- #
90
- # @param [Symbols*] name of associations to decorate
91
- def self.decorates_associations(*association_symbols)
92
- options = association_symbols.extract_options!
93
- association_symbols.each{ |sym| decorates_association(sym, options) }
94
- end
95
-
96
- # Specifies a black list of methods which may *not* be proxied to
97
- # to the wrapped object.
98
- #
99
- # Do not use both `.allows` and `.denies` together, either write
100
- # a whitelist with `.allows` or a blacklist with `.denies`
101
- #
102
- # @param [Symbols*] methods to deny like `:find, :find_by_name`
103
- def self.denies(*input_denied)
104
- raise ArgumentError, "Specify at least one method (as a symbol) to exclude when using denies" if input_denied.empty?
105
- raise ArgumentError, "Use either 'allows' or 'denies', but not both." unless (self.allowed == DEFAULT_ALLOWED)
106
- self.denied += input_denied
107
- end
108
-
109
- # Specifies that all methods may *not* be proxied to
110
- # to the wrapped object.
111
- #
112
- # Do not use `.allows` and `.denies` in combination with '.denies_all'
113
- def self.denies_all
114
- raise ArgumentError, "Use either 'allows' or 'denies', but not both." unless ((self.allowed == DEFAULT_ALLOWED && self.denied == DEFAULT_DENIED) || (self.allowed != DEFAULT_ALLOWED && self.denied != DEFAULT_DENIED))
115
- self.denied += [nil] # Add dummy value to denied to prevent calls to #allows. Hacky???
116
- self.allowed += [nil] # Add dummy value to allowed to prevent calls to #denies
117
- end
118
-
119
- # Specifies a white list of methods which *may* be proxied to
120
- # to the wrapped object. When `allows` is used, only the listed
121
- # methods and methods defined in the decorator itself will be
122
- # available.
123
- #
124
- # Do not use both `.allows` and `.denies` together, either write
125
- # a whitelist with `.allows` or a blacklist with `.denies`
126
- #
127
- # @param [Symbols*] methods to allow like `:find, :find_by_name`
128
- def self.allows(*input_allows)
129
- raise ArgumentError, "Specify at least one method (as a symbol) to allow when using allows" if input_allows.empty?
130
- raise ArgumentError, "Use either 'allows' or 'denies', but not both." unless (self.denied == DEFAULT_DENIED)
131
- self.allowed += input_allows
132
- end
133
-
134
- # Initialize a new decorator instance by passing in
135
- # an instance of the source class. Pass in an optional
136
- # context into the options hash is stored for later use.
137
- #
138
- # When passing in a single object, using `.decorate` is
139
- # identical to calling `.new`. However, `.decorate` can
140
- # also accept a collection and return a collection of
141
- # individually decorated objects.
142
- #
143
- # @param [Object] instance(s) to wrap
144
- # @param [Hash] options (optional)
145
- # @option options [Boolean] :infer If true, each model will be
146
- # wrapped by its inferred decorator.
147
- def self.decorate(input, options = {})
148
- if input.instance_of?(self)
149
- input.options = options unless options.empty?
150
- return input
151
- elsif input.respond_to?(:each) && !input.is_a?(Struct) && (!defined?(Sequel) || !input.is_a?(Sequel::Model))
152
- Draper::DecoratedEnumerableProxy.new(input, self, options)
153
- elsif options[:infer]
154
- input.decorator(options)
155
- else
156
- new(input, options)
157
- end
158
- end
159
-
160
- # Fetch all instances of the decorated class and decorate them.
161
- #
162
- # @param [Hash] options (optional)
163
- # @return [Draper::DecoratedEnumerableProxy]
164
- def self.all(options = {})
165
- Draper::DecoratedEnumerableProxy.new(model_class.all, self, options)
166
- end
167
-
168
- def self.first(options = {})
169
- decorate(model_class.first, options)
170
- end
171
-
172
- def self.last(options = {})
173
- decorate(model_class.last, options)
174
- end
175
-
176
- # Some helpers are private, for example html_escape... as a workaround
177
- # we are wrapping the helpers in a delegator that passes the methods
178
- # along through a send, which will ignore private/public distinctions
179
- class HelpersWrapper
180
- def initialize(helpers)
181
- @helpers = helpers
182
- end
183
-
184
- def method_missing(method, *args, &block)
185
- @helpers.send(method, *args, &block)
186
- end
187
-
188
- #needed for tests
189
- def ==(other)
190
- other.instance_variable_get(:@helpers) == @helpers
191
- end
192
- end
193
-
194
- # Access the helpers proxy to call built-in and user-defined
195
- # Rails helpers. Aliased to `.h` for convenience.
196
- #
197
- # @return [Object] proxy
198
- def helpers
199
- HelpersWrapper.new self.class.helpers
200
- end
201
- alias :h :helpers
202
-
203
- # Localize is something that's used quite often. Even though
204
- # it's available through helpers, that's annoying. Aliased
205
- # to `.l` for convenience.
206
- def localize(object, options = {})
207
- self.class.helpers.localize(object, options)
208
- end
209
- alias :l :localize
210
-
211
- # Access the helpers proxy to call built-in and user-defined
212
- # Rails helpers from a class context.
213
- #
214
- # @return [Object] proxy
215
- class << self
216
- def helpers
217
- Draper::ViewContext.current
218
- end
219
- alias :h :helpers
220
- end
221
-
222
- # Fetch the original wrapped model.
223
- #
224
- # @return [Object] original_model
225
- def wrapped_object
226
- @model
227
- end
228
-
229
- # Delegates == to the decorated models
230
- #
231
- # @return [Boolean] true if other's model == self's model
232
- def ==(other)
233
- @model == (other.respond_to?(:model) ? other.model : other)
234
- end
235
-
236
- def kind_of?(klass)
237
- super || model.kind_of?(klass)
238
- end
239
- alias :is_a? :kind_of?
240
-
241
- def respond_to?(method, include_private = false)
242
- super || (allow?(method) && model.respond_to?(method, include_private))
243
- end
244
-
245
- # We always want to delegate present, in case we decorate a nil object.
246
- #
247
- # I don't like the idea of decorating a nil object, but we'll deal with
248
- # that later.
249
- def present?
250
- model.present?
251
- end
252
-
253
- def method_missing(method, *args, &block)
254
- super unless allow?(method)
255
-
256
- if model.respond_to?(method)
257
- self.class.send :define_method, method do |*args, &blokk|
258
- model.send method, *args, &blokk
259
- end
260
-
261
- send method, *args, &block
262
- else
263
- super
264
- end
265
-
266
- rescue NoMethodError => no_method_error
267
- super if no_method_error.name == method
268
- raise no_method_error
269
- end
270
-
271
- def self.method_missing(method, *args, &block)
272
- if method.to_s.match(/^find_((all_|last_)?by_|or_(initialize|create)_by_).*/)
273
- self.decorate(model_class.send(method, *args, &block), :context => args.dup.extract_options!)
274
- else
275
- model_class.send(method, *args, &block)
276
- end
277
- end
278
-
279
- def self.respond_to?(method, include_private = false)
280
- super || model_class.respond_to?(method)
281
- end
282
-
283
- def context
284
- options.fetch(:context, {})
285
- end
286
-
287
- def context=(input)
288
- options[:context] = input
289
- end
290
-
291
- def source
292
- model
293
- end
294
- alias_method :to_source, :model
295
-
296
- private
297
-
298
- def allow?(method)
299
- (allowed.empty? || allowed.include?(method)) && !denied.include?(method)
300
- end
301
-
302
- def find_association_reflection(association)
303
- if model.class.respond_to?(:reflect_on_association)
304
- model.class.reflect_on_association(association)
305
- end
306
- end
307
-
308
- def decorated_associations
309
- @decorated_associations ||= {}
310
- end
311
- end
312
- end
@@ -1,90 +0,0 @@
1
- require 'active_support/core_ext/object/blank'
2
- module Draper
3
- class DecoratedEnumerableProxy
4
- include Enumerable
5
-
6
- delegate :as_json, :collect, :map, :each, :[], :all?, :include?, :first, :last, :shift, :in_groups_of, :to => :decorated_collection
7
-
8
- # Initialize a new collection decorator instance by passing in
9
- # an instance of a collection. Pass in an optional
10
- # context into the options hash is stored for later use.
11
- #
12
- #
13
- # @param [Object] instances to wrap
14
- # @param [Hash] options (optional)
15
- # @option options [Class] :class The decorator class to use
16
- # for each item in the collection.
17
- # @option options all other options are passed to Decorator
18
- # class for each item.
19
-
20
- def self.decorate(collection, options = {})
21
- new( collection, discern_class_from_my_class(options.delete(:class)), options)
22
- end
23
- class << self
24
- alias_method :decorates, :decorate
25
- end
26
-
27
- def initialize(collection, klass, options = {})
28
- @wrapped_collection, @klass, @options = collection, klass, options
29
- end
30
-
31
- def decorated_collection
32
- @decorated_collection ||= @wrapped_collection.collect { |member| @klass.decorate(member, @options) }
33
- end
34
- alias_method :to_ary, :decorated_collection
35
-
36
- def find(ifnone_or_id = nil, &blk)
37
- if block_given?
38
- source.find(ifnone_or_id, &blk)
39
- else
40
- obj = decorated_collection.first
41
- return nil if obj.blank?
42
- obj.class.find(ifnone_or_id)
43
- end
44
- end
45
-
46
- def method_missing (method, *args, &block)
47
- @wrapped_collection.send(method, *args, &block)
48
- end
49
-
50
- def respond_to?(method, include_private = false)
51
- super || @wrapped_collection.respond_to?(method, include_private)
52
- end
53
-
54
- def kind_of?(klass)
55
- @wrapped_collection.kind_of?(klass) || super
56
- end
57
- alias :is_a? :kind_of?
58
-
59
- def ==(other)
60
- @wrapped_collection == (other.respond_to?(:source) ? other.source : other)
61
- end
62
-
63
- def to_s
64
- "#<DecoratedEnumerableProxy of #{@klass} for #{@wrapped_collection.inspect}>"
65
- end
66
-
67
- def context=(input)
68
- self.map { |member| member.context = input }
69
- end
70
-
71
- def source
72
- @wrapped_collection
73
- end
74
- alias_method :to_source, :source
75
-
76
- def helpers
77
- Draper::ViewContext.current
78
- end
79
- alias_method :h, :helpers
80
-
81
- private
82
- def self.discern_class_from_my_class default_class
83
- return default_class if default_class
84
- name = self.to_s.gsub("Decorator", "")
85
- "#{name.singularize}Decorator".constantize
86
- rescue NameError
87
- raise NameError("You must supply a class (as the klass option) for the members of your collection or the class must be inferable from the name of this class ('#{new.class}')")
88
- end
89
- end
90
- end
@@ -1,25 +0,0 @@
1
- module Draper::ModelSupport
2
- extend ActiveSupport::Concern
3
-
4
- def decorator(options = {})
5
- @decorator ||= decorator_class.decorate(self, options.merge(:infer => false))
6
- block_given? ? yield(@decorator) : @decorator
7
- end
8
-
9
- def decorator_class
10
- "#{self.class.name}Decorator".constantize
11
- end
12
-
13
- alias :decorate :decorator
14
-
15
- module ClassMethods
16
- def decorate(options = {})
17
- decorator_proxy = decorator_class.decorate(self.scoped, options)
18
- block_given? ? yield(decorator_proxy) : decorator_proxy
19
- end
20
-
21
- def decorator_class
22
- "#{model_name}Decorator".constantize
23
- end
24
- end
25
- end