pluginaweek-has_messages 0.4.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/CHANGELOG.rdoc ADDED
@@ -0,0 +1,59 @@
1
+ == master
2
+
3
+ == 0.4.0 / 2009-04-19
4
+
5
+ * Add dependency on Rails 2.3
6
+ * Add dependency on plugin_test_helper 0.3.0
7
+ * Add dependency on state_machine 0.7.0
8
+
9
+ == 0.3.1 / 2009-01-11
10
+
11
+ * Use a state machine for the hidden_at field on Message/MessageRecipient
12
+ * Add dependency on state_machine 0.5.0
13
+
14
+ == 0.3.0 / 2008-12-14
15
+
16
+ * Remove the PluginAWeek namespace
17
+ * Rename Message#hide!/unhide! to Message#hide/unhide and MessageRecipient#hide!/unhide! to MessageRecipient#hide/unhide
18
+
19
+ == 0.2.0 / 2008-10-26
20
+
21
+ * Add mass-assignment protection in the Message/MessageRecipient models
22
+ * Change how the base module is included to prevent namespacing conflicts
23
+
24
+ == 0.1.3 / 2008-09-07
25
+
26
+ * Add dependency on state_machine 0.3.0
27
+
28
+ == 0.1.2 / 2008-06-29
29
+
30
+ * Add compatibility with state_machine 0.2.0
31
+
32
+ == 0.1.1 / 2008-06-22
33
+
34
+ * Remove log files from gems
35
+
36
+ == 0.1.0 / 2008-05-05
37
+
38
+ * Remove dependency on acts_as_list
39
+ * Update to reflect changes from has_states to state_machine
40
+ * Introduce an ActionMailer-type api for adding the various types of recipients
41
+ * Update migrations to support Rails 2.0 syntax
42
+ * Replace has_finder dependency with named_scope
43
+ * Remove has_messages helper
44
+ * Update documentation
45
+
46
+ == 0.0.1 / 2007-09-26
47
+
48
+ * Add state_changes fixtures for tests
49
+ * Use has_finder for tracking the deletion status of Messages/MessageRecipients
50
+ * Message#forward and Message#reply should create new instances of the current class, not always Message
51
+ * Add type field to MessageRecipient table
52
+ * Allow message senders to not be model recipients in third-party plugins
53
+ * Remove old references to Message::StateExtension
54
+ * Update with dependency on custom_callbacks
55
+ * Add documentation
56
+ * Convert dos newlines to unix newlines
57
+ * Move ReceiverMessage state into the MessageRecipient class
58
+ * Moved Build extensions out of the MessageRecipient class
59
+ * Fix determining whether classes exist in has_messages
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006-2009 Aaron Pfeifer
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.rdoc ADDED
@@ -0,0 +1,101 @@
1
+ = has_messages
2
+
3
+ +has_messages+ demonstrates a reference implementation for sending messages between users.
4
+
5
+ == Resources
6
+
7
+ API
8
+
9
+ * http://api.pluginaweek.org/has_messages
10
+
11
+ Bugs
12
+
13
+ * http://pluginaweek.lighthouseapp.com/projects/13274-has_messages
14
+
15
+ Development
16
+
17
+ * http://github.com/pluginaweek/has_messages
18
+
19
+ Source
20
+
21
+ * git://github.com/pluginaweek/has_messages.git
22
+
23
+ == Description
24
+
25
+ Messaging between users is fairly common in web applications, especially those
26
+ that support social networking. Messaging doesn't necessarily need to be
27
+ between users, but can also act as a way for the web application to send notices
28
+ and other notifications to users.
29
+
30
+ Designing and building a framework that supports this can be complex and takes
31
+ away from the business focus. This plugin can help ease that process by
32
+ demonstrating a reference implementation of these features.
33
+
34
+ == Usage
35
+
36
+ === Adding message support
37
+
38
+ class User < ActiveRecord::Base
39
+ has_messages
40
+ end
41
+
42
+ This will build the following associations:
43
+ * +messages+
44
+ * +unsent_messages+
45
+ * +sent_messages+
46
+ * +received_messages+
47
+
48
+ If you have more specific needs, you can create the same associations manually
49
+ that +has_messages+ builds. See HasMessages::MacroMethods#has_messages
50
+ for more information about the asssociations that are generated from this macro.
51
+
52
+ === Creating new messages
53
+
54
+ message = user.messages.build
55
+ message.to user1, user2
56
+ message.subject = 'Hey!'
57
+ message.body = 'Does anyone want to go out tonight?'
58
+ message.deliver
59
+
60
+ === Replying to messages
61
+
62
+ reply = message.reply_to_all
63
+ reply.body = "I'd love to go out!"
64
+ reply.deliver
65
+
66
+ === Forwarding messages
67
+
68
+ forward = message.forward
69
+ forward.body = 'Interested?'
70
+ forward.deliver
71
+
72
+ === Processing messages asynchronously
73
+
74
+ In addition to delivering messages immediately, you can also *queue* messages so
75
+ that an external application processes and delivers them. This is especially
76
+ useful for messages that need to be sent outside of the confines of the application.
77
+
78
+ To queue messages for external processing, you can use the +queue+ event,
79
+ rather than +deliver+. This will indicate to any external processes that
80
+ the message is ready to be sent.
81
+
82
+ To process queued emails, you need an external cron job that checks and sends
83
+ them like so:
84
+
85
+ Message.with_state('queued').each do |message|
86
+ message.deliver
87
+ end
88
+
89
+ == Testing
90
+
91
+ Before you can run any tests, the following gem must be installed:
92
+ * plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper]
93
+
94
+ To run against a specific version of Rails:
95
+
96
+ rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
97
+
98
+ == Dependencies
99
+
100
+ * Rails 2.3 or later
101
+ * state_machine[http://github.com/pluginaweek/state_machine]
data/Rakefile ADDED
@@ -0,0 +1,97 @@
1
+ require 'rake/testtask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/contrib/sshpublisher'
5
+
6
+ spec = Gem::Specification.new do |s|
7
+ s.name = 'has_messages'
8
+ s.version = '0.4.0'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.summary = 'Demonstrates a reference implementation for sending messages between users in ActiveRecord'
11
+ s.description = s.summary
12
+
13
+ s.files = FileList['{app,db,lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
14
+ s.require_path = 'lib'
15
+ s.has_rdoc = true
16
+ s.test_files = Dir['test/**/*_test.rb']
17
+ s.add_dependency 'state_machine', '>= 0.7.0'
18
+
19
+ s.author = 'Aaron Pfeifer'
20
+ s.email = 'aaron@pluginaweek.org'
21
+ s.homepage = 'http://www.pluginaweek.org'
22
+ s.rubyforge_project = 'pluginaweek'
23
+ end
24
+
25
+ desc 'Default: run all tests.'
26
+ task :default => :test
27
+
28
+ desc "Test the #{spec.name} plugin."
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.test_files = spec.test_files
32
+ t.verbose = true
33
+ end
34
+
35
+ begin
36
+ require 'rcov/rcovtask'
37
+ namespace :test do
38
+ desc "Test the #{spec.name} plugin with Rcov."
39
+ Rcov::RcovTask.new(:rcov) do |t|
40
+ t.libs << 'lib'
41
+ t.test_files = spec.test_files
42
+ t.rcov_opts << '--exclude="^(?!lib/|app/)"'
43
+ t.verbose = true
44
+ end
45
+ end
46
+ rescue LoadError
47
+ end
48
+
49
+ desc "Generate documentation for the #{spec.name} plugin."
50
+ Rake::RDocTask.new(:rdoc) do |rdoc|
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = spec.name
53
+ rdoc.template = '../rdoc_template.rb'
54
+ rdoc.options << '--line-numbers' << '--inline-source'
55
+ rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb', 'app/**/*.rb')
56
+ end
57
+
58
+ desc 'Generate a gemspec file.'
59
+ task :gemspec do
60
+ File.open("#{spec.name}.gemspec", 'w') do |f|
61
+ f.write spec.to_ruby
62
+ end
63
+ end
64
+
65
+ Rake::GemPackageTask.new(spec) do |p|
66
+ p.gem_spec = spec
67
+ p.need_tar = true
68
+ p.need_zip = true
69
+ end
70
+
71
+ desc 'Publish the beta gem.'
72
+ task :pgem => [:package] do
73
+ Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
74
+ end
75
+
76
+ desc 'Publish the API documentation.'
77
+ task :pdoc => [:rdoc] do
78
+ Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
79
+ end
80
+
81
+ desc 'Publish the API docs and gem'
82
+ task :publish => [:pgem, :pdoc, :release]
83
+
84
+ desc 'Publish the release files to RubyForge.'
85
+ task :release => [:gem, :package] do
86
+ require 'rubyforge'
87
+
88
+ ruby_forge = RubyForge.new.configure
89
+ ruby_forge.login
90
+
91
+ %w(gem tgz zip).each do |ext|
92
+ file = "pkg/#{spec.name}-#{spec.version}.#{ext}"
93
+ puts "Releasing #{File.basename(file)}..."
94
+
95
+ ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
96
+ end
97
+ end
@@ -0,0 +1,145 @@
1
+ # Represents a message sent from one user to one or more others.
2
+ #
3
+ # == States
4
+ #
5
+ # Messages can be in 1 of 3 states:
6
+ # * +unsent+ - The message has not yet been sent. This is the *initial* state.
7
+ # * +queued+ - The message has been queued for future delivery.
8
+ # * +sent+ - The message has been sent.
9
+ #
10
+ # == Interacting with the message
11
+ #
12
+ # In order to perform actions on the message, such as queueing or delivering,
13
+ # you should always use the associated event action:
14
+ # * +queue+ - Queues the message so that you can send it in a separate process
15
+ # * +deliver+ - Sends the message to all of the recipients
16
+ #
17
+ # == Message visibility
18
+ #
19
+ # Although you can delete a message, it will also delete it from the inbox of all
20
+ # the message's recipients. Instead, you can change the *visibility* of the
21
+ # message. Messages have 1 of 2 states that define its visibility:
22
+ # * +visible+ - The message is visible to the sender
23
+ # * +hidden+ - The message is hidden from the sender
24
+ #
25
+ # The visibility of a message can be changed by running the associated action:
26
+ # * +hide+ -Hides the message from the sender
27
+ # * +unhide+ - Makes the message visible again
28
+ class Message < ActiveRecord::Base
29
+ belongs_to :sender, :polymorphic => true
30
+ has_many :recipients, :class_name => 'MessageRecipient', :order => 'kind DESC, position ASC', :dependent => :destroy
31
+
32
+ validates_presence_of :state, :sender_id, :sender_type
33
+
34
+ attr_accessible :subject, :body, :to, :cc, :bcc
35
+
36
+ after_save :update_recipients
37
+
38
+ named_scope :visible, :conditions => {:hidden_at => nil}
39
+
40
+ # Define actions for the message
41
+ state_machine :state, :initial => :unsent do
42
+ # Queues the message so that it's sent in a separate process
43
+ event :queue do
44
+ transition :unsent => :queued, :if => :has_recipients?
45
+ end
46
+
47
+ # Sends the message to all of the recipients as long as at least one
48
+ # recipient has been added
49
+ event :deliver do
50
+ transition [:unsent, :queued] => :sent, :if => :has_recipients?
51
+ end
52
+ end
53
+
54
+ # Defines actions for the visibility of the message
55
+ state_machine :hidden_at, :initial => :visible do
56
+ # Hides the message from the recipient's inbox
57
+ event :hide do
58
+ transition all => :hidden
59
+ end
60
+
61
+ # Makes the message visible in the recipient's inbox
62
+ event :unhide do
63
+ transition all => :visible
64
+ end
65
+
66
+ state :visible, :value => nil
67
+ state :hidden, :value => lambda {Time.now}, :if => lambda {|value| value}
68
+ end
69
+
70
+ # Directly adds the receivers on the message (i.e. they are visible to all recipients)
71
+ def to(*receivers)
72
+ receivers(receivers, 'to')
73
+ end
74
+ alias_method :to=, :to
75
+
76
+ # Carbon copies the receivers on the message
77
+ def cc(*receivers)
78
+ receivers(receivers, 'cc')
79
+ end
80
+ alias_method :cc=, :cc
81
+
82
+ # Blind carbon copies the receivers on the message
83
+ def bcc(*receivers)
84
+ receivers(receivers, 'bcc')
85
+ end
86
+ alias_method :bcc=, :bcc
87
+
88
+ # Forwards this message, including the original subject and body in the new
89
+ # message
90
+ def forward
91
+ message = self.class.new(:subject => subject, :body => body)
92
+ message.sender = sender
93
+ message
94
+ end
95
+
96
+ # Replies to this message, including the original subject and body in the new
97
+ # message. Only the original direct receivers are added to the reply.
98
+ def reply
99
+ message = self.class.new(:subject => subject, :body => body)
100
+ message.sender = sender
101
+ message.to(to)
102
+ message
103
+ end
104
+
105
+ # Replies to all recipients on this message, including the original subject
106
+ # and body in the new message. All receivers (direct, cc, and bcc) are added
107
+ # to the reply.
108
+ def reply_to_all
109
+ message = reply
110
+ message.cc(cc)
111
+ message.bcc(bcc)
112
+ message
113
+ end
114
+
115
+ private
116
+ # Create/destroy any receivers that were added/removed
117
+ def update_recipients
118
+ if @receivers
119
+ @receivers.each do |kind, receivers|
120
+ kind_recipients = recipients.select {|recipient| recipient.kind == kind}
121
+ new_receivers = receivers - kind_recipients.map(&:receiver)
122
+ removed_recipients = kind_recipients.reject {|recipient| receivers.include?(recipient.receiver)}
123
+
124
+ recipients.delete(*removed_recipients) if removed_recipients.any?
125
+ new_receivers.each {|receiver| self.recipients.create!(:receiver => receiver, :kind => kind)}
126
+ end
127
+
128
+ @receivers = nil
129
+ end
130
+ end
131
+
132
+ # Does this message have any recipients on it?
133
+ def has_recipients?
134
+ (to + cc + bcc).any?
135
+ end
136
+
137
+ # Creates new receivers or gets the current receivers for the given kind (to, cc, or bcc)
138
+ def receivers(receivers, kind)
139
+ if receivers.any?
140
+ (@receivers ||= {})[kind] = receivers.flatten.compact
141
+ else
142
+ @receivers && @receivers[kind] || recipients.select {|recipient| recipient.kind == kind}.map(&:receiver)
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,120 @@
1
+ # Represents a recipient on a message. The kind of recipient (to, cc, or bcc) is
2
+ # determined by the +kind+ attribute.
3
+ #
4
+ # == States
5
+ #
6
+ # Recipients can be in 1 of 2 states:
7
+ # * +unread+ - The message has been sent, but not yet read by the recipient. This is the *initial* state.
8
+ # * +read+ - The message has been read by the recipient
9
+ #
10
+ # == Interacting with the message
11
+ #
12
+ # In order to perform actions on the message, such as viewing, you should always
13
+ # use the associated event action:
14
+ # * +view+ - Marks the message as read by the recipient
15
+ #
16
+ # == Hiding messages
17
+ #
18
+ # Although you can delete a recipient, it will also delete it from everyone else's
19
+ # message, meaning that no one will know that person was ever a recipient of the
20
+ # message. Instead, you can change the *visibility* of the message. Messages
21
+ # have 1 of 2 states that define its visibility:
22
+ # * +visible+ - The message is visible to the recipient
23
+ # * +hidden+ - The message is hidden from the recipient
24
+ #
25
+ # The visibility of a message can be changed by running the associated action:
26
+ # * +hide+ -Hides the message from the recipient
27
+ # * +unhide+ - Makes the message visible again
28
+ class MessageRecipient < ActiveRecord::Base
29
+ belongs_to :message
30
+ belongs_to :receiver, :polymorphic => true
31
+
32
+ validates_presence_of :message_id, :kind, :state, :receiver_id, :receiver_type
33
+
34
+ attr_protected :state, :position, :hidden_at
35
+
36
+ before_create :set_position
37
+ before_destroy :reorder_positions
38
+
39
+ # Make this class look like the actual message
40
+ delegate :sender, :subject, :body, :recipients, :to, :cc, :bcc, :created_at,
41
+ :to => :message
42
+
43
+ named_scope :visible, :conditions => {:hidden_at => nil}
44
+
45
+ # Defines actions for the recipient
46
+ state_machine :state, :initial => :unread do
47
+ # Indicates that the message has been viewed by the receiver
48
+ event :view do
49
+ transition :unread => :read, :if => :message_sent?
50
+ end
51
+ end
52
+
53
+ # Defines actions for the visibility of the message to the recipient
54
+ state_machine :hidden_at, :initial => :visible do
55
+ # Hides the message from the recipient's inbox
56
+ event :hide do
57
+ transition all => :hidden
58
+ end
59
+
60
+ # Makes the message visible in the recipient's inbox
61
+ event :unhide do
62
+ transition all => :visible
63
+ end
64
+
65
+ state :visible, :value => nil
66
+ state :hidden, :value => lambda {Time.now}, :if => lambda {|value| value}
67
+ end
68
+
69
+ # Forwards this message, including the original subject and body in the new
70
+ # message
71
+ def forward
72
+ message = self.message.class.new(:subject => subject, :body => body)
73
+ message.sender = receiver
74
+ message
75
+ end
76
+
77
+ # Replies to this message, including the original subject and body in the new
78
+ # message. Only the original direct receivers are added to the reply.
79
+ def reply
80
+ message = self.message.class.new(:subject => subject, :body => body)
81
+ message.sender = receiver
82
+ message.to(sender)
83
+ message
84
+ end
85
+
86
+ # Replies to all recipients on this message, including the original subject
87
+ # and body in the new message. All receivers (sender, direct, cc, and bcc) are
88
+ # added to the reply.
89
+ def reply_to_all
90
+ message = reply
91
+ message.to(to - [receiver] + [sender])
92
+ message.cc(cc - [receiver])
93
+ message.bcc(bcc - [receiver])
94
+ message
95
+ end
96
+
97
+ private
98
+ # Has the message this recipient is on been sent?
99
+ def message_sent?
100
+ message.sent?
101
+ end
102
+
103
+ # Sets the position of the current recipient based on existing recipients
104
+ def set_position
105
+ if last_recipient = message.recipients.find(:first, :conditions => {:kind => kind}, :order => 'position DESC')
106
+ self.position = last_recipient.position + 1
107
+ else
108
+ self.position = 1
109
+ end
110
+ end
111
+
112
+ # Reorders the positions of the message's recipients
113
+ def reorder_positions
114
+ if position
115
+ position = self.position
116
+ update_attribute(:position, nil)
117
+ self.class.update_all('position = (position - 1)', ['message_id = ? AND kind = ? AND position > ?', message_id, kind, position])
118
+ end
119
+ end
120
+ end