has_messages 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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