has_messages_huacnlee 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,112 @@
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://rdoc.info/projects/pluginaweek/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
+ === Installation
37
+
38
+ +has_messages+ requires additional database tables to work. You can generate
39
+ a migration for these tables like so:
40
+
41
+ rails g has_messages
42
+
43
+ Then simply migrate your database:
44
+
45
+ rake db:migrate
46
+
47
+ === Adding message support
48
+
49
+ class User < ActiveRecord::Base
50
+ has_messages
51
+ end
52
+
53
+ This will build the following associations:
54
+ * +messages+
55
+ * +unsent_messages+
56
+ * +sent_messages+
57
+ * +received_messages+
58
+
59
+ If you have more specific needs, you can create the same associations manually
60
+ that +has_messages+ builds. See HasMessages::MacroMethods#has_messages
61
+ for more information about the asssociations that are generated from this macro.
62
+
63
+ === Creating new messages
64
+
65
+ message = user.messages.build
66
+ message.to user1, user2
67
+ message.subject = 'Hey!'
68
+ message.body = 'Does anyone want to go out tonight?'
69
+ message.deliver
70
+
71
+ === Replying to messages
72
+
73
+ reply = message.reply_to_all
74
+ reply.body = "I'd love to go out!"
75
+ reply.deliver
76
+
77
+ === Forwarding messages
78
+
79
+ forward = message.forward
80
+ forward.body = 'Interested?'
81
+ forward.deliver
82
+
83
+ === Processing messages asynchronously
84
+
85
+ In addition to delivering messages immediately, you can also *queue* messages so
86
+ that an external application processes and delivers them. This is especially
87
+ useful for messages that need to be sent outside of the confines of the application.
88
+
89
+ To queue messages for external processing, you can use the +queue+ event,
90
+ rather than +deliver+. This will indicate to any external processes that
91
+ the message is ready to be sent.
92
+
93
+ To process queued emails, you need an external cron job that checks and sends
94
+ them like so:
95
+
96
+ Message.with_state('queued').each do |message|
97
+ message.deliver
98
+ end
99
+
100
+ == Testing
101
+
102
+ Before you can run any tests, the following gem must be installed:
103
+ * plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper]
104
+
105
+ To run against a specific version of Rails:
106
+
107
+ rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
108
+
109
+ == Dependencies
110
+
111
+ * Rails 2.3 or later
112
+ * state_machine[http://github.com/pluginaweek/state_machine]
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'has_messages'
@@ -0,0 +1,5 @@
1
+ Usage:
2
+
3
+ rails g has_messages
4
+
5
+ This will create migrations that will add the proper tables to store messages.
@@ -0,0 +1,24 @@
1
+ class HasMessagesGenerator < Rails::Generators::Base
2
+ include Rails::Generators::Migration
3
+
4
+ def self.source_root
5
+ @source_root ||= File.expand_path('../templates', __FILE__)
6
+ end
7
+
8
+ # Implement the required interface for Rails::Generators::Migration.
9
+ # taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
10
+ def self.next_migration_number(dirname)
11
+ if ActiveRecord::Base.timestamped_migrations
12
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+ else
14
+ "%.3d" % (current_migration_number(dirname) + 1)
15
+ end
16
+ end
17
+
18
+ def generate_migration
19
+ migration_template "001_create_messages.rb", "db/migrate/create_messages.rb"
20
+ sleep(1)
21
+ migration_template "002_create_message_recipients.rb", "db/migrate/create_message_recipients.rb"
22
+ end
23
+
24
+ end
@@ -0,0 +1,17 @@
1
+ class CreateMessages < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :messages do |t|
4
+ t.references :sender, :polymorphic => true, :null => false
5
+ t.text :subject, :body
6
+ t.string :state, :null => false
7
+ t.datetime :hidden_at
8
+ t.string :type
9
+ t.belongs_to :original_message
10
+ t.timestamps
11
+ end
12
+ end
13
+
14
+ def self.down
15
+ drop_table :messages
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ class CreateMessageRecipients < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :message_recipients do |t|
4
+ t.references :message, :null => false
5
+ t.references :receiver, :polymorphic => true, :null => false
6
+ t.string :kind, :null => false
7
+ t.integer :position
8
+ t.string :state, :null => false
9
+ t.datetime :hidden_at
10
+ end
11
+ add_index :message_recipients, [:message_id, :kind, :position], :unique => true
12
+ end
13
+
14
+ def self.down
15
+ drop_table :message_recipients
16
+ end
17
+ end
@@ -0,0 +1,167 @@
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
+ belongs_to :original_message, :class_name => 'Message'
31
+ has_many :recipients, :class_name => 'MessageRecipient', :order => 'kind DESC', :dependent => :destroy
32
+ has_many :follow_up_messages, :class_name => 'Message', :foreign_key => 'original_message_id'
33
+
34
+ validates_presence_of :state, :sender_id, :sender_type
35
+
36
+ attr_accessible :subject, :body, :to, :cc, :bcc
37
+
38
+ after_save :update_recipients
39
+
40
+ scope :visible, :conditions => {:hidden_at => nil}
41
+
42
+ # Define actions for the message
43
+ state_machine :state, :initial => :unsent do
44
+ # Queues the message so that it's sent in a separate process
45
+ event :queue do
46
+ transition :unsent => :queued, :if => :has_recipients?
47
+ end
48
+
49
+ # Sends the message to all of the recipients as long as at least one
50
+ # recipient has been added
51
+ event :deliver do
52
+ transition [:unsent, :queued] => :sent, :if => :has_recipients?
53
+ end
54
+ end
55
+
56
+ # Defines actions for the visibility of the message
57
+ state_machine :hidden_at, :initial => :visible do
58
+ # Hides the message from the recipient's inbox
59
+ event :hide do
60
+ transition all => :hidden
61
+ end
62
+
63
+ # Makes the message visible in the recipient's inbox
64
+ event :unhide do
65
+ transition all => :visible
66
+ end
67
+
68
+ state :visible, :value => nil
69
+ state :hidden, :value => lambda {Time.now}, :if => lambda {|value| value}
70
+ end
71
+
72
+ def thread
73
+ [self] + follow_up_messages
74
+ end
75
+
76
+ def latest_message
77
+ follow_up_messages.last || self
78
+ end
79
+
80
+ # Directly adds the receivers on the message (i.e. they are visible to all recipients)
81
+ def to(*receivers)
82
+ receivers(receivers, 'to')
83
+ end
84
+ alias_method :to=, :to
85
+
86
+ # Carbon copies the receivers on the message
87
+ def cc(*receivers)
88
+ receivers(receivers, 'cc')
89
+ end
90
+ alias_method :cc=, :cc
91
+
92
+ # Blind carbon copies the receivers on the message
93
+ def bcc(*receivers)
94
+ receivers(receivers, 'bcc')
95
+ end
96
+ alias_method :bcc=, :bcc
97
+
98
+ # Forwards this message, including the original subject and body in the new
99
+ # message
100
+ def forward
101
+ message = self.class.new(:subject => subject, :body => body)
102
+ message.sender = sender
103
+ message
104
+ end
105
+
106
+ # Replies to this message, including the original subject and body in the new
107
+ # message. Only the original direct receivers are added to the reply.
108
+ def reply
109
+ message = self.class.new(:subject => subject, :body => body)
110
+ message.sender = sender
111
+ message.to(to)
112
+ # use the very first message to anchor all replies
113
+ if self.original_message
114
+ message.original_message = self.original_message
115
+ else
116
+ message.original_message = self
117
+ end
118
+ message
119
+ end
120
+
121
+ # Replies to all recipients on this message, including the original subject
122
+ # and body in the new message. All receivers (direct, cc, and bcc) are added
123
+ # to the reply.
124
+ def reply_to_all
125
+ message = reply
126
+ message.cc(cc)
127
+ message.bcc(bcc)
128
+ # use the very first message to anchor all replies
129
+ if self.original_message
130
+ message.original_message = self.original_message
131
+ else
132
+ message.original_message = self
133
+ end
134
+ message
135
+ end
136
+
137
+ private
138
+ # Create/destroy any receivers that were added/removed
139
+ def update_recipients
140
+ if @receivers
141
+ @receivers.each do |kind, receivers|
142
+ kind_recipients = recipients.select {|recipient| recipient.kind == kind}
143
+ new_receivers = receivers - kind_recipients.map(&:receiver)
144
+ removed_recipients = kind_recipients.reject {|recipient| receivers.include?(recipient.receiver)}
145
+
146
+ recipients.delete(*removed_recipients) if removed_recipients.any?
147
+ new_receivers.each {|receiver| self.recipients.create!(:receiver => receiver, :kind => kind)}
148
+ end
149
+
150
+ @receivers = nil
151
+ end
152
+ end
153
+
154
+ # Does this message have any recipients on it?
155
+ def has_recipients?
156
+ (to + cc + bcc).any?
157
+ end
158
+
159
+ # Creates new receivers or gets the current receivers for the given kind (to, cc, or bcc)
160
+ def receivers(receivers, kind)
161
+ if receivers.any?
162
+ (@receivers ||= {})[kind] = receivers.flatten.compact
163
+ else
164
+ @receivers && @receivers[kind] || recipients.select {|recipient| recipient.kind == kind}.map(&:receiver)
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,132 @@
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
+ 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
+ # use the very first message to anchor all replies
84
+ if self.message.original_message
85
+ message.original_message = self.message.original_message
86
+ else
87
+ message.original_message = self.message
88
+ end
89
+ message
90
+ end
91
+
92
+ # Replies to all recipients on this message, including the original subject
93
+ # and body in the new message. All receivers (sender, direct, cc, and bcc) are
94
+ # added to the reply.
95
+ def reply_to_all
96
+ message = reply
97
+ message.to( (to + [sender]).uniq )
98
+ message.cc(cc - [receiver])
99
+ message.bcc(bcc - [receiver])
100
+ # use the very first message to anchor all replies
101
+ if self.message.original_message
102
+ message.original_message = self.message.original_message
103
+ else
104
+ message.original_message = self.message
105
+ end
106
+ message
107
+ end
108
+
109
+ private
110
+ # Has the message this recipient is on been sent?
111
+ def message_sent?
112
+ message.sent?
113
+ end
114
+
115
+ # Sets the position of the current recipient based on existing recipients
116
+ def set_position
117
+ if last_recipient = message.recipients.find(:first, :conditions => {:kind => kind}, :order => 'position DESC')
118
+ self.position = last_recipient.position + 1
119
+ else
120
+ self.position = 1
121
+ end
122
+ end
123
+
124
+ # Reorders the positions of the message's recipients
125
+ def reorder_positions
126
+ if position
127
+ position = self.position
128
+ update_attribute(:position, nil)
129
+ self.class.update_all('position = (position - 1)', ['message_id = ? AND kind = ? AND position > ?', message_id, kind, position])
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,103 @@
1
+ require 'state_machine'
2
+
3
+ # Adds a generic implementation for sending messages between users
4
+ module HasMessages
5
+ module MacroMethods
6
+ # Creates the following message associations:
7
+ # * +messages+ - Messages that were composed and are visible to the owner.
8
+ # Mesages may have been sent or unsent.
9
+ # * +received_messages - Messages that have been received from others and
10
+ # are visible. Messages may have been read or unread.
11
+ #
12
+ # == Creating new messages
13
+ #
14
+ # To create a new message, the +messages+ association should be used,
15
+ # for example:
16
+ #
17
+ # user = User.find(123)
18
+ # message = user.messages.build
19
+ # message.subject = 'Hello'
20
+ # message.body = 'How are you?'
21
+ # message.to User.find(456)
22
+ # message.save
23
+ # message.deliver
24
+ #
25
+ # == Drafts
26
+ #
27
+ # You can get the drafts for a particular user by using the +unsent_messages+
28
+ # helper method. This will find all messages in the "unsent" state. For example,
29
+ #
30
+ # user = User.find(123)
31
+ # user.unsent_messages
32
+ #
33
+ # You can also get at the messages that *have* been sent, using the +sent_messages+
34
+ # helper method. For example,
35
+ #
36
+ # user = User.find(123)
37
+ # user.sent_messages
38
+ def has_messages
39
+ has_many :messages,
40
+ :as => :sender,
41
+ :class_name => 'Message',
42
+ :conditions => {:hidden_at => nil},
43
+ :order => 'messages.created_at DESC'
44
+ has_many :received_messages,
45
+ :as => :receiver,
46
+ :class_name => 'MessageRecipient',
47
+ :include => :message,
48
+ :conditions => ['message_recipients.hidden_at IS NULL AND messages.state = ?', 'sent'],
49
+ :order => 'messages.created_at DESC'
50
+ # has_many :received_message_threads,
51
+ # :as => :receiver,
52
+ # :class_name => 'MessageRecipient',
53
+ # :include => :message,
54
+ # :conditions => ['message_recipients.hidden_at IS NULL AND messages.state = ? and messages.original_message_id IS NOT NULL', 'sent'],
55
+ # :group => 'messages.original_message_id',
56
+ # :order => 'messages.created_at DESC'
57
+
58
+ include HasMessages::InstanceMethods
59
+ end
60
+ end
61
+
62
+ module InstanceMethods
63
+ # Composed messages that have not yet been sent. These consists of all
64
+ # messages that are currently in the "unsent" state.
65
+ def unsent_messages
66
+ messages.with_state(:unsent)
67
+ end
68
+
69
+ # Composed messages that have already been sent. These consists of all
70
+ # messages that are currently in the "queued" or "sent" states.
71
+ def sent_messages
72
+ messages.with_states(:queued, :sent)
73
+ end
74
+
75
+ # Returns the most recent message of each thread
76
+ def last_received_message_per_thread
77
+ MessageRecipient.find_all_by_receiver_id(id, :order => 'id desc', :joins => :message, :conditions => 'message_recipients.hidden_at is null', :group => 'COALESCE(original_message_id,messages.id)')
78
+ end
79
+
80
+ def conversations
81
+ (messages + received_messages.map(&:message)).compact.uniq
82
+ end
83
+
84
+ def original_conversations
85
+ conversations.select{ |message| message.original_message_id == nil }
86
+ end
87
+
88
+ def find_conversation_by_id(id)
89
+ conversations.select{ |message| message.id == id.to_i }.first
90
+ end
91
+
92
+ def unread_messages
93
+ received_messages.select(&:unread?).map(&:message)
94
+ end
95
+ end
96
+ end
97
+
98
+ ActiveRecord::Base.class_eval do
99
+ extend HasMessages::MacroMethods
100
+ end
101
+
102
+ require 'has_messages/models/message.rb'
103
+ require 'has_messages/models/message_recipient.rb'
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_messages
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'config/boot'
2
+
3
+ Rails::Initializer.run do |config|
4
+ config.plugin_paths << '..'
5
+ config.plugins = %w(state_machine has_messages)
6
+ config.cache_classes = false
7
+ config.whiny_nils = true
8
+ config.action_controller.session = {:key => 'rails_session', :secret => 'd229e4d22437432705ab3985d4d246'}
9
+ end
@@ -0,0 +1,11 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.string :login, :null => false
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :users
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ class MigrateHasMessagesToVersion2 < ActiveRecord::Migration
2
+ def self.up
3
+ ActiveRecord::Migrator.new(:up, "#{Rails.root}/../../generators/has_messages/templates", 0).migrations.each do |migration|
4
+ migration.migrate(:up)
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ ActiveRecord::Migrator.new(:down, "#{Rails.root}/../../generators/has_messages/templates", 0).migrations.each do |migration|
10
+ migration.migrate(:down)
11
+ end
12
+ end
13
+ end
data/test/factory.rb ADDED
@@ -0,0 +1,58 @@
1
+ module Factory
2
+ # Build actions for the model
3
+ def self.build(model, &block)
4
+ name = model.to_s.underscore
5
+
6
+ define_method("#{name}_attributes", block)
7
+ define_method("valid_#{name}_attributes") {|*args| valid_attributes_for(model, *args)}
8
+ define_method("new_#{name}") {|*args| new_record(model, *args)}
9
+ define_method("create_#{name}") {|*args| create_record(model, *args)}
10
+ end
11
+
12
+ # Get valid attributes for the model
13
+ def valid_attributes_for(model, attributes = {})
14
+ name = model.to_s.underscore
15
+ send("#{name}_attributes", attributes)
16
+ attributes.stringify_keys!
17
+ attributes
18
+ end
19
+
20
+ # Build an unsaved record
21
+ def new_record(model, *args)
22
+ attributes = valid_attributes_for(model, *args)
23
+ record = model.new(attributes)
24
+ attributes.each {|attr, value| record.send("#{attr}=", value) if model.accessible_attributes && !model.accessible_attributes.include?(attr) || model.protected_attributes && model.protected_attributes.include?(attr)}
25
+ record
26
+ end
27
+
28
+ # Build and save/reload a record
29
+ def create_record(model, *args)
30
+ record = new_record(model, *args)
31
+ record.save!
32
+ record.reload
33
+ record
34
+ end
35
+
36
+ build Message do |attributes|
37
+ attributes[:sender] = create_user unless attributes.include?(:sender)
38
+ attributes.reverse_merge!(
39
+ :subject => 'New features',
40
+ :body => 'Lots of new things to talk about... come to the meeting tonight to find out!',
41
+ :created_at => Time.current + Message.count
42
+ )
43
+ end
44
+
45
+ build MessageRecipient do |attributes|
46
+ attributes[:message] = create_message unless attributes.include?(:message)
47
+ attributes[:receiver] = create_user(:login => 'me') unless attributes.include?(:receiver)
48
+ attributes.reverse_merge!(
49
+ :kind => 'to'
50
+ )
51
+ end
52
+
53
+ build User do |attributes|
54
+ attributes.reverse_merge!(
55
+ :login => 'admin'
56
+ )
57
+ end
58
+ end