diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 663b71f..0000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2008 Daniel Haran, James Golick, GiraffeSoft, Inc. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/README b/README deleted file mode 120000 index 257e9c4..0000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -README.rdoc \ No newline at end of file diff --git a/README.rdoc b/README.rdoc index 8407ddd..a94e1c4 100644 --- a/README.rdoc +++ b/README.rdoc @@ -6,19 +6,17 @@ By acting nearly identically to ActiveRecord models, ActivePresenter makes prese == Get It -As a gem: +As a gem (Gemfile): - $ sudo gem install active_presenter + gem 'active_presenter', :git => 'git://github.com/galetahub/active_presenter.git' -As a rails gem dependency: +As a rails plugin: - config.gem 'active_presenter' + rails install plugin git://github.com/galetahub/active_presenter.git Or get the source from github: - $ git clone git://github.com/giraffesoft/active_presenter.git - -(or fork it at http://github.com/giraffesoft/active_presenter) + $ git clone git://github.com/galetahub/active_presenter.git == Usage diff --git a/Rakefile b/Rakefile index 15ee492..700871f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,24 @@ +# encoding: utf-8 require 'rake' +require 'rake/testtask' require 'rake/rdoctask' -require File.dirname(__FILE__)+'/lib/active_presenter' -Dir.glob(File.dirname(__FILE__)+'/lib/tasks/**/*.rake').each { |l| load l } +desc 'Default: run unit tests.' task :default => :test -task :test do - Dir['test/**/*_test.rb'].each { |l| require l } +desc 'Test the active_presenter plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the active_presenter plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'ActivePresenter' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') end diff --git a/TODO b/TODO deleted file mode 100644 index 17d5f44..0000000 --- a/TODO +++ /dev/null @@ -1,3 +0,0 @@ -Document protected API -Add support for namespaced models. -Add support for presented collections \ No newline at end of file diff --git a/active_presenter.gemspec b/active_presenter.gemspec new file mode 100644 index 0000000..82d6c65 --- /dev/null +++ b/active_presenter.gemspec @@ -0,0 +1,22 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "active_presenter/version" + +Gem::Specification.new do |s| + s.name = "galetahub-active_presenter" + s.version = ActivePresenter::VERSION.dup + s.platform = Gem::Platform::RUBY + s.summary = "ActivePresenter is the presenter library" + s.description = "ActivePresenter is the presenter library you already know! (...if you know ActiveRecord)" + s.authors = ["James Golick", "Daniel Haran", "Igor Galeta"] + s.email = "galeta.igor@gmail.com" + s.rubyforge_project = "active_presenter" + s.homepage = "https://github.com/galetahub/active_presenter" + + s.files = Dir["{app,lib}/**/*"] + ["Rakefile", "README.rdoc"] + s.test_files = Dir["{test}/**/*"] + s.extra_rdoc_files = ["README.rdoc"] + s.require_paths = ["lib"] + + s.add_dependency("activerecord", ">= 0") +end diff --git a/lib/active_presenter.rb b/lib/active_presenter.rb index 4a6d0ea..f1f5edf 100644 --- a/lib/active_presenter.rb +++ b/lib/active_presenter.rb @@ -1,7 +1,7 @@ require 'rubygems' require 'active_record' -Dir.glob(File.dirname(__FILE__)+'/active_presenter/**/*.rb').each { |l| require l } module ActivePresenter - NAME = 'active_presenter' + autoload :Base, 'active_presenter/base' + autoload :Version, 'active_presenter/version' end diff --git a/lib/active_presenter/base.rb b/lib/active_presenter/base.rb index 23f86bf..aa6caa2 100644 --- a/lib/active_presenter/base.rb +++ b/lib/active_presenter/base.rb @@ -2,11 +2,18 @@ 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::Conversion + include ActiveModel::MassAssignmentSecurity + + 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. @@ -22,76 +29,54 @@ class Base # presents :primary_address => Address, :secondary_address => Address # end # - def self.presents(*types) - types_and_classes = types.extract_options! - types.each { |t| types_and_classes[t] = t.to_s.tableize.classify.constantize } + class << self + def 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 + 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 - types_and_classes.keys.each do |t| - define_method("#{t}_errors") do - send(t).errors + def 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}_", "") - presented[t] = types_and_classes[t] + 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 - 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 + + # 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_and_descendants_from_active_record # :nodoc: + [self] 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])) + def 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 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') @@ -105,13 +90,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,12 +105,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.each do |k,v| + sanitize_for_mass_assignment(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] ||= {} @@ -153,26 +136,20 @@ 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) - presented.keys.each do |type| - presented_inst = send(type) - + result = _run_validation_callbacks do + presented.each do |type, klass| + presented_inst = (send(type) || klass.new) 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? @@ -184,18 +161,16 @@ def changed? # # Returns true or false based on success. # - def save + def save(options={}) 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 !perform_validations?(options) || (perform_validations?(options) && 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 @@ -203,17 +178,17 @@ def save # # 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) - + def save!(options={}) + 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) if perform_validations?(options) && !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 @@ -244,11 +219,22 @@ def save?(presented_key, presented_instance) def id # :nodoc: nil end - + def new_record? - true + presented_instances.map(&:new_record?).all? + end + + def persisted? + presented_instances.map(&:persisted?).all? + end + + def column_for_attribute(attr) + p = presentable_for(attr) + return nil unless p + send(p).send(:column_for_attribute, flatten_attribute_name(attr,p)) end + protected def presented_instances presented.keys.map { |key| send(key) } @@ -280,10 +266,10 @@ def attribute_prefix(type) def merge_errors(presented_inst, type) presented_inst.errors.each do |att,msg| - if att == 'base' + if att == :base errors.add(type, msg) else - errors.add(attribute_prefix(type)+att, msg) + errors.add(attribute_prefix(type)+att.to_s, msg) end end end @@ -291,25 +277,13 @@ def merge_errors(presented_inst, type) 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? + #remove_att... normally takes a hash, so we use a '' + flat_attribute = {flatten_attribute_name(name, presentable) => ''} + presented[presentable].new.send(:sanitize_for_mass_assignment, flat_attribute).empty? end - def run_callbacks_with_halt(callback) - run_callbacks(callback) { |result, object| result == false } + def perform_validations?(options={}) + options[:validate] != 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(/\(.+/,""))} - else - raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both." - end - end - end end diff --git a/lib/active_presenter/version.rb b/lib/active_presenter/version.rb index e0f7d8a..5c48de7 100644 --- a/lib/active_presenter/version.rb +++ b/lib/active_presenter/version.rb @@ -1,9 +1,4 @@ +# encoding: utf-8 module ActivePresenter - module VERSION - MAJOR = 1 - MINOR = 3 - TINY = 0 - - STRING = [MAJOR, MINOR, TINY].join('.') - end + VERSION = "0.1.0".freeze end diff --git a/lib/tasks/doc.rake b/lib/tasks/doc.rake deleted file mode 100644 index 86c5d71..0000000 --- a/lib/tasks/doc.rake +++ /dev/null @@ -1,19 +0,0 @@ -desc 'Generate documentation for the ResourceController plugin.' -Rake::RDocTask.new(:rdoc) do |rdoc| - rdoc.rdoc_dir = 'rdoc' - rdoc.title = 'ActivePresenter' - rdoc.options << '--line-numbers' << '--inline-source' - rdoc.rdoc_files.include('README') - rdoc.rdoc_files.include('lib/**/*.rb') -end - -task :upload_docs => :rdoc do - puts 'Deleting previous rdoc' - `ssh jamesgolick.com 'rm -Rf /home/apps/jamesgolick.com/public/active_presenter/rdoc'` - - puts "Uploading current rdoc" - `scp -r rdoc jamesgolick.com:/home/apps/jamesgolick.com/public/active_presenter/rdoc` - - puts "Deleting rdoc" - `rm -Rf rdoc` -end diff --git a/lib/tasks/gem.rake b/lib/tasks/gem.rake deleted file mode 100644 index 5df7c97..0000000 --- a/lib/tasks/gem.rake +++ /dev/null @@ -1,62 +0,0 @@ -require 'rake/gempackagetask' - -task :clean => :clobber_package - -spec = Gem::Specification.new do |s| - s.name = ActivePresenter::NAME - s.version = ActivePresenter::VERSION::STRING - s.summary = - s.description = "ActivePresenter is the presenter library you already know! (...if you know ActiveRecord)" - s.author = "James Golick & Daniel Haran" - s.email = 'james@giraffesoft.ca' - s.homepage = 'http://jamesgolick.com/active_presenter' - s.rubyforge_project = 'active_presenter' - s.has_rdoc = true - - s.required_ruby_version = '>= 1.8.5' - - s.files = %w(README LICENSE Rakefile) + - Dir.glob("{lib,test}/**/*") - - s.require_path = "lib" -end - -Rake::GemPackageTask.new(spec) do |p| - p.gem_spec = spec -end - -task :tag_warn do - puts "*" * 40 - puts "Don't forget to tag the release:" - puts - puts " git tag -a v#{ActivePresenter::VERSION::STRING}" - puts - puts "or run rake tag" - puts "*" * 40 -end - -task :tag do - sh "git tag -a v#{ActivePresenter::VERSION::STRING}" -end -task :gem => :tag_warn - -namespace :gem do - namespace :upload do - - desc 'Upload gems (ruby & win32) to rubyforge.org' - task :rubyforge => :gem do - sh 'rubyforge login' - sh "rubyforge add_release giraffesoft active_presenter #{ActivePresenter::VERSION::STRING} pkg/#{spec.full_name}.gem" - sh "rubyforge add_file giraffesoft active_presenter #{ActivePresenter::VERSION::STRING} pkg/#{spec.full_name}.gem" - end - - end -end - -task :install => [:clobber, :package] do - sh "sudo gem install pkg/#{spec.full_name}.gem" -end - -task :uninstall => :clean do - sh "sudo gem uninstall -v #{ActivePresenter::VERSION::STRING} -x #{ActivePresenter::NAME}" -end