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
@@ -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