pluginaweek-has_messages 0.4.0

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