has_messages 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/CHANGELOG +43 -0
- data/MIT-LICENSE +20 -0
- data/README +97 -0
- data/Rakefile +80 -0
- data/app/models/message.rb +141 -0
- data/app/models/message_recipient.rb +120 -0
- data/db/migrate/001_create_messages.rb +17 -0
- data/db/migrate/002_create_message_recipients.rb +17 -0
- data/init.rb +1 -0
- data/lib/has_messages.rb +75 -0
- data/test/app_root/app/models/user.rb +3 -0
- data/test/app_root/config/environment.rb +9 -0
- data/test/app_root/db/migrate/001_create_users.rb +11 -0
- data/test/app_root/db/migrate/002_migrate_has_messages_to_version_2.rb +9 -0
- data/test/factory.rb +47 -0
- data/test/functional/has_messages_test.rb +139 -0
- data/test/test_helper.rb +13 -0
- data/test/unit/message_recipient_test.rb +419 -0
- data/test/unit/message_test.rb +431 -0
- metadata +91 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
*SVN*
|
2
|
+
|
3
|
+
*0.1.0* (May 5th, 2008)
|
4
|
+
|
5
|
+
* Remove dependency on acts_as_list
|
6
|
+
|
7
|
+
* Update to reflect changes from has_states to state_machine
|
8
|
+
|
9
|
+
* Introduce an ActionMailer-type api for adding the various types of recipients
|
10
|
+
|
11
|
+
* Update migrations to support Rails 2.0 syntax
|
12
|
+
|
13
|
+
* Replace has_finder dependency with named_scope
|
14
|
+
|
15
|
+
* Remove has_messages helper
|
16
|
+
|
17
|
+
* Update documentation
|
18
|
+
|
19
|
+
*0.0.1* (September 26th, 2007)
|
20
|
+
|
21
|
+
* Add state_changes fixtures for tests
|
22
|
+
|
23
|
+
* Use has_finder for tracking the deletion status of Messages/MessageRecipients
|
24
|
+
|
25
|
+
* Message#forward and Message#reply should create new instances of the current class, not always Message
|
26
|
+
|
27
|
+
* Add type field to MessageRecipient table
|
28
|
+
|
29
|
+
* Allow message senders to not be model recipients in third-party plugins
|
30
|
+
|
31
|
+
* Remove old references to Message::StateExtension
|
32
|
+
|
33
|
+
* Update with dependency on custom_callbacks
|
34
|
+
|
35
|
+
* Add documentation
|
36
|
+
|
37
|
+
* Convert dos newlines to unix newlines
|
38
|
+
|
39
|
+
* Move ReceiverMessage state into the MessageRecipient class
|
40
|
+
|
41
|
+
* Moved Build extensions out of the MessageRecipient class
|
42
|
+
|
43
|
+
* Fix determining whether classes exist in has_messages
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006-2008 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
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
= has_messages
|
2
|
+
|
3
|
+
+has_messages+ demonstrates a reference implementation for sending messages between users.
|
4
|
+
|
5
|
+
== Resources
|
6
|
+
|
7
|
+
Wiki
|
8
|
+
|
9
|
+
* http://wiki.pluginaweek.org/Has_messages
|
10
|
+
|
11
|
+
API
|
12
|
+
|
13
|
+
* http://api.pluginaweek.org/has_messages
|
14
|
+
|
15
|
+
Development
|
16
|
+
|
17
|
+
* http://dev.pluginaweek.org/browser/trunk/has_messages
|
18
|
+
|
19
|
+
Source
|
20
|
+
|
21
|
+
* http://svn.pluginaweek.org/trunk/has_messages
|
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 demonstrating
|
32
|
+
a complete 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 specified needs, you can create the same associations manually
|
49
|
+
that +has_messages+ builds. See PluginAWeek::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 <tt>queue!</tt> event,
|
79
|
+
rather than <tt>deliver!</tt>. 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://wiki.pluginaweek.org/Plugin_test_helper]
|
93
|
+
|
94
|
+
== Dependencies
|
95
|
+
|
96
|
+
* plugins_plus[http://wiki.pluginaweek.org/Plugins_plus]
|
97
|
+
* state_machine[http://wiki.pluginaweek.org/State_machine]
|
data/Rakefile
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/sshpublisher'
|
5
|
+
|
6
|
+
PKG_NAME = 'has_messages'
|
7
|
+
PKG_VERSION = '0.1.0'
|
8
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
9
|
+
RUBY_FORGE_PROJECT = 'pluginaweek'
|
10
|
+
|
11
|
+
desc 'Default: run unit tests.'
|
12
|
+
task :default => :test
|
13
|
+
|
14
|
+
desc 'Test the has_messages plugin.'
|
15
|
+
Rake::TestTask.new(:test) do |t|
|
16
|
+
t.libs << 'lib'
|
17
|
+
t.pattern = 'test/**/*_test.rb'
|
18
|
+
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Generate documentation for the has_messages plugin.'
|
22
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
23
|
+
rdoc.rdoc_dir = 'rdoc'
|
24
|
+
rdoc.title = 'HasMessages'
|
25
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
26
|
+
rdoc.rdoc_files.include('README')
|
27
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
28
|
+
end
|
29
|
+
|
30
|
+
spec = Gem::Specification.new do |s|
|
31
|
+
s.name = PKG_NAME
|
32
|
+
s.version = PKG_VERSION
|
33
|
+
s.platform = Gem::Platform::RUBY
|
34
|
+
s.summary = 'Demonstrates a reference implementation for sending messages between users.'
|
35
|
+
|
36
|
+
s.files = FileList['{app,db,lib,test}/**/*'].to_a + %w(CHANGELOG init.rb MIT-LICENSE Rakefile README)
|
37
|
+
s.require_path = 'lib'
|
38
|
+
s.autorequire = 'has_messages'
|
39
|
+
s.has_rdoc = true
|
40
|
+
s.test_files = Dir['test/**/*_test.rb']
|
41
|
+
s.add_dependency 'state_machine', '>= 0.1.0'
|
42
|
+
|
43
|
+
s.author = 'Aaron Pfeifer'
|
44
|
+
s.email = 'aaron@pluginaweek.org'
|
45
|
+
s.homepage = 'http://www.pluginaweek.org'
|
46
|
+
end
|
47
|
+
|
48
|
+
Rake::GemPackageTask.new(spec) do |p|
|
49
|
+
p.gem_spec = spec
|
50
|
+
p.need_tar = true
|
51
|
+
p.need_zip = true
|
52
|
+
end
|
53
|
+
|
54
|
+
desc 'Publish the beta gem'
|
55
|
+
task :pgem => [:package] do
|
56
|
+
Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{PKG_FILE_NAME}.gem").upload
|
57
|
+
end
|
58
|
+
|
59
|
+
desc 'Publish the API documentation'
|
60
|
+
task :pdoc => [:rdoc] do
|
61
|
+
Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{PKG_NAME}", 'rdoc').upload
|
62
|
+
end
|
63
|
+
|
64
|
+
desc 'Publish the API docs and gem'
|
65
|
+
task :publish => [:pdoc, :release]
|
66
|
+
|
67
|
+
desc 'Publish the release files to RubyForge.'
|
68
|
+
task :release => [:gem, :package] do
|
69
|
+
require 'rubyforge'
|
70
|
+
|
71
|
+
ruby_forge = RubyForge.new
|
72
|
+
ruby_forge.login
|
73
|
+
|
74
|
+
%w( gem tgz zip ).each do |ext|
|
75
|
+
file = "pkg/#{PKG_FILE_NAME}.#{ext}"
|
76
|
+
puts "Releasing #{File.basename(file)}..."
|
77
|
+
|
78
|
+
ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,141 @@
|
|
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
|
+
# == Hiding messages
|
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 hide messages from users with the
|
21
|
+
# following actions:
|
22
|
+
# * +hide!+ -Hides the message from the sender's inbox
|
23
|
+
# * +unhide!+ - Makes the message visible again
|
24
|
+
class Message < ActiveRecord::Base
|
25
|
+
belongs_to :sender,
|
26
|
+
:polymorphic => true
|
27
|
+
has_many :recipients,
|
28
|
+
:class_name => 'MessageRecipient',
|
29
|
+
:order => 'kind DESC, position ASC',
|
30
|
+
:dependent => :destroy
|
31
|
+
|
32
|
+
validates_presence_of :state,
|
33
|
+
:sender_id,
|
34
|
+
:sender_type
|
35
|
+
|
36
|
+
after_save :update_recipients
|
37
|
+
|
38
|
+
named_scope :visible,
|
39
|
+
:conditions => {:hidden_at => nil}
|
40
|
+
|
41
|
+
# Define actions for the message
|
42
|
+
state_machine :state, :initial => 'unsent' do
|
43
|
+
# Queues the message so that it's sent in a separate process
|
44
|
+
event :queue do
|
45
|
+
transition :to => 'queued', :from => 'unsent', :if => :has_recipients?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sends the message to all of the recipients as long as at least one
|
49
|
+
# recipient has been added
|
50
|
+
event :deliver do
|
51
|
+
transition :to => 'sent', :from => %w(unsent queued), :if => :has_recipients?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Directly adds the receivers on the message (i.e. they are visible to all recipients)
|
56
|
+
def to(*receivers)
|
57
|
+
receivers(receivers, 'to')
|
58
|
+
end
|
59
|
+
alias_method :to=, :to
|
60
|
+
|
61
|
+
# Carbon copies the receivers on the message
|
62
|
+
def cc(*receivers)
|
63
|
+
receivers(receivers, 'cc')
|
64
|
+
end
|
65
|
+
alias_method :cc=, :cc
|
66
|
+
|
67
|
+
# Blind carbon copies the receivers on the message
|
68
|
+
def bcc(*receivers)
|
69
|
+
receivers(receivers, 'bcc')
|
70
|
+
end
|
71
|
+
alias_method :bcc=, :bcc
|
72
|
+
|
73
|
+
# Forwards this message
|
74
|
+
def forward
|
75
|
+
message = self.class.new(:subject => subject, :body => body)
|
76
|
+
message.sender = sender
|
77
|
+
message
|
78
|
+
end
|
79
|
+
|
80
|
+
# Replies to this message
|
81
|
+
def reply
|
82
|
+
message = self.class.new(:subject => subject, :body => body)
|
83
|
+
message.sender = sender
|
84
|
+
message.to(to)
|
85
|
+
message
|
86
|
+
end
|
87
|
+
|
88
|
+
# Replies to all recipients on this message
|
89
|
+
def reply_to_all
|
90
|
+
message = reply
|
91
|
+
message.cc(cc)
|
92
|
+
message.bcc(bcc)
|
93
|
+
message
|
94
|
+
end
|
95
|
+
|
96
|
+
# Hides the message from the sender's inbox
|
97
|
+
def hide!
|
98
|
+
update_attribute(:hidden_at, Time.now)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Makes the message visible in the sender's inbox
|
102
|
+
def unhide!
|
103
|
+
update_attribute(:hidden_at, nil)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Is this message still hidden from the sender's inbox?
|
107
|
+
def hidden?
|
108
|
+
hidden_at?
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
# Create/destroy any receivers that were added/removed
|
113
|
+
def update_recipients
|
114
|
+
if @receivers
|
115
|
+
@receivers.each do |kind, receivers|
|
116
|
+
kind_recipients = recipients.select {|recipient| recipient.kind == kind}
|
117
|
+
new_receivers = receivers - kind_recipients.map(&:receiver)
|
118
|
+
removed_recipients = kind_recipients.reject {|recipient| receivers.include?(recipient.receiver)}
|
119
|
+
|
120
|
+
recipients.delete(*removed_recipients) if removed_recipients.any?
|
121
|
+
new_receivers.each {|receiver| self.recipients.create!(:receiver => receiver, :kind => kind)}
|
122
|
+
end
|
123
|
+
|
124
|
+
@receivers = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Does this message have any recipients on it?
|
129
|
+
def has_recipients?
|
130
|
+
(to + cc + bcc).any?
|
131
|
+
end
|
132
|
+
|
133
|
+
# Creates new receivers or gets the current receivers for the given kind (to, cc, or bcc)
|
134
|
+
def receivers(receivers, kind)
|
135
|
+
if receivers.any?
|
136
|
+
(@receivers ||= {})[kind] = receivers.flatten.compact
|
137
|
+
else
|
138
|
+
@receivers && @receivers[kind] || recipients.select {|recipient| recipient.kind == kind}.map(&:receiver)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
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 hide messages from users with the following actions:
|
21
|
+
# * +hide!+ -Hides the message from the recipient's inbox
|
22
|
+
# * +unhide!+ - Makes the message visible again
|
23
|
+
class MessageRecipient < ActiveRecord::Base
|
24
|
+
belongs_to :message
|
25
|
+
belongs_to :receiver,
|
26
|
+
:polymorphic => true
|
27
|
+
|
28
|
+
validates_presence_of :message_id,
|
29
|
+
:kind,
|
30
|
+
:state,
|
31
|
+
:receiver_id,
|
32
|
+
:receiver_type
|
33
|
+
|
34
|
+
before_create :set_position
|
35
|
+
before_destroy :reorder_positions
|
36
|
+
|
37
|
+
# Make this class look like the actual message
|
38
|
+
delegate :sender,
|
39
|
+
:subject,
|
40
|
+
:body,
|
41
|
+
:recipients,
|
42
|
+
:to,
|
43
|
+
:cc,
|
44
|
+
:bcc,
|
45
|
+
:created_at,
|
46
|
+
:to => :message
|
47
|
+
|
48
|
+
named_scope :visible,
|
49
|
+
:conditions => {:hidden_at => nil}
|
50
|
+
|
51
|
+
state_machine :state, :initial => 'unread' do
|
52
|
+
# Indicates that the message has been viewed by the receiver
|
53
|
+
event :view do
|
54
|
+
transition :to => 'read', :from => 'unread', :if => :message_sent?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Forwards the message
|
59
|
+
def forward
|
60
|
+
message = self.message.class.new(:subject => subject, :body => body)
|
61
|
+
message.sender = receiver
|
62
|
+
message
|
63
|
+
end
|
64
|
+
|
65
|
+
# Replies to the message
|
66
|
+
def reply
|
67
|
+
message = self.message.class.new(:subject => subject, :body => body)
|
68
|
+
message.sender = receiver
|
69
|
+
message.to(sender)
|
70
|
+
message
|
71
|
+
end
|
72
|
+
|
73
|
+
# Replies to all recipients on the message, including the original sender
|
74
|
+
def reply_to_all
|
75
|
+
message = reply
|
76
|
+
message.to(to - [receiver] + [sender])
|
77
|
+
message.cc(cc - [receiver])
|
78
|
+
message.bcc(bcc - [receiver])
|
79
|
+
message
|
80
|
+
end
|
81
|
+
|
82
|
+
# Hides the message from the recipient's inbox
|
83
|
+
def hide!
|
84
|
+
update_attribute(:hidden_at, Time.now)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Makes the message visible in the recipient's inbox
|
88
|
+
def unhide!
|
89
|
+
update_attribute(:hidden_at, nil)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Is this message still hidden from the recipient's inbox?
|
93
|
+
def hidden?
|
94
|
+
hidden_at?
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
# Has the message this recipient is on been sent?
|
99
|
+
def message_sent?
|
100
|
+
message.state == '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
|