Class: ActivePresenter::Base

Inherits:
Object
  • Object
show all
Includes:
ActiveSupport::Callbacks
Defined in:
lib/active_presenter/base.rb

Overview

Base class for presenters. See README for usage.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = {}) ⇒ Base

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:

1. SignupPresenter.new(:user_login => 'james', :user_password => 'swordfish', :user_password_confirmation => 'swordfish', :account_subdomain => 'giraffesoft')
  - This form is useful for initializing a new presenter from the params hash: i.e. SignupPresenter.new(params[:signup_presenter])
2. SignupPresenter.new(:user => User.find(1), :account => Account.find(2))
  - 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.

Both forms can also be mixed together: SignupPresenter.new(:user => User.find(1), :user_login => ‘james’)

In this case, the login attribute will be updated on the user instance provided.

If you don’t specify an instance, one will be created by calling Model.new



107
108
109
110
111
112
113
114
115
116
# File 'lib/active_presenter/base.rb', line 107

def initialize(args = {})
  args ||= {}
  
  presented.each do |type, klass|
    value = args.delete(type)
    send("#{type}=", value.is_a?(klass) ? value : klass.new)
  end
  
  self.attributes = args
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object

Handles the decision about whether to delegate getters and setters to presentable instances.



152
153
154
# File 'lib/active_presenter/base.rb', line 152

def method_missing(method_name, *args, &block)
  presented_attribute?(method_name) ? delegate_message(method_name, *args, &block) : super
end

Class Method Details

.accessible_attributesObject

Returns an array of all the attributes that have been made accessible to mass-assignment.



91
92
93
# File 'lib/active_presenter/base.rb', line 91

def self.accessible_attributes # :nodoc:
  read_inheritable_attribute(:attr_accessible)
end

.attr_accessible(*attributes) ⇒ Object

Note that attr_accessible is still applied to the received hash. Thus, with this technique you can at most narrow the list of accessible attributes for a particular mass-assignment call.



86
87
88
# File 'lib/active_presenter/base.rb', line 86

def self.attr_accessible(*attributes)
  write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
end

.attr_protected(*attributes) ⇒ Object

Note that attr_protected is still applied to the received hash. Thus, with this technique you can at most extend the list of protected attributes for a particular mass-assignment call.



74
75
76
# File 'lib/active_presenter/base.rb', line 74

def self.attr_protected(*attributes)
  write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || []))
end

.human_attribute_name(attribute_key_name, options = {}) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/active_presenter/base.rb', line 40

def self.human_attribute_name(attribute_key_name, options = {})
  presentable_type = presented.keys.detect do |type|
    attribute_key_name.to_s.starts_with?("#{type}_") || attribute_key_name.to_s == type.to_s
  end
  attribute_key_name_without_class = attribute_key_name.to_s.gsub("#{presentable_type}_", "")
  
  if presented[presentable_type] and attribute_key_name_without_class != presentable_type.to_s
    presented[presentable_type].human_attribute_name(attribute_key_name_without_class, options)
  else
    I18n.translate(presentable_type, options.merge(:default => presentable_type.to_s.humanize, :scope => [:activerecord, :models]))
  end
end

.human_name(options = {}) ⇒ Object

:nodoc:



63
64
65
66
67
68
69
# File 'lib/active_presenter/base.rb', line 63

def self.human_name(options = {}) # :nodoc:
  defaults = self_and_descendants_from_active_record.map do |klass|
    :"#{klass.name.underscore}"
  end 
  defaults << self.name.humanize
  I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options))
end

.presents(*types) ⇒ Object

Indicates which models are to be presented by this presenter. i.e.

class SignupPresenter < ActivePresenter::Base
  presents :user, :account
end

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:

class PresenterWithTwoAddresses < ActivePresenter::Base
  presents :primary_address => Address, :secondary_address => Address
end


25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/active_presenter/base.rb', line 25

def self.presents(*types)
  types_and_classes = types.extract_options!
  types.each { |t| types_and_classes[t] = t.to_s.tableize.classify.constantize }

  attr_accessor *types_and_classes.keys
  
  types_and_classes.keys.each do |t|
    define_method("#{t}_errors") do
      send(t).errors
    end
    
    presented[t] = types_and_classes[t]
  end
end

.protected_attributesObject

Returns an array of all the attributes that have been protected from mass-assignment.



79
80
81
# File 'lib/active_presenter/base.rb', line 79

def self.protected_attributes # :nodoc:
  read_inheritable_attribute(:attr_protected)
end

.self_and_descendants_from_active_recordObject

Since ActivePresenter does not descend from ActiveRecord, we need to mimic some ActiveRecord behavior in order for the ActiveRecord::Errors object we’re using to work properly.

This problem was introduced with Rails 2.3.4. Fix courtesy gist.github.com/191263



59
60
61
# File 'lib/active_presenter/base.rb', line 59

def self.self_and_descendants_from_active_record # :nodoc:
  [self]
end

Instance Method Details

#attributes=(attrs) ⇒ Object

Set the attributes of the presentable instances using the type_attribute form (i.e. user_login => ‘james’), or the multiparameter attribute form (i.e. => “1980”, user_birthday(2i) => “3”)



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/active_presenter/base.rb', line 122

def attributes=(attrs)
  return if attrs.nil?
  
  attrs = attrs.stringify_keys
  multi_parameter_attributes = {}
  attrs = remove_attributes_protected_from_mass_assignment(attrs)
  
  attrs.each do |k,v|
    if (base_attribute = k.to_s.split("(").first) != k.to_s
      presentable = presentable_for(base_attribute)
      multi_parameter_attributes[presentable] ||= {}
      multi_parameter_attributes[presentable].merge!(flatten_attribute_name(k,presentable).to_sym => v)
    else
      send("#{k}=", v) unless attribute_protected?(k)
    end
  end
  
  multi_parameter_attributes.each do |presentable,multi_attrs|
    send(presentable).send(:attributes=, multi_attrs)
  end
end

#changed?Boolean

Do any of the attributes have unsaved changes?

Returns:

  • (Boolean)


179
180
181
# File 'lib/active_presenter/base.rb', line 179

def changed?
  presented_instances.map(&:changed?).any?
end

#errorsObject

Returns an instance of ActiveRecord::Errors with all the errors from the presentables merged in using the type_attribute form (i.e. user_login).



158
159
160
# File 'lib/active_presenter/base.rb', line 158

def errors
  @errors ||= ActiveRecord::Errors.new(self)
end

#idObject

We define #id and #new_record? to play nice with form_for(@presenter) in Rails



244
245
246
# File 'lib/active_presenter/base.rb', line 244

def id # :nodoc:
  nil
end

#new_record?Boolean

Returns:

  • (Boolean)


248
249
250
# File 'lib/active_presenter/base.rb', line 248

def new_record?
  true
end

#respond_to?(method, include_private = false) ⇒ Boolean

Makes sure that the presenter is accurate about responding to presentable’s attributes, even though they are handled by method_missing.

Returns:

  • (Boolean)


146
147
148
# File 'lib/active_presenter/base.rb', line 146

def respond_to?(method, include_private = false)
  presented_attribute?(method) || super
end

#saveObject

Save all of the presentables, wrapped in a transaction.

Returns true or false based on success.



187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/active_presenter/base.rb', line 187

def save
  saved = false
  
  ActiveRecord::Base.transaction do
    if valid? && run_callbacks_with_halt(:before_save)
      saved = presented.keys.select {|key| save?(key, send(key))}.all? {|key| send(key).save}
      raise ActiveRecord::Rollback unless saved # TODO: Does this happen implicitly?
    end

    run_callbacks_with_halt(:after_save) if saved
  end
  
  saved
end

#save!Object

Save all of the presentables wrapped in a transaction.

Returns true on success, will raise otherwise.

Raises:

  • (ActiveRecord::RecordInvalid)


206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/active_presenter/base.rb', line 206

def save!
  raise ActiveRecord::RecordInvalid.new(self) unless valid?
  raise ActiveRecord::RecordNotSaved unless run_callbacks_with_halt(:before_save)
  
  ActiveRecord::Base.transaction do
    presented.keys.select {|key| save?(key, send(key))}.each {|key| send(key).save!}

    run_callbacks_with_halt(:after_save)
  end

  true
end

#save?(presented_key, presented_instance) ⇒ Boolean

Should this presented instance be saved? By default, this returns true Called from #save and #save!

For

class SignupPresenter < ActivePresenter::Base
  presents :account, :user
end

#save? will be called twice:

save?(:account, #<Account:0x1234dead>)
save?(:user, #<User:0xdeadbeef>)

Returns:

  • (Boolean)


239
240
241
# File 'lib/active_presenter/base.rb', line 239

def save?(presented_key, presented_instance)
  true
end

#update_attributes(attrs) ⇒ Object

Update attributes, and save the presentables

Returns true or false based on success.



223
224
225
226
# File 'lib/active_presenter/base.rb', line 223

def update_attributes(attrs)
  self.attributes = attrs
  save
end

#valid?Boolean

Returns boolean based on the validity of the presentables by calling valid? on each of them.

Returns:

  • (Boolean)


164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/active_presenter/base.rb', line 164

def valid?
  errors.clear
  if run_callbacks_with_halt(:before_validation)
    presented.keys.each do |type|
      presented_inst = send(type)

      next unless save?(type, presented_inst)
      merge_errors(presented_inst, type) unless presented_inst.valid?
    end

    errors.empty?
  end
end