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 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