galetahub-active_presenter 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +86 -0
- data/Rakefile +24 -0
- data/lib/active_presenter.rb +7 -0
- data/lib/active_presenter/base.rb +282 -0
- data/lib/active_presenter/version.rb +4 -0
- data/test/base_test.rb +362 -0
- data/test/test_helper.rb +218 -0
- metadata +87 -0
data/README.rdoc
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
= ActivePresenter
|
2
|
+
|
3
|
+
ActivePresenter is the presenter library you already know! (...if you know ActiveRecord)
|
4
|
+
|
5
|
+
By acting nearly identically to ActiveRecord models, ActivePresenter makes presenters highly approachable to anybody who is already familiar with ActiveRecord.
|
6
|
+
|
7
|
+
== Get It
|
8
|
+
|
9
|
+
As a gem (Gemfile):
|
10
|
+
|
11
|
+
gem 'active_presenter', :git => 'git://github.com/galetahub/active_presenter.git'
|
12
|
+
|
13
|
+
As a rails plugin:
|
14
|
+
|
15
|
+
rails install plugin git://github.com/galetahub/active_presenter.git
|
16
|
+
|
17
|
+
Or get the source from github:
|
18
|
+
|
19
|
+
$ git clone git://github.com/galetahub/active_presenter.git
|
20
|
+
|
21
|
+
== Usage
|
22
|
+
|
23
|
+
Creating a presenter is as simple as subclassing ActivePresenter::Base. Use the presents method to indicate which models the presenter should present.
|
24
|
+
|
25
|
+
class SignupPresenter < ActivePresenter::Base
|
26
|
+
presents :user, :account
|
27
|
+
end
|
28
|
+
|
29
|
+
In the above example, :user will (predictably) become User. If you want to override this behaviour, specify the desired types in a hash, as so:
|
30
|
+
|
31
|
+
class PresenterWithTwoAddresses < ActivePresenter::Base
|
32
|
+
presents :primary_address => Address, :secondary_address => Address
|
33
|
+
end
|
34
|
+
|
35
|
+
=== Instantiation
|
36
|
+
|
37
|
+
Then, you can instantiate the presenter using either, or both of two forms.
|
38
|
+
|
39
|
+
For example, if you had a SignupPresenter that presented User, and Account, you could specify arguments in the following two forms:
|
40
|
+
|
41
|
+
1. SignupPresenter.new(:user_login => 'james', :user_password => 'swordfish', :user_password_confirmation => 'swordfish', :account_subdomain => 'giraffesoft')
|
42
|
+
|
43
|
+
- This form is useful for initializing a new presenter from the params hash: i.e. SignupPresenter.new(params[:signup_presenter])
|
44
|
+
|
45
|
+
2. SignupPresenter.new(:user => User.find(1), :account => Account.find(2))
|
46
|
+
|
47
|
+
- This form is useful if you have instances that you'd like to edit using the presenter. You can subsequently call presenter.update_attributes(params[:signup_presenter]) just like with a regular AR instance.
|
48
|
+
|
49
|
+
Both forms can also be mixed together: SignupPresenter.new(:user => User.find(1), :user_login => 'james').
|
50
|
+
|
51
|
+
In this case, the login attribute will be updated on the user instance provided.
|
52
|
+
|
53
|
+
If you don't specify an instance, one will be created by calling Model.new
|
54
|
+
|
55
|
+
=== Validation
|
56
|
+
|
57
|
+
The #valid? method will return true or false based on the validity of the presented objects.
|
58
|
+
|
59
|
+
This is calculated by calling #valid? on them.
|
60
|
+
|
61
|
+
You can retrieve the errors in two ways.
|
62
|
+
|
63
|
+
1. By calling #errors on the presenter, which returns an instance of ActiveRecord::Errors where all the attributes are in type_name_attribute_name form (i.e. You'd retrieve an error on User#login, by calling @presenter.errors.on(:user_login)).
|
64
|
+
|
65
|
+
2. By calling @presenter.user_errors, or @presenter.user.errors to retrieve the errors from one presentable.
|
66
|
+
|
67
|
+
Both of these methods are compatible with error_messages_for. It just depends whether you'd like to show all the errors in one block, or whether you'd prefer to break them up.
|
68
|
+
|
69
|
+
=== Saving
|
70
|
+
|
71
|
+
You can save your presenter the same way you'd save an ActiveRecord object. Both #save, and #save! behave the same way they do on a normal AR model.
|
72
|
+
|
73
|
+
=== Callbacks
|
74
|
+
|
75
|
+
Callbacks work exactly like ActiveRecord callbacks. before_save, and after_save are available.
|
76
|
+
|
77
|
+
Note that if any of your after_save callbacks return false, the rest of them will not be run. This is consistent with AR behavior.
|
78
|
+
|
79
|
+
== Credits
|
80
|
+
|
81
|
+
ActivePresenter was created, and is maintained by {Daniel Haran}[http://danielharan.com] and {James Golick}[http://jamesgolick.com] on the train ride to {RubyFringe}[http://rubyfringe.com] from Montreal.
|
82
|
+
|
83
|
+
== License
|
84
|
+
|
85
|
+
ActivePresenter is available under the {MIT License}[http://en.wikipedia.org/wiki/MIT_License]
|
86
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc 'Test the active_presenter plugin.'
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.libs << 'test'
|
13
|
+
t.pattern = 'test/**/*_test.rb'
|
14
|
+
t.verbose = true
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generate documentation for the active_presenter plugin.'
|
18
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
19
|
+
rdoc.rdoc_dir = 'rdoc'
|
20
|
+
rdoc.title = 'ActivePresenter'
|
21
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
22
|
+
rdoc.rdoc_files.include('README')
|
23
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
24
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
module ActivePresenter
|
2
|
+
# Base class for presenters. See README for usage.
|
3
|
+
#
|
4
|
+
class Base
|
5
|
+
extend ActiveModel::Callbacks
|
6
|
+
extend ActiveModel::Naming
|
7
|
+
extend ActiveModel::Translation
|
8
|
+
|
9
|
+
include ActiveModel::Conversion
|
10
|
+
include ActiveModel::MassAssignmentSecurity
|
11
|
+
|
12
|
+
attr_reader :errors
|
13
|
+
|
14
|
+
define_model_callbacks :validation, :save
|
15
|
+
|
16
|
+
class_inheritable_accessor :presented
|
17
|
+
self.presented = {}
|
18
|
+
|
19
|
+
# Indicates which models are to be presented by this presenter.
|
20
|
+
# i.e.
|
21
|
+
#
|
22
|
+
# class SignupPresenter < ActivePresenter::Base
|
23
|
+
# presents :user, :account
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# In the above example, :user will (predictably) become User. If you want to override this behaviour, specify the desired types in a hash, as so:
|
27
|
+
#
|
28
|
+
# class PresenterWithTwoAddresses < ActivePresenter::Base
|
29
|
+
# presents :primary_address => Address, :secondary_address => Address
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
class << self
|
33
|
+
def presents(*types)
|
34
|
+
types_and_classes = types.extract_options!
|
35
|
+
types.each { |t| types_and_classes[t] = t.to_s.tableize.classify.constantize }
|
36
|
+
|
37
|
+
attr_accessor *types_and_classes.keys
|
38
|
+
|
39
|
+
types_and_classes.keys.each do |t|
|
40
|
+
define_method("#{t}_errors") do
|
41
|
+
send(t).errors
|
42
|
+
end
|
43
|
+
|
44
|
+
presented[t] = types_and_classes[t]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def human_attribute_name(attribute_key_name, options = {})
|
49
|
+
presentable_type = presented.keys.detect do |type|
|
50
|
+
attribute_key_name.to_s.starts_with?("#{type}_") || attribute_key_name.to_s == type.to_s
|
51
|
+
end
|
52
|
+
attribute_key_name_without_class = attribute_key_name.to_s.gsub("#{presentable_type}_", "")
|
53
|
+
|
54
|
+
if presented[presentable_type] and attribute_key_name_without_class != presentable_type.to_s
|
55
|
+
presented[presentable_type].human_attribute_name(attribute_key_name_without_class, options)
|
56
|
+
else
|
57
|
+
I18n.translate(presentable_type, options.merge(:default => presentable_type.to_s.humanize, :scope => [:activerecord, :models]))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Since ActivePresenter does not descend from ActiveRecord, we need to
|
62
|
+
# mimic some ActiveRecord behavior in order for the ActiveRecord::Errors
|
63
|
+
# object we're using to work properly.
|
64
|
+
#
|
65
|
+
# This problem was introduced with Rails 2.3.4.
|
66
|
+
# Fix courtesy http://gist.github.com/191263
|
67
|
+
def self_and_descendants_from_active_record # :nodoc:
|
68
|
+
[self]
|
69
|
+
end
|
70
|
+
|
71
|
+
def human_name(options = {}) # :nodoc:
|
72
|
+
defaults = self_and_descendants_from_active_record.map do |klass|
|
73
|
+
:"#{klass.name.underscore}"
|
74
|
+
end
|
75
|
+
defaults << self.name.humanize
|
76
|
+
I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Accepts arguments in two forms. For example, if you had a SignupPresenter that presented User, and Account, you could specify arguments in the following two forms:
|
81
|
+
#
|
82
|
+
# 1. SignupPresenter.new(:user_login => 'james', :user_password => 'swordfish', :user_password_confirmation => 'swordfish', :account_subdomain => 'giraffesoft')
|
83
|
+
# - This form is useful for initializing a new presenter from the params hash: i.e. SignupPresenter.new(params[:signup_presenter])
|
84
|
+
# 2. SignupPresenter.new(:user => User.find(1), :account => Account.find(2))
|
85
|
+
# - This form is useful if you have instances that you'd like to edit using the presenter. You can subsequently call presenter.update_attributes(params[:signup_presenter]) just like with a regular AR instance.
|
86
|
+
#
|
87
|
+
# Both forms can also be mixed together: SignupPresenter.new(:user => User.find(1), :user_login => 'james')
|
88
|
+
# In this case, the login attribute will be updated on the user instance provided.
|
89
|
+
#
|
90
|
+
# If you don't specify an instance, one will be created by calling Model.new
|
91
|
+
#
|
92
|
+
def initialize(args = {})
|
93
|
+
@errors = ActiveModel::Errors.new(self)
|
94
|
+
return self unless args
|
95
|
+
presented.each do |type, klass|
|
96
|
+
value = args.delete(type)
|
97
|
+
send("#{type}=", value.is_a?(klass) ? value : klass.new)
|
98
|
+
end
|
99
|
+
self.attributes = args
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set the attributes of the presentable instances using
|
103
|
+
# the type_attribute form (i.e. user_login => 'james'), or
|
104
|
+
# the multiparameter attribute form (i.e. {user_birthday(1i) => "1980", user_birthday(2i) => "3"})
|
105
|
+
#
|
106
|
+
def attributes=(attrs)
|
107
|
+
return if attrs.nil?
|
108
|
+
|
109
|
+
attrs = attrs.stringify_keys
|
110
|
+
multi_parameter_attributes = {}
|
111
|
+
|
112
|
+
sanitize_for_mass_assignment(attrs).each do |k,v|
|
113
|
+
if (base_attribute = k.to_s.split("(").first) != k.to_s
|
114
|
+
presentable = presentable_for(base_attribute)
|
115
|
+
multi_parameter_attributes[presentable] ||= {}
|
116
|
+
multi_parameter_attributes[presentable].merge!(flatten_attribute_name(k,presentable).to_sym => v)
|
117
|
+
else
|
118
|
+
send("#{k}=", v) unless attribute_protected?(k)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
multi_parameter_attributes.each do |presentable,multi_attrs|
|
123
|
+
send(presentable).send(:attributes=, multi_attrs)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Makes sure that the presenter is accurate about responding to presentable's attributes, even though they are handled by method_missing.
|
128
|
+
#
|
129
|
+
def respond_to?(method, include_private = false)
|
130
|
+
presented_attribute?(method) || super
|
131
|
+
end
|
132
|
+
|
133
|
+
# Handles the decision about whether to delegate getters and setters to presentable instances.
|
134
|
+
#
|
135
|
+
def method_missing(method_name, *args, &block)
|
136
|
+
presented_attribute?(method_name) ? delegate_message(method_name, *args, &block) : super
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns boolean based on the validity of the presentables by calling valid? on each of them.
|
140
|
+
#
|
141
|
+
def valid?
|
142
|
+
validated = false
|
143
|
+
errors.clear
|
144
|
+
result = _run_validation_callbacks do
|
145
|
+
presented.each do |type, klass|
|
146
|
+
presented_inst = (send(type) || klass.new)
|
147
|
+
next unless save?(type, presented_inst)
|
148
|
+
merge_errors(presented_inst, type) unless presented_inst.valid?
|
149
|
+
end
|
150
|
+
validated = true
|
151
|
+
end
|
152
|
+
errors.empty? && validated
|
153
|
+
end
|
154
|
+
|
155
|
+
# Do any of the attributes have unsaved changes?
|
156
|
+
def changed?
|
157
|
+
presented_instances.map(&:changed?).any?
|
158
|
+
end
|
159
|
+
|
160
|
+
# Save all of the presentables, wrapped in a transaction.
|
161
|
+
#
|
162
|
+
# Returns true or false based on success.
|
163
|
+
#
|
164
|
+
def save(options={})
|
165
|
+
saved = false
|
166
|
+
ActiveRecord::Base.transaction do
|
167
|
+
if !perform_validations?(options) || (perform_validations?(options) && valid?)
|
168
|
+
_run_save_callbacks do
|
169
|
+
saved = presented.keys.select {|key| save?(key, send(key))}.all? {|key| send(key).save}
|
170
|
+
raise ActiveRecord::Rollback unless saved
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
saved
|
175
|
+
end
|
176
|
+
|
177
|
+
# Save all of the presentables wrapped in a transaction.
|
178
|
+
#
|
179
|
+
# Returns true on success, will raise otherwise.
|
180
|
+
#
|
181
|
+
def save!(options={})
|
182
|
+
saved = false
|
183
|
+
ActiveRecord::Base.transaction do
|
184
|
+
raise ActiveRecord::RecordInvalid.new(self) if perform_validations?(options) && !valid?
|
185
|
+
_run_save_callbacks do
|
186
|
+
presented.keys.select {|key| save?(key, send(key))}.all? {|key| send(key).save!}
|
187
|
+
saved = true
|
188
|
+
end
|
189
|
+
raise ActiveRecord::RecordNotSaved.new(self) unless saved
|
190
|
+
end
|
191
|
+
saved
|
192
|
+
end
|
193
|
+
|
194
|
+
# Update attributes, and save the presentables
|
195
|
+
#
|
196
|
+
# Returns true or false based on success.
|
197
|
+
#
|
198
|
+
def update_attributes(attrs)
|
199
|
+
self.attributes = attrs
|
200
|
+
save
|
201
|
+
end
|
202
|
+
|
203
|
+
# Should this presented instance be saved? By default, this returns true
|
204
|
+
# Called from #save and #save!
|
205
|
+
#
|
206
|
+
# For
|
207
|
+
# class SignupPresenter < ActivePresenter::Base
|
208
|
+
# presents :account, :user
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# #save? will be called twice:
|
212
|
+
# save?(:account, #<Account:0x1234dead>)
|
213
|
+
# save?(:user, #<User:0xdeadbeef>)
|
214
|
+
def save?(presented_key, presented_instance)
|
215
|
+
true
|
216
|
+
end
|
217
|
+
|
218
|
+
# We define #id and #new_record? to play nice with form_for(@presenter) in Rails
|
219
|
+
def id # :nodoc:
|
220
|
+
nil
|
221
|
+
end
|
222
|
+
|
223
|
+
def new_record?
|
224
|
+
presented_instances.map(&:new_record?).all?
|
225
|
+
end
|
226
|
+
|
227
|
+
def persisted?
|
228
|
+
presented_instances.map(&:persisted?).all?
|
229
|
+
end
|
230
|
+
|
231
|
+
protected
|
232
|
+
def presented_instances
|
233
|
+
presented.keys.map { |key| send(key) }
|
234
|
+
end
|
235
|
+
|
236
|
+
def delegate_message(method_name, *args, &block)
|
237
|
+
presentable = presentable_for(method_name)
|
238
|
+
send(presentable).send(flatten_attribute_name(method_name, presentable), *args, &block)
|
239
|
+
end
|
240
|
+
|
241
|
+
def presentable_for(method_name)
|
242
|
+
presented.keys.sort_by { |k| k.to_s.size }.reverse.detect do |type|
|
243
|
+
method_name.to_s.starts_with?(attribute_prefix(type))
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def presented_attribute?(method_name)
|
248
|
+
p = presentable_for(method_name)
|
249
|
+
!p.nil? && send(p).respond_to?(flatten_attribute_name(method_name,p))
|
250
|
+
end
|
251
|
+
|
252
|
+
def flatten_attribute_name(name, type)
|
253
|
+
name.to_s.gsub(/^#{attribute_prefix(type)}/, '')
|
254
|
+
end
|
255
|
+
|
256
|
+
def attribute_prefix(type)
|
257
|
+
"#{type}_"
|
258
|
+
end
|
259
|
+
|
260
|
+
def merge_errors(presented_inst, type)
|
261
|
+
presented_inst.errors.each do |att,msg|
|
262
|
+
if att == :base
|
263
|
+
errors.add(type, msg)
|
264
|
+
else
|
265
|
+
errors.add(attribute_prefix(type)+att.to_s, msg)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def attribute_protected?(name)
|
271
|
+
presentable = presentable_for(name)
|
272
|
+
return false unless presentable
|
273
|
+
#remove_att... normally takes a hash, so we use a ''
|
274
|
+
flat_attribute = {flatten_attribute_name(name, presentable) => ''}
|
275
|
+
presented[presentable].new.send(:sanitize_for_mass_assignment, flat_attribute).empty?
|
276
|
+
end
|
277
|
+
|
278
|
+
def perform_validations?(options={})
|
279
|
+
options[:validate] != false
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
data/test/base_test.rb
ADDED
@@ -0,0 +1,362 @@
|
|
1
|
+
require File.dirname(__FILE__)+'/test_helper'
|
2
|
+
|
3
|
+
Expectations do
|
4
|
+
expect nil do
|
5
|
+
SignupPresenter.new.id
|
6
|
+
end
|
7
|
+
|
8
|
+
expect true do
|
9
|
+
SignupPresenter.new.new_record?
|
10
|
+
end
|
11
|
+
|
12
|
+
expect :user => User, :account => Account do
|
13
|
+
SignupPresenter.presented
|
14
|
+
end
|
15
|
+
|
16
|
+
expect User.create!(hash_for_user) do |u|
|
17
|
+
SignupPresenter.new(:user => u.expected).user
|
18
|
+
end
|
19
|
+
|
20
|
+
expect true do
|
21
|
+
SignupPresenter.new(:user => nil).user.new_record?
|
22
|
+
end
|
23
|
+
|
24
|
+
expect User do
|
25
|
+
SignupPresenter.new.user
|
26
|
+
end
|
27
|
+
|
28
|
+
expect User.any_instance.to.receive(:login=).with('james') do
|
29
|
+
SignupPresenter.new(:user_login => 'james')
|
30
|
+
end
|
31
|
+
|
32
|
+
# admin= should be protected from mass assignment
|
33
|
+
expect SignupPresenter.new.to.be.attribute_protected?(:user_admin)
|
34
|
+
expect SignupPresenter.new(:user_admin => true).user.not.to.be.admin?
|
35
|
+
|
36
|
+
expect 'mymockvalue' do
|
37
|
+
User.any_instance.stubs(:login).returns('mymockvalue')
|
38
|
+
SignupPresenter.new.user_login
|
39
|
+
end
|
40
|
+
|
41
|
+
expect User.any_instance.to.receive(:login=).with('mymockvalue') do
|
42
|
+
SignupPresenter.new.user_login = 'mymockvalue'
|
43
|
+
end
|
44
|
+
|
45
|
+
expect SignupPresenter.new.not.to.be.valid?
|
46
|
+
expect SignupPresenter.new(:user => User.new(hash_for_user)).to.be.valid?
|
47
|
+
|
48
|
+
expect ActiveRecord::Errors do
|
49
|
+
s = SignupPresenter.new
|
50
|
+
s.valid?
|
51
|
+
s.errors
|
52
|
+
end
|
53
|
+
|
54
|
+
expect ActiveRecord::Errors do
|
55
|
+
s = SignupPresenter.new
|
56
|
+
s.valid?
|
57
|
+
s.user_errors
|
58
|
+
end
|
59
|
+
|
60
|
+
expect ActiveRecord::Errors do
|
61
|
+
s = SignupPresenter.new
|
62
|
+
s.valid?
|
63
|
+
s.account_errors
|
64
|
+
end
|
65
|
+
|
66
|
+
expect String do
|
67
|
+
s = SignupPresenter.new
|
68
|
+
s.valid?
|
69
|
+
s.errors.on(:user_login)
|
70
|
+
end
|
71
|
+
|
72
|
+
expect "can't be blank" do
|
73
|
+
s = SignupPresenter.new
|
74
|
+
s.valid?
|
75
|
+
s.errors.on(:user_login)
|
76
|
+
end
|
77
|
+
|
78
|
+
expect ["User Password can't be blank"] do
|
79
|
+
s = SignupPresenter.new(:user_login => 'login')
|
80
|
+
s.valid?
|
81
|
+
s.errors.full_messages
|
82
|
+
end
|
83
|
+
|
84
|
+
expect 'c4N n07 83 8L4nK' do
|
85
|
+
old_locale = I18n.locale
|
86
|
+
I18n.locale = '1337'
|
87
|
+
|
88
|
+
s = SignupPresenter.new(:user_login => nil)
|
89
|
+
s.valid?
|
90
|
+
message = s.errors.on(:user_login)
|
91
|
+
|
92
|
+
I18n.locale = old_locale
|
93
|
+
|
94
|
+
message
|
95
|
+
end
|
96
|
+
|
97
|
+
expect ['U53R pa22w0rD c4N n07 83 8L4nK'] do
|
98
|
+
old_locale = I18n.locale
|
99
|
+
I18n.locale = '1337'
|
100
|
+
|
101
|
+
s = SignupPresenter.new(:user_login => 'login')
|
102
|
+
s.valid?
|
103
|
+
message = s.errors.full_messages
|
104
|
+
|
105
|
+
I18n.locale = old_locale
|
106
|
+
|
107
|
+
message
|
108
|
+
end
|
109
|
+
|
110
|
+
expect ActiveRecord::Base.to.receive(:transaction) do
|
111
|
+
s = SignupPresenter.new
|
112
|
+
s.save
|
113
|
+
end
|
114
|
+
|
115
|
+
expect User.any_instance.to.receive(:save) do
|
116
|
+
s = SignupPresenter.new :user => User.new(hash_for_user)
|
117
|
+
s.save
|
118
|
+
end
|
119
|
+
|
120
|
+
expect Account.any_instance.to.receive(:save) do
|
121
|
+
s = SignupPresenter.new :user => User.new(hash_for_user)
|
122
|
+
s.save
|
123
|
+
end
|
124
|
+
|
125
|
+
expect SignupPresenter.new.not.to.be.save
|
126
|
+
|
127
|
+
expect ActiveRecord::Rollback do
|
128
|
+
ActiveRecord::Base.stubs(:transaction).yields
|
129
|
+
User.any_instance.stubs(:save).returns(false)
|
130
|
+
Account.any_instance.stubs(:save).returns(false)
|
131
|
+
s = SignupPresenter.new :user => User.new(hash_for_user)
|
132
|
+
s.save
|
133
|
+
end
|
134
|
+
|
135
|
+
expect ActiveRecord::Base.to.receive(:transaction) do
|
136
|
+
s = SignupPresenter.new(:user_login => "da", :user_password => "seekrit")
|
137
|
+
s.save!
|
138
|
+
end
|
139
|
+
|
140
|
+
expect User.any_instance.to.receive(:save!) do
|
141
|
+
s = SignupPresenter.new(:user_login => "da", :user_password => "seekrit")
|
142
|
+
s.save!
|
143
|
+
end
|
144
|
+
|
145
|
+
expect Account.any_instance.to.receive(:save!) do
|
146
|
+
User.any_instance.stubs(:save!)
|
147
|
+
s = SignupPresenter.new(:user_login => "da", :user_password => "seekrit")
|
148
|
+
s.save!
|
149
|
+
end
|
150
|
+
|
151
|
+
expect ActiveRecord::RecordInvalid do
|
152
|
+
SignupPresenter.new.save!
|
153
|
+
end
|
154
|
+
|
155
|
+
expect SignupPresenter.new(:user => User.new(hash_for_user)).to.be.save!
|
156
|
+
|
157
|
+
expect SignupPresenter.new.to.be.respond_to?(:user_login)
|
158
|
+
expect SignupPresenter.new.to.be.respond_to?(:user_password_confirmation)
|
159
|
+
expect SignupPresenter.new.to.be.respond_to?(:valid?) # just making sure i didn't break everything :)
|
160
|
+
expect SignupPresenter.new.to.be.respond_to?(:nil?, false) # making sure it's possible to pass 2 arguments
|
161
|
+
|
162
|
+
expect User.create!(hash_for_user).not.to.be.login_changed? do |user|
|
163
|
+
s = SignupPresenter.new(:user => user)
|
164
|
+
s.update_attributes :user_login => 'Something Totally Different'
|
165
|
+
end
|
166
|
+
|
167
|
+
expect SignupPresenter.new(:user => User.create!(hash_for_user)).to.receive(:save) do |s|
|
168
|
+
s.update_attributes :user_login => 'Something'
|
169
|
+
end
|
170
|
+
|
171
|
+
expect 'Something Different' do
|
172
|
+
s = SignupPresenter.new
|
173
|
+
s.update_attributes :user_login => 'Something Different'
|
174
|
+
s.user_login
|
175
|
+
end
|
176
|
+
|
177
|
+
# Multiparameter assignment
|
178
|
+
expect Time.parse('March 27 1980 9:30:59 am') do
|
179
|
+
s = SignupPresenter.new
|
180
|
+
s.update_attributes({
|
181
|
+
:"user_birthday(1i)" => '1980',
|
182
|
+
:"user_birthday(2i)" => '3',
|
183
|
+
:"user_birthday(3i)" => '27',
|
184
|
+
:"user_birthday(4i)" => '9',
|
185
|
+
:"user_birthday(5i)" => '30',
|
186
|
+
:"user_birthday(6i)" => '59'
|
187
|
+
})
|
188
|
+
s.user_birthday
|
189
|
+
end
|
190
|
+
|
191
|
+
expect nil do
|
192
|
+
s = SignupPresenter.new
|
193
|
+
s.attributes = nil
|
194
|
+
end
|
195
|
+
|
196
|
+
# this is a regression test to make sure that _title is working. we had a weird conflict with using String#delete
|
197
|
+
expect 'something' do
|
198
|
+
s = SignupPresenter.new :account_title => 'something'
|
199
|
+
s.account_title
|
200
|
+
end
|
201
|
+
|
202
|
+
expect String do
|
203
|
+
s = SignupPresenter.new
|
204
|
+
s.save
|
205
|
+
s.errors.on(:user_login)
|
206
|
+
end
|
207
|
+
|
208
|
+
expect String do
|
209
|
+
s = SignupPresenter.new
|
210
|
+
s.save! rescue
|
211
|
+
s.errors.on(:user_login)
|
212
|
+
end
|
213
|
+
|
214
|
+
expect 'Login' do
|
215
|
+
SignupPresenter.human_attribute_name(:user_login)
|
216
|
+
end
|
217
|
+
|
218
|
+
# it was raising with nil
|
219
|
+
expect SignupPresenter do
|
220
|
+
SignupPresenter.new(nil)
|
221
|
+
end
|
222
|
+
|
223
|
+
expect EndingWithSPresenter.new.address.not.to.be.nil?
|
224
|
+
|
225
|
+
# this should act as ActiveRecord models do
|
226
|
+
expect NoMethodError do
|
227
|
+
SignupPresenter.new({:i_dont_exist=>"blah"})
|
228
|
+
end
|
229
|
+
|
230
|
+
# ActiveRecord::Base uses nil id to signify an unsaved model
|
231
|
+
expect nil do
|
232
|
+
SignupPresenter.new.id
|
233
|
+
end
|
234
|
+
|
235
|
+
expect nil do
|
236
|
+
returning(SignupPresenter.new(:user => User.new(hash_for_user))) do |presenter|
|
237
|
+
presenter.save!
|
238
|
+
end.id
|
239
|
+
end
|
240
|
+
|
241
|
+
expect CantSavePresenter.new.not.to.be.save # it won't save because the filter chain will halt
|
242
|
+
|
243
|
+
expect ActiveRecord::RecordNotSaved do
|
244
|
+
CantSavePresenter.new.save!
|
245
|
+
end
|
246
|
+
|
247
|
+
expect 'Some Street' do
|
248
|
+
p = AfterSavePresenter.new
|
249
|
+
p.save
|
250
|
+
p.address.street
|
251
|
+
end
|
252
|
+
|
253
|
+
expect 'Some Street' do
|
254
|
+
p = AfterSavePresenter.new
|
255
|
+
p.save!
|
256
|
+
p.address.street
|
257
|
+
end
|
258
|
+
|
259
|
+
expect SamePrefixPresenter.new.to.be.respond_to?(:account_title)
|
260
|
+
expect SamePrefixPresenter.new.to.be.respond_to?(:account_info_info)
|
261
|
+
|
262
|
+
expect [:before_validation, :before_save, :after_save] do
|
263
|
+
returning(CallbackOrderingPresenter.new) do |presenter|
|
264
|
+
presenter.save!
|
265
|
+
end.steps
|
266
|
+
end
|
267
|
+
|
268
|
+
expect [:before_validation, :before_save] do
|
269
|
+
returning(CallbackCantSavePresenter.new) do |presenter|
|
270
|
+
presenter.save
|
271
|
+
end.steps
|
272
|
+
end
|
273
|
+
|
274
|
+
expect [:before_validation, :before_save] do
|
275
|
+
returning(CallbackCantSavePresenter.new) do |presenter|
|
276
|
+
begin
|
277
|
+
presenter.save!
|
278
|
+
rescue ActiveRecord::RecordNotSaved
|
279
|
+
# NOP
|
280
|
+
end
|
281
|
+
end.steps
|
282
|
+
end
|
283
|
+
|
284
|
+
expect ActiveRecord::RecordNotSaved do
|
285
|
+
CallbackCantSavePresenter.new.save!
|
286
|
+
end
|
287
|
+
|
288
|
+
expect ActiveRecord::RecordInvalid do
|
289
|
+
CallbackCantValidatePresenter.new.save!
|
290
|
+
end
|
291
|
+
|
292
|
+
expect [:before_validation] do
|
293
|
+
returning(CallbackCantValidatePresenter.new) do |presenter|
|
294
|
+
begin
|
295
|
+
presenter.save!
|
296
|
+
rescue ActiveRecord::RecordInvalid
|
297
|
+
# NOP
|
298
|
+
end
|
299
|
+
end.steps
|
300
|
+
end
|
301
|
+
|
302
|
+
expect [:before_validation] do
|
303
|
+
returning(CallbackCantValidatePresenter.new) do |presenter|
|
304
|
+
presenter.save
|
305
|
+
end.steps
|
306
|
+
end
|
307
|
+
|
308
|
+
expect ActiveRecord::Errors.any_instance.to.receive(:clear).twice do
|
309
|
+
CallbackCantValidatePresenter.new.valid?
|
310
|
+
end
|
311
|
+
|
312
|
+
# this should act as ActiveRecord models do
|
313
|
+
expect NoMethodError do
|
314
|
+
SignupPresenter.new({:i_dont_exist=>"blah"})
|
315
|
+
end
|
316
|
+
|
317
|
+
expect false do
|
318
|
+
SignupNoAccountPresenter.new.save
|
319
|
+
end
|
320
|
+
|
321
|
+
expect true do
|
322
|
+
SignupNoAccountPresenter.new(:user => User.new(hash_for_user), :account => nil).save
|
323
|
+
end
|
324
|
+
|
325
|
+
expect true do
|
326
|
+
SignupNoAccountPresenter.new(:user => User.new(hash_for_user), :account => nil).save!
|
327
|
+
end
|
328
|
+
|
329
|
+
expect Address do
|
330
|
+
PresenterWithTwoAddresses.new.secondary_address
|
331
|
+
end
|
332
|
+
|
333
|
+
expect "123 awesome st" do
|
334
|
+
p = PresenterWithTwoAddresses.new(:secondary_address_street => "123 awesome st")
|
335
|
+
p.save
|
336
|
+
p.secondary_address_street
|
337
|
+
end
|
338
|
+
|
339
|
+
# attr_protected
|
340
|
+
expect "" do
|
341
|
+
p = SignupPresenter.new(:account_secret => 'swordfish')
|
342
|
+
p.account.secret
|
343
|
+
end
|
344
|
+
|
345
|
+
expect "comment" do
|
346
|
+
p = HistoricalPresenter.new(:history_comment => 'comment', :user => User.new(hash_for_user))
|
347
|
+
p.save
|
348
|
+
p.history_comment
|
349
|
+
end
|
350
|
+
|
351
|
+
expect false do
|
352
|
+
SignupPresenter.new.changed?
|
353
|
+
end
|
354
|
+
|
355
|
+
expect true do
|
356
|
+
p = SignupPresenter.new(:user => User.new(hash_for_user))
|
357
|
+
p.save
|
358
|
+
p.user_login = 'something_else'
|
359
|
+
p.changed?
|
360
|
+
end
|
361
|
+
|
362
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
require File.dirname(__FILE__)+'/../lib/active_presenter' unless defined?(ActivePresenter)
|
2
|
+
require 'expectations'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
ActiveRecord::Base.configurations = {'sqlite3' => {:adapter => 'sqlite3', :database => ':memory:'}}
|
6
|
+
ActiveRecord::Base.establish_connection('sqlite3')
|
7
|
+
|
8
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
9
|
+
ActiveRecord::Base.logger.level = Logger::WARN
|
10
|
+
|
11
|
+
I18n.backend.store_translations '1337',
|
12
|
+
:activerecord => {
|
13
|
+
:models => {
|
14
|
+
:user => 'U53R'
|
15
|
+
},
|
16
|
+
:attributes => {
|
17
|
+
:user => {:password => 'pa22w0rD'}
|
18
|
+
},
|
19
|
+
:errors => {
|
20
|
+
:messages => {
|
21
|
+
:blank => 'c4N n07 83 8L4nK'
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
ActiveRecord::Schema.define(:version => 0) do
|
27
|
+
create_table :users do |t|
|
28
|
+
t.boolean :admin, :default => false
|
29
|
+
t.string :login, :default => ''
|
30
|
+
t.string :password, :default => ''
|
31
|
+
t.datetime :birthday
|
32
|
+
end
|
33
|
+
|
34
|
+
create_table :accounts do |t|
|
35
|
+
t.string :subdomain, :default => ''
|
36
|
+
t.string :title, :default => ''
|
37
|
+
t.string :secret, :default => ''
|
38
|
+
end
|
39
|
+
|
40
|
+
create_table :addresses do |t|
|
41
|
+
t.string :street
|
42
|
+
end
|
43
|
+
|
44
|
+
create_table :account_infos do |t|
|
45
|
+
t.string :info
|
46
|
+
end
|
47
|
+
|
48
|
+
create_table :histories do |t|
|
49
|
+
t.integer :user_id
|
50
|
+
t.string :comment, :default => ''
|
51
|
+
t.string :action, :default => ''
|
52
|
+
t.datetime :created_at
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
class User < ActiveRecord::Base
|
58
|
+
validates_presence_of :login
|
59
|
+
validate :presence_of_password
|
60
|
+
attr_accessible :login, :password, :birthday
|
61
|
+
attr_accessor :password_confirmation
|
62
|
+
|
63
|
+
def presence_of_password
|
64
|
+
if password.blank?
|
65
|
+
attribute_name = I18n.t(:password, {:default => "Password", :scope => [:activerecord, :attributes, :user]})
|
66
|
+
error_message = I18n.t(:blank, {:default => "can't be blank", :scope => [:activerecord, :errors, :messages]})
|
67
|
+
errors.add_to_base("#{attribute_name} #{error_message}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
class Account < ActiveRecord::Base; end
|
72
|
+
class History < ActiveRecord::Base; end
|
73
|
+
class Address < ActiveRecord::Base; end
|
74
|
+
class AccountInfo < ActiveRecord::Base; end
|
75
|
+
|
76
|
+
class PresenterWithTwoAddresses < ActivePresenter::Base
|
77
|
+
presents :address, :secondary_address => Address
|
78
|
+
end
|
79
|
+
|
80
|
+
class SignupPresenter < ActivePresenter::Base
|
81
|
+
presents :account, :user
|
82
|
+
attr_protected :account_secret
|
83
|
+
end
|
84
|
+
|
85
|
+
class EndingWithSPresenter < ActivePresenter::Base
|
86
|
+
presents :address
|
87
|
+
end
|
88
|
+
|
89
|
+
class HistoricalPresenter < ActivePresenter::Base
|
90
|
+
presents :user, :history
|
91
|
+
attr_accessible :history_comment
|
92
|
+
end
|
93
|
+
|
94
|
+
class CantSavePresenter < ActivePresenter::Base
|
95
|
+
presents :address
|
96
|
+
|
97
|
+
before_save :halt
|
98
|
+
|
99
|
+
def halt; false; end
|
100
|
+
end
|
101
|
+
|
102
|
+
class SignupNoAccountPresenter < ActivePresenter::Base
|
103
|
+
presents :account, :user
|
104
|
+
|
105
|
+
def save?(key, instance)
|
106
|
+
key != :account
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class AfterSavePresenter < ActivePresenter::Base
|
111
|
+
presents :address
|
112
|
+
|
113
|
+
after_save :set_street
|
114
|
+
|
115
|
+
def set_street
|
116
|
+
address.street = 'Some Street'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class SamePrefixPresenter < ActivePresenter::Base
|
121
|
+
presents :account, :account_info
|
122
|
+
end
|
123
|
+
|
124
|
+
class CallbackOrderingPresenter < ActivePresenter::Base
|
125
|
+
presents :account
|
126
|
+
|
127
|
+
before_validation :do_before_validation
|
128
|
+
before_save :do_before_save
|
129
|
+
after_save :do_after_save
|
130
|
+
|
131
|
+
attr_reader :steps
|
132
|
+
|
133
|
+
def initialize(params={})
|
134
|
+
super
|
135
|
+
@steps = []
|
136
|
+
end
|
137
|
+
|
138
|
+
def do_before_validation
|
139
|
+
@steps << :before_validation
|
140
|
+
end
|
141
|
+
|
142
|
+
def do_before_save
|
143
|
+
@steps << :before_save
|
144
|
+
end
|
145
|
+
|
146
|
+
def do_after_save
|
147
|
+
@steps << :after_save
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class CallbackCantSavePresenter < ActivePresenter::Base
|
152
|
+
presents :account
|
153
|
+
|
154
|
+
before_validation :do_before_validation
|
155
|
+
before_save :do_before_save
|
156
|
+
before_save :halt
|
157
|
+
after_save :do_after_save
|
158
|
+
|
159
|
+
attr_reader :steps
|
160
|
+
|
161
|
+
def initialize(params={})
|
162
|
+
super
|
163
|
+
@steps = []
|
164
|
+
end
|
165
|
+
|
166
|
+
def do_before_validation
|
167
|
+
@steps << :before_validation
|
168
|
+
end
|
169
|
+
|
170
|
+
def do_before_save
|
171
|
+
@steps << :before_save
|
172
|
+
end
|
173
|
+
|
174
|
+
def do_after_save
|
175
|
+
@steps << :after_save
|
176
|
+
end
|
177
|
+
|
178
|
+
def halt
|
179
|
+
false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class CallbackCantValidatePresenter < ActivePresenter::Base
|
184
|
+
presents :account
|
185
|
+
|
186
|
+
before_validation :do_before_validation
|
187
|
+
before_validation :halt
|
188
|
+
before_save :do_before_save
|
189
|
+
after_save :do_after_save
|
190
|
+
|
191
|
+
attr_reader :steps
|
192
|
+
|
193
|
+
def initialize(params={})
|
194
|
+
super
|
195
|
+
@steps = []
|
196
|
+
end
|
197
|
+
|
198
|
+
def do_before_validation
|
199
|
+
@steps << :before_validation
|
200
|
+
end
|
201
|
+
|
202
|
+
def do_before_save
|
203
|
+
@steps << :before_save
|
204
|
+
end
|
205
|
+
|
206
|
+
def do_after_save
|
207
|
+
@steps << :after_save
|
208
|
+
end
|
209
|
+
|
210
|
+
def halt
|
211
|
+
false
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def hash_for_user(opts = {})
|
216
|
+
{:login => 'jane', :password => 'seekrit' }.merge(opts)
|
217
|
+
end
|
218
|
+
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: galetahub-active_presenter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- James Golick
|
14
|
+
- Daniel Haran
|
15
|
+
- Igor Galeta
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2011-10-20 00:00:00 Z
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: activerecord
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
description: ActivePresenter is the presenter library you already know! (...if you know ActiveRecord)
|
37
|
+
email: galeta.igor@gmail.com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- lib/active_presenter/base.rb
|
46
|
+
- lib/active_presenter/version.rb
|
47
|
+
- lib/active_presenter.rb
|
48
|
+
- Rakefile
|
49
|
+
- README.rdoc
|
50
|
+
- test/base_test.rb
|
51
|
+
- test/test_helper.rb
|
52
|
+
homepage: https://github.com/galetahub/active_presenter
|
53
|
+
licenses: []
|
54
|
+
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project: active_presenter
|
81
|
+
rubygems_version: 1.8.11
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: ActivePresenter is the presenter library
|
85
|
+
test_files:
|
86
|
+
- test/base_test.rb
|
87
|
+
- test/test_helper.rb
|