exchanger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/lib/exchanger.rb +79 -0
- data/lib/exchanger/attributes.rb +61 -0
- data/lib/exchanger/boolean.rb +4 -0
- data/lib/exchanger/client.rb +19 -0
- data/lib/exchanger/config.rb +32 -0
- data/lib/exchanger/dirty.rb +239 -0
- data/lib/exchanger/element.rb +161 -0
- data/lib/exchanger/elements/attendee.rb +10 -0
- data/lib/exchanger/elements/base_folder.rb +61 -0
- data/lib/exchanger/elements/calendar_folder.rb +11 -0
- data/lib/exchanger/elements/calendar_item.rb +59 -0
- data/lib/exchanger/elements/complete_name.rb +16 -0
- data/lib/exchanger/elements/contact.rb +49 -0
- data/lib/exchanger/elements/contacts_folder.rb +17 -0
- data/lib/exchanger/elements/distribution_list.rb +8 -0
- data/lib/exchanger/elements/email_address.rb +16 -0
- data/lib/exchanger/elements/entry.rb +11 -0
- data/lib/exchanger/elements/folder.rb +9 -0
- data/lib/exchanger/elements/identifier.rb +7 -0
- data/lib/exchanger/elements/im_address.rb +20 -0
- data/lib/exchanger/elements/item.rb +86 -0
- data/lib/exchanger/elements/mailbox.rb +34 -0
- data/lib/exchanger/elements/meeting_cancellation.rb +4 -0
- data/lib/exchanger/elements/meeting_message.rb +16 -0
- data/lib/exchanger/elements/meeting_request.rb +6 -0
- data/lib/exchanger/elements/meeting_response.rb +4 -0
- data/lib/exchanger/elements/message.rb +24 -0
- data/lib/exchanger/elements/phone_number.rb +13 -0
- data/lib/exchanger/elements/physical_address.rb +15 -0
- data/lib/exchanger/elements/search_folder.rb +5 -0
- data/lib/exchanger/elements/single_recipient.rb +6 -0
- data/lib/exchanger/elements/task.rb +5 -0
- data/lib/exchanger/elements/tasks_folder.rb +4 -0
- data/lib/exchanger/field.rb +139 -0
- data/lib/exchanger/operation.rb +110 -0
- data/lib/exchanger/operations/create_item.rb +64 -0
- data/lib/exchanger/operations/expand_dl.rb +42 -0
- data/lib/exchanger/operations/find_folder.rb +55 -0
- data/lib/exchanger/operations/find_item.rb +54 -0
- data/lib/exchanger/operations/get_folder.rb +55 -0
- data/lib/exchanger/operations/get_item.rb +44 -0
- data/lib/exchanger/operations/resolve_names.rb +43 -0
- data/lib/exchanger/operations/update_item.rb +43 -0
- data/lib/exchanger/persistence.rb +27 -0
- data/spec/calendar_item_spec.rb +26 -0
- data/spec/client_spec.rb +11 -0
- data/spec/contact_spec.rb +68 -0
- data/spec/element_spec.rb +4 -0
- data/spec/field_spec.rb +118 -0
- data/spec/folder_spec.rb +13 -0
- data/spec/mailbox_spec.rb +9 -0
- data/spec/spec_helper.rb +6 -0
- metadata +208 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Edgars Beigarts
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
Exchanger
|
2
|
+
=========
|
3
|
+
|
4
|
+
Ruby library for accessing Microsoft Exchange using Exchange Web Services.
|
5
|
+
This library tries to make creating and updating items as easy as possible.
|
6
|
+
It will keep track of changed properties and will update only them.
|
7
|
+
|
8
|
+
Supported operations
|
9
|
+
====================
|
10
|
+
|
11
|
+
* FindItem, GetItem, CreateItem, UpdateItem
|
12
|
+
* FindFolder, GetFolder
|
13
|
+
|
14
|
+
|
15
|
+
Installing
|
16
|
+
==========
|
17
|
+
|
18
|
+
gem install exchanger
|
19
|
+
|
20
|
+
Configuration
|
21
|
+
=============
|
22
|
+
|
23
|
+
Exchanger.configure do |config|
|
24
|
+
config.endpoint = "https://domain.com/EWS/Exchanger.asmx"
|
25
|
+
config.username = "username"
|
26
|
+
config.password = "password"
|
27
|
+
end
|
28
|
+
|
29
|
+
or configure from YAML
|
30
|
+
|
31
|
+
Exchanger::Config.instance.from_hash(YAML.load_file("#{Rails.root}/config/exchanger.yml")[Rails.env])
|
32
|
+
|
33
|
+
Examples
|
34
|
+
========
|
35
|
+
|
36
|
+
Creating and updating contacts
|
37
|
+
------------------------------
|
38
|
+
|
39
|
+
folder = Exchanger::Folder.find(:contacts)
|
40
|
+
contact = folder.new_contact
|
41
|
+
contact.given_name = "Edgars"
|
42
|
+
contact.surname = "Beigarts"
|
43
|
+
contact.email_addresses = [ Exchanger::EmailAddress.new(:key => "EmailAddress1", :text => "me@domain.com") ]
|
44
|
+
contact.phone_numbers = [ Exchanger::PhoneNumber.new(:key => "MobilePhone", :text => "+371 80000000") ]
|
45
|
+
contact.save # CreateItem operation
|
46
|
+
contact.company_name = "Tieto"
|
47
|
+
contact.save # UpdateItem operation
|
48
|
+
|
49
|
+
Alternatives
|
50
|
+
============
|
51
|
+
|
52
|
+
* [github.com/jrun/ews-api](http://github.com/jrun/ews-api)
|
53
|
+
* [github.com/zenchild/Viewpoint](http://github.com/zenchild/Viewpoint)
|
data/lib/exchanger.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require "singleton"
|
2
|
+
require "delegate"
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
require "active_support/core_ext"
|
6
|
+
require "nokogiri"
|
7
|
+
require "httpclient"
|
8
|
+
require "net/ntlm"
|
9
|
+
|
10
|
+
require "exchanger/config"
|
11
|
+
require "exchanger/client"
|
12
|
+
require "exchanger/boolean"
|
13
|
+
require "exchanger/field"
|
14
|
+
require "exchanger/dirty"
|
15
|
+
require "exchanger/attributes"
|
16
|
+
require "exchanger/persistence"
|
17
|
+
|
18
|
+
# Elements
|
19
|
+
require "exchanger/element"
|
20
|
+
require "exchanger/elements/identifier"
|
21
|
+
require "exchanger/elements/mailbox"
|
22
|
+
require "exchanger/elements/single_recipient"
|
23
|
+
require "exchanger/elements/attendee"
|
24
|
+
require "exchanger/elements/complete_name"
|
25
|
+
# Entry elements
|
26
|
+
require "exchanger/elements/entry"
|
27
|
+
require "exchanger/elements/email_address"
|
28
|
+
require "exchanger/elements/phone_number"
|
29
|
+
require "exchanger/elements/physical_address"
|
30
|
+
require "exchanger/elements/im_address"
|
31
|
+
# Folder elements
|
32
|
+
require "exchanger/elements/base_folder"
|
33
|
+
require "exchanger/elements/folder"
|
34
|
+
require "exchanger/elements/calendar_folder"
|
35
|
+
require "exchanger/elements/contacts_folder"
|
36
|
+
require "exchanger/elements/tasks_folder"
|
37
|
+
require "exchanger/elements/search_folder"
|
38
|
+
# Item elements
|
39
|
+
require "exchanger/elements/item"
|
40
|
+
require "exchanger/elements/message"
|
41
|
+
require "exchanger/elements/calendar_item"
|
42
|
+
require "exchanger/elements/contact"
|
43
|
+
require "exchanger/elements/meeting_message"
|
44
|
+
require "exchanger/elements/meeting_request"
|
45
|
+
require "exchanger/elements/meeting_response"
|
46
|
+
require "exchanger/elements/meeting_cancellation"
|
47
|
+
require "exchanger/elements/task"
|
48
|
+
require "exchanger/elements/distribution_list"
|
49
|
+
|
50
|
+
# Operations
|
51
|
+
require "exchanger/operation"
|
52
|
+
require "exchanger/operations/get_folder"
|
53
|
+
require "exchanger/operations/find_folder"
|
54
|
+
require "exchanger/operations/get_item"
|
55
|
+
require "exchanger/operations/find_item"
|
56
|
+
require "exchanger/operations/create_item"
|
57
|
+
require "exchanger/operations/update_item"
|
58
|
+
require "exchanger/operations/resolve_names"
|
59
|
+
require "exchanger/operations/expand_dl"
|
60
|
+
|
61
|
+
module Exchanger
|
62
|
+
NS = {
|
63
|
+
"xsi" => "http://www.w3.org/2001/XMLSchema-instance",
|
64
|
+
"xsd" => "http://www.w3.org/2001/XMLSchema",
|
65
|
+
"soap" => "http://schemas.xmlsoap.org/soap/envelope/",
|
66
|
+
"m" => "http://schemas.microsoft.com/exchange/services/2006/messages",
|
67
|
+
"t" => "http://schemas.microsoft.com/exchange/services/2006/types"
|
68
|
+
}
|
69
|
+
|
70
|
+
class << self
|
71
|
+
# The Exchanger +Config+ singleton instance.
|
72
|
+
def configure
|
73
|
+
config = Config.instance
|
74
|
+
block_given? ? yield(config) : config
|
75
|
+
end
|
76
|
+
|
77
|
+
alias :config :configure
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Exchanger
|
2
|
+
module Attributes
|
3
|
+
def attributes=(values = {})
|
4
|
+
values.each do |name, value|
|
5
|
+
write_attribute(name, value)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# Return the attributes hash with indifferent access.
|
10
|
+
def attributes
|
11
|
+
@attributes.with_indifferent_access
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: Add typecasting
|
15
|
+
# Read a value from the +Document+ attributes. If the value does not exist
|
16
|
+
# it will return nil.
|
17
|
+
def read_attribute(name)
|
18
|
+
name = name.to_s
|
19
|
+
value = @attributes[name]
|
20
|
+
accessed(name, value)
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: Add typecasting
|
24
|
+
def write_attribute(name, value)
|
25
|
+
name = name.to_s
|
26
|
+
modify(name, @attributes[name], value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def identifier
|
30
|
+
if self.class.identifier_name
|
31
|
+
@identifier ||= self.send(self.class.identifier_name)
|
32
|
+
@identifier.tag_name = self.class.identifier_name.to_s.camelize if @identifier
|
33
|
+
@identifier
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def id
|
38
|
+
identifier && identifier.id
|
39
|
+
end
|
40
|
+
|
41
|
+
def change_key
|
42
|
+
identifier && identifier.change_key
|
43
|
+
end
|
44
|
+
|
45
|
+
# Override respond_to? so it responds properly for dynamic attributes
|
46
|
+
def respond_to?(name)
|
47
|
+
(@attributes && @attributes.has_key?(name.to_s)) || super
|
48
|
+
end
|
49
|
+
|
50
|
+
# Used for allowing accessor methods for dynamic attributes
|
51
|
+
def method_missing(name, *args)
|
52
|
+
attr = name.to_s.sub("=", "")
|
53
|
+
return super unless attributes.has_key?(attr)
|
54
|
+
if name.to_s.ends_with?("=")
|
55
|
+
write_attribute(attr, *args)
|
56
|
+
else
|
57
|
+
read_attribute(attr)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Exchanger
|
2
|
+
# SOAP Client for Exhange Web Services
|
3
|
+
class Client
|
4
|
+
delegate :endpoint, :timeout, :username, :password, :debug, :to => "Exchanger.config"
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@client = HTTPClient.new
|
8
|
+
@client.debug_dev = STDERR if debug
|
9
|
+
@client.set_auth nil, username, password if username
|
10
|
+
end
|
11
|
+
|
12
|
+
# Does the actual HTTP level interaction.
|
13
|
+
def request(post_body, headers)
|
14
|
+
# @client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
15
|
+
response = @client.post(endpoint, post_body, headers)
|
16
|
+
return { :status => response.status, :body => response.content, :content_type => response.contenttype }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Exchanger
|
2
|
+
class Config
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_accessor :endpoint, :timeout, :username, :password, :debug
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
reset
|
9
|
+
end
|
10
|
+
|
11
|
+
# Reset the configuration options to the defaults.
|
12
|
+
def reset
|
13
|
+
@endpoint = nil
|
14
|
+
@timeout = 5
|
15
|
+
@username = nil
|
16
|
+
@password = nil
|
17
|
+
@debug = false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Configure Exchanger client from a hash. This is usually called after parsing a
|
21
|
+
# yaml config file such as exchanger.yml.
|
22
|
+
#
|
23
|
+
# Example:
|
24
|
+
#
|
25
|
+
# <tt>Exchanger::Config.instance.from_hash({})</tt>
|
26
|
+
def from_hash(settings)
|
27
|
+
settings.each do |name, value|
|
28
|
+
send("#{name}=", value) if respond_to?("#{name}=")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
module Exchanger
|
2
|
+
module Dirty
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
# Gets the changes for a specific field.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# person = Person.new(:title => "Sir")
|
12
|
+
# person.title = "Madam"
|
13
|
+
# person.attribute_change("title") # [ "Sir", "Madam" ]
|
14
|
+
#
|
15
|
+
# Returns:
|
16
|
+
#
|
17
|
+
# An +Array+ containing the old and new values.
|
18
|
+
def attribute_change(name)
|
19
|
+
modifications[name]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Determines if a specific field has chaged.
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# person = Person.new(:title => "Sir")
|
27
|
+
# person.title = "Madam"
|
28
|
+
# person.attribute_changed?("title") # true
|
29
|
+
#
|
30
|
+
# Returns:
|
31
|
+
#
|
32
|
+
# +true+ if changed, +false+ if not.
|
33
|
+
def attribute_changed?(name)
|
34
|
+
modifications.include?(name)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Gets the old value for a specific field.
|
38
|
+
#
|
39
|
+
# Example:
|
40
|
+
#
|
41
|
+
# person = Person.new(:title => "Sir")
|
42
|
+
# person.title = "Madam"
|
43
|
+
# person.attribute_was("title") # "Sir"
|
44
|
+
#
|
45
|
+
# Returns:
|
46
|
+
#
|
47
|
+
# The old field value.
|
48
|
+
def attribute_was(name)
|
49
|
+
change = modifications[name]
|
50
|
+
change ? change[0] : nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# Gets the names of all the fields that have changed in the document.
|
54
|
+
#
|
55
|
+
# Example:
|
56
|
+
#
|
57
|
+
# person = Person.new(:title => "Sir")
|
58
|
+
# person.title = "Madam"
|
59
|
+
# person.changed # returns [ "title" ]
|
60
|
+
#
|
61
|
+
# Returns:
|
62
|
+
#
|
63
|
+
# An +Array+ of changed field names.
|
64
|
+
def changed
|
65
|
+
modifications.keys
|
66
|
+
end
|
67
|
+
|
68
|
+
# Alerts to whether the document has been modified or not.
|
69
|
+
#
|
70
|
+
# Example:
|
71
|
+
#
|
72
|
+
# person = Person.new(:title => "Sir")
|
73
|
+
# person.title = "Madam"
|
74
|
+
# person.changed? # returns true
|
75
|
+
#
|
76
|
+
# Returns:
|
77
|
+
#
|
78
|
+
# +true+ if changed, +false+ if not.
|
79
|
+
def changed?
|
80
|
+
!modifications.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Gets all the modifications that have happened to the object as a +Hash+
|
84
|
+
# with the keys being the names of the fields, and the values being an
|
85
|
+
# +Array+ with the old value and new value.
|
86
|
+
#
|
87
|
+
# Example:
|
88
|
+
#
|
89
|
+
# person = Person.new(:title => "Sir")
|
90
|
+
# person.title = "Madam"
|
91
|
+
# person.changes # returns { "title" => [ "Sir", "Madam" ] }
|
92
|
+
#
|
93
|
+
# Returns:
|
94
|
+
#
|
95
|
+
# A +Hash+ of changes.
|
96
|
+
def changes
|
97
|
+
modifications
|
98
|
+
end
|
99
|
+
|
100
|
+
# Call this method after save, so the changes can be properly switched.
|
101
|
+
#
|
102
|
+
# Example:
|
103
|
+
#
|
104
|
+
# <tt>person.move_changes</tt>
|
105
|
+
def move_changes
|
106
|
+
@previous_modifications = modifications.dup
|
107
|
+
@modifications = {}
|
108
|
+
end
|
109
|
+
|
110
|
+
# Gets all the modifications that have happened to the object before the
|
111
|
+
# object was saved.
|
112
|
+
#
|
113
|
+
# Example:
|
114
|
+
#
|
115
|
+
# person = Person.new(:title => "Sir")
|
116
|
+
# person.title = "Madam"
|
117
|
+
# person.save!
|
118
|
+
# person.previous_changes # returns { "title" => [ "Sir", "Madam" ] }
|
119
|
+
#
|
120
|
+
# Returns:
|
121
|
+
#
|
122
|
+
# A +Hash+ of changes before save.
|
123
|
+
def previous_changes
|
124
|
+
@previous_modifications
|
125
|
+
end
|
126
|
+
|
127
|
+
# Resets a changed field back to its old value.
|
128
|
+
#
|
129
|
+
# Example:
|
130
|
+
#
|
131
|
+
# person = Person.new(:title => "Sir")
|
132
|
+
# person.title = "Madam"
|
133
|
+
# person.reset_attribute!("title")
|
134
|
+
# person.title # "Sir"
|
135
|
+
#
|
136
|
+
# Returns:
|
137
|
+
#
|
138
|
+
# The old field value.
|
139
|
+
def reset_attribute!(name)
|
140
|
+
value = attribute_was(name)
|
141
|
+
if value
|
142
|
+
@attributes[name] = value
|
143
|
+
modifications.delete(name)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Sets up the modifications hash. This occurs just after the document is
|
148
|
+
# instantiated.
|
149
|
+
#
|
150
|
+
# Example:
|
151
|
+
#
|
152
|
+
# <tt>document.setup_notifications</tt>
|
153
|
+
def setup_modifications
|
154
|
+
@accessed ||= {}
|
155
|
+
@modifications ||= {}
|
156
|
+
@previous_modifications ||= {}
|
157
|
+
end
|
158
|
+
|
159
|
+
# Reset all modifications for the document. This will wipe all the marked
|
160
|
+
# changes, but not reset the values.
|
161
|
+
#
|
162
|
+
# Example:
|
163
|
+
#
|
164
|
+
# <tt>document.reset_modifications</tt>
|
165
|
+
def reset_modifications
|
166
|
+
@accessed = {}
|
167
|
+
@modifications = {}
|
168
|
+
end
|
169
|
+
|
170
|
+
protected
|
171
|
+
|
172
|
+
# Audit the original value for a field that can be modified in place.
|
173
|
+
#
|
174
|
+
# Example:
|
175
|
+
#
|
176
|
+
# <tt>person.accessed("aliases", [ "007" ])</tt>
|
177
|
+
def accessed(name, value)
|
178
|
+
@accessed ||= {}
|
179
|
+
@accessed[name] = value.dup if (value.is_a?(Array) || value.is_a?(Hash)) && !@accessed.has_key?(name)
|
180
|
+
value
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get all normal modifications plus in place potential changes.
|
184
|
+
# Also checks changes in array attributes with +Element+ objects.
|
185
|
+
#
|
186
|
+
# Example:
|
187
|
+
#
|
188
|
+
# <tt>person.modifications</tt>
|
189
|
+
#
|
190
|
+
# Returns:
|
191
|
+
#
|
192
|
+
# All changes to the document.
|
193
|
+
def modifications
|
194
|
+
@accessed.each_pair do |field, value|
|
195
|
+
current = @attributes[field]
|
196
|
+
if current != value || (current.is_a?(Array) &&
|
197
|
+
current.any? { |v| v.respond_to?(:changed?) && v.changed? })
|
198
|
+
@modifications[field] = [ value, current ]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
@accessed.clear
|
202
|
+
@modifications
|
203
|
+
end
|
204
|
+
|
205
|
+
# Audit the change of a field's value.
|
206
|
+
#
|
207
|
+
# Example:
|
208
|
+
#
|
209
|
+
# <tt>person.modify("name", "Jack", "John")</tt>
|
210
|
+
def modify(name, old_value, new_value)
|
211
|
+
@attributes[name] = new_value
|
212
|
+
if @modifications && (old_value != new_value)
|
213
|
+
original = @modifications[name].first if @modifications[name]
|
214
|
+
@modifications[name] = [ (original || old_value), new_value ]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
module ClassMethods #:nodoc:
|
219
|
+
# Add the dynamic dirty methods. These are custom methods defined on a
|
220
|
+
# field by field basis that wrap the dirty attribute methods.
|
221
|
+
#
|
222
|
+
# Example:
|
223
|
+
#
|
224
|
+
# person = Person.new(:title => "Sir")
|
225
|
+
# person.title = "Madam"
|
226
|
+
# person.title_change # [ "Sir", "Madam" ]
|
227
|
+
# person.title_changed? # true
|
228
|
+
# person.title_was # "Sir"
|
229
|
+
# person.reset_title!
|
230
|
+
def add_dirty_methods(name)
|
231
|
+
name = name.to_s
|
232
|
+
define_method("#{name}_change") { attribute_change(name) } unless instance_methods.include?("#{name}_change")
|
233
|
+
define_method("#{name}_changed?") { attribute_changed?(name) } unless instance_methods.include?("#{name}_changed?")
|
234
|
+
define_method("#{name}_was") { attribute_was(name) } unless instance_methods.include?("#{name}_was")
|
235
|
+
define_method("reset_#{name}!") { reset_attribute!(name) } unless instance_methods.include?("reset_#{name}!")
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|