##// END OF EJS Templates
Support for subforums (#3831)....
Jean-Philippe Lang -
r9959:bc153cb61df3
parent child
Show More
@@ -0,0 +1,9
1 class AddBoardsParentId < ActiveRecord::Migration
2 def up
3 add_column :boards, :parent_id, :integer
4 end
5
6 def down
7 remove_column :boards, :parent_id
8 end
9 end
@@ -22,6 +22,7 class MessagesController < ApplicationController
22 22 before_filter :find_message, :except => [:new, :preview]
23 23 before_filter :authorize, :except => [:preview, :edit, :destroy]
24 24
25 helper :boards
25 26 helper :watchers
26 27 helper :attachments
27 28 include AttachmentsHelper
@@ -18,4 +18,24
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module BoardsHelper
21 def board_breadcrumb(item)
22 board = item.is_a?(Message) ? item.board : item
23 links = [link_to(l(:label_board_plural), project_boards_path(item.project))]
24 boards = board.ancestors.reverse
25 if item.is_a?(Message)
26 boards << board
27 end
28 links += boards.map {|ancestor| link_to(h(ancestor.name), project_board_path(ancestor.project, ancestor))}
29 breadcrumb links
30 end
31
32 def boards_options_for_select(boards)
33 options = []
34 Board.board_tree(boards) do |board, level|
35 label = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
36 label << board.name
37 options << [label, board.id]
38 end
39 options
40 end
21 41 end
@@ -21,26 +21,37 class Board < ActiveRecord::Base
21 21 has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
22 22 has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
23 23 belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
24 acts_as_list :scope => :project_id
24 acts_as_tree :dependent => :nullify
25 acts_as_list :scope => '(project_id = #{project_id} AND parent_id #{parent_id ? "= #{parent_id}" : "IS NULL"})'
25 26 acts_as_watchable
26 27
27 28 validates_presence_of :name, :description
28 29 validates_length_of :name, :maximum => 30
29 30 validates_length_of :description, :maximum => 255
31 validate :validate_board
30 32
31 33 scope :visible, lambda {|*args| { :include => :project,
32 34 :conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
33 35
34 safe_attributes 'name', 'description', 'move_to'
36 safe_attributes 'name', 'description', 'parent_id', 'move_to'
35 37
36 38 def visible?(user=User.current)
37 39 !user.nil? && user.allowed_to?(:view_messages, project)
38 40 end
39 41
42 def reload(*args)
43 @valid_parents = nil
44 super
45 end
46
40 47 def to_s
41 48 name
42 49 end
43 50
51 def valid_parents
52 @valid_parents ||= project.boards - self_and_descendants
53 end
54
44 55 def reset_counters!
45 56 self.class.reset_counters!(id)
46 57 end
@@ -53,4 +64,26 class Board < ActiveRecord::Base
53 64 " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
54 65 ["id = ?", board_id])
55 66 end
67
68 def self.board_tree(boards, parent_id=nil, level=0)
69 tree = []
70 boards.select {|board| board.parent_id == parent_id}.sort_by(&:position).each do |board|
71 tree << [board, level]
72 tree += board_tree(boards, board.id, level+1)
73 end
74 if block_given?
75 tree.each do |board, level|
76 yield board, level
77 end
78 end
79 tree
80 end
81
82 protected
83
84 def validate_board
85 if parent_id && parent_id_changed?
86 errors.add(:parent_id, :invalid) unless valid_parents.include?(parent)
87 end
88 end
56 89 end
@@ -3,4 +3,7
3 3 <div class="box tabular">
4 4 <p><%= f.text_field :name, :required => true %></p>
5 5 <p><%= f.text_field :description, :required => true, :size => 80 %></p>
6 <% if @board.valid_parents.any? %>
7 <p><%= f.select :parent_id, boards_options_for_select(@board.valid_parents), :include_blank => true, :label => :field_board_parent %></p>
8 <% end %>
6 9 </div>
@@ -8,9 +8,9
8 8 <th><%= l(:label_message_last) %></th>
9 9 </tr></thead>
10 10 <tbody>
11 <% for board in @boards %>
11 <% Board.board_tree(@boards) do |board, level| %>
12 12 <tr class="<%= cycle 'odd', 'even' %>">
13 <td>
13 <td style="padding-left: <%= level * 18 %>px;">
14 14 <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "board" %><br />
15 15 <%=h board.description %>
16 16 </td>
@@ -1,4 +1,4
1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)) %>
1 <%= board_breadcrumb(@board) %>
2 2
3 3 <div class="contextual">
4 4 <%= link_to_if_authorized l(:label_message_new),
@@ -18,7 +18,7
18 18
19 19 <% if !replying && !@message.new_record? && @message.safe_attribute?('board_id') %>
20 20 <p><label><%= l(:label_board) %></label><br />
21 <%= f.select :board_id, @project.boards.collect {|b| [b.name, b.id]} %></p>
21 <%= f.select :board_id, boards_options_for_select(@message.project.boards) %></p>
22 22 <% end %>
23 23
24 24 <p>
@@ -1,5 +1,4
1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)),
2 link_to(h(@board.name), project_board_path(@project, @board)) %>
1 <%= board_breadcrumb(@message) %>
3 2
4 3 <h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
5 4
@@ -1,5 +1,4
1 <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)),
2 link_to(h(@board.name), project_board_path(@project, @board)) %>
1 <%= board_breadcrumb(@message) %>
3 2
4 3 <div class="contextual">
5 4 <%= watcher_tag(@topic, User.current) %>
@@ -7,10 +7,10
7 7 <th></th>
8 8 </tr></thead>
9 9 <tbody>
10 <% @project.boards.each do |board|
10 <% Board.board_tree(@project.boards) do |board, level|
11 11 next if board.new_record? %>
12 12 <tr class="<%= cycle 'odd', 'even' %>">
13 <td><%= link_to board.name, project_board_path(@project, board) %></td>
13 <td style="padding-left: <%= level * 18 %>px;"><%= link_to board.name, project_board_path(@project, board) %></td>
14 14 <td><%=h board.description %></td>
15 15 <td align="center">
16 16 <% if authorize_for("boards", "edit") %>
@@ -5,7 +5,9 module ActiveRecord
5 5 include Redmine::I18n
6 6 # Translate attribute names for validation errors display
7 7 def self.human_attribute_name(attr, *args)
8 l("field_#{attr.to_s.gsub(/_id$/, '')}", :default => attr)
8 attr = attr.to_s.sub(/_id$/, '')
9
10 l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
9 11 end
10 12 end
11 13 end
@@ -330,6 +330,7 en:
330 330 field_ldap_filter: LDAP filter
331 331 field_core_fields: Standard fields
332 332 field_timeout: "Timeout (in seconds)"
333 field_board_parent: Parent forum
333 334
334 335 setting_app_title: Application title
335 336 setting_app_subtitle: Application subtitle
@@ -329,6 +329,7 fr:
329 329 field_ldap_filter: Filtre LDAP
330 330 field_core_fields: Champs standards
331 331 field_timeout: "Timeout (en secondes)"
332 field_board_parent: Forum parent
332 333
333 334 setting_app_title: Titre de l'application
334 335 setting_app_subtitle: Sous-titre de l'application
@@ -74,7 +74,7 module ActiveRecord
74 74 #
75 75 # root.descendants # => [child1, subchild1, subchild2]
76 76 def descendants
77 children + children.collect(&:children).flatten
77 children + children.collect(&:descendants).flatten
78 78 end
79 79
80 80 # Returns list of descendants and a reference to the current node.
@@ -98,6 +98,23 class BoardsControllerTest < ActionController::TestCase
98 98 get :new, :project_id => 1
99 99 assert_response :success
100 100 assert_template 'new'
101
102 assert_select 'select[name=?]', 'board[parent_id]' do
103 assert_select 'option', (Project.find(1).boards.size + 1)
104 assert_select 'option[value=]', :text => ''
105 assert_select 'option[value=1]', :text => 'Help'
106 end
107 end
108
109 def test_new_without_project_boards
110 Project.find(1).boards.delete_all
111 @request.session[:user_id] = 2
112
113 get :new, :project_id => 1
114 assert_response :success
115 assert_template 'new'
116
117 assert_select 'select[name=?]', 'board[parent_id]', 0
101 118 end
102 119
103 120 def test_create
@@ -111,6 +128,16 class BoardsControllerTest < ActionController::TestCase
111 128 assert_equal 'Testing board creation', board.description
112 129 end
113 130
131 def test_create_with_parent
132 @request.session[:user_id] = 2
133 assert_difference 'Board.count' do
134 post :create, :project_id => 1, :board => { :name => 'Testing', :description => 'Testing', :parent_id => 2}
135 end
136 assert_redirected_to '/projects/ecookbook/settings/boards'
137 board = Board.first(:order => 'id DESC')
138 assert_equal Board.find(2), board.parent
139 end
140
114 141 def test_create_with_failure
115 142 @request.session[:user_id] = 2
116 143 assert_no_difference 'Board.count' do
@@ -127,6 +154,18 class BoardsControllerTest < ActionController::TestCase
127 154 assert_template 'edit'
128 155 end
129 156
157 def test_edit_with_parent
158 board = Board.generate!(:project_id => 1, :parent_id => 2)
159 @request.session[:user_id] = 2
160 get :edit, :project_id => 1, :id => board.id
161 assert_response :success
162 assert_template 'edit'
163
164 assert_select 'select[name=?]', 'board[parent_id]' do
165 assert_select 'option[value=2][selected=selected]'
166 end
167 end
168
130 169 def test_update
131 170 @request.session[:user_id] = 2
132 171 assert_no_difference 'Board.count' do
@@ -99,4 +99,15 module ObjectHelpers
99 99 source.save!
100 100 source
101 101 end
102
103 def Board.generate!(attributes={})
104 @generated_board_name ||= 'Forum 0'
105 @generated_board_name.succ!
106 board = Board.new(attributes)
107 board.name = @generated_board_name if board.name.blank?
108 board.description = @generated_board_name if board.description.blank?
109 yield board if block_given?
110 board.save!
111 board
112 end
102 113 end
@@ -1,3 +1,22
1 # encoding: utf-8
2 #
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
1 20 require File.expand_path('../../test_helper', __FILE__)
2 21
3 22 class BoardTest < ActiveSupport::TestCase
@@ -21,6 +40,54 class BoardTest < ActiveSupport::TestCase
21 40 assert_equal @project.boards.size, board.position
22 41 end
23 42
43 def test_parent_should_be_in_same_project
44 board = Board.new(:project_id => 3, :name => 'Test', :description => 'Test', :parent_id => 1)
45 assert !board.save
46 assert_include "Parent forum is invalid", board.errors.full_messages
47 end
48
49 def test_valid_parents_should_not_include_self_nor_a_descendant
50 board1 = Board.generate!(:project_id => 3)
51 board2 = Board.generate!(:project_id => 3, :parent => board1)
52 board3 = Board.generate!(:project_id => 3, :parent => board2)
53 board4 = Board.generate!(:project_id => 3)
54
55 assert_equal [board4], board1.reload.valid_parents.sort_by(&:id)
56 assert_equal [board1, board4], board2.reload.valid_parents.sort_by(&:id)
57 assert_equal [board1, board2, board4], board3.reload.valid_parents.sort_by(&:id)
58 assert_equal [board1, board2, board3], board4.reload.valid_parents.sort_by(&:id)
59 end
60
61 def test_position_should_be_assigned_with_parent_scope
62 parent1 = Board.generate!(:project_id => 3)
63 parent2 = Board.generate!(:project_id => 3)
64 child1 = Board.generate!(:project_id => 3, :parent => parent1)
65 child2 = Board.generate!(:project_id => 3, :parent => parent1)
66
67 assert_equal 1, parent1.reload.position
68 assert_equal 1, child1.reload.position
69 assert_equal 2, child2.reload.position
70 assert_equal 2, parent2.reload.position
71 end
72
73 def test_board_tree_should_yield_boards_with_level
74 parent1 = Board.generate!(:project_id => 3)
75 parent2 = Board.generate!(:project_id => 3)
76 child1 = Board.generate!(:project_id => 3, :parent => parent1)
77 child2 = Board.generate!(:project_id => 3, :parent => parent1)
78 child3 = Board.generate!(:project_id => 3, :parent => child1)
79
80 tree = Board.board_tree(Project.find(3).boards)
81
82 assert_equal [
83 [parent1, 0],
84 [child1, 1],
85 [child3, 2],
86 [child2, 1],
87 [parent2, 0]
88 ], tree
89 end
90
24 91 def test_destroy
25 92 board = Board.find(1)
26 93 assert_difference 'Message.count', -6 do
@@ -32,4 +99,15 class BoardTest < ActiveSupport::TestCase
32 99 end
33 100 assert_equal 0, Message.count(:conditions => {:board_id => 1})
34 101 end
102
103 def test_destroy_should_nullify_children
104 parent = Board.generate!(:project => @project)
105 child = Board.generate!(:project => @project, :parent => parent)
106 assert_equal parent, child.parent
107
108 assert parent.destroy
109 child.reload
110 assert_nil child.parent
111 assert_nil child.parent_id
112 end
35 113 end
General Comments 0
You need to be logged in to leave comments. Login now