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 +112 -0
- data/init.rb +1 -0
- data/lib/generators/has_messages/USAGE +5 -0
- data/lib/generators/has_messages/has_messages_generator.rb +24 -0
- data/lib/generators/has_messages/templates/001_create_messages.rb +17 -0
- data/lib/generators/has_messages/templates/002_create_message_recipients.rb +17 -0
- data/lib/has_messages/models/message.rb +167 -0
- data/lib/has_messages/models/message_recipient.rb +132 -0
- data/lib/has_messages.rb +103 -0
- data/test/app_root/app/models/user.rb +3 -0
- data/test/app_root/config/environment.rb +9 -0
- data/test/app_root/db/migrate/001_create_users.rb +11 -0
- data/test/app_root/db/migrate/002_migrate_has_messages_to_version_2.rb +13 -0
- data/test/factory.rb +58 -0
- data/test/functional/has_messages_test.rb +140 -0
- data/test/test_helper.rb +13 -0
- data/test/unit/message_recipient_test.rb +449 -0
- data/test/unit/message_test.rb +465 -0
- metadata +105 -0
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,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
|
data/lib/has_messages.rb
ADDED
|
@@ -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,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,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
|