Browse files

Merge branch 'rails3'

Conflicts:
	lib/active_presenter/base.rb
	lib/active_presenter/version.rb
	test/base_test.rb
	test/test_helper.rb
  • Loading branch information...
2 parents 5b49575 + c2d26cb commit 2173ee4079a55d58a27a64f4cc2ec027c6e4d987 @jamesgolick committed Aug 27, 2012
Showing with 216 additions and 182 deletions.
  1. +2 −1 .gitignore
  2. +3 −0 Gemfile
  3. +12 −1 README.rdoc
  4. +2 −2 Rakefile
  5. +41 −0 active_presenter.gemspec
  6. +99 −137 lib/active_presenter/base.rb
  7. +4 −4 lib/active_presenter/version.rb
  8. +2 −2 lib/tasks/gem.rake
  9. +28 −29 test/base_test.rb
  10. +10 −0 test/lint_test.rb
  11. +13 −6 test/test_helper.rb
View
3 .gitignore
@@ -1,2 +1,3 @@
+Gemfile.lock
rdoc
-pkg
+pkg
View
3 Gemfile
@@ -0,0 +1,3 @@
+source :rubygems
+
+gemspec
View
13 README.rdoc 100644 → 100755
@@ -28,7 +28,7 @@ Creating a presenter is as simple as subclassing ActivePresenter::Base. Use the
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:
+In the above example, :user will (predictably) become User. If you want to override this behavior, specify the desired types in a hash, as so:
class PresenterWithTwoAddresses < ActivePresenter::Base
presents :primary_address => Address, :secondary_address => Address
@@ -68,6 +68,15 @@ You can retrieve the errors in two ways.
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.
+=== Protected and Accessible Attributes
+
+ActivePresenter supports +attr_protected+ and +attr_accessible+ just like an ActiveRecord object to avoid mass assignment. This can be leveraged to provide an additional layer of protection at the presenter level.
+
+ class AccountPresenter < ActivePresenter::Base
+ presents :user, :profile
+ attr_accessible :user_email, :profile_birthday
+ end
+
=== Saving
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.
@@ -82,6 +91,8 @@ Note that if any of your after_save callbacks return false, the rest of them wil
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.
+ActivePresenter for Rails 3 is currently maintained by {Josh Martin}[http://github.com/skiz] and {Johnno Loggie}[http://github.com/johnno]
+
== License
ActivePresenter is available under the {MIT License}[http://en.wikipedia.org/wiki/MIT_License]
View
4 Rakefile
@@ -1,10 +1,10 @@
require 'rake'
-require 'rake/rdoctask'
+require 'rdoc/task'
require File.dirname(__FILE__)+'/lib/active_presenter'
Dir.glob(File.dirname(__FILE__)+'/lib/tasks/**/*.rake').each { |l| load l }
task :default => :test
task :test do
- Dir['test/**/*_test.rb'].each { |l| require l }
+ Dir['test/**/*_test.rb'].each { |l| require File.join(File.dirname(__FILE__),l)}
end
View
41 active_presenter.gemspec
@@ -0,0 +1,41 @@
+Gem::Specification.new do |s|
+ s.name = %q{active_presenter}
+ s.version = "3.2.2"
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["James Golick", "Daniel Haran", "Josh Martin", "Johnno Loggie"]
+ s.date = %q{2012-03-26}
+ s.extra_rdoc_files = [
+ "LICENSE",
+ "README.rdoc"
+ ]
+ s.files = [
+ "LICENSE",
+ "Gemfile",
+ "README",
+ "README.rdoc",
+ "Rakefile",
+ "TODO",
+ "active_presenter.gemspec",
+ "lib/active_presenter.rb",
+ "lib/active_presenter/base.rb",
+ "lib/active_presenter/version.rb",
+ "lib/tasks/doc.rake",
+ "lib/tasks/gem.rake",
+ "test/test_helper.rb",
+ "test/base_test.rb",
+ "test/lint_test.rb"
+ ]
+ s.homepage = %q{http://github.com/jamesgolick/active_presenter}
+ s.rdoc_options = ["--charset=UTF-8"]
+ s.require_paths = ["lib"]
+ s.rubygems_version = %q{1.3.7}
+ s.summary = %q{The presenter library you already know.}
+ s.test_files = [
+ "test/base_test.rb",
+ "test/lint_test.rb",
+ "test/test_helper.rb"
+ ]
+ s.add_runtime_dependency(%q<activerecord>, [">= 3.0.10"])
+ s.add_development_dependency(%q<expectations>, [">= 2.0.0"])
+ s.add_development_dependency(%q<sqlite3>, [">= 1.3.5"])
+end
View
236 lib/active_presenter/base.rb
@@ -2,13 +2,19 @@ module ActivePresenter
# Base class for presenters. See README for usage.
#
class Base
- include ActiveSupport::Callbacks
- define_callbacks :before_validation, :before_save, :after_save
-
- class_inheritable_accessor :presented
- class_inheritable_accessor :attr_protected, :attr_accessible
+ extend ActiveModel::Callbacks
+ extend ActiveModel::Naming
+ extend ActiveModel::Translation
+ include ActiveModel::MassAssignmentSecurity
+ include ActiveModel::Conversion
+
+ attr_reader :errors
+
+ define_model_callbacks :validation, :save
+
+ class_attribute :presented
self.presented = {}
-
+
# Indicates which models are to be presented by this presenter.
# i.e.
#
@@ -27,29 +33,30 @@ def self.presents(*types)
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]
+
+ # 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
-
+
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.
@@ -59,39 +66,15 @@ def self.human_attribute_name(attribute_key_name, options = {})
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')
@@ -105,13 +88,12 @@ def self.accessible_attributes # :nodoc:
# 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
@@ -121,11 +103,11 @@ def initialize(args = {})
#
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)
@@ -135,87 +117,79 @@ def attributes=(attrs)
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.
@@ -224,7 +198,7 @@ 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!
#
@@ -246,70 +220,58 @@ def id # :nodoc:
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) }
- 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 }
+
+ 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 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
View
8 lib/active_presenter/version.rb
@@ -1,9 +1,9 @@
module ActivePresenter
module VERSION
- MAJOR = 1
- MINOR = 4
- TINY = 0
-
+ MAJOR = 3
+ MINOR = 2
+ TINY = 2
+
STRING = [MAJOR, MINOR, TINY].join('.')
end
end
View
4 lib/tasks/gem.rake
@@ -1,4 +1,4 @@
-require 'rake/gempackagetask'
+require 'rubygems/package_task'
task :clean => :clobber_package
@@ -21,7 +21,7 @@ spec = Gem::Specification.new do |s|
s.require_path = "lib"
end
-Rake::GemPackageTask.new(spec) do |p|
+Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
View
57 test/base_test.rb
@@ -45,34 +45,34 @@
expect SignupPresenter.new.not.to.be.valid?
expect SignupPresenter.new(:user => User.new(hash_for_user)).to.be.valid?
- expect ActiveRecord::Errors do
+ expect ActiveModel::Errors do
s = SignupPresenter.new
s.valid?
s.errors
end
- expect ActiveRecord::Errors do
+ expect ActiveModel::Errors do
s = SignupPresenter.new
s.valid?
s.user_errors
end
- expect ActiveRecord::Errors do
+ expect ActiveModel::Errors do
s = SignupPresenter.new
s.valid?
s.account_errors
end
- expect String do
+ expect ["can't be blank"] do
s = SignupPresenter.new
s.valid?
- s.errors.on(:user_login)
+ s.errors[:user_login]
end
- expect "can't be blank" do
+ expect ["can't be blank"] do
s = SignupPresenter.new
s.valid?
- s.errors.on(:user_login)
+ s.errors[:user_login]
end
expect ["User Password can't be blank"] do
@@ -81,13 +81,13 @@
s.errors.full_messages
end
- expect 'c4N n07 83 8L4nK' do
+ expect ['c4N n07 83 8L4nK'] do
old_locale = I18n.locale
I18n.locale = '1337'
s = SignupPresenter.new(:user_login => nil)
s.valid?
- message = s.errors.on(:user_login)
+ message = s.errors[:user_login]
I18n.locale = old_locale
@@ -143,7 +143,7 @@
end
expect Account.any_instance.to.receive(:save!) do
- User.any_instance.stubs(:save!)
+ User.any_instance.stubs(:save!).returns(true)
s = SignupPresenter.new(:user_login => "da", :user_password => "seekrit")
s.save!
end
@@ -173,7 +173,7 @@
s.update_attributes :user_login => 'Something Different'
s.user_login
end
-
+
# Multiparameter assignment
expect Time.parse('March 27 1980 9:30:59 am') do
s = SignupPresenter.new
@@ -192,23 +192,23 @@
s = SignupPresenter.new
s.attributes = nil
end
-
+
# this is a regression test to make sure that _title is working. we had a weird conflict with using String#delete
expect 'something' do
s = SignupPresenter.new :account_title => 'something'
s.account_title
end
- expect String do
+ expect ["can't be blank"] do
s = SignupPresenter.new
s.save
- s.errors.on(:user_login)
+ s.errors[:user_login]
end
- expect String do
+ expect ["can't be blank"] do
s = SignupPresenter.new
s.save! rescue
- s.errors.on(:user_login)
+ s.errors[:user_login]
end
expect 'Login' do
@@ -219,7 +219,7 @@
expect SignupPresenter do
SignupPresenter.new(nil)
end
-
+
expect EndingWithSPresenter.new.address.not.to.be.nil?
# this should act as ActiveRecord models do
@@ -237,25 +237,25 @@
presenter.save!
end.id
end
-
+
expect CantSavePresenter.new.not.to.be.save # it won't save because the filter chain will halt
-
+
expect ActiveRecord::RecordNotSaved do
CantSavePresenter.new.save!
end
-
+
expect 'Some Street' do
p = AfterSavePresenter.new
p.save
p.address.street
end
-
+
expect 'Some Street' do
p = AfterSavePresenter.new
p.save!
p.address.street
end
-
+
expect SamePrefixPresenter.new.to.be.respond_to?(:account_title)
expect SamePrefixPresenter.new.to.be.respond_to?(:account_info_info)
@@ -305,7 +305,7 @@
end.steps
end
- expect ActiveRecord::Errors.any_instance.to.receive(:clear).twice do
+ expect ActiveModel::Errors.any_instance.to.receive(:clear) do
CallbackCantValidatePresenter.new.valid?
end
@@ -329,34 +329,33 @@
expect Address do
PresenterWithTwoAddresses.new.secondary_address
end
-
+
expect "123 awesome st" do
p = PresenterWithTwoAddresses.new(:secondary_address_street => "123 awesome st")
p.save
p.secondary_address_street
end
-
+
# attr_protected
expect "" do
p = SignupPresenter.new(:account_secret => 'swordfish')
p.account.secret
end
-
+
expect "comment" do
p = HistoricalPresenter.new(:history_comment => 'comment', :user => User.new(hash_for_user))
p.save
p.history_comment
end
-
+
expect false do
SignupPresenter.new.changed?
end
-
+
expect true do
p = SignupPresenter.new(:user => User.new(hash_for_user))
p.save
p.user_login = 'something_else'
p.changed?
end
-
end
View
10 test/lint_test.rb
@@ -0,0 +1,10 @@
+require File.dirname(__FILE__)+'/test_helper'
+require 'test/unit'
+
+class LintTest < ActiveModel::TestCase
+ include ActiveModel::Lint::Tests
+
+ def setup
+ @model = SignupPresenter.new
+ end
+end
View
19 test/test_helper.rb
@@ -1,4 +1,3 @@
-require File.dirname(__FILE__)+'/../lib/active_presenter' unless defined?(ActivePresenter)
require 'expectations'
require 'logger'
@@ -51,7 +50,6 @@
t.string :action, :default => ''
t.datetime :created_at
end
-
end
class User < ActiveRecord::Base
@@ -64,12 +62,12 @@ def presence_of_password
if password.blank?
attribute_name = I18n.t(:password, {:default => "Password", :scope => [:activerecord, :attributes, :user]})
error_message = I18n.t(:blank, {:default => "can't be blank", :scope => [:activerecord, :errors, :messages]})
- errors.add_to_base("#{attribute_name} #{error_message}")
+ errors[:base] << ("#{attribute_name} #{error_message}")
end
end
end
-class Account < ActiveRecord::Base; end
-class History < ActiveRecord::Base; end
+class Account < ActiveRecord::Base ;end
+class History < ActiveRecord::Base ;end
class Address < ActiveRecord::Base; end
class AccountInfo < ActiveRecord::Base; end
@@ -103,7 +101,7 @@ class SignupNoAccountPresenter < ActivePresenter::Base
presents :account, :user
def save?(key, instance)
- key != :account
+ key.to_sym != :account
end
end
@@ -212,7 +210,16 @@ def halt
end
end
+class HistoricalPresenter < ActivePresenter::Base
+ presents :user, :history
+ attr_accessible :history_comment
+end
+
def hash_for_user(opts = {})
{:login => 'jane', :password => 'seekrit' }.merge(opts)
end
+def returning(value)
+ yield(value)
+ value
+end

0 comments on commit 2173ee4

Please sign in to comment.