lib/active_presenter/base.rb in active_presenter-1.4.0 vs lib/active_presenter/base.rb in active_presenter-2.0.0a
- old
+ new
@@ -1,16 +1,22 @@
module ActivePresenter
# Base class for presenters. See README for usage.
#
class Base
- include ActiveSupport::Callbacks
- define_callbacks :before_validation, :before_save, :after_save
-
+ extend ActiveModel::Callbacks
+ extend ActiveModel::Naming
+ extend ActiveModel::Translation
+ include ActiveModel::MassAssignmentSecurity
+ include ActiveModel::Conversion
+
+ attr_reader :errors
+
+ define_model_callbacks :validation, :save
+
class_inheritable_accessor :presented
- class_inheritable_accessor :attr_protected, :attr_accessible
self.presented = {}
-
+
# Indicates which models are to be presented by this presenter.
# i.e.
#
# class SignupPresenter < ActivePresenter::Base
# presents :user, :account
@@ -25,75 +31,51 @@
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
-
+
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
-
+
# 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 http://gist.github.com/191263
def self.self_and_descendants_from_active_record # :nodoc:
[self]
end
-
+
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
-
- # 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.
- def self.attr_protected(*attributes)
- write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || []))
- end
-
- # Returns an array of all the attributes that have been protected from mass-assignment.
- def self.protected_attributes # :nodoc:
- read_inheritable_attribute(:attr_protected)
- end
-
- # 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.
- def self.attr_accessible(*attributes)
- write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
- end
-
- # Returns an array of all the attributes that have been made accessible to mass-assignment.
- def self.accessible_attributes # :nodoc:
- read_inheritable_attribute(:attr_accessible)
- end
-
+
# 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))
@@ -103,130 +85,121 @@
# 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
#
def initialize(args = {})
- 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
# Set the attributes of the presentable instances using
# the type_attribute form (i.e. user_login => 'james'), or
# the multiparameter attribute form (i.e. {user_birthday(1i) => "1980", user_birthday(2i) => "3"})
#
def attributes=(attrs)
return if attrs.nil?
-
- attrs = attrs.stringify_keys
+
+ attrs = attrs.stringify_keys
multi_parameter_attributes = {}
- attrs = remove_attributes_protected_from_mass_assignment(attrs)
-
+ 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
-
+
# Makes sure that the presenter is accurate about responding to presentable's attributes, even though they are handled by method_missing.
#
def respond_to?(method, include_private = false)
presented_attribute?(method) || super
end
-
+
# Handles the decision about whether to delegate getters and setters to presentable instances.
#
def method_missing(method_name, *args, &block)
presented_attribute?(method_name) ? delegate_message(method_name, *args, &block) : super
end
-
- # Returns an instance of ActiveRecord::Errors with all the errors from the presentables merged in using the type_attribute form (i.e. user_login).
- #
- def errors
- @errors ||= ActiveRecord::Errors.new(self)
- end
-
+
# Returns boolean based on the validity of the presentables by calling valid? on each of them.
#
def valid?
+ validated = false
errors.clear
- if run_callbacks_with_halt(:before_validation)
+ 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
-
- errors.empty?
+ validated = true
end
+ errors.empty? && validated
end
-
+
# Do any of the attributes have unsaved changes?
def changed?
presented_instances.map(&:changed?).any?
end
-
+
# Save all of the presentables, wrapped in a transaction.
#
# Returns true or false based on success.
#
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?
+ 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
-
- run_callbacks_with_halt(:after_save) if saved
end
-
saved
end
-
+
# Save all of the presentables wrapped in a transaction.
#
# Returns true on success, will raise otherwise.
#
def save!
- raise ActiveRecord::RecordInvalid.new(self) unless valid?
- raise ActiveRecord::RecordNotSaved unless run_callbacks_with_halt(:before_save)
-
+ saved = false
ActiveRecord::Base.transaction do
- presented.keys.select {|key| save?(key, send(key))}.each {|key| send(key).save!}
-
- run_callbacks_with_halt(:after_save)
+ 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
-
- true
+ saved
end
-
+
# Update attributes, and save the presentables
#
# Returns true or false based on success.
#
def update_attributes(attrs)
self.attributes = attrs
save
end
-
+
# Should this presented instance be saved? By default, this returns true
# Called from #save and #save!
#
# For
# class SignupPresenter < ActivePresenter::Base
@@ -242,74 +215,63 @@
# We define #id and #new_record? to play nice with form_for(@presenter) in Rails
def id # :nodoc:
nil
end
-
+
def new_record?
- true
+ presented_instances.map(&:new_record?).all?
end
+ def persisted?
+ presented_instances.map(&:persisted?).all?
+ end
+
protected
- def presented_instances
- presented.keys.map { |key| send(key) }
+
+ def presented_instances
+ presented.keys.map { |key| send(key) }
+ end
+
+ def delegate_message(method_name, *args, &block)
+ presentable = presentable_for(method_name)
+ send(presentable).send(flatten_attribute_name(method_name, presentable), *args, &block)
+ end
+
+ def presentable_for(method_name)
+ presented.keys.sort_by { |k| k.to_s.size }.reverse.detect do |type|
+ method_name.to_s.starts_with?(attribute_prefix(type))
end
-
- def delegate_message(method_name, *args, &block)
- presentable = presentable_for(method_name)
- send(presentable).send(flatten_attribute_name(method_name, presentable), *args, &block)
- end
-
- def presentable_for(method_name)
- presented.keys.sort_by { |k| k.to_s.size }.reverse.detect do |type|
- method_name.to_s.starts_with?(attribute_prefix(type))
- end
- end
-
- def presented_attribute?(method_name)
- p = presentable_for(method_name)
- !p.nil? && send(p).respond_to?(flatten_attribute_name(method_name,p))
- end
-
- def flatten_attribute_name(name, type)
- name.to_s.gsub(/^#{attribute_prefix(type)}/, '')
- end
-
- def attribute_prefix(type)
- "#{type}_"
- end
-
- def merge_errors(presented_inst, type)
- presented_inst.errors.each do |att,msg|
- if att == 'base'
- errors.add(type, msg)
- else
- errors.add(attribute_prefix(type)+att, msg)
- end
- end
- end
-
- def attribute_protected?(name)
- presentable = presentable_for(name)
- return false unless presentable
- flat_attribute = {flatten_attribute_name(name, presentable) => ''} #remove_att... normally takes a hash, so we use a ''
- presented[presentable].new.send(:remove_attributes_protected_from_mass_assignment, flat_attribute).empty?
- end
-
- def run_callbacks_with_halt(callback)
- run_callbacks(callback) { |result, object| result == false }
- end
-
- def remove_attributes_protected_from_mass_assignment(attributes)
- if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
- attributes
- elsif self.class.protected_attributes.nil?
- attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, ""))}
- elsif self.class.accessible_attributes.nil?
- attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,""))}
+ end
+
+ def presented_attribute?(method_name)
+ p = presentable_for(method_name)
+ !p.nil? && send(p).respond_to?(flatten_attribute_name(method_name,p))
+ end
+
+ def flatten_attribute_name(name, type)
+ name.to_s.gsub(/^#{attribute_prefix(type)}/, '')
+ end
+
+ def attribute_prefix(type)
+ "#{type}_"
+ end
+
+ def merge_errors(presented_inst, type)
+ presented_inst.errors.each do |att,msg|
+ if att == :base
+ errors.add(type, msg)
else
- raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
+ errors.add(attribute_prefix(type)+att.to_s, msg)
end
end
-
+ end
+
+ def attribute_protected?(name)
+ presentable = presentable_for(name)
+ return false unless presentable
+ flat_attribute = {flatten_attribute_name(name, presentable) => ''} #remove_att... normally takes a hash, so we use a ''
+ presented[presentable].new.send(:sanitize_for_mass_assignment, flat_attribute).empty?
+ end
+
end
end