##// END OF EJS Templates
Adds the ability to move threads between project forums (#2452). 'Edit message' permission is required....
Jean-Philippe Lang -
r2560:ca166b30e1b2
parent child
Show More
@@ -1,81 +1,82
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 BoardsController < ApplicationController
19 19 before_filter :find_project, :authorize
20 20
21 21 helper :messages
22 22 include MessagesHelper
23 23 helper :sort
24 24 include SortHelper
25 25 helper :watchers
26 26 include WatchersHelper
27 27
28 28 def index
29 29 @boards = @project.boards
30 30 # show the board if there is only one
31 31 if @boards.size == 1
32 32 @board = @boards.first
33 33 show
34 34 end
35 35 end
36 36
37 37 def show
38 38 sort_init 'updated_on', 'desc'
39 39 sort_update 'created_on' => "#{Message.table_name}.created_on",
40 40 'replies' => "#{Message.table_name}.replies_count",
41 41 'updated_on' => "#{Message.table_name}.updated_on"
42 42
43 43 @topic_count = @board.topics.count
44 44 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
45 45 @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
46 46 :include => [:author, {:last_reply => :author}],
47 47 :limit => @topic_pages.items_per_page,
48 48 :offset => @topic_pages.current.offset
49 @message = Message.new
49 50 render :action => 'show', :layout => !request.xhr?
50 51 end
51 52
52 53 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
53 54
54 55 def new
55 56 @board = Board.new(params[:board])
56 57 @board.project = @project
57 58 if request.post? && @board.save
58 59 flash[:notice] = l(:notice_successful_create)
59 60 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
60 61 end
61 62 end
62 63
63 64 def edit
64 65 if request.post? && @board.update_attributes(params[:board])
65 66 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
66 67 end
67 68 end
68 69
69 70 def destroy
70 71 @board.destroy
71 72 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
72 73 end
73 74
74 75 private
75 76 def find_project
76 77 @project = Project.find(params[:project_id])
77 78 @board = @project.boards.find(params[:id]) if params[:id]
78 79 rescue ActiveRecord::RecordNotFound
79 80 render_404
80 81 end
81 82 end
@@ -1,125 +1,126
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, :edit, :destroy]
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.find(:all, :include => [:author, :attachments, {:board => :project}])
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 render_403 and return false unless @message.editable_by?(User.current)
69 69 if params[:message]
70 70 @message.locked = params[:message]['locked']
71 71 @message.sticky = params[:message]['sticky']
72 72 end
73 73 if request.post? && @message.update_attributes(params[:message])
74 74 attach_files(@message, params[:attachments])
75 75 flash[:notice] = l(:notice_successful_update)
76 redirect_to :action => 'show', :id => @topic
76 @message.reload
77 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
77 78 end
78 79 end
79 80
80 81 # Delete a messages
81 82 def destroy
82 83 render_403 and return false unless @message.destroyable_by?(User.current)
83 84 @message.destroy
84 85 redirect_to @message.parent.nil? ?
85 86 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
86 87 { :action => 'show', :id => @message.parent }
87 88 end
88 89
89 90 def quote
90 91 user = @message.author
91 92 text = @message.content
92 93 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
93 94 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
94 95 render(:update) { |page|
95 96 page.<< "$('message_content').value = \"#{content}\";"
96 97 page.show 'reply'
97 98 page << "Form.Element.focus('message_content');"
98 99 page << "Element.scrollTo('reply');"
99 100 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
100 101 }
101 102 end
102 103
103 104 def preview
104 105 message = @board.messages.find_by_id(params[:id])
105 106 @attachements = message.attachments if message
106 107 @text = (params[:message] || params[:reply])[:content]
107 108 render :partial => 'common/preview'
108 109 end
109 110
110 111 private
111 112 def find_message
112 113 find_board
113 114 @message = @board.messages.find(params[:id], :include => :parent)
114 115 @topic = @message.root
115 116 rescue ActiveRecord::RecordNotFound
116 117 render_404
117 118 end
118 119
119 120 def find_board
120 121 @board = Board.find(params[:board_id], :include => :project)
121 122 @project = @board.project
122 123 rescue ActiveRecord::RecordNotFound
123 124 render_404
124 125 end
125 126 end
@@ -1,29 +1,42
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 Board < ActiveRecord::Base
19 19 belongs_to :project
20 20 has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
21 21 has_many :messages, :dependent => :delete_all, :order => "#{Message.table_name}.created_on DESC"
22 22 belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
23 23 acts_as_list :scope => :project_id
24 24 acts_as_watchable
25 25
26 26 validates_presence_of :name, :description
27 27 validates_length_of :name, :maximum => 30
28 28 validates_length_of :description, :maximum => 255
29
30 def reset_counters!
31 self.class.reset_counters!(id)
32 end
33
34 # Updates topics_count, messages_count and last_message_id attributes for +board_id+
35 def self.reset_counters!(board_id)
36 board_id = board_id.to_i
37 update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
38 " messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
39 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
40 ["id = ?", board_id])
41 end
29 42 end
@@ -1,89 +1,90
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 acts_as_attachable
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 36 :author_key => :author_id
37 37 acts_as_watchable
38 38
39 39 attr_protected :locked, :sticky
40 validates_presence_of :subject, :content
40 validates_presence_of :board, :subject, :content
41 41 validates_length_of :subject, :maximum => 255
42 42
43 43 after_create :add_author_as_watcher
44 44
45 45 def validate_on_create
46 46 # Can not reply to a locked topic
47 47 errors.add_to_base 'Topic is locked' if root.locked? && self != root
48 48 end
49 49
50 50 def after_create
51 board.update_attribute(:last_message_id, self.id)
52 board.increment! :messages_count
53 51 if parent
54 52 parent.reload.update_attribute(:last_reply_id, self.id)
55 else
56 board.increment! :topics_count
53 end
54 board.reset_counters!
55 end
56
57 def after_update
58 if board_id_changed?
59 Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
60 Board.reset_counters!(board_id_was)
61 Board.reset_counters!(board_id)
57 62 end
58 63 end
59 64
60 65 def after_destroy
61 # The following line is required so that the previous counter
62 # updates (due to children removal) are not overwritten
63 board.reload
64 board.decrement! :messages_count
65 board.decrement! :topics_count unless parent
66 board.reset_counters!
66 67 end
67 68
68 69 def sticky?
69 70 sticky == 1
70 71 end
71 72
72 73 def project
73 74 board.project
74 75 end
75 76
76 77 def editable_by?(usr)
77 78 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
78 79 end
79 80
80 81 def destroyable_by?(usr)
81 82 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
82 83 end
83 84
84 85 private
85 86
86 87 def add_author_as_watcher
87 88 Watcher.create(:watchable => self.root, :user => author)
88 89 end
89 90 end
@@ -1,21 +1,26
1 1 <%= error_messages_for 'message' %>
2 2 <% replying ||= false %>
3 3
4 4 <div class="box">
5 5 <!--[form:message]-->
6 6 <p><label><%= l(:field_subject) %></label><br />
7 7 <%= f.text_field :subject, :size => 120 %>
8 8
9 9 <% if !replying && User.current.allowed_to?(:edit_messages, @project) %>
10 10 <label><%= f.check_box :sticky %> Sticky</label>
11 11 <label><%= f.check_box :locked %> Locked</label>
12 12 <% end %>
13 13 </p>
14 14
15 <% if !replying && !@message.new_record? && User.current.allowed_to?(:edit_messages, @project) %>
16 <p><label><%= l(:label_board) %></label><br />
17 <%= f.select :board_id, @project.boards.collect {|b| [b.name, b.id]} %></p>
18 <% end %>
19
15 20 <p><%= f.text_area :content, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %></p>
16 21 <%= wikitoolbar_for 'message_content' %>
17 22 <!--[eoform:message]-->
18 23
19 24 <p><%= l(:label_attachment_plural) %><br />
20 25 <%= render :partial => 'attachments/form' %></p>
21 26 </div>
@@ -1,19 +1,19
1 1 ---
2 2 boards_001:
3 3 name: Help
4 4 project_id: 1
5 5 topics_count: 2
6 6 id: 1
7 7 description: Help board
8 8 position: 1
9 last_message_id: 5
10 messages_count: 5
9 last_message_id: 6
10 messages_count: 6
11 11 boards_002:
12 12 name: Discussion
13 13 project_id: 1
14 14 topics_count: 0
15 15 id: 2
16 16 description: Discussion board
17 17 position: 2
18 18 last_message_id:
19 19 messages_count: 0
@@ -1,97 +1,131
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
1 18 require File.dirname(__FILE__) + '/../test_helper'
2 19
3 20 class MessageTest < Test::Unit::TestCase
4 21 fixtures :projects, :roles, :members, :boards, :messages, :users, :watchers
5 22
6 23 def setup
7 24 @board = Board.find(1)
8 25 @user = User.find(1)
9 26 end
10 27
11 28 def test_create
12 29 topics_count = @board.topics_count
13 30 messages_count = @board.messages_count
14 31
15 32 message = Message.new(:board => @board, :subject => 'Test message', :content => 'Test message content', :author => @user)
16 33 assert message.save
17 34 @board.reload
18 35 # topics count incremented
19 36 assert_equal topics_count+1, @board[:topics_count]
20 37 # messages count incremented
21 38 assert_equal messages_count+1, @board[:messages_count]
22 39 assert_equal message, @board.last_message
23 40 # author should be watching the message
24 41 assert message.watched_by?(@user)
25 42 end
26 43
27 44 def test_reply
28 45 topics_count = @board.topics_count
29 46 messages_count = @board.messages_count
30 47 @message = Message.find(1)
31 48 replies_count = @message.replies_count
32 49
33 50 reply_author = User.find(2)
34 51 reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', :parent => @message, :author => reply_author)
35 52 assert reply.save
36 53 @board.reload
37 54 # same topics count
38 55 assert_equal topics_count, @board[:topics_count]
39 56 # messages count incremented
40 57 assert_equal messages_count+1, @board[:messages_count]
41 58 assert_equal reply, @board.last_message
42 59 @message.reload
43 60 # replies count incremented
44 61 assert_equal replies_count+1, @message[:replies_count]
45 62 assert_equal reply, @message.last_reply
46 63 # author should be watching the message
47 64 assert @message.watched_by?(reply_author)
48 65 end
49 66
67 def test_moving_message_should_update_counters
68 @message = Message.find(1)
69 assert_no_difference 'Message.count' do
70 # Previous board
71 assert_difference 'Board.find(1).topics_count', -1 do
72 assert_difference 'Board.find(1).messages_count', -(1 + @message.replies_count) do
73 # New board
74 assert_difference 'Board.find(2).topics_count' do
75 assert_difference 'Board.find(2).messages_count', (1 + @message.replies_count) do
76 @message.update_attributes(:board_id => 2)
77 end
78 end
79 end
80 end
81 end
82 end
83
50 84 def test_destroy_topic
51 85 message = Message.find(1)
52 86 board = message.board
53 87 topics_count, messages_count = board.topics_count, board.messages_count
54 88
55 89 assert_difference('Watcher.count', -1) do
56 90 assert message.destroy
57 91 end
58 92 board.reload
59 93
60 94 # Replies deleted
61 95 assert Message.find_all_by_parent_id(1).empty?
62 96 # Checks counters
63 97 assert_equal topics_count - 1, board.topics_count
64 98 assert_equal messages_count - 3, board.messages_count
65 99 # Watchers removed
66 100 end
67 101
68 102 def test_destroy_reply
69 103 message = Message.find(5)
70 104 board = message.board
71 105 topics_count, messages_count = board.topics_count, board.messages_count
72 106 assert message.destroy
73 107 board.reload
74 108
75 109 # Checks counters
76 110 assert_equal topics_count, board.topics_count
77 111 assert_equal messages_count - 1, board.messages_count
78 112 end
79 113
80 114 def test_editable_by
81 115 message = Message.find(6)
82 116 author = message.author
83 117 assert message.editable_by?(author)
84 118
85 119 author.role_for_project(message.project).remove_permission!(:edit_own_messages)
86 120 assert !message.reload.editable_by?(author.reload)
87 121 end
88 122
89 123 def test_destroyable_by
90 124 message = Message.find(6)
91 125 author = message.author
92 126 assert message.destroyable_by?(author)
93 127
94 128 author.role_for_project(message.project).remove_permission!(:delete_own_messages)
95 129 assert !message.reload.destroyable_by?(author.reload)
96 130 end
97 131 end
General Comments 0
You need to be logged in to leave comments. Login now