@@ -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 ? ' ' * 2 * level + '» ' : '').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, |
|
|
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 |
|
|
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 |
|
|
|
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(&: |
|
|
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