Class: ActivePresenter::Base

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Callbacks, ActiveModel::Naming, ActiveModel::Translation
Includes:
ActiveModel::Conversion, ActiveModel::MassAssignmentSecurity
Defined in:
lib/active_presenter/base.rb

Overview

Base class for presenters. See README for usage.

Instance Attribute Summary collapse

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



90
91
92
93
94
95
96
97
98
# File 'lib/active_presenter/base.rb', line 90

def initialize(args = {})
  @errors = ActiveModel::Errors.new(self)
  return self unless 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.



134
135
136
# File 'lib/active_presenter/base.rb', line 134

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

Instance Attribute Details

#errorsObject (readonly)

Returns the value of attribute errors.



11
12
13
# File 'lib/active_presenter/base.rb', line 11

def errors
  @errors
end

Class Method Details

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



47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/active_presenter/base.rb', line 47

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:



70
71
72
73
74
75
76
# File 'lib/active_presenter/base.rb', line 70

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


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/active_presenter/base.rb', line 31

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

    # We must reassign in derrived classes rather than mutating the attribute in Base
    self.presented = self.presented.merge(t => types_and_classes[t])
  end
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



66
67
68
# File 'lib/active_presenter/base.rb', line 66

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”)



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/active_presenter/base.rb', line 104

def attributes=(attrs)
  return if attrs.nil?

  attrs = attrs.stringify_keys      
  multi_parameter_attributes = {}
  attrs = sanitize_for_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)


155
156
157
# File 'lib/active_presenter/base.rb', line 155

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

#idObject

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



218
219
220
# File 'lib/active_presenter/base.rb', line 218

def id # :nodoc:
  nil
end

#new_record?Boolean

Returns:

  • (Boolean)


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

def new_record?
  presented_instances.map(&:new_record?).all?
end

#persisted?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'lib/active_presenter/base.rb', line 226

def persisted?
  presented_instances.map(&:persisted?).all?
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)


128
129
130
# File 'lib/active_presenter/base.rb', line 128

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.



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

def save
  saved = false
  ActiveRecord::Base.transaction do
    if valid?
      _run_save_callbacks do
        saved = presented.keys.select {|key| save?(key, send(key))}.all? {|key| send(key).save}
        raise ActiveRecord::Rollback unless saved
      end
    end
  end
  saved
end

#save!Object

Save all of the presentables wrapped in a transaction.

Returns true on success, will raise otherwise.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/active_presenter/base.rb', line 180

def save!
  saved = false
  ActiveRecord::Base.transaction do
    raise ActiveRecord::RecordInvalid.new(self) unless valid?
    _run_save_callbacks do
      presented.keys.select {|key| save?(key, send(key))}.all? {|key| send(key).save!}
      saved = true
    end
    raise ActiveRecord::RecordNotSaved.new(self) unless saved
  end
  saved
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)


213
214
215
# File 'lib/active_presenter/base.rb', line 213

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.



197
198
199
200
# File 'lib/active_presenter/base.rb', line 197

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)


140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/active_presenter/base.rb', line 140

def valid?
  validated = false
  errors.clear
  result = _run_validation_callbacks do
    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
    validated = true
  end
  errors.empty? && validated
end