@@ -0,0 +1,14 | |||
|
1 | class SetTopicAuthorsAsWatchers < ActiveRecord::Migration | |
|
2 | def self.up | |
|
3 | # Sets active users who created/replied a topic as watchers of the topic | |
|
4 | # so that the new watch functionality at topic level doesn't affect notifications behaviour | |
|
5 | Message.connection.execute("INSERT INTO watchers (watchable_type, watchable_id, user_id)" + | |
|
6 | " SELECT DISTINCT 'Message', COALESCE(messages.parent_id, messages.id), messages.author_id FROM messages, users" + | |
|
7 | " WHERE messages.author_id = users.id AND users.status = 1") | |
|
8 | end | |
|
9 | ||
|
10 | def self.down | |
|
11 | # Removes all message watchers | |
|
12 | Watcher.delete_all("watchable_type = 'Message'") | |
|
13 | end | |
|
14 | end |
@@ -1,123 +1,123 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class MessagesController < ApplicationController |
|
19 | 19 | menu_item :boards |
|
20 | 20 | before_filter :find_board, :only => [:new, :preview] |
|
21 | 21 | before_filter :find_message, :except => [:new, :preview] |
|
22 | 22 | before_filter :authorize, :except => :preview |
|
23 | 23 | |
|
24 | 24 | verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show } |
|
25 | 25 | verify :xhr => true, :only => :quote |
|
26 | 26 | |
|
27 | ||
|
27 | helper :watchers | |
|
28 | 28 | helper :attachments |
|
29 | 29 | include AttachmentsHelper |
|
30 | 30 | |
|
31 | 31 | # Show a topic and its replies |
|
32 | 32 | def show |
|
33 | 33 | @replies = @topic.children |
|
34 | 34 | @replies.reverse! if User.current.wants_comments_in_reverse_order? |
|
35 | 35 | @reply = Message.new(:subject => "RE: #{@message.subject}") |
|
36 | 36 | render :action => "show", :layout => false if request.xhr? |
|
37 | 37 | end |
|
38 | 38 | |
|
39 | 39 | # Create a new topic |
|
40 | 40 | def new |
|
41 | 41 | @message = Message.new(params[:message]) |
|
42 | 42 | @message.author = User.current |
|
43 | 43 | @message.board = @board |
|
44 | 44 | if params[:message] && User.current.allowed_to?(:edit_messages, @project) |
|
45 | 45 | @message.locked = params[:message]['locked'] |
|
46 | 46 | @message.sticky = params[:message]['sticky'] |
|
47 | 47 | end |
|
48 | 48 | if request.post? && @message.save |
|
49 | 49 | attach_files(@message, params[:attachments]) |
|
50 | 50 | redirect_to :action => 'show', :id => @message |
|
51 | 51 | end |
|
52 | 52 | end |
|
53 | 53 | |
|
54 | 54 | # Reply to a topic |
|
55 | 55 | def reply |
|
56 | 56 | @reply = Message.new(params[:reply]) |
|
57 | 57 | @reply.author = User.current |
|
58 | 58 | @reply.board = @board |
|
59 | 59 | @topic.children << @reply |
|
60 | 60 | if !@reply.new_record? |
|
61 | 61 | attach_files(@reply, params[:attachments]) |
|
62 | 62 | end |
|
63 | 63 | redirect_to :action => 'show', :id => @topic |
|
64 | 64 | end |
|
65 | 65 | |
|
66 | 66 | # Edit a message |
|
67 | 67 | def edit |
|
68 | 68 | if params[:message] && User.current.allowed_to?(:edit_messages, @project) |
|
69 | 69 | @message.locked = params[:message]['locked'] |
|
70 | 70 | @message.sticky = params[:message]['sticky'] |
|
71 | 71 | end |
|
72 | 72 | if request.post? && @message.update_attributes(params[:message]) |
|
73 | 73 | attach_files(@message, params[:attachments]) |
|
74 | 74 | flash[:notice] = l(:notice_successful_update) |
|
75 | 75 | redirect_to :action => 'show', :id => @topic |
|
76 | 76 | end |
|
77 | 77 | end |
|
78 | 78 | |
|
79 | 79 | # Delete a messages |
|
80 | 80 | def destroy |
|
81 | 81 | @message.destroy |
|
82 | 82 | redirect_to @message.parent.nil? ? |
|
83 | 83 | { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } : |
|
84 | 84 | { :action => 'show', :id => @message.parent } |
|
85 | 85 | end |
|
86 | 86 | |
|
87 | 87 | def quote |
|
88 | 88 | user = @message.author |
|
89 | 89 | text = @message.content |
|
90 | 90 | content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " |
|
91 | 91 | content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" |
|
92 | 92 | render(:update) { |page| |
|
93 | 93 | page.<< "$('message_content').value = \"#{content}\";" |
|
94 | 94 | page.show 'reply' |
|
95 | 95 | page << "Form.Element.focus('message_content');" |
|
96 | 96 | page << "Element.scrollTo('reply');" |
|
97 | 97 | page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;" |
|
98 | 98 | } |
|
99 | 99 | end |
|
100 | 100 | |
|
101 | 101 | def preview |
|
102 | 102 | message = @board.messages.find_by_id(params[:id]) |
|
103 | 103 | @attachements = message.attachments if message |
|
104 | 104 | @text = (params[:message] || params[:reply])[:content] |
|
105 | 105 | render :partial => 'common/preview' |
|
106 | 106 | end |
|
107 | 107 | |
|
108 | 108 | private |
|
109 | 109 | def find_message |
|
110 | 110 | find_board |
|
111 | 111 | @message = @board.messages.find(params[:id], :include => :parent) |
|
112 | 112 | @topic = @message.root |
|
113 | 113 | rescue ActiveRecord::RecordNotFound |
|
114 | 114 | render_404 |
|
115 | 115 | end |
|
116 | 116 | |
|
117 | 117 | def find_board |
|
118 | 118 | @board = Board.find(params[:board_id], :include => :project) |
|
119 | 119 | @project = @board.project |
|
120 | 120 | rescue ActiveRecord::RecordNotFound |
|
121 | 121 | render_404 |
|
122 | 122 | end |
|
123 | 123 | end |
@@ -1,71 +1,80 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Message < ActiveRecord::Base |
|
19 | 19 | belongs_to :board |
|
20 | 20 | belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' |
|
21 | 21 | acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC" |
|
22 | 22 | has_many :attachments, :as => :container, :dependent => :destroy |
|
23 | 23 | belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id' |
|
24 | 24 | |
|
25 | 25 | acts_as_searchable :columns => ['subject', 'content'], |
|
26 | 26 | :include => {:board, :project}, |
|
27 | 27 | :project_key => 'project_id', |
|
28 | 28 | :date_column => "#{table_name}.created_on" |
|
29 | 29 | acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, |
|
30 | 30 | :description => :content, |
|
31 | 31 | :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'}, |
|
32 | 32 | :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} : |
|
33 | 33 | {:id => o.parent_id, :anchor => "message-#{o.id}"})} |
|
34 | 34 | |
|
35 | 35 | acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]} |
|
36 | acts_as_watchable | |
|
36 | 37 | |
|
37 | 38 | attr_protected :locked, :sticky |
|
38 | 39 | validates_presence_of :subject, :content |
|
39 | 40 | validates_length_of :subject, :maximum => 255 |
|
40 | 41 | |
|
42 | after_create :add_author_as_watcher | |
|
43 | ||
|
41 | 44 | def validate_on_create |
|
42 | 45 | # Can not reply to a locked topic |
|
43 | 46 | errors.add_to_base 'Topic is locked' if root.locked? && self != root |
|
44 | 47 | end |
|
45 | 48 | |
|
46 | 49 | def after_create |
|
47 | 50 | board.update_attribute(:last_message_id, self.id) |
|
48 | 51 | board.increment! :messages_count |
|
49 | 52 | if parent |
|
50 | 53 | parent.reload.update_attribute(:last_reply_id, self.id) |
|
51 | 54 | else |
|
52 | 55 | board.increment! :topics_count |
|
53 | 56 | end |
|
54 | 57 | end |
|
55 | 58 | |
|
56 | 59 | def after_destroy |
|
57 | 60 | # The following line is required so that the previous counter |
|
58 | 61 | # updates (due to children removal) are not overwritten |
|
59 | 62 | board.reload |
|
60 | 63 | board.decrement! :messages_count |
|
61 | 64 | board.decrement! :topics_count unless parent |
|
62 | 65 | end |
|
63 | 66 | |
|
64 | 67 | def sticky? |
|
65 | 68 | sticky == 1 |
|
66 | 69 | end |
|
67 | 70 | |
|
68 | 71 | def project |
|
69 | 72 | board.project |
|
70 | 73 | end |
|
74 | ||
|
75 | private | |
|
76 | ||
|
77 | def add_author_as_watcher | |
|
78 | Watcher.create(:watchable => self.root, :user => author) | |
|
79 | end | |
|
71 | 80 | end |
@@ -1,29 +1,30 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class MessageObserver < ActiveRecord::Observer |
|
19 | 19 | def after_create(message) |
|
20 | # send notification to the authors of the thread | |
|
21 | recipients = ([message.root] + message.root.children).collect {|m| m.author.mail if m.author && m.author.active?} | |
|
20 | recipients = [] | |
|
21 | # send notification to the topic watchers | |
|
22 | recipients += message.root.watcher_recipients | |
|
22 | 23 | # send notification to the board watchers |
|
23 | 24 | recipients += message.board.watcher_recipients |
|
24 | 25 | # send notification to project members who want to be notified |
|
25 | 26 | recipients += message.board.project.recipients |
|
26 | 27 | recipients = recipients.compact.uniq |
|
27 | 28 | Mailer.deliver_message_posted(message, recipients) if !recipients.empty? && Setting.notified_events.include?('message_posted') |
|
28 | 29 | end |
|
29 | 30 | end |
@@ -1,60 +1,61 | |||
|
1 | 1 | <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}), |
|
2 | 2 | link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %> |
|
3 | 3 | |
|
4 | 4 | <div class="contextual"> |
|
5 | <%= watcher_tag(@topic, User.current) %> | |
|
5 | 6 | <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %> |
|
6 | 7 | <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit' %> |
|
7 | 8 | <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %> |
|
8 | 9 | </div> |
|
9 | 10 | |
|
10 | 11 | <h2><%=h @topic.subject %></h2> |
|
11 | 12 | |
|
12 | 13 | <div class="message"> |
|
13 | 14 | <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p> |
|
14 | 15 | <div class="wiki"> |
|
15 | 16 | <%= textilizable(@topic.content, :attachments => @topic.attachments) %> |
|
16 | 17 | </div> |
|
17 | 18 | <%= link_to_attachments @topic.attachments, :no_author => true %> |
|
18 | 19 | </div> |
|
19 | 20 | <br /> |
|
20 | 21 | |
|
21 | 22 | <% unless @replies.empty? %> |
|
22 | 23 | <h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3> |
|
23 | 24 | <% @replies.each do |message| %> |
|
24 | 25 | <a name="<%= "message-#{message.id}" %>"></a> |
|
25 | 26 | <div class="contextual"> |
|
26 | 27 | <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %> |
|
27 | 28 | <%= link_to_if_authorized image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit) %> |
|
28 | 29 | <%= link_to_if_authorized image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete) %> |
|
29 | 30 | </div> |
|
30 | 31 | <div class="message reply"> |
|
31 | 32 | <h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4> |
|
32 | 33 | <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div> |
|
33 | 34 | <%= link_to_attachments message.attachments, :no_author => true %> |
|
34 | 35 | </div> |
|
35 | 36 | <% end %> |
|
36 | 37 | <% end %> |
|
37 | 38 | |
|
38 | 39 | <% if !@topic.locked? && authorize_for('messages', 'reply') %> |
|
39 | 40 | <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p> |
|
40 | 41 | <div id="reply" style="display:none;"> |
|
41 | 42 | <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %> |
|
42 | 43 | <%= render :partial => 'form', :locals => {:f => f, :replying => true} %> |
|
43 | 44 | <%= submit_tag l(:button_submit) %> |
|
44 | 45 | <%= link_to_remote l(:label_preview), |
|
45 | 46 | { :url => { :controller => 'messages', :action => 'preview', :board_id => @board }, |
|
46 | 47 | :method => 'post', |
|
47 | 48 | :update => 'preview', |
|
48 | 49 | :with => "Form.serialize('message-form')", |
|
49 | 50 | :complete => "Element.scrollTo('preview')" |
|
50 | 51 | }, :accesskey => accesskey(:preview) %> |
|
51 | 52 | <% end %> |
|
52 | 53 | <div id="preview" class="wiki"></div> |
|
53 | 54 | </div> |
|
54 | 55 | <% end %> |
|
55 | 56 | |
|
56 | 57 | <% content_for :header_tags do %> |
|
57 | 58 | <%= stylesheet_link_tag 'scm' %> |
|
58 | 59 | <% end %> |
|
59 | 60 | |
|
60 | 61 | <% html_title h(@topic.subject) %> |
@@ -1,6 +1,10 | |||
|
1 | 1 | --- |
|
2 | 2 | watchers_001: |
|
3 | 3 | watchable_type: Issue |
|
4 | 4 | watchable_id: 2 |
|
5 | 5 | user_id: 3 |
|
6 | watchers_002: | |
|
7 | watchable_type: Message | |
|
8 | watchable_id: 1 | |
|
9 | user_id: 1 | |
|
6 | 10 | No newline at end of file |
@@ -1,70 +1,79 | |||
|
1 | 1 | require File.dirname(__FILE__) + '/../test_helper' |
|
2 | 2 | |
|
3 | 3 | class MessageTest < Test::Unit::TestCase |
|
4 | fixtures :projects, :boards, :messages | |
|
4 | fixtures :projects, :boards, :messages, :users, :watchers | |
|
5 | 5 | |
|
6 | 6 | def setup |
|
7 | 7 | @board = Board.find(1) |
|
8 | 8 | @user = User.find(1) |
|
9 | 9 | end |
|
10 | 10 | |
|
11 | 11 | def test_create |
|
12 | 12 | topics_count = @board.topics_count |
|
13 | 13 | messages_count = @board.messages_count |
|
14 | 14 | |
|
15 | 15 | message = Message.new(:board => @board, :subject => 'Test message', :content => 'Test message content', :author => @user) |
|
16 | 16 | assert message.save |
|
17 | 17 | @board.reload |
|
18 | 18 | # topics count incremented |
|
19 | 19 | assert_equal topics_count+1, @board[:topics_count] |
|
20 | 20 | # messages count incremented |
|
21 | 21 | assert_equal messages_count+1, @board[:messages_count] |
|
22 | 22 | assert_equal message, @board.last_message |
|
23 | # author should be watching the message | |
|
24 | assert message.watched_by?(@user) | |
|
23 | 25 | end |
|
24 | 26 | |
|
25 | 27 | def test_reply |
|
26 | 28 | topics_count = @board.topics_count |
|
27 | 29 | messages_count = @board.messages_count |
|
28 | 30 | @message = Message.find(1) |
|
29 | 31 | replies_count = @message.replies_count |
|
30 | 32 | |
|
31 | reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', :parent => @message, :author => @user) | |
|
33 | reply_author = User.find(2) | |
|
34 | reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', :parent => @message, :author => reply_author) | |
|
32 | 35 | assert reply.save |
|
33 | 36 | @board.reload |
|
34 | 37 | # same topics count |
|
35 | 38 | assert_equal topics_count, @board[:topics_count] |
|
36 | 39 | # messages count incremented |
|
37 | 40 | assert_equal messages_count+1, @board[:messages_count] |
|
38 | 41 | assert_equal reply, @board.last_message |
|
39 | 42 | @message.reload |
|
40 | 43 | # replies count incremented |
|
41 | 44 | assert_equal replies_count+1, @message[:replies_count] |
|
42 | 45 | assert_equal reply, @message.last_reply |
|
46 | # author should be watching the message | |
|
47 | assert @message.watched_by?(reply_author) | |
|
43 | 48 | end |
|
44 | 49 | |
|
45 | 50 | def test_destroy_topic |
|
46 | 51 | message = Message.find(1) |
|
47 | 52 | board = message.board |
|
48 | 53 | topics_count, messages_count = board.topics_count, board.messages_count |
|
49 | assert message.destroy | |
|
54 | ||
|
55 | assert_difference('Watcher.count', -1) do | |
|
56 | assert message.destroy | |
|
57 | end | |
|
50 | 58 | board.reload |
|
51 | 59 | |
|
52 | 60 | # Replies deleted |
|
53 | 61 | assert Message.find_all_by_parent_id(1).empty? |
|
54 | 62 | # Checks counters |
|
55 | 63 | assert_equal topics_count - 1, board.topics_count |
|
56 | 64 | assert_equal messages_count - 3, board.messages_count |
|
65 | # Watchers removed | |
|
57 | 66 | end |
|
58 | 67 | |
|
59 | 68 | def test_destroy_reply |
|
60 | 69 | message = Message.find(5) |
|
61 | 70 | board = message.board |
|
62 | 71 | topics_count, messages_count = board.topics_count, board.messages_count |
|
63 | 72 | assert message.destroy |
|
64 | 73 | board.reload |
|
65 | 74 | |
|
66 | 75 | # Checks counters |
|
67 | 76 | assert_equal topics_count, board.topics_count |
|
68 | 77 | assert_equal messages_count - 1, board.messages_count |
|
69 | 78 | end |
|
70 | 79 | end |
General Comments 0
You need to be logged in to leave comments.
Login now