has_messages_huacnlee 0.4.1

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