Skip to content

Commit

Permalink
Version one of reworked Plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyolliver committed Dec 5, 2008
0 parents commit 9b9119a
Show file tree
Hide file tree
Showing 16 changed files with 5,830 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,20 @@
Copyright (c) 2008 [name of plugin creator]

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.
83 changes: 83 additions & 0 deletions README
@@ -0,0 +1,83 @@
CustomChangeMessages
====================

CustomChangeMessages is a rails plugin for extending the dirty objects methods introduced in rails 2.1
http://ryandaigle.com/articles/2008/3/31/what-s-new-in-edge-rails-dirty-objects
It provides customizable messages for the changed attributes on a record, and works nicely with any foriegn
keys that are a belongs_to association.

ActiveRecord extensions:

change_message_for(attribute) # Returns a string message representation of the attribute that has changed
change_messages # Returns an array of the messages for each changed attribute

Installation
============

installation: script/plugin install git://github.com/jeremyolliver/custom_message_changes.git

Requirements: rails v2.1 or greater, if you use an earlier version check out either:
modelnotifier http://github.com/jeremyolliver/modelnotifier/tree/master The original version of this plugin
or http://code.bitsweat.net/svn/dirty a plugin that implements the code for dirty models introduced in 2.1 that
CustomChangeMesssages relies on.

Example
=======

The main use of this is to help clean up controller actions, such as:

class ItemsController < ApplicationController

def update
@item.attributes = params[:item]
Mailer.deliver_item_update(@item, @item.change_messages.to_sentence)
@item.save!

rescue ActiveRecord::RecordInvalid => e
flash[:error] = e.reord.error_messages
redirect_to item_url(@item)
end
#...
end


@item.change_messages.to_sentence will return human readable mesages such as:
=> "Description has changed from 'Nice and easy' to 'This task is now rather long and arduous', User has changed from 'Jeremy' to 'Guy', and Due Date has been rescheduled from '09/11/2008' to '10/11/2008'"

The messages for each attribute are also customizable, which is especially handy for dealing with belongs_to
assocations. Here's a more complicated example:

Use the custom_message_for method to customize the message for the attribute, specifying :display => :name
will use the method/attribute :name for displaying the record that the item belongs_to

The skip_message_for method can be used to prevent stop any changes to a particular attribute showing up

class Item < ActiveRecord::Base
belongs_to :person

custom_message_for :person, :display => :username # display the person's username instead of the id
custom_message_for :due_on, :as => "Due Date", :message => "has been rescheduled", :format => :pretty_print_date
# change the syntax of the message for working with dates, because it makes more sense that way

# this method is used for formatting the due_on field when it changes
def pretty_print_date(value = self.due_on)
value.strftime("%d/%m/%Y")
end
end

class Person < ActiveRecord::Base
custom_message_for :username, :as => "Name"
skip_message_for :internal_calculation
end

p = Person.create!(:username => "Jeremy")
p2 = Person.create!(:username => "Optimus Prime")
i = Item.create!(:name => "My Task", :description => nil, :person => p, :due_on => Date.today)
i.attributes = {:person => p2, :description => "This task is now rather long and arduous"}

i.change_messages
=> ["Due Date has been rescheduled from '4/12/2008' to '5/12/2008'", "Person has changed from 'Jeremy' to 'Optimus Prime'", "Description has changed from '' to 'This task is now rather long and arduous'"]



Copyright (c) 2008 Jeremy Olliver, released under the MIT license
22 changes: 22 additions & 0 deletions Rakefile
@@ -0,0 +1,22 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

desc 'Default: run unit tests.'
task :default => :test

desc 'Test the model_notifier plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

desc 'Generate documentation for the model_notifier plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'ModelNotifier'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
1 change: 1 addition & 0 deletions install.rb
@@ -0,0 +1 @@
# Install hook code here
1 change: 1 addition & 0 deletions lib/custom_change_messages.rb
@@ -0,0 +1 @@
require 'custom_change_messages/active_record'
134 changes: 134 additions & 0 deletions lib/custom_change_messages/active_record.rb
@@ -0,0 +1,134 @@
ActiveRecord::Base.class_eval do

# hash and array to keep track of the customised messages, belongs_to associations, and any skipped attributes
class_inheritable_hash :custom_dirty_messages
class_inheritable_array :skipped_dirty_attributes

class << self

def init_messages
unless self.custom_dirty_messages
self.custom_dirty_messages = {}
self.reflect_on_all_associations(:belongs_to).each do |association|
self.custom_dirty_messages[association.primary_key_name.to_sym] = {:type => :belongs_to, :association_name => association.name, :as => association.name.to_s.capitalize}
end
end
end

def custom_message_for(*attr_names)
init_messages
options = attr_names.extract_options!
attr_names.each do |attribute|
key = key_for(attribute)
if self.custom_dirty_messages[key]
# if options are being passed for an associations attribute, or an association
self.custom_dirty_messages[key].merge!(options)
else
self.custom_dirty_messages.merge!({attribute.to_sym => options})
end
end
end

def skip_message_for(*attr_names)
self.skipped_dirty_attributes ||= [:updated_at, :created_at, :id]
attr_names.extract_options!
self.skipped_dirty_attributes += attr_names
end

private

def key_for(attribute)
# first check if it's a belongs_to association
if (assoc = self.reflect_on_association(attribute))
assoc.primary_key_name.to_sym
else
attribute.to_sym
end
end

end

def change_messages
messages = []
changes.each do |attribute, diff|
attribute = attribute.to_sym
self.class.skipped_dirty_attributes ||= [:updated_at, :created_at, :id]
next if self.class.skipped_dirty_attributes.include?(attribute)
messages << change_message_for(attribute, diff)
end
messages
end

def change_message_for(attribute, changes = nil)
attribute = key_for(attribute)
changes ||= self.send((attribute.to_s + "_change").to_sym)
val = "#{attr_name(attribute)} #{watch_value(attribute, :message)}"
val += " #{watch_value(attribute, :prefix)} \'#{attr_display(attribute, changes.first)}\'" unless watch_value(attribute, :no_prefix)
val += " #{watch_value(attribute, :suffix)} \'#{attr_display(attribute, changes.last)}\'" unless watch_value(attribute, :no_suffix)
val
end

private

def key_for(attribute)
# first check if it's a belongs_to association
if (assoc = self.class.reflect_on_association(attribute))
assoc.primary_key_name.to_sym
else
attribute.to_sym
end
end

# check if it's an association name, or if the attribute is being watched
def attr_name(attribute)
if self.class.custom_dirty_messages[attribute]
if (name = self.class.custom_dirty_messages[attribute][:as])
name
elsif is_association?(attribute)
(n = self.class.custom_dirty_messages[attribute][:association_name]) ? n.to_s.capitalize : attribute.to_s.capitalize
else
attribute.to_s.capitalize
end
else
attribute.to_s.capitalize
end
end

def attr_display(attribute, value)
attribute = key_for(attribute)
if self.class.custom_dirty_messages[attribute]
if (meth = self.class.custom_dirty_messages[attribute.to_sym][:format])
return self.send(meth, value)
elsif (meth = self.class.custom_dirty_messages[attribute.to_sym][:display])
raise ":display option set on an attribute which isn't a belongs_to association" unless is_association?(attribute)
assoc = self.class.reflect_on_association(association_name(attribute))
finder = ("find_by_" + assoc.klass.primary_key).to_sym
return assoc.klass.send(finder, value).send(meth.to_sym)
end
end
return value.to_s
end

def association_name(attribute)
self.class.custom_dirty_messages[attribute][:association_name]
end

def is_association?(attribute)
attribute = key_for(attribute)
self.class.custom_dirty_messages[attribute][:association_name]
end

def watch_value(attribute, option)
if self.class.custom_dirty_messages[attribute.to_sym]
self.class.custom_dirty_messages[attribute.to_sym][option] || watch_option_defaults[option]
else
watch_option_defaults[option]
end
end

def watch_option_defaults
{:message => "has changed", :prefix => "from", :suffix => "to"}
end


end
1 change: 1 addition & 0 deletions rails/init.rb
@@ -0,0 +1 @@
require 'custom_change_messages'
4 changes: 4 additions & 0 deletions tasks/custom_change_messages.rake
@@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :custom_change_messages do
# # Task goes here
# end
95 changes: 95 additions & 0 deletions test/active_record_test.rb
@@ -0,0 +1,95 @@
require File.dirname(__FILE__) + '/test_helper.rb'

class ActiveRecordTest < Test::Unit::TestCase

load_schema

class Item < ActiveRecord::Base
belongs_to :person

custom_message_for :person, :display => :username
custom_message_for :due_on, :as => "Due Date", :message => "has been rescheduled", :format => :pretty_print_date

def pretty_print_date(value = self.due_on)
value.strftime("%d/%m/%Y")
end
end

class Person < ActiveRecord::Base
custom_message_for :username, :as => "Name"
skip_message_for :internal_calculation
end

def test_active_record_extension
i = Item.create!
assert i.respond_to?(:change_messages)
end

def test_ignores_timestamps
i = Item.create!
i.attributes = {:created_at => Date.tomorrow, :updated_at => Date.tomorrow}
puts i.change_messages
assert i.change_messages.empty?
end

def test_unwatching
p = Person.create!(:username => "Robot", :internal_calculation => 1)
p.internal_calculation = 42
assert p.change_messages.empty?
end

def test_labeling_attributes
# ensure associations are given the correct name. In this case username has been renamed to 'Name'
u = Person.create!(:username => "Jeremy")
u.username = "Jeremy O"

assert_equal "Name has changed from \'Jeremy\' to \'Jeremy O\'", u.change_message_for(:username)
end

def test_associations_loaded
i = Item.create!(:name => "My Cool Task")
assert i.class.custom_dirty_messages[:person_id]
assert_equal :belongs_to, i.class.custom_dirty_messages[:person_id][:type]
end

def test_display_of_associations
u = Person.create!(:username => "Jeremy")
u2 = Person.create!(:username => "Guy")
i = Item.create!(:name => "My Task", :description => "super", :person => u)
i.person = u2

assert_equal "Person has changed from \'Jeremy\' to \'Guy\'", i.change_message_for(:person)
end

def test_handling_of_nil_attrs
i = Item.create!(:name => "Namae wa", :description => nil)
i.description = "Japanese sentence"

assert_nothing_raised do
i.change_messages
end
end

def test_formatting_attributes
i = Item.create!(:name => "Task", :due_on => Date.today)
i.due_on = Date.tomorrow

today = Date.today.strftime("%d/%m/%Y")
tomorrow = Date.tomorrow.strftime("%d/%m/%Y")
assert_equal "Due Date has been rescheduled from '#{today}' to '#{tomorrow}'", i.change_message_for(:due_on)
end

def test_full_sentence_changes
p = Person.create!(:username => "Jeremy")
p2 = Person.create!(:username => "Optimus")
i = Item.create!(:name => "My Task", :description => "Nice and easy", :person => p, :due_on => Date.today)
i.attributes = {:person => p2, :description => "This task is now rather long and arduous", :due_on => Date.tomorrow }

today = Date.today.strftime("%d/%m/%Y")
tomorrow = Date.tomorrow.strftime("%d/%m/%Y")

assert_equal "Description has changed from 'Nice and easy' to 'This task is now rather long and arduous', Person has changed from 'Jeremy' to 'Optimus', and Due Date has been rescheduled from '#{today}' to '#{tomorrow}'", \
i.change_messages.to_sentence
end

end
Binary file added test/custom_change_messages.sqlite3.db
Binary file not shown.
21 changes: 21 additions & 0 deletions test/custom_change_messages_test.rb
@@ -0,0 +1,21 @@
require File.dirname(__FILE__) + '/test_helper.rb'

class CustomChangeMessagesTest < Test::Unit::TestCase

class Item < ActiveRecord::Base
end

def setup
# schema needs to be loaded in the other test, so don't load it here a second time, unless this is run first or isolated
unless Item.connected?
load_schema
end
end

def test_schema_has_loaded_correctly
assert_nothing_raised do
Item.all
end
end

end
3 changes: 3 additions & 0 deletions test/database.yml
@@ -0,0 +1,3 @@
sqlite3:
adapter: sqlite3
dbfile: vendor/plugins/custom_change_messages/test/custom_change_messages.sqlite3.db

0 comments on commit 9b9119a

Please sign in to comment.