@@ -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 | before_filter :find_message, :except => [:new, :preview] |
|
22 | before_filter :find_message, :except => [:new, :preview] | |
23 | before_filter :authorize, :except => [:preview, :edit, :destroy] |
|
23 | before_filter :authorize, :except => [:preview, :edit, :destroy] | |
24 |
|
24 | |||
|
25 | helper :boards | |||
25 | helper :watchers |
|
26 | helper :watchers | |
26 | helper :attachments |
|
27 | helper :attachments | |
27 | include AttachmentsHelper |
|
28 | include AttachmentsHelper |
@@ -18,4 +18,24 | |||||
18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
19 |
|
19 | |||
20 | module BoardsHelper |
|
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 | end |
|
41 | end |
@@ -21,26 +21,37 class Board < ActiveRecord::Base | |||||
21 | has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC" |
|
21 | has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC" | |
22 | has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC" |
|
22 | has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC" | |
23 | belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id |
|
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 | acts_as_watchable |
|
26 | acts_as_watchable | |
26 |
|
27 | |||
27 | validates_presence_of :name, :description |
|
28 | validates_presence_of :name, :description | |
28 | validates_length_of :name, :maximum => 30 |
|
29 | validates_length_of :name, :maximum => 30 | |
29 | validates_length_of :description, :maximum => 255 |
|
30 | validates_length_of :description, :maximum => 255 | |
|
31 | validate :validate_board | |||
30 |
|
32 | |||
31 | scope :visible, lambda {|*args| { :include => :project, |
|
33 | scope :visible, lambda {|*args| { :include => :project, | |
32 | :conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } } |
|
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 | def visible?(user=User.current) |
|
38 | def visible?(user=User.current) | |
37 | !user.nil? && user.allowed_to?(:view_messages, project) |
|
39 | !user.nil? && user.allowed_to?(:view_messages, project) | |
38 | end |
|
40 | end | |
39 |
|
41 | |||
|
42 | def reload(*args) | |||
|
43 | @valid_parents = nil | |||
|
44 | super | |||
|
45 | end | |||
|
46 | ||||
40 | def to_s |
|
47 | def to_s | |
41 | name |
|
48 | name | |
42 | end |
|
49 | end | |
43 |
|
50 | |||
|
51 | def valid_parents | |||
|
52 | @valid_parents ||= project.boards - self_and_descendants | |||
|
53 | end | |||
|
54 | ||||
44 | def reset_counters! |
|
55 | def reset_counters! | |
45 | self.class.reset_counters!(id) |
|
56 | self.class.reset_counters!(id) | |
46 | end |
|
57 | end | |
@@ -53,4 +64,26 class Board < ActiveRecord::Base | |||||
53 | " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})", |
|
64 | " last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})", | |
54 | ["id = ?", board_id]) |
|
65 | ["id = ?", board_id]) | |
55 | end |
|
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 | end |
|
89 | end |
@@ -3,4 +3,7 | |||||
3 | <div class="box tabular"> |
|
3 | <div class="box tabular"> | |
4 | <p><%= f.text_field :name, :required => true %></p> |
|
4 | <p><%= f.text_field :name, :required => true %></p> | |
5 | <p><%= f.text_field :description, :required => true, :size => 80 %></p> |
|
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 | </div> |
|
9 | </div> |
@@ -8,9 +8,9 | |||||
8 | <th><%= l(:label_message_last) %></th> |
|
8 | <th><%= l(:label_message_last) %></th> | |
9 | </tr></thead> |
|
9 | </tr></thead> | |
10 | <tbody> |
|
10 | <tbody> | |
11 | <% for board in @boards %> |
|
11 | <% Board.board_tree(@boards) do |board, level| %> | |
12 | <tr class="<%= cycle 'odd', 'even' %>"> |
|
12 | <tr class="<%= cycle 'odd', 'even' %>"> | |
13 | <td> |
|
13 | <td style="padding-left: <%= level * 18 %>px;"> | |
14 | <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "board" %><br /> |
|
14 | <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "board" %><br /> | |
15 | <%=h board.description %> |
|
15 | <%=h board.description %> | |
16 | </td> |
|
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 | <div class="contextual"> |
|
3 | <div class="contextual"> | |
4 | <%= link_to_if_authorized l(:label_message_new), |
|
4 | <%= link_to_if_authorized l(:label_message_new), |
@@ -18,7 +18,7 | |||||
18 |
|
18 | |||
19 | <% if !replying && !@message.new_record? && @message.safe_attribute?('board_id') %> |
|
19 | <% if !replying && !@message.new_record? && @message.safe_attribute?('board_id') %> | |
20 | <p><label><%= l(:label_board) %></label><br /> |
|
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 | <% end %> |
|
22 | <% end %> | |
23 |
|
23 | |||
24 | <p> |
|
24 | <p> |
@@ -1,5 +1,4 | |||||
1 | <%= breadcrumb link_to(l(:label_board_plural), project_boards_path(@project)), |
|
1 | <%= board_breadcrumb(@message) %> | |
2 | link_to(h(@board.name), project_board_path(@project, @board)) %> |
|
|||
3 |
|
2 | |||
4 | <h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2> |
|
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)), |
|
1 | <%= board_breadcrumb(@message) %> | |
2 | link_to(h(@board.name), project_board_path(@project, @board)) %> |
|
|||
3 |
|
2 | |||
4 | <div class="contextual"> |
|
3 | <div class="contextual"> | |
5 | <%= watcher_tag(@topic, User.current) %> |
|
4 | <%= watcher_tag(@topic, User.current) %> |
@@ -7,10 +7,10 | |||||
7 | <th></th> |
|
7 | <th></th> | |
8 | </tr></thead> |
|
8 | </tr></thead> | |
9 | <tbody> |
|
9 | <tbody> | |
10 |
<% @project.boards |
|
10 | <% Board.board_tree(@project.boards) do |board, level| | |
11 | next if board.new_record? %> |
|
11 | next if board.new_record? %> | |
12 | <tr class="<%= cycle 'odd', 'even' %>"> |
|
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 | <td><%=h board.description %></td> |
|
14 | <td><%=h board.description %></td> | |
15 | <td align="center"> |
|
15 | <td align="center"> | |
16 | <% if authorize_for("boards", "edit") %> |
|
16 | <% if authorize_for("boards", "edit") %> |
@@ -5,7 +5,9 module ActiveRecord | |||||
5 | include Redmine::I18n |
|
5 | include Redmine::I18n | |
6 | # Translate attribute names for validation errors display |
|
6 | # Translate attribute names for validation errors display | |
7 | def self.human_attribute_name(attr, *args) |
|
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 | end |
|
11 | end | |
10 | end |
|
12 | end | |
11 | end |
|
13 | end |
@@ -330,6 +330,7 en: | |||||
330 | field_ldap_filter: LDAP filter |
|
330 | field_ldap_filter: LDAP filter | |
331 | field_core_fields: Standard fields |
|
331 | field_core_fields: Standard fields | |
332 | field_timeout: "Timeout (in seconds)" |
|
332 | field_timeout: "Timeout (in seconds)" | |
|
333 | field_board_parent: Parent forum | |||
333 |
|
334 | |||
334 | setting_app_title: Application title |
|
335 | setting_app_title: Application title | |
335 | setting_app_subtitle: Application subtitle |
|
336 | setting_app_subtitle: Application subtitle |
@@ -329,6 +329,7 fr: | |||||
329 | field_ldap_filter: Filtre LDAP |
|
329 | field_ldap_filter: Filtre LDAP | |
330 | field_core_fields: Champs standards |
|
330 | field_core_fields: Champs standards | |
331 | field_timeout: "Timeout (en secondes)" |
|
331 | field_timeout: "Timeout (en secondes)" | |
|
332 | field_board_parent: Forum parent | |||
332 |
|
333 | |||
333 | setting_app_title: Titre de l'application |
|
334 | setting_app_title: Titre de l'application | |
334 | setting_app_subtitle: Sous-titre de l'application |
|
335 | setting_app_subtitle: Sous-titre de l'application |
@@ -74,7 +74,7 module ActiveRecord | |||||
74 | # |
|
74 | # | |
75 | # root.descendants # => [child1, subchild1, subchild2] |
|
75 | # root.descendants # => [child1, subchild1, subchild2] | |
76 | def descendants |
|
76 | def descendants | |
77 |
children + children.collect(&: |
|
77 | children + children.collect(&:descendants).flatten | |
78 | end |
|
78 | end | |
79 |
|
79 | |||
80 | # Returns list of descendants and a reference to the current node. |
|
80 | # Returns list of descendants and a reference to the current node. |
@@ -98,6 +98,23 class BoardsControllerTest < ActionController::TestCase | |||||
98 | get :new, :project_id => 1 |
|
98 | get :new, :project_id => 1 | |
99 | assert_response :success |
|
99 | assert_response :success | |
100 | assert_template 'new' |
|
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 | end |
|
118 | end | |
102 |
|
119 | |||
103 | def test_create |
|
120 | def test_create | |
@@ -111,6 +128,16 class BoardsControllerTest < ActionController::TestCase | |||||
111 | assert_equal 'Testing board creation', board.description |
|
128 | assert_equal 'Testing board creation', board.description | |
112 | end |
|
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 | def test_create_with_failure |
|
141 | def test_create_with_failure | |
115 | @request.session[:user_id] = 2 |
|
142 | @request.session[:user_id] = 2 | |
116 | assert_no_difference 'Board.count' do |
|
143 | assert_no_difference 'Board.count' do | |
@@ -127,6 +154,18 class BoardsControllerTest < ActionController::TestCase | |||||
127 | assert_template 'edit' |
|
154 | assert_template 'edit' | |
128 | end |
|
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 | def test_update |
|
169 | def test_update | |
131 | @request.session[:user_id] = 2 |
|
170 | @request.session[:user_id] = 2 | |
132 | assert_no_difference 'Board.count' do |
|
171 | assert_no_difference 'Board.count' do |
@@ -99,4 +99,15 module ObjectHelpers | |||||
99 | source.save! |
|
99 | source.save! | |
100 | source |
|
100 | source | |
101 | end |
|
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 | end |
|
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 | require File.expand_path('../../test_helper', __FILE__) |
|
20 | require File.expand_path('../../test_helper', __FILE__) | |
2 |
|
21 | |||
3 | class BoardTest < ActiveSupport::TestCase |
|
22 | class BoardTest < ActiveSupport::TestCase | |
@@ -21,6 +40,54 class BoardTest < ActiveSupport::TestCase | |||||
21 | assert_equal @project.boards.size, board.position |
|
40 | assert_equal @project.boards.size, board.position | |
22 | end |
|
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 | def test_destroy |
|
91 | def test_destroy | |
25 | board = Board.find(1) |
|
92 | board = Board.find(1) | |
26 | assert_difference 'Message.count', -6 do |
|
93 | assert_difference 'Message.count', -6 do | |
@@ -32,4 +99,15 class BoardTest < ActiveSupport::TestCase | |||||
32 | end |
|
99 | end | |
33 | assert_equal 0, Message.count(:conditions => {:board_id => 1}) |
|
100 | assert_equal 0, Message.count(:conditions => {:board_id => 1}) | |
34 | end |
|
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 | end |
|
113 | end |
General Comments 0
You need to be logged in to leave comments.
Login now