##// END OF EJS Templates
Forums enhancements:...
Jean-Philippe Lang -
r913:29b3614bcb75
parent child
Show More
@@ -0,0 +1,6
1 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%=h @message.subject %></h2>
2
3 <% form_for :message, @message, :url => {:action => 'edit'}, :html => {:multipart => true} do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
@@ -0,0 +1,9
1 class AddMessagesLocked < ActiveRecord::Migration
2 def self.up
3 add_column :messages, :locked, :boolean, :default => false
4 end
5
6 def self.down
7 remove_column :messages, :locked
8 end
9 end
@@ -0,0 +1,9
1 class AddMessagesSticky < ActiveRecord::Migration
2 def self.up
3 add_column :messages, :sticky, :integer, :default => 0
4 end
5
6 def self.down
7 remove_column :messages, :sticky
8 end
9 end
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,50
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'boards_controller'
20
21 # Re-raise errors caught by the controller.
22 class BoardsController; def rescue_action(e) raise e end; end
23
24 class BoardsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :boards, :messages, :enabled_modules
26
27 def setup
28 @controller = BoardsController.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
32 end
33
34 def test_index
35 get :index, :project_id => 1
36 assert_response :success
37 assert_template 'index'
38 assert_not_nil assigns(:boards)
39 assert_not_nil assigns(:project)
40 end
41
42 def test_show
43 get :show, :project_id => 1, :id => 1
44 assert_response :success
45 assert_template 'show'
46 assert_not_nil assigns(:board)
47 assert_not_nil assigns(:project)
48 assert_not_nil assigns(:topics)
49 end
50 end
@@ -0,0 +1,49
1 # redMine - project management software
2 # Copyright (C) 2006-2007 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
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'messages_controller'
20
21 # Re-raise errors caught by the controller.
22 class MessagesController; def rescue_action(e) raise e end; end
23
24 class MessagesControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :roles, :boards, :messages, :enabled_modules
26
27 def setup
28 @controller = MessagesController.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
32 end
33
34 def test_show
35 get :show, :board_id => 1, :id => 1
36 assert_response :success
37 assert_template 'show'
38 assert_not_nil assigns(:board)
39 assert_not_nil assigns(:project)
40 assert_not_nil assigns(:topic)
41 end
42
43 def test_reply
44 @request.session[:user_id] = 2
45 post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' }
46 assert_redirected_to 'messages/show'
47 assert Message.find_by_subject('Test reply')
48 end
49 end
@@ -1,86 +1,86
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class BoardsController < ApplicationController
18 class BoardsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 before_filter :find_project, :authorize
20 before_filter :find_project, :authorize
21
21
22 helper :messages
22 helper :messages
23 include MessagesHelper
23 include MessagesHelper
24 helper :sort
24 helper :sort
25 include SortHelper
25 include SortHelper
26 helper :watchers
26 helper :watchers
27 include WatchersHelper
27 include WatchersHelper
28
28
29 def index
29 def index
30 @boards = @project.boards
30 @boards = @project.boards
31 # show the board if there is only one
31 # show the board if there is only one
32 if @boards.size == 1
32 if @boards.size == 1
33 @board = @boards.first
33 @board = @boards.first
34 show
34 show
35 end
35 end
36 end
36 end
37
37
38 def show
38 def show
39 sort_init "#{Message.table_name}.updated_on", "desc"
39 sort_init "#{Message.table_name}.updated_on", "desc"
40 sort_update
40 sort_update
41
41
42 @topic_count = @board.topics.count
42 @topic_count = @board.topics.count
43 @topic_pages = Paginator.new self, @topic_count, 25, params['page']
43 @topic_pages = Paginator.new self, @topic_count, 25, params['page']
44 @topics = @board.topics.find :all, :order => sort_clause,
44 @topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}",
45 :include => [:author, {:last_reply => :author}],
45 :include => [:author, {:last_reply => :author}],
46 :limit => @topic_pages.items_per_page,
46 :limit => @topic_pages.items_per_page,
47 :offset => @topic_pages.current.offset
47 :offset => @topic_pages.current.offset
48 render :action => 'show', :layout => !request.xhr?
48 render :action => 'show', :layout => !request.xhr?
49 end
49 end
50
50
51 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
51 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
52
52
53 def new
53 def new
54 @board = Board.new(params[:board])
54 @board = Board.new(params[:board])
55 @board.project = @project
55 @board.project = @project
56 if request.post? && @board.save
56 if request.post? && @board.save
57 flash[:notice] = l(:notice_successful_create)
57 flash[:notice] = l(:notice_successful_create)
58 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
58 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
59 end
59 end
60 end
60 end
61
61
62 def edit
62 def edit
63 if request.post? && @board.update_attributes(params[:board])
63 if request.post? && @board.update_attributes(params[:board])
64 case params[:position]
64 case params[:position]
65 when 'highest'; @board.move_to_top
65 when 'highest'; @board.move_to_top
66 when 'higher'; @board.move_higher
66 when 'higher'; @board.move_higher
67 when 'lower'; @board.move_lower
67 when 'lower'; @board.move_lower
68 when 'lowest'; @board.move_to_bottom
68 when 'lowest'; @board.move_to_bottom
69 end if params[:position]
69 end if params[:position]
70 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
70 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
71 end
71 end
72 end
72 end
73
73
74 def destroy
74 def destroy
75 @board.destroy
75 @board.destroy
76 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
76 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
77 end
77 end
78
78
79 private
79 private
80 def find_project
80 def find_project
81 @project = Project.find(params[:project_id])
81 @project = Project.find(params[:project_id])
82 @board = @project.boards.find(params[:id]) if params[:id]
82 @board = @project.boards.find(params[:id]) if params[:id]
83 rescue ActiveRecord::RecordNotFound
83 rescue ActiveRecord::RecordNotFound
84 render_404
84 render_404
85 end
85 end
86 end
86 end
@@ -1,65 +1,104
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MessagesController < ApplicationController
18 class MessagesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 before_filter :find_project, :authorize
20 before_filter :find_board, :only => :new
21 before_filter :find_message, :except => :new
22 before_filter :authorize
21
23
22 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
24 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
23
25
24 helper :attachments
26 helper :attachments
25 include AttachmentsHelper
27 include AttachmentsHelper
26
28
29 # Show a topic and its replies
27 def show
30 def show
28 @reply = Message.new(:subject => "RE: #{@message.subject}")
31 @reply = Message.new(:subject => "RE: #{@message.subject}")
29 render :action => "show", :layout => false if request.xhr?
32 render :action => "show", :layout => false if request.xhr?
30 end
33 end
31
34
35 # Create a new topic
32 def new
36 def new
33 @message = Message.new(params[:message])
37 @message = Message.new(params[:message])
34 @message.author = User.current
38 @message.author = User.current
35 @message.board = @board
39 @message.board = @board
40 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
41 @message.locked = params[:message]['locked']
42 @message.sticky = params[:message]['sticky']
43 end
36 if request.post? && @message.save
44 if request.post? && @message.save
37 params[:attachments].each { |file|
45 params[:attachments].each { |file|
38 Attachment.create(:container => @message, :file => file, :author => User.current) if file.size > 0
46 Attachment.create(:container => @message, :file => file, :author => User.current) if file.size > 0
39 } if params[:attachments] and params[:attachments].is_a? Array
47 } if params[:attachments] and params[:attachments].is_a? Array
40 redirect_to :action => 'show', :id => @message
48 redirect_to :action => 'show', :id => @message
41 end
49 end
42 end
50 end
43
51
52 # Reply to a topic
44 def reply
53 def reply
45 @reply = Message.new(params[:reply])
54 @reply = Message.new(params[:reply])
46 @reply.author = User.current
55 @reply.author = User.current
47 @reply.board = @board
56 @reply.board = @board
48 @message.children << @reply
57 @topic.children << @reply
49 if !@reply.new_record?
58 if !@reply.new_record?
50 params[:attachments].each { |file|
59 params[:attachments].each { |file|
51 Attachment.create(:container => @reply, :file => file, :author => User.current) if file.size > 0
60 Attachment.create(:container => @reply, :file => file, :author => User.current) if file.size > 0
52 } if params[:attachments] and params[:attachments].is_a? Array
61 } if params[:attachments] and params[:attachments].is_a? Array
53 end
62 end
54 redirect_to :action => 'show', :id => @message
63 redirect_to :action => 'show', :id => @topic
64 end
65
66 # Edit a message
67 def edit
68 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
69 @message.locked = params[:message]['locked']
70 @message.sticky = params[:message]['sticky']
71 end
72 if request.post? && @message.update_attributes(params[:message])
73 params[:attachments].each { |file|
74 Attachment.create(:container => @message, :file => file, :author => User.current) if file.size > 0
75 } if params[:attachments] and params[:attachments].is_a? Array
76 flash[:notice] = l(:notice_successful_update)
77 redirect_to :action => 'show', :id => @topic
78 end
79 end
80
81 # Delete a messages
82 def destroy
83 @message.destroy
84 redirect_to @message.parent.nil? ?
85 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
86 { :action => 'show', :id => @message.parent }
55 end
87 end
56
88
57 private
89 private
58 def find_project
90 def find_message
91 find_board
92 @message = @board.messages.find(params[:id], :include => :parent)
93 @topic = @message.root
94 rescue ActiveRecord::RecordNotFound
95 render_404
96 end
97
98 def find_board
59 @board = Board.find(params[:board_id], :include => :project)
99 @board = Board.find(params[:board_id], :include => :project)
60 @project = @board.project
100 @project = @board.project
61 @message = @board.topics.find(params[:id]) if params[:id]
62 rescue ActiveRecord::RecordNotFound
101 rescue ActiveRecord::RecordNotFound
63 render_404
102 render_404
64 end
103 end
65 end
104 end
@@ -1,403 +1,403
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module ApplicationHelper
18 module ApplicationHelper
19 include Redmine::WikiFormatting::Macros::Definitions
19 include Redmine::WikiFormatting::Macros::Definitions
20
20
21 def current_role
21 def current_role
22 @current_role ||= User.current.role_for_project(@project)
22 @current_role ||= User.current.role_for_project(@project)
23 end
23 end
24
24
25 # Return true if user is authorized for controller/action, otherwise false
25 # Return true if user is authorized for controller/action, otherwise false
26 def authorize_for(controller, action)
26 def authorize_for(controller, action)
27 User.current.allowed_to?({:controller => controller, :action => action}, @project)
27 User.current.allowed_to?({:controller => controller, :action => action}, @project)
28 end
28 end
29
29
30 # Display a link if user is authorized
30 # Display a link if user is authorized
31 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
31 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
32 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
32 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
33 end
33 end
34
34
35 # Display a link to user's account page
35 # Display a link to user's account page
36 def link_to_user(user)
36 def link_to_user(user)
37 link_to user.name, :controller => 'account', :action => 'show', :id => user
37 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
38 end
38 end
39
39
40 def link_to_issue(issue)
40 def link_to_issue(issue)
41 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
41 link_to "#{issue.tracker.name} ##{issue.id}", :controller => "issues", :action => "show", :id => issue
42 end
42 end
43
43
44 def toggle_link(name, id, options={})
44 def toggle_link(name, id, options={})
45 onclick = "Element.toggle('#{id}'); "
45 onclick = "Element.toggle('#{id}'); "
46 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
46 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
47 onclick << "return false;"
47 onclick << "return false;"
48 link_to(name, "#", :onclick => onclick)
48 link_to(name, "#", :onclick => onclick)
49 end
49 end
50
50
51 def show_and_goto_link(name, id, options={})
51 def show_and_goto_link(name, id, options={})
52 onclick = "Element.show('#{id}'); "
52 onclick = "Element.show('#{id}'); "
53 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
53 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
54 onclick << "location.href='##{id}-anchor'; "
54 onclick << "location.href='##{id}-anchor'; "
55 onclick << "return false;"
55 onclick << "return false;"
56 link_to(name, "#", options.merge(:onclick => onclick))
56 link_to(name, "#", options.merge(:onclick => onclick))
57 end
57 end
58
58
59 def image_to_function(name, function, html_options = {})
59 def image_to_function(name, function, html_options = {})
60 html_options.symbolize_keys!
60 html_options.symbolize_keys!
61 tag(:input, html_options.merge({
61 tag(:input, html_options.merge({
62 :type => "image", :src => image_path(name),
62 :type => "image", :src => image_path(name),
63 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
63 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
64 }))
64 }))
65 end
65 end
66
66
67 def prompt_to_remote(name, text, param, url, html_options = {})
67 def prompt_to_remote(name, text, param, url, html_options = {})
68 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
68 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
69 link_to name, {}, html_options
69 link_to name, {}, html_options
70 end
70 end
71
71
72 def format_date(date)
72 def format_date(date)
73 return nil unless date
73 return nil unless date
74 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
74 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
75 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
75 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
76 date.strftime(@date_format)
76 date.strftime(@date_format)
77 end
77 end
78
78
79 def format_time(time, include_date = true)
79 def format_time(time, include_date = true)
80 return nil unless time
80 return nil unless time
81 time = time.to_time if time.is_a?(String)
81 time = time.to_time if time.is_a?(String)
82 zone = User.current.time_zone
82 zone = User.current.time_zone
83 if time.utc?
83 if time.utc?
84 local = zone ? zone.adjust(time) : time.getlocal
84 local = zone ? zone.adjust(time) : time.getlocal
85 else
85 else
86 local = zone ? zone.adjust(time.getutc) : time
86 local = zone ? zone.adjust(time.getutc) : time
87 end
87 end
88 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
88 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
89 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
89 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
90 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
90 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
91 end
91 end
92
92
93 def authoring(created, author)
93 def authoring(created, author)
94 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
94 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
95 l(:label_added_time_by, author.name, time_tag)
95 l(:label_added_time_by, author || 'Anonymous', time_tag)
96 end
96 end
97
97
98 def day_name(day)
98 def day_name(day)
99 l(:general_day_names).split(',')[day-1]
99 l(:general_day_names).split(',')[day-1]
100 end
100 end
101
101
102 def month_name(month)
102 def month_name(month)
103 l(:actionview_datehelper_select_month_names).split(',')[month-1]
103 l(:actionview_datehelper_select_month_names).split(',')[month-1]
104 end
104 end
105
105
106 def pagination_links_full(paginator, options={}, html_options={})
106 def pagination_links_full(paginator, options={}, html_options={})
107 page_param = options.delete(:page_param) || :page
107 page_param = options.delete(:page_param) || :page
108
108
109 html = ''
109 html = ''
110 html << link_to_remote(('&#171; ' + l(:label_previous)),
110 html << link_to_remote(('&#171; ' + l(:label_previous)),
111 {:update => "content", :url => options.merge(page_param => paginator.current.previous)},
111 {:update => "content", :url => options.merge(page_param => paginator.current.previous)},
112 {:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
112 {:href => url_for(:params => options.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
113
113
114 html << (pagination_links_each(paginator, options) do |n|
114 html << (pagination_links_each(paginator, options) do |n|
115 link_to_remote(n.to_s,
115 link_to_remote(n.to_s,
116 {:url => {:params => options.merge(page_param => n)}, :update => 'content'},
116 {:url => {:params => options.merge(page_param => n)}, :update => 'content'},
117 {:href => url_for(:params => options.merge(page_param => n))})
117 {:href => url_for(:params => options.merge(page_param => n))})
118 end || '')
118 end || '')
119
119
120 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
120 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
121 {:update => "content", :url => options.merge(page_param => paginator.current.next)},
121 {:update => "content", :url => options.merge(page_param => paginator.current.next)},
122 {:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next
122 {:href => url_for(:params => options.merge(page_param => paginator.current.next))}) if paginator.current.next
123 html
123 html
124 end
124 end
125
125
126 def set_html_title(text)
126 def set_html_title(text)
127 @html_header_title = text
127 @html_header_title = text
128 end
128 end
129
129
130 def html_title
130 def html_title
131 title = []
131 title = []
132 title << @project.name if @project
132 title << @project.name if @project
133 title << @html_header_title
133 title << @html_header_title
134 title << Setting.app_title
134 title << Setting.app_title
135 title.compact.join(' - ')
135 title.compact.join(' - ')
136 end
136 end
137
137
138 ACCESSKEYS = {:edit => 'e',
138 ACCESSKEYS = {:edit => 'e',
139 :preview => 'r',
139 :preview => 'r',
140 :quick_search => 'f',
140 :quick_search => 'f',
141 :search => '4',
141 :search => '4',
142 }.freeze unless const_defined?(:ACCESSKEYS)
142 }.freeze unless const_defined?(:ACCESSKEYS)
143
143
144 def accesskey(s)
144 def accesskey(s)
145 ACCESSKEYS[s]
145 ACCESSKEYS[s]
146 end
146 end
147
147
148 # Formats text according to system settings.
148 # Formats text according to system settings.
149 # 2 ways to call this method:
149 # 2 ways to call this method:
150 # * with a String: textilizable(text, options)
150 # * with a String: textilizable(text, options)
151 # * with an object and one of its attribute: textilizable(issue, :description, options)
151 # * with an object and one of its attribute: textilizable(issue, :description, options)
152 def textilizable(*args)
152 def textilizable(*args)
153 options = args.last.is_a?(Hash) ? args.pop : {}
153 options = args.last.is_a?(Hash) ? args.pop : {}
154 case args.size
154 case args.size
155 when 1
155 when 1
156 obj = nil
156 obj = nil
157 text = args.shift || ''
157 text = args.shift || ''
158 when 2
158 when 2
159 obj = args.shift
159 obj = args.shift
160 text = obj.send(args.shift)
160 text = obj.send(args.shift)
161 else
161 else
162 raise ArgumentError, 'invalid arguments to textilizable'
162 raise ArgumentError, 'invalid arguments to textilizable'
163 end
163 end
164
164
165 # when using an image link, try to use an attachment, if possible
165 # when using an image link, try to use an attachment, if possible
166 attachments = options[:attachments]
166 attachments = options[:attachments]
167 if attachments
167 if attachments
168 text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
168 text = text.gsub(/!([<>=]*)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
169 align = $1
169 align = $1
170 filename = $2
170 filename = $2
171 rf = Regexp.new(filename, Regexp::IGNORECASE)
171 rf = Regexp.new(filename, Regexp::IGNORECASE)
172 # search for the picture in attachments
172 # search for the picture in attachments
173 if found = attachments.detect { |att| att.filename =~ rf }
173 if found = attachments.detect { |att| att.filename =~ rf }
174 image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
174 image_url = url_for :controller => 'attachments', :action => 'download', :id => found.id
175 "!#{align}#{image_url}!"
175 "!#{align}#{image_url}!"
176 else
176 else
177 "!#{align}#{filename}!"
177 "!#{align}#{filename}!"
178 end
178 end
179 end
179 end
180 end
180 end
181
181
182 text = (Setting.text_formatting == 'textile') ?
182 text = (Setting.text_formatting == 'textile') ?
183 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
183 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
184 simple_format(auto_link(h(text)))
184 simple_format(auto_link(h(text)))
185
185
186 # different methods for formatting wiki links
186 # different methods for formatting wiki links
187 case options[:wiki_links]
187 case options[:wiki_links]
188 when :local
188 when :local
189 # used for local links to html files
189 # used for local links to html files
190 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
190 format_wiki_link = Proc.new {|project, title| "#{title}.html" }
191 when :anchor
191 when :anchor
192 # used for single-file wiki export
192 # used for single-file wiki export
193 format_wiki_link = Proc.new {|project, title| "##{title}" }
193 format_wiki_link = Proc.new {|project, title| "##{title}" }
194 else
194 else
195 format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
195 format_wiki_link = Proc.new {|project, title| url_for :controller => 'wiki', :action => 'index', :id => project, :page => title }
196 end
196 end
197
197
198 project = options[:project] || @project
198 project = options[:project] || @project
199
199
200 # turn wiki links into html links
200 # turn wiki links into html links
201 # example:
201 # example:
202 # [[mypage]]
202 # [[mypage]]
203 # [[mypage|mytext]]
203 # [[mypage|mytext]]
204 # wiki links can refer other project wikis, using project name or identifier:
204 # wiki links can refer other project wikis, using project name or identifier:
205 # [[project:]] -> wiki starting page
205 # [[project:]] -> wiki starting page
206 # [[project:|mytext]]
206 # [[project:|mytext]]
207 # [[project:mypage]]
207 # [[project:mypage]]
208 # [[project:mypage|mytext]]
208 # [[project:mypage|mytext]]
209 text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m|
209 text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) do |m|
210 link_project = project
210 link_project = project
211 page = $1
211 page = $1
212 title = $3
212 title = $3
213 if page =~ /^([^\:]+)\:(.*)$/
213 if page =~ /^([^\:]+)\:(.*)$/
214 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
214 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
215 page = title || $2
215 page = title || $2
216 title = $1 if page.blank?
216 title = $1 if page.blank?
217 end
217 end
218
218
219 if link_project && link_project.wiki
219 if link_project && link_project.wiki
220 # check if page exists
220 # check if page exists
221 wiki_page = link_project.wiki.find_page(page)
221 wiki_page = link_project.wiki.find_page(page)
222 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
222 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
223 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
223 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
224 else
224 else
225 # project or wiki doesn't exist
225 # project or wiki doesn't exist
226 title || page
226 title || page
227 end
227 end
228 end
228 end
229
229
230 # turn issue and revision ids into links
230 # turn issue and revision ids into links
231 # example:
231 # example:
232 # #52 -> <a href="/issues/show/52">#52</a>
232 # #52 -> <a href="/issues/show/52">#52</a>
233 # r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (project.id is 6)
233 # r52 -> <a href="/repositories/revision/6?rev=52">r52</a> (project.id is 6)
234 text = text.gsub(%r{([\s\(,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m|
234 text = text.gsub(%r{([\s\(,-^])(#|r)(\d+)(?=[[:punct:]]|\s|<|$)}) do |m|
235 leading, otype, oid = $1, $2, $3
235 leading, otype, oid = $1, $2, $3
236 link = nil
236 link = nil
237 if otype == 'r'
237 if otype == 'r'
238 if project && (changeset = project.changesets.find_by_revision(oid))
238 if project && (changeset = project.changesets.find_by_revision(oid))
239 link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset',
239 link = link_to("r#{oid}", {:controller => 'repositories', :action => 'revision', :id => project.id, :rev => oid}, :class => 'changeset',
240 :title => truncate(changeset.comments, 100))
240 :title => truncate(changeset.comments, 100))
241 end
241 end
242 else
242 else
243 if issue = Issue.find_by_id(oid.to_i, :include => [:project, :status], :conditions => Project.visible_by(User.current))
243 if issue = Issue.find_by_id(oid.to_i, :include => [:project, :status], :conditions => Project.visible_by(User.current))
244 link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue',
244 link = link_to("##{oid}", {:controller => 'issues', :action => 'show', :id => oid}, :class => 'issue',
245 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
245 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
246 link = content_tag('del', link) if issue.closed?
246 link = content_tag('del', link) if issue.closed?
247 end
247 end
248 end
248 end
249 leading + (link || "#{otype}#{oid}")
249 leading + (link || "#{otype}#{oid}")
250 end
250 end
251
251
252 text
252 text
253 end
253 end
254
254
255 # Same as Rails' simple_format helper without using paragraphs
255 # Same as Rails' simple_format helper without using paragraphs
256 def simple_format_without_paragraph(text)
256 def simple_format_without_paragraph(text)
257 text.to_s.
257 text.to_s.
258 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
258 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
259 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
259 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
260 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
260 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
261 end
261 end
262
262
263 def error_messages_for(object_name, options = {})
263 def error_messages_for(object_name, options = {})
264 options = options.symbolize_keys
264 options = options.symbolize_keys
265 object = instance_variable_get("@#{object_name}")
265 object = instance_variable_get("@#{object_name}")
266 if object && !object.errors.empty?
266 if object && !object.errors.empty?
267 # build full_messages here with controller current language
267 # build full_messages here with controller current language
268 full_messages = []
268 full_messages = []
269 object.errors.each do |attr, msg|
269 object.errors.each do |attr, msg|
270 next if msg.nil?
270 next if msg.nil?
271 msg = msg.first if msg.is_a? Array
271 msg = msg.first if msg.is_a? Array
272 if attr == "base"
272 if attr == "base"
273 full_messages << l(msg)
273 full_messages << l(msg)
274 else
274 else
275 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
275 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
276 end
276 end
277 end
277 end
278 # retrieve custom values error messages
278 # retrieve custom values error messages
279 if object.errors[:custom_values]
279 if object.errors[:custom_values]
280 object.custom_values.each do |v|
280 object.custom_values.each do |v|
281 v.errors.each do |attr, msg|
281 v.errors.each do |attr, msg|
282 next if msg.nil?
282 next if msg.nil?
283 msg = msg.first if msg.is_a? Array
283 msg = msg.first if msg.is_a? Array
284 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
284 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
285 end
285 end
286 end
286 end
287 end
287 end
288 content_tag("div",
288 content_tag("div",
289 content_tag(
289 content_tag(
290 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
290 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
291 ) +
291 ) +
292 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
292 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
293 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
293 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
294 )
294 )
295 else
295 else
296 ""
296 ""
297 end
297 end
298 end
298 end
299
299
300 def lang_options_for_select(blank=true)
300 def lang_options_for_select(blank=true)
301 (blank ? [["(auto)", ""]] : []) +
301 (blank ? [["(auto)", ""]] : []) +
302 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first }
302 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.first <=> y.first }
303 end
303 end
304
304
305 def label_tag_for(name, option_tags = nil, options = {})
305 def label_tag_for(name, option_tags = nil, options = {})
306 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
306 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
307 content_tag("label", label_text)
307 content_tag("label", label_text)
308 end
308 end
309
309
310 def labelled_tabular_form_for(name, object, options, &proc)
310 def labelled_tabular_form_for(name, object, options, &proc)
311 options[:html] ||= {}
311 options[:html] ||= {}
312 options[:html].store :class, "tabular"
312 options[:html].store :class, "tabular"
313 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
313 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
314 end
314 end
315
315
316 def check_all_links(form_name)
316 def check_all_links(form_name)
317 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
317 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
318 " | " +
318 " | " +
319 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
319 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
320 end
320 end
321
321
322 def progress_bar(pct, options={})
322 def progress_bar(pct, options={})
323 width = options[:width] || '100px;'
323 width = options[:width] || '100px;'
324 legend = options[:legend] || ''
324 legend = options[:legend] || ''
325 content_tag('table',
325 content_tag('table',
326 content_tag('tr',
326 content_tag('tr',
327 (pct > 0 ? content_tag('td', '', :width => "#{pct.floor}%;", :class => 'closed') : '') +
327 (pct > 0 ? content_tag('td', '', :width => "#{pct.floor}%;", :class => 'closed') : '') +
328 (pct < 100 ? content_tag('td', '', :width => "#{100-pct.floor}%;", :class => 'open') : '')
328 (pct < 100 ? content_tag('td', '', :width => "#{100-pct.floor}%;", :class => 'open') : '')
329 ), :class => 'progress', :style => "width: #{width};") +
329 ), :class => 'progress', :style => "width: #{width};") +
330 content_tag('p', legend, :class => 'pourcent')
330 content_tag('p', legend, :class => 'pourcent')
331 end
331 end
332
332
333 def context_menu_link(name, url, options={})
333 def context_menu_link(name, url, options={})
334 options[:class] ||= ''
334 options[:class] ||= ''
335 if options.delete(:selected)
335 if options.delete(:selected)
336 options[:class] << ' icon-checked disabled'
336 options[:class] << ' icon-checked disabled'
337 options[:disabled] = true
337 options[:disabled] = true
338 end
338 end
339 if options.delete(:disabled)
339 if options.delete(:disabled)
340 options.delete(:method)
340 options.delete(:method)
341 options.delete(:confirm)
341 options.delete(:confirm)
342 options.delete(:onclick)
342 options.delete(:onclick)
343 options[:class] << ' disabled'
343 options[:class] << ' disabled'
344 url = '#'
344 url = '#'
345 end
345 end
346 link_to name, url, options
346 link_to name, url, options
347 end
347 end
348
348
349 def calendar_for(field_id)
349 def calendar_for(field_id)
350 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
350 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
351 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
351 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
352 end
352 end
353
353
354 def wikitoolbar_for(field_id)
354 def wikitoolbar_for(field_id)
355 return '' unless Setting.text_formatting == 'textile'
355 return '' unless Setting.text_formatting == 'textile'
356 javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
356 javascript_include_tag('jstoolbar') + javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.draw();")
357 end
357 end
358
358
359 def content_for(name, content = nil, &block)
359 def content_for(name, content = nil, &block)
360 @has_content ||= {}
360 @has_content ||= {}
361 @has_content[name] = true
361 @has_content[name] = true
362 super(name, content, &block)
362 super(name, content, &block)
363 end
363 end
364
364
365 def has_content?(name)
365 def has_content?(name)
366 (@has_content && @has_content[name]) || false
366 (@has_content && @has_content[name]) || false
367 end
367 end
368 end
368 end
369
369
370 class TabularFormBuilder < ActionView::Helpers::FormBuilder
370 class TabularFormBuilder < ActionView::Helpers::FormBuilder
371 include GLoc
371 include GLoc
372
372
373 def initialize(object_name, object, template, options, proc)
373 def initialize(object_name, object, template, options, proc)
374 set_language_if_valid options.delete(:lang)
374 set_language_if_valid options.delete(:lang)
375 @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
375 @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
376 end
376 end
377
377
378 (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
378 (field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
379 src = <<-END_SRC
379 src = <<-END_SRC
380 def #{selector}(field, options = {})
380 def #{selector}(field, options = {})
381 return super if options.delete :no_label
381 return super if options.delete :no_label
382 label_text = l(options[:label]) if options[:label]
382 label_text = l(options[:label]) if options[:label]
383 label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
383 label_text ||= l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym)
384 label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
384 label_text << @template.content_tag("span", " *", :class => "required") if options.delete(:required)
385 label = @template.content_tag("label", label_text,
385 label = @template.content_tag("label", label_text,
386 :class => (@object && @object.errors[field] ? "error" : nil),
386 :class => (@object && @object.errors[field] ? "error" : nil),
387 :for => (@object_name.to_s + "_" + field.to_s))
387 :for => (@object_name.to_s + "_" + field.to_s))
388 label + super
388 label + super
389 end
389 end
390 END_SRC
390 END_SRC
391 class_eval src, __FILE__, __LINE__
391 class_eval src, __FILE__, __LINE__
392 end
392 end
393
393
394 def select(field, choices, options = {}, html_options = {})
394 def select(field, choices, options = {}, html_options = {})
395 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
395 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
396 label = @template.content_tag("label", label_text,
396 label = @template.content_tag("label", label_text,
397 :class => (@object && @object.errors[field] ? "error" : nil),
397 :class => (@object && @object.errors[field] ? "error" : nil),
398 :for => (@object_name.to_s + "_" + field.to_s))
398 :for => (@object_name.to_s + "_" + field.to_s))
399 label + super
399 label + super
400 end
400 end
401
401
402 end
402 end
403
403
@@ -1,49 +1,67
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Message < ActiveRecord::Base
18 class Message < ActiveRecord::Base
19 belongs_to :board
19 belongs_to :board
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 has_many :attachments, :as => :container, :dependent => :destroy
22 has_many :attachments, :as => :container, :dependent => :destroy
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24
24
25 acts_as_searchable :columns => ['subject', 'content'],
25 acts_as_searchable :columns => ['subject', 'content'],
26 :include => :board,
26 :include => :board,
27 :project_key => 'project_id',
27 :project_key => 'project_id',
28 :date_column => 'created_on'
28 :date_column => 'created_on'
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 :description => :content,
30 :description => :content,
31 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}}
31 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}}
32
32
33 attr_protected :locked, :sticky
33 validates_presence_of :subject, :content
34 validates_presence_of :subject, :content
34 validates_length_of :subject, :maximum => 255
35 validates_length_of :subject, :maximum => 255
35
36
37 def validate_on_create
38 # Can not reply to a locked topic
39 errors.add_to_base 'Topic is locked' if root.locked?
40 end
41
36 def after_create
42 def after_create
37 board.update_attribute(:last_message_id, self.id)
43 board.update_attribute(:last_message_id, self.id)
38 board.increment! :messages_count
44 board.increment! :messages_count
39 if parent
45 if parent
40 parent.reload.update_attribute(:last_reply_id, self.id)
46 parent.reload.update_attribute(:last_reply_id, self.id)
41 else
47 else
42 board.increment! :topics_count
48 board.increment! :topics_count
43 end
49 end
44 end
50 end
45
51
52 def after_destroy
53 # The following line is required so that the previous counter
54 # updates (due to children removal) are not overwritten
55 board.reload
56 board.decrement! :messages_count
57 board.decrement! :topics_count unless parent
58 end
59
60 def sticky?
61 sticky == 1
62 end
63
46 def project
64 def project
47 board.project
65 board.project
48 end
66 end
49 end
67 end
@@ -1,30 +1,30
1 <h2><%= l(:label_board_plural) %></h2>
1 <h2><%= l(:label_board_plural) %></h2>
2
2
3 <table class="list">
3 <table class="list">
4 <thead><tr>
4 <thead><tr>
5 <th><%= l(:label_board) %></th>
5 <th><%= l(:label_board) %></th>
6 <th><%= l(:label_topic_plural) %></th>
6 <th><%= l(:label_topic_plural) %></th>
7 <th><%= l(:label_message_plural) %></th>
7 <th><%= l(:label_message_plural) %></th>
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 <% for board in @boards %>
12 <tr class="<%= cycle 'odd', 'even' %>">
12 <tr class="<%= cycle 'odd', 'even' %>">
13 <td>
13 <td>
14 <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "icon22 icon22-comment" %><br />
14 <%= link_to h(board.name), {:action => 'show', :id => board}, :class => "icon22 icon22-comment" %><br />
15 <%=h board.description %>
15 <%=h board.description %>
16 </td>
16 </td>
17 <td align="center"><%= board.topics_count %></td>
17 <td align="center"><%= board.topics_count %></td>
18 <td align="center"><%= board.messages_count %></td>
18 <td align="center"><%= board.messages_count %></td>
19 <td>
19 <td>
20 <small>
20 <small>
21 <% if board.last_message %>
21 <% if board.last_message %>
22 <%= board.last_message.author.name %>, <%= format_time(board.last_message.created_on) %><br />
22 <%= authoring board.last_message.created_on, board.last_message.author %><br />
23 <%= link_to_message board.last_message %>
23 <%= link_to_message board.last_message %>
24 <% end %>
24 <% end %>
25 </small>
25 </small>
26 </td>
26 </td>
27 </tr>
27 </tr>
28 <% end %>
28 <% end %>
29 </tbody>
29 </tbody>
30 </table>
30 </table>
@@ -1,52 +1,50
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:label_message_new),
2 <%= link_to_if_authorized l(:label_message_new),
3 {:controller => 'messages', :action => 'new', :board_id => @board},
3 {:controller => 'messages', :action => 'new', :board_id => @board},
4 :class => 'icon icon-add',
4 :class => 'icon icon-add',
5 :onclick => 'Element.show("add-message"); return false;' %>
5 :onclick => 'Element.show("add-message"); return false;' %>
6 <%= watcher_tag(@board, User.current) %>
6 <%= watcher_tag(@board, User.current) %>
7 </div>
7 </div>
8
8
9 <div id="add-message" style="display:none;">
9 <div id="add-message" style="display:none;">
10 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
10 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
11 <% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true} do |f| %>
11 <% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true} do |f| %>
12 <%= render :partial => 'messages/form', :locals => {:f => f} %>
12 <%= render :partial => 'messages/form', :locals => {:f => f} %>
13 <p><%= submit_tag l(:button_create) %>
13 <p><%= submit_tag l(:button_create) %>
14 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
14 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
15 <% end %>
15 <% end %>
16 </div>
16 </div>
17
17
18 <h2><%=h @board.name %></h2>
18 <h2><%=h @board.name %></h2>
19
19
20 <% if @topics.any? %>
20 <% if @topics.any? %>
21 <table class="list">
21 <table class="list messages">
22 <thead><tr>
22 <thead><tr>
23 <th><%= l(:field_subject) %></th>
23 <th><%= l(:field_subject) %></th>
24 <th><%= l(:field_author) %></th>
24 <th><%= l(:field_author) %></th>
25 <%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %>
25 <%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %>
26 <th><%= l(:label_reply_plural) %></th>
26 <th><%= l(:label_reply_plural) %></th>
27 <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %>
27 <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %>
28 </tr></thead>
28 </tr></thead>
29 <tbody>
29 <tbody>
30 <% @topics.each do |topic| %>
30 <% @topics.each do |topic| %>
31 <tr class="<%= cycle 'odd', 'even' %>">
31 <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
32 <td><%= link_to h(topic.subject), :controller => 'messages', :action => 'show', :board_id => @board, :id => topic %></td>
32 <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %></td>
33 <td align="center"><%= link_to_user topic.author %></td>
33 <td class="author" align="center"><%= topic.author %></td>
34 <td align="center"><%= format_time(topic.created_on) %></td>
34 <td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
35 <td align="center"><%= topic.replies_count %></td>
35 <td class="replies" align="center"><%= topic.replies_count %></td>
36 <td>
36 <td class="last_message">
37 <small>
38 <% if topic.last_reply %>
37 <% if topic.last_reply %>
39 <%= topic.last_reply.author.name %>, <%= format_time(topic.last_reply.created_on) %><br />
38 <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
40 <%= link_to_message topic.last_reply %>
39 <%= link_to_message topic.last_reply %>
41 <% end %>
40 <% end %>
42 </small>
43 </td>
41 </td>
44 </tr>
42 </tr>
45 <% end %>
43 <% end %>
46 </tbody>
44 </tbody>
47 </table>
45 </table>
48 <p><%= pagination_links_full @topic_pages %>
46 <p><%= pagination_links_full @topic_pages %>
49 [ <%= @topic_pages.current.first_item %> - <%= @topic_pages.current.last_item %> / <%= @topic_count %> ]</p>
47 [ <%= @topic_pages.current.first_item %> - <%= @topic_pages.current.last_item %> / <%= @topic_count %> ]</p>
50 <% else %>
48 <% else %>
51 <p class="nodata"><%= l(:label_no_data) %></p>
49 <p class="nodata"><%= l(:label_no_data) %></p>
52 <% end %>
50 <% end %>
@@ -1,15 +1,21
1 <%= error_messages_for 'message' %>
1 <%= error_messages_for 'message' %>
2
2
3 <div class="box">
3 <div class="box">
4 <!--[form:message]-->
4 <!--[form:message]-->
5 <p><label><%= l(:field_subject) %></label><br />
5 <p><label><%= l(:field_subject) %></label><br />
6 <%= f.text_field :subject, :required => true, :size => 120 %></p>
6 <%= f.text_field :subject, :required => true, :size => 120 %>
7
8 <% if User.current.allowed_to?(:edit_messages, @project) %>
9 <label><%= f.check_box :sticky %> Sticky</label>
10 <label><%= f.check_box :locked %> Locked</label>
11 <% end %>
12 </p>
7
13
8 <p><%= f.text_area :content, :required => true, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %></p>
14 <p><%= f.text_area :content, :required => true, :cols => 80, :rows => 15, :class => 'wiki-edit', :id => 'message_content' %></p>
9 <%= wikitoolbar_for 'message_content' %>
15 <%= wikitoolbar_for 'message_content' %>
10 <!--[eoform:message]-->
16 <!--[eoform:message]-->
11
17
12 <span class="tabular">
18 <span class="tabular">
13 <%= render :partial => 'attachments/form' %>
19 <%= render :partial => 'attachments/form' %>
14 </span>
20 </span>
15 </div>
21 </div>
@@ -1,30 +1,39
1 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%=h @message.subject %></h2>
1 <div class="contextual">
2 <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit' %>
3 <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %>
4 </div>
5
6 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%=h @topic.subject %></h2>
2
7
3 <div class="message">
8 <div class="message">
4 <p><span class="author"><%= authoring @message.created_on, @message.author %></span></p>
9 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
5 <div class="wiki">
10 <div class="wiki">
6 <%= textilizable(@message.content, :attachments => @message.attachments) %>
11 <%= textilizable(@topic.content, :attachments => @topic.attachments) %>
7 </div>
12 </div>
8 <%= link_to_attachments @message.attachments, :no_author => true %>
13 <%= link_to_attachments @topic.attachments, :no_author => true %>
9 </div>
14 </div>
10 <br />
15 <br />
11
16
12 <div class="message reply">
13 <h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3>
17 <h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3>
14 <% @message.children.each do |message| %>
18 <% @topic.children.each do |message| %>
15 <a name="<%= "message-#{message.id}" %>"></a>
19 <a name="<%= "message-#{message.id}" %>"></a>
20 <div class="contextual">
21 <%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => message}, :class => 'icon icon-edit' %>
22 <%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %>
23 </div>
24 <div class="message reply">
16 <h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4>
25 <h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4>
17 <div class="wiki"><%= textilizable message.content %></div>
26 <div class="wiki"><%= textilizable message.content %></div>
18 <%= link_to_attachments message.attachments, :no_author => true %>
27 <%= link_to_attachments message.attachments, :no_author => true %>
19 <% end %>
20 </div>
28 </div>
29 <% end %>
21
30
22 <% if authorize_for('messages', 'reply') %>
31 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
23 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
32 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
24 <div id="reply" style="display:none;">
33 <div id="reply" style="display:none;">
25 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @message}, :html => {:multipart => true} do |f| %>
34 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true} do |f| %>
26 <%= render :partial => 'form', :locals => {:f => f} %>
35 <%= render :partial => 'form', :locals => {:f => f} %>
27 <%= submit_tag l(:button_submit) %>
36 <%= submit_tag l(:button_submit) %>
28 <% end %>
37 <% end %>
29 </div>
38 </div>
30 <% end %>
39 <% end %>
@@ -1,103 +1,105
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/mime_type'
3 require 'redmine/mime_type'
4 require 'redmine/themes'
4 require 'redmine/themes'
5 require 'redmine/plugin'
5 require 'redmine/plugin'
6
6
7 begin
7 begin
8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
8 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
9 rescue LoadError
9 rescue LoadError
10 # RMagick is not available
10 # RMagick is not available
11 end
11 end
12
12
13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs )
13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs )
14
14
15 # Permissions
15 # Permissions
16 Redmine::AccessControl.map do |map|
16 Redmine::AccessControl.map do |map|
17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
17 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
18 map.permission :search_project, {:search => :index}, :public => true
18 map.permission :search_project, {:search => :index}, :public => true
19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
19 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
20 map.permission :select_project_modules, {:projects => :modules}, :require => :member
21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
21 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
22 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
23
23
24 map.project_module :issue_tracking do |map|
24 map.project_module :issue_tracking do |map|
25 # Issue categories
25 # Issue categories
26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
26 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
27 # Issues
27 # Issues
28 map.permission :view_issues, {:projects => [:changelog, :roadmap],
28 map.permission :view_issues, {:projects => [:changelog, :roadmap],
29 :issues => [:index, :changes, :show, :context_menu],
29 :issues => [:index, :changes, :show, :context_menu],
30 :queries => :index,
30 :queries => :index,
31 :reports => :issue_report}, :public => true
31 :reports => :issue_report}, :public => true
32 map.permission :add_issues, {:projects => :add_issue}
32 map.permission :add_issues, {:projects => :add_issue}
33 map.permission :edit_issues, {:projects => :bulk_edit_issues,
33 map.permission :edit_issues, {:projects => :bulk_edit_issues,
34 :issues => [:edit, :destroy_attachment]}
34 :issues => [:edit, :destroy_attachment]}
35 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
35 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
36 map.permission :add_issue_notes, {:issues => :add_note}
36 map.permission :add_issue_notes, {:issues => :add_note}
37 map.permission :change_issue_status, {:issues => :change_status}, :require => :loggedin
37 map.permission :change_issue_status, {:issues => :change_status}, :require => :loggedin
38 map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin
38 map.permission :move_issues, {:projects => :move_issues}, :require => :loggedin
39 map.permission :delete_issues, {:issues => :destroy}, :require => :member
39 map.permission :delete_issues, {:issues => :destroy}, :require => :member
40 # Queries
40 # Queries
41 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
41 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
42 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
42 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
43 # Gantt & calendar
43 # Gantt & calendar
44 map.permission :view_gantt, :projects => :gantt
44 map.permission :view_gantt, :projects => :gantt
45 map.permission :view_calendar, :projects => :calendar
45 map.permission :view_calendar, :projects => :calendar
46 end
46 end
47
47
48 map.project_module :time_tracking do |map|
48 map.project_module :time_tracking do |map|
49 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
49 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
50 map.permission :view_time_entries, :timelog => [:details, :report]
50 map.permission :view_time_entries, :timelog => [:details, :report]
51 end
51 end
52
52
53 map.project_module :news do |map|
53 map.project_module :news do |map|
54 map.permission :manage_news, {:projects => :add_news, :news => [:edit, :destroy, :destroy_comment]}, :require => :member
54 map.permission :manage_news, {:projects => :add_news, :news => [:edit, :destroy, :destroy_comment]}, :require => :member
55 map.permission :view_news, {:news => [:index, :show]}, :public => true
55 map.permission :view_news, {:news => [:index, :show]}, :public => true
56 map.permission :comment_news, {:news => :add_comment}
56 map.permission :comment_news, {:news => :add_comment}
57 end
57 end
58
58
59 map.project_module :documents do |map|
59 map.project_module :documents do |map|
60 map.permission :manage_documents, {:projects => :add_document, :documents => [:edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
60 map.permission :manage_documents, {:projects => :add_document, :documents => [:edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
61 map.permission :view_documents, :projects => :list_documents, :documents => [:show, :download]
61 map.permission :view_documents, :projects => :list_documents, :documents => [:show, :download]
62 end
62 end
63
63
64 map.project_module :files do |map|
64 map.project_module :files do |map|
65 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
65 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
66 map.permission :view_files, :projects => :list_files, :versions => :download
66 map.permission :view_files, :projects => :list_files, :versions => :download
67 end
67 end
68
68
69 map.project_module :wiki do |map|
69 map.project_module :wiki do |map|
70 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
70 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
71 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
71 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
72 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
72 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
73 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :special]
73 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :special]
74 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
74 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
75 end
75 end
76
76
77 map.project_module :repository do |map|
77 map.project_module :repository do |map|
78 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
78 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
79 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :changes, :diff, :stats, :graph]
79 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :changes, :diff, :stats, :graph]
80 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
80 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
81 end
81 end
82
82
83 map.project_module :boards do |map|
83 map.project_module :boards do |map|
84 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
84 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
85 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
85 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
86 map.permission :add_messages, {:messages => [:new, :reply]}
86 map.permission :add_messages, {:messages => [:new, :reply]}
87 map.permission :edit_messages, {:messages => :edit}, :require => :member
88 map.permission :delete_messages, {:messages => :destroy}, :require => :member
87 end
89 end
88 end
90 end
89
91
90 # Project menu configuration
92 # Project menu configuration
91 Redmine::MenuManager.map :project_menu do |menu|
93 Redmine::MenuManager.map :project_menu do |menu|
92 menu.push :label_overview, :controller => 'projects', :action => 'show'
94 menu.push :label_overview, :controller => 'projects', :action => 'show'
93 menu.push :label_activity, :controller => 'projects', :action => 'activity'
95 menu.push :label_activity, :controller => 'projects', :action => 'activity'
94 menu.push :label_roadmap, :controller => 'projects', :action => 'roadmap'
96 menu.push :label_roadmap, :controller => 'projects', :action => 'roadmap'
95 menu.push :label_issue_plural, { :controller => 'issues', :action => 'index' }, :param => :project_id
97 menu.push :label_issue_plural, { :controller => 'issues', :action => 'index' }, :param => :project_id
96 menu.push :label_news_plural, { :controller => 'news', :action => 'index' }, :param => :project_id
98 menu.push :label_news_plural, { :controller => 'news', :action => 'index' }, :param => :project_id
97 menu.push :label_document_plural, :controller => 'projects', :action => 'list_documents'
99 menu.push :label_document_plural, :controller => 'projects', :action => 'list_documents'
98 menu.push :label_wiki, { :controller => 'wiki', :action => 'index', :page => nil }, :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
100 menu.push :label_wiki, { :controller => 'wiki', :action => 'index', :page => nil }, :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
99 menu.push :label_board_plural, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id, :if => Proc.new { |p| p.boards.any? }
101 menu.push :label_board_plural, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id, :if => Proc.new { |p| p.boards.any? }
100 menu.push :label_attachment_plural, :controller => 'projects', :action => 'list_files'
102 menu.push :label_attachment_plural, :controller => 'projects', :action => 'list_files'
101 menu.push :label_repository, { :controller => 'repositories', :action => 'show' }, :if => Proc.new { |p| p.repository && !p.repository.new_record? }
103 menu.push :label_repository, { :controller => 'repositories', :action => 'show' }, :if => Proc.new { |p| p.repository && !p.repository.new_record? }
102 menu.push :label_settings, :controller => 'projects', :action => 'settings'
104 menu.push :label_settings, :controller => 'projects', :action => 'settings'
103 end
105 end
@@ -1,482 +1,487
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2
2
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 h1 {margin:0; padding:0; font-size: 24px;}
4 h1 {margin:0; padding:0; font-size: 24px;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 12px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8
8
9 /***** Layout *****/
9 /***** Layout *****/
10 #top-menu {background: #2C4056;color: #fff;height:1.5em; padding: 2px 6px 0px 6px;}
10 #top-menu {background: #2C4056;color: #fff;height:1.5em; padding: 2px 6px 0px 6px;}
11 #top-menu a {color: #fff; padding-right: 4px;}
11 #top-menu a {color: #fff; padding-right: 4px;}
12 #account {float:right;}
12 #account {float:right;}
13
13
14 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
14 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
15 #header a {color:#f8f8f8;}
15 #header a {color:#f8f8f8;}
16 #quick-search {float:right;}
16 #quick-search {float:right;}
17
17
18 #main-menu {position: absolute; bottom: 0px; left:6px;}
18 #main-menu {position: absolute; bottom: 0px; left:6px;}
19 #main-menu ul {margin: 0; padding: 0;}
19 #main-menu ul {margin: 0; padding: 0;}
20 #main-menu li {
20 #main-menu li {
21 float:left;
21 float:left;
22 list-style-type:none;
22 list-style-type:none;
23 margin: 0px 10px 0px 0px;
23 margin: 0px 10px 0px 0px;
24 padding: 0px 0px 0px 0px;
24 padding: 0px 0px 0px 0px;
25 white-space:nowrap;
25 white-space:nowrap;
26 }
26 }
27 #main-menu li a {
27 #main-menu li a {
28 display: block;
28 display: block;
29 color: #fff;
29 color: #fff;
30 text-decoration: none;
30 text-decoration: none;
31 margin: 0;
31 margin: 0;
32 padding: 4px 4px 4px 4px;
32 padding: 4px 4px 4px 4px;
33 background: #2C4056;
33 background: #2C4056;
34 }
34 }
35 #main-menu li a:hover {background:#759FCF;}
35 #main-menu li a:hover {background:#759FCF;}
36
36
37 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
37 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
38
38
39 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
39 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
40 * html #sidebar{ width: 17%; }
40 * html #sidebar{ width: 17%; }
41 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
41 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
42 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
42 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
43 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
43 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
44
44
45 #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
45 #content { width: 80%; background: url(../images/contentbg.png) repeat-x; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;}
46 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
46 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
47 html>body #content {
47 html>body #content {
48 height: auto;
48 height: auto;
49 min-height: 600px;
49 min-height: 600px;
50 }
50 }
51
51
52 #main.nosidebar #sidebar{ display: none; }
52 #main.nosidebar #sidebar{ display: none; }
53 #main.nosidebar #content{ width: auto; border-right: 0; }
53 #main.nosidebar #content{ width: auto; border-right: 0; }
54
54
55 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
55 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
56
56
57 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
57 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
58 #login-form table td {padding: 6px;}
58 #login-form table td {padding: 6px;}
59 #login-form label {font-weight: bold;}
59 #login-form label {font-weight: bold;}
60
60
61 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
61 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
62
62
63 /***** Links *****/
63 /***** Links *****/
64 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
64 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
65 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
65 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
66 a img{ border: 0; }
66 a img{ border: 0; }
67
67
68 /***** Tables *****/
68 /***** Tables *****/
69 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
69 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
70 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
70 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
71 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
71 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
72 table.list td.id { width: 2%; text-align: center;}
72 table.list td.id { width: 2%; text-align: center;}
73 table.list td.checkbox { width: 15px; padding: 0px;}
73 table.list td.checkbox { width: 15px; padding: 0px;}
74
74
75 tr.issue { text-align: center; white-space: nowrap; }
75 tr.issue { text-align: center; white-space: nowrap; }
76 tr.issue td.subject, tr.issue td.category { white-space: normal; }
76 tr.issue td.subject, tr.issue td.category { white-space: normal; }
77 tr.issue td.subject { text-align: left; }
77 tr.issue td.subject { text-align: left; }
78 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
78 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
79
79
80 tr.message { height: 2.6em; }
81 tr.message td.last_message { font-size: 80%; }
82 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
83 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
84
80 table.list tbody tr:hover { background-color:#ffffdd; }
85 table.list tbody tr:hover { background-color:#ffffdd; }
81 table td {padding:2px;}
86 table td {padding:2px;}
82 table p {margin:0;}
87 table p {margin:0;}
83 .odd {background-color:#f6f7f8;}
88 .odd {background-color:#f6f7f8;}
84 .even {background-color: #fff;}
89 .even {background-color: #fff;}
85
90
86 .highlight { background-color: #FCFD8D;}
91 .highlight { background-color: #FCFD8D;}
87 .highlight.token-1 { background-color: #faa;}
92 .highlight.token-1 { background-color: #faa;}
88 .highlight.token-2 { background-color: #afa;}
93 .highlight.token-2 { background-color: #afa;}
89 .highlight.token-3 { background-color: #aaf;}
94 .highlight.token-3 { background-color: #aaf;}
90
95
91 .box{
96 .box{
92 padding:6px;
97 padding:6px;
93 margin-bottom: 10px;
98 margin-bottom: 10px;
94 background-color:#f6f6f6;
99 background-color:#f6f6f6;
95 color:#505050;
100 color:#505050;
96 line-height:1.5em;
101 line-height:1.5em;
97 border: 1px solid #e4e4e4;
102 border: 1px solid #e4e4e4;
98 }
103 }
99
104
100 div.square {
105 div.square {
101 border: 1px solid #999;
106 border: 1px solid #999;
102 float: left;
107 float: left;
103 margin: .3em .4em 0 .4em;
108 margin: .3em .4em 0 .4em;
104 overflow: hidden;
109 overflow: hidden;
105 width: .6em; height: .6em;
110 width: .6em; height: .6em;
106 }
111 }
107
112
108 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;}
113 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;}
109 .splitcontentleft{float:left; width:49%;}
114 .splitcontentleft{float:left; width:49%;}
110 .splitcontentright{float:right; width:49%;}
115 .splitcontentright{float:right; width:49%;}
111 form {display: inline;}
116 form {display: inline;}
112 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
117 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
113 fieldset {border: 1px solid #e4e4e4; margin:0;}
118 fieldset {border: 1px solid #e4e4e4; margin:0;}
114 legend {color: #484848;}
119 legend {color: #484848;}
115 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
120 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
116 textarea.wiki-edit { width: 99%; }
121 textarea.wiki-edit { width: 99%; }
117 li p {margin-top: 0;}
122 li p {margin-top: 0;}
118 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
123 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
119 .autoscroll {overflow-x: auto; padding:1px; width:100%;}
124 .autoscroll {overflow-x: auto; padding:1px; width:100%;}
120 #user_firstname, #user_lastname, #user_mail, #notification_option { width: 90%; }
125 #user_firstname, #user_lastname, #user_mail, #notification_option { width: 90%; }
121
126
122 /***** Tabular forms ******/
127 /***** Tabular forms ******/
123 .tabular p{
128 .tabular p{
124 margin: 0;
129 margin: 0;
125 padding: 5px 0 8px 0;
130 padding: 5px 0 8px 0;
126 padding-left: 180px; /*width of left column containing the label elements*/
131 padding-left: 180px; /*width of left column containing the label elements*/
127 height: 1%;
132 height: 1%;
128 clear:left;
133 clear:left;
129 }
134 }
130
135
131 .tabular label{
136 .tabular label{
132 font-weight: bold;
137 font-weight: bold;
133 float: left;
138 float: left;
134 text-align: right;
139 text-align: right;
135 margin-left: -180px; /*width of left column*/
140 margin-left: -180px; /*width of left column*/
136 width: 175px; /*width of labels. Should be smaller than left column to create some right
141 width: 175px; /*width of labels. Should be smaller than left column to create some right
137 margin*/
142 margin*/
138 }
143 }
139
144
140 .tabular label.floating{
145 .tabular label.floating{
141 font-weight: normal;
146 font-weight: normal;
142 margin-left: 0px;
147 margin-left: 0px;
143 text-align: left;
148 text-align: left;
144 width: 200px;
149 width: 200px;
145 }
150 }
146
151
147 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
152 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
148
153
149 #settings .tabular p{ padding-left: 300px; }
154 #settings .tabular p{ padding-left: 300px; }
150 #settings .tabular label{ margin-left: -300px; width: 295px; }
155 #settings .tabular label{ margin-left: -300px; width: 295px; }
151
156
152 .required {color: #bb0000;}
157 .required {color: #bb0000;}
153 .summary {font-style: italic;}
158 .summary {font-style: italic;}
154
159
155 div.attachments p { margin:4px 0 2px 0; }
160 div.attachments p { margin:4px 0 2px 0; }
156
161
157 /***** Flash & error messages ****/
162 /***** Flash & error messages ****/
158 #errorExplanation, div.flash, .nodata {
163 #errorExplanation, div.flash, .nodata {
159 padding: 4px 4px 4px 30px;
164 padding: 4px 4px 4px 30px;
160 margin-bottom: 12px;
165 margin-bottom: 12px;
161 font-size: 1.1em;
166 font-size: 1.1em;
162 border: 2px solid;
167 border: 2px solid;
163 }
168 }
164
169
165 div.flash {margin-top: 8px;}
170 div.flash {margin-top: 8px;}
166
171
167 div.flash.error, #errorExplanation {
172 div.flash.error, #errorExplanation {
168 background: url(../images/false.png) 8px 5px no-repeat;
173 background: url(../images/false.png) 8px 5px no-repeat;
169 background-color: #ffe3e3;
174 background-color: #ffe3e3;
170 border-color: #dd0000;
175 border-color: #dd0000;
171 color: #550000;
176 color: #550000;
172 }
177 }
173
178
174 div.flash.notice {
179 div.flash.notice {
175 background: url(../images/true.png) 8px 5px no-repeat;
180 background: url(../images/true.png) 8px 5px no-repeat;
176 background-color: #dfffdf;
181 background-color: #dfffdf;
177 border-color: #9fcf9f;
182 border-color: #9fcf9f;
178 color: #005f00;
183 color: #005f00;
179 }
184 }
180
185
181 .nodata {
186 .nodata {
182 text-align: center;
187 text-align: center;
183 background-color: #FFEBC1;
188 background-color: #FFEBC1;
184 border-color: #FDBF3B;
189 border-color: #FDBF3B;
185 color: #A6750C;
190 color: #A6750C;
186 }
191 }
187
192
188 #errorExplanation ul { font-size: 0.9em;}
193 #errorExplanation ul { font-size: 0.9em;}
189
194
190 /***** Ajax indicator ******/
195 /***** Ajax indicator ******/
191 #ajax-indicator {
196 #ajax-indicator {
192 position: absolute; /* fixed not supported by IE */
197 position: absolute; /* fixed not supported by IE */
193 background-color:#eee;
198 background-color:#eee;
194 border: 1px solid #bbb;
199 border: 1px solid #bbb;
195 top:35%;
200 top:35%;
196 left:40%;
201 left:40%;
197 width:20%;
202 width:20%;
198 font-weight:bold;
203 font-weight:bold;
199 text-align:center;
204 text-align:center;
200 padding:0.6em;
205 padding:0.6em;
201 z-index:100;
206 z-index:100;
202 filter:alpha(opacity=50);
207 filter:alpha(opacity=50);
203 -moz-opacity:0.5;
208 -moz-opacity:0.5;
204 opacity: 0.5;
209 opacity: 0.5;
205 -khtml-opacity: 0.5;
210 -khtml-opacity: 0.5;
206 }
211 }
207
212
208 html>body #ajax-indicator { position: fixed; }
213 html>body #ajax-indicator { position: fixed; }
209
214
210 #ajax-indicator span {
215 #ajax-indicator span {
211 background-position: 0% 40%;
216 background-position: 0% 40%;
212 background-repeat: no-repeat;
217 background-repeat: no-repeat;
213 background-image: url(../images/loading.gif);
218 background-image: url(../images/loading.gif);
214 padding-left: 26px;
219 padding-left: 26px;
215 vertical-align: bottom;
220 vertical-align: bottom;
216 }
221 }
217
222
218 /***** Calendar *****/
223 /***** Calendar *****/
219 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
224 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
220 table.cal thead th {width: 14%;}
225 table.cal thead th {width: 14%;}
221 table.cal tbody tr {height: 100px;}
226 table.cal tbody tr {height: 100px;}
222 table.cal th { background-color:#EEEEEE; padding: 4px; }
227 table.cal th { background-color:#EEEEEE; padding: 4px; }
223 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
228 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
224 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
229 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
225 table.cal td.odd p.day-num {color: #bbb;}
230 table.cal td.odd p.day-num {color: #bbb;}
226 table.cal td.today {background:#ffffdd;}
231 table.cal td.today {background:#ffffdd;}
227 table.cal td.today p.day-num {font-weight: bold;}
232 table.cal td.today p.day-num {font-weight: bold;}
228
233
229 /***** Tooltips ******/
234 /***** Tooltips ******/
230 .tooltip{position:relative;z-index:24;}
235 .tooltip{position:relative;z-index:24;}
231 .tooltip:hover{z-index:25;color:#000;}
236 .tooltip:hover{z-index:25;color:#000;}
232 .tooltip span.tip{display: none; text-align:left;}
237 .tooltip span.tip{display: none; text-align:left;}
233
238
234 div.tooltip:hover span.tip{
239 div.tooltip:hover span.tip{
235 display:block;
240 display:block;
236 position:absolute;
241 position:absolute;
237 top:12px; left:24px; width:270px;
242 top:12px; left:24px; width:270px;
238 border:1px solid #555;
243 border:1px solid #555;
239 background-color:#fff;
244 background-color:#fff;
240 padding: 4px;
245 padding: 4px;
241 font-size: 0.8em;
246 font-size: 0.8em;
242 color:#505050;
247 color:#505050;
243 }
248 }
244
249
245 /***** Progress bar *****/
250 /***** Progress bar *****/
246 table.progress {
251 table.progress {
247 border: 1px solid #D7D7D7;
252 border: 1px solid #D7D7D7;
248 border-collapse: collapse;
253 border-collapse: collapse;
249 border-spacing: 0pt;
254 border-spacing: 0pt;
250 empty-cells: show;
255 empty-cells: show;
251 text-align: center;
256 text-align: center;
252 float:left;
257 float:left;
253 margin: 1px 6px 1px 0px;
258 margin: 1px 6px 1px 0px;
254 }
259 }
255
260
256 table.progress td { height: 0.9em; }
261 table.progress td { height: 0.9em; }
257 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
262 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
258 table.progress td.open { background: #FFF none repeat scroll 0%; }
263 table.progress td.open { background: #FFF none repeat scroll 0%; }
259 p.pourcent {font-size: 80%;}
264 p.pourcent {font-size: 80%;}
260 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
265 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
261
266
262 /***** Tabs *****/
267 /***** Tabs *****/
263 #content .tabs{height: 2.6em;}
268 #content .tabs{height: 2.6em;}
264 #content .tabs ul{margin:0;}
269 #content .tabs ul{margin:0;}
265 #content .tabs ul li{
270 #content .tabs ul li{
266 float:left;
271 float:left;
267 list-style-type:none;
272 list-style-type:none;
268 white-space:nowrap;
273 white-space:nowrap;
269 margin-right:8px;
274 margin-right:8px;
270 background:#fff;
275 background:#fff;
271 }
276 }
272 #content .tabs ul li a{
277 #content .tabs ul li a{
273 display:block;
278 display:block;
274 font-size: 0.9em;
279 font-size: 0.9em;
275 text-decoration:none;
280 text-decoration:none;
276 line-height:1em;
281 line-height:1em;
277 padding:4px;
282 padding:4px;
278 border: 1px solid #c0c0c0;
283 border: 1px solid #c0c0c0;
279 }
284 }
280
285
281 #content .tabs ul li a.selected, #content .tabs ul li a:hover{
286 #content .tabs ul li a.selected, #content .tabs ul li a:hover{
282 background-color: #507AAA;
287 background-color: #507AAA;
283 border: 1px solid #507AAA;
288 border: 1px solid #507AAA;
284 color: #fff;
289 color: #fff;
285 text-decoration:none;
290 text-decoration:none;
286 }
291 }
287
292
288 /***** Diff *****/
293 /***** Diff *****/
289 .diff_out { background: #fcc; }
294 .diff_out { background: #fcc; }
290 .diff_in { background: #cfc; }
295 .diff_in { background: #cfc; }
291
296
292 /***** Wiki *****/
297 /***** Wiki *****/
293 div.wiki table {
298 div.wiki table {
294 border: 1px solid #505050;
299 border: 1px solid #505050;
295 border-collapse: collapse;
300 border-collapse: collapse;
296 }
301 }
297
302
298 div.wiki table, div.wiki td, div.wiki th {
303 div.wiki table, div.wiki td, div.wiki th {
299 border: 1px solid #bbb;
304 border: 1px solid #bbb;
300 padding: 4px;
305 padding: 4px;
301 }
306 }
302
307
303 div.wiki .external {
308 div.wiki .external {
304 background-position: 0% 60%;
309 background-position: 0% 60%;
305 background-repeat: no-repeat;
310 background-repeat: no-repeat;
306 padding-left: 12px;
311 padding-left: 12px;
307 background-image: url(../images/external.png);
312 background-image: url(../images/external.png);
308 }
313 }
309
314
310 div.wiki a.new {
315 div.wiki a.new {
311 color: #b73535;
316 color: #b73535;
312 }
317 }
313
318
314 div.wiki pre {
319 div.wiki pre {
315 margin: 1em 1em 1em 1.6em;
320 margin: 1em 1em 1em 1.6em;
316 padding: 2px;
321 padding: 2px;
317 background-color: #fafafa;
322 background-color: #fafafa;
318 border: 1px solid #dadada;
323 border: 1px solid #dadada;
319 width:95%;
324 width:95%;
320 overflow-x: auto;
325 overflow-x: auto;
321 }
326 }
322
327
323 div.wiki div.toc {
328 div.wiki div.toc {
324 background-color: #ffffdd;
329 background-color: #ffffdd;
325 border: 1px solid #e4e4e4;
330 border: 1px solid #e4e4e4;
326 padding: 4px;
331 padding: 4px;
327 line-height: 1.2em;
332 line-height: 1.2em;
328 margin-bottom: 12px;
333 margin-bottom: 12px;
329 margin-right: 12px;
334 margin-right: 12px;
330 display: table
335 display: table
331 }
336 }
332 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
337 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
333
338
334 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
339 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
335 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
340 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
336
341
337 div.wiki div.toc a {
342 div.wiki div.toc a {
338 display: block;
343 display: block;
339 font-size: 0.9em;
344 font-size: 0.9em;
340 font-weight: normal;
345 font-weight: normal;
341 text-decoration: none;
346 text-decoration: none;
342 color: #606060;
347 color: #606060;
343 }
348 }
344 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
349 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
345
350
346 div.wiki div.toc a.heading2 { margin-left: 6px; }
351 div.wiki div.toc a.heading2 { margin-left: 6px; }
347 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
352 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
348
353
349 /***** My page layout *****/
354 /***** My page layout *****/
350 .block-receiver {
355 .block-receiver {
351 border:1px dashed #c0c0c0;
356 border:1px dashed #c0c0c0;
352 margin-bottom: 20px;
357 margin-bottom: 20px;
353 padding: 15px 0 15px 0;
358 padding: 15px 0 15px 0;
354 }
359 }
355
360
356 .mypage-box {
361 .mypage-box {
357 margin:0 0 20px 0;
362 margin:0 0 20px 0;
358 color:#505050;
363 color:#505050;
359 line-height:1.5em;
364 line-height:1.5em;
360 }
365 }
361
366
362 .handle {
367 .handle {
363 cursor: move;
368 cursor: move;
364 }
369 }
365
370
366 a.close-icon {
371 a.close-icon {
367 display:block;
372 display:block;
368 margin-top:3px;
373 margin-top:3px;
369 overflow:hidden;
374 overflow:hidden;
370 width:12px;
375 width:12px;
371 height:12px;
376 height:12px;
372 background-repeat: no-repeat;
377 background-repeat: no-repeat;
373 cursor:pointer;
378 cursor:pointer;
374 background-image:url('../images/close.png');
379 background-image:url('../images/close.png');
375 }
380 }
376
381
377 a.close-icon:hover {
382 a.close-icon:hover {
378 background-image:url('../images/close_hl.png');
383 background-image:url('../images/close_hl.png');
379 }
384 }
380
385
381 /***** Gantt chart *****/
386 /***** Gantt chart *****/
382 .gantt_hdr {
387 .gantt_hdr {
383 position:absolute;
388 position:absolute;
384 top:0;
389 top:0;
385 height:16px;
390 height:16px;
386 border-top: 1px solid #c0c0c0;
391 border-top: 1px solid #c0c0c0;
387 border-bottom: 1px solid #c0c0c0;
392 border-bottom: 1px solid #c0c0c0;
388 border-right: 1px solid #c0c0c0;
393 border-right: 1px solid #c0c0c0;
389 text-align: center;
394 text-align: center;
390 overflow: hidden;
395 overflow: hidden;
391 }
396 }
392
397
393 .task {
398 .task {
394 position: absolute;
399 position: absolute;
395 height:8px;
400 height:8px;
396 font-size:0.8em;
401 font-size:0.8em;
397 color:#888;
402 color:#888;
398 padding:0;
403 padding:0;
399 margin:0;
404 margin:0;
400 line-height:0.8em;
405 line-height:0.8em;
401 }
406 }
402
407
403 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
408 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
404 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
409 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
405 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
410 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
406 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
411 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
407
412
408 /***** Icons *****/
413 /***** Icons *****/
409 .icon {
414 .icon {
410 background-position: 0% 40%;
415 background-position: 0% 40%;
411 background-repeat: no-repeat;
416 background-repeat: no-repeat;
412 padding-left: 20px;
417 padding-left: 20px;
413 padding-top: 2px;
418 padding-top: 2px;
414 padding-bottom: 3px;
419 padding-bottom: 3px;
415 }
420 }
416
421
417 .icon22 {
422 .icon22 {
418 background-position: 0% 40%;
423 background-position: 0% 40%;
419 background-repeat: no-repeat;
424 background-repeat: no-repeat;
420 padding-left: 26px;
425 padding-left: 26px;
421 line-height: 22px;
426 line-height: 22px;
422 vertical-align: middle;
427 vertical-align: middle;
423 }
428 }
424
429
425 .icon-add { background-image: url(../images/add.png); }
430 .icon-add { background-image: url(../images/add.png); }
426 .icon-edit { background-image: url(../images/edit.png); }
431 .icon-edit { background-image: url(../images/edit.png); }
427 .icon-copy { background-image: url(../images/copy.png); }
432 .icon-copy { background-image: url(../images/copy.png); }
428 .icon-del { background-image: url(../images/delete.png); }
433 .icon-del { background-image: url(../images/delete.png); }
429 .icon-move { background-image: url(../images/move.png); }
434 .icon-move { background-image: url(../images/move.png); }
430 .icon-save { background-image: url(../images/save.png); }
435 .icon-save { background-image: url(../images/save.png); }
431 .icon-cancel { background-image: url(../images/cancel.png); }
436 .icon-cancel { background-image: url(../images/cancel.png); }
432 .icon-pdf { background-image: url(../images/pdf.png); }
437 .icon-pdf { background-image: url(../images/pdf.png); }
433 .icon-csv { background-image: url(../images/csv.png); }
438 .icon-csv { background-image: url(../images/csv.png); }
434 .icon-html { background-image: url(../images/html.png); }
439 .icon-html { background-image: url(../images/html.png); }
435 .icon-image { background-image: url(../images/image.png); }
440 .icon-image { background-image: url(../images/image.png); }
436 .icon-txt { background-image: url(../images/txt.png); }
441 .icon-txt { background-image: url(../images/txt.png); }
437 .icon-file { background-image: url(../images/file.png); }
442 .icon-file { background-image: url(../images/file.png); }
438 .icon-folder { background-image: url(../images/folder.png); }
443 .icon-folder { background-image: url(../images/folder.png); }
439 .open .icon-folder { background-image: url(../images/folder_open.png); }
444 .open .icon-folder { background-image: url(../images/folder_open.png); }
440 .icon-package { background-image: url(../images/package.png); }
445 .icon-package { background-image: url(../images/package.png); }
441 .icon-home { background-image: url(../images/home.png); }
446 .icon-home { background-image: url(../images/home.png); }
442 .icon-user { background-image: url(../images/user.png); }
447 .icon-user { background-image: url(../images/user.png); }
443 .icon-mypage { background-image: url(../images/user_page.png); }
448 .icon-mypage { background-image: url(../images/user_page.png); }
444 .icon-admin { background-image: url(../images/admin.png); }
449 .icon-admin { background-image: url(../images/admin.png); }
445 .icon-projects { background-image: url(../images/projects.png); }
450 .icon-projects { background-image: url(../images/projects.png); }
446 .icon-logout { background-image: url(../images/logout.png); }
451 .icon-logout { background-image: url(../images/logout.png); }
447 .icon-help { background-image: url(../images/help.png); }
452 .icon-help { background-image: url(../images/help.png); }
448 .icon-attachment { background-image: url(../images/attachment.png); }
453 .icon-attachment { background-image: url(../images/attachment.png); }
449 .icon-index { background-image: url(../images/index.png); }
454 .icon-index { background-image: url(../images/index.png); }
450 .icon-history { background-image: url(../images/history.png); }
455 .icon-history { background-image: url(../images/history.png); }
451 .icon-feed { background-image: url(../images/feed.png); }
456 .icon-feed { background-image: url(../images/feed.png); }
452 .icon-time { background-image: url(../images/time.png); }
457 .icon-time { background-image: url(../images/time.png); }
453 .icon-stats { background-image: url(../images/stats.png); }
458 .icon-stats { background-image: url(../images/stats.png); }
454 .icon-warning { background-image: url(../images/warning.png); }
459 .icon-warning { background-image: url(../images/warning.png); }
455 .icon-fav { background-image: url(../images/fav.png); }
460 .icon-fav { background-image: url(../images/fav.png); }
456 .icon-fav-off { background-image: url(../images/fav_off.png); }
461 .icon-fav-off { background-image: url(../images/fav_off.png); }
457 .icon-reload { background-image: url(../images/reload.png); }
462 .icon-reload { background-image: url(../images/reload.png); }
458 .icon-lock { background-image: url(../images/locked.png); }
463 .icon-lock { background-image: url(../images/locked.png); }
459 .icon-unlock { background-image: url(../images/unlock.png); }
464 .icon-unlock { background-image: url(../images/unlock.png); }
460 .icon-note { background-image: url(../images/note.png); }
465 .icon-note { background-image: url(../images/note.png); }
461 .icon-checked { background-image: url(../images/true.png); }
466 .icon-checked { background-image: url(../images/true.png); }
462
467
463 .icon22-projects { background-image: url(../images/22x22/projects.png); }
468 .icon22-projects { background-image: url(../images/22x22/projects.png); }
464 .icon22-users { background-image: url(../images/22x22/users.png); }
469 .icon22-users { background-image: url(../images/22x22/users.png); }
465 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
470 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
466 .icon22-role { background-image: url(../images/22x22/role.png); }
471 .icon22-role { background-image: url(../images/22x22/role.png); }
467 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
472 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
468 .icon22-options { background-image: url(../images/22x22/options.png); }
473 .icon22-options { background-image: url(../images/22x22/options.png); }
469 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
474 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
470 .icon22-authent { background-image: url(../images/22x22/authent.png); }
475 .icon22-authent { background-image: url(../images/22x22/authent.png); }
471 .icon22-info { background-image: url(../images/22x22/info.png); }
476 .icon22-info { background-image: url(../images/22x22/info.png); }
472 .icon22-comment { background-image: url(../images/22x22/comment.png); }
477 .icon22-comment { background-image: url(../images/22x22/comment.png); }
473 .icon22-package { background-image: url(../images/22x22/package.png); }
478 .icon22-package { background-image: url(../images/22x22/package.png); }
474 .icon22-settings { background-image: url(../images/22x22/settings.png); }
479 .icon22-settings { background-image: url(../images/22x22/settings.png); }
475 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
480 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
476
481
477 /***** Media print specific styles *****/
482 /***** Media print specific styles *****/
478 @media print {
483 @media print {
479 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
484 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
480 #main { background: #fff; }
485 #main { background: #fff; }
481 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
486 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
482 }
487 }
@@ -1,19 +1,19
1 ---
1 ---
2 boards_001:
2 boards_001:
3 name: Help
3 name: Help
4 project_id: 1
4 project_id: 1
5 topics_count: 1
5 topics_count: 2
6 id: 1
6 id: 1
7 description: Help board
7 description: Help board
8 position: 1
8 position: 1
9 last_message_id: 2
9 last_message_id: 5
10 messages_count: 2
10 messages_count: 5
11 boards_002:
11 boards_002:
12 name: Discussion
12 name: Discussion
13 project_id: 1
13 project_id: 1
14 topics_count: 0
14 topics_count: 0
15 id: 2
15 id: 2
16 description: Discussion board
16 description: Discussion board
17 position: 2
17 position: 2
18 last_message_id:
18 last_message_id:
19 messages_count: 0
19 messages_count: 0
@@ -1,25 +1,57
1 ---
1 ---
2 messages_001:
2 messages_001:
3 created_on: 2007-05-12 17:15:32 +02:00
3 created_on: 2007-05-12 17:15:32 +02:00
4 updated_on: 2007-05-12 17:15:32 +02:00
4 updated_on: 2007-05-12 17:15:32 +02:00
5 subject: First post
5 subject: First post
6 id: 1
6 id: 1
7 replies_count: 1
7 replies_count: 2
8 last_reply_id: 2
8 last_reply_id: 3
9 content: "This is the very first post\n\
9 content: "This is the very first post\n\
10 in the forum"
10 in the forum"
11 author_id: 1
11 author_id: 1
12 parent_id:
12 parent_id:
13 board_id: 1
13 board_id: 1
14 messages_002:
14 messages_002:
15 created_on: 2007-05-12 17:18:00 +02:00
15 created_on: 2007-05-12 17:18:00 +02:00
16 updated_on: 2007-05-12 17:18:00 +02:00
16 updated_on: 2007-05-12 17:18:00 +02:00
17 subject: First reply
17 subject: First reply
18 id: 2
18 id: 2
19 replies_count: 0
19 replies_count: 0
20 last_reply_id:
20 last_reply_id:
21 content: "Reply to the first post"
21 content: "Reply to the first post"
22 author_id: 1
22 author_id: 1
23 parent_id: 1
23 parent_id: 1
24 board_id: 1
24 board_id: 1
25 No newline at end of file
25 messages_003:
26 created_on: 2007-05-12 17:18:02 +02:00
27 updated_on: 2007-05-12 17:18:02 +02:00
28 subject: "RE: First post"
29 id: 3
30 replies_count: 0
31 last_reply_id:
32 content: "An other reply"
33 author_id:
34 parent_id: 1
35 board_id: 1
36 messages_004:
37 created_on: 2007-08-12 17:15:32 +02:00
38 updated_on: 2007-08-12 17:15:32 +02:00
39 subject: Post 2
40 id: 4
41 replies_count: 1
42 last_reply_id: 5
43 content: "This is an other post"
44 author_id:
45 parent_id:
46 board_id: 1
47 messages_005:
48 created_on: 2007-09-12 17:18:00 +02:00
49 updated_on: 2007-09-12 17:18:00 +02:00
50 subject: 'RE: post 2'
51 id: 5
52 replies_count: 0
53 last_reply_id:
54 content: "Reply to the second post"
55 author_id: 1
56 parent_id: 4
57 board_id: 1
@@ -1,44 +1,70
1 require File.dirname(__FILE__) + '/../test_helper'
1 require File.dirname(__FILE__) + '/../test_helper'
2
2
3 class MessageTest < Test::Unit::TestCase
3 class MessageTest < Test::Unit::TestCase
4 fixtures :projects, :boards, :messages
4 fixtures :projects, :boards, :messages
5
5
6 def setup
6 def setup
7 @board = Board.find(1)
7 @board = Board.find(1)
8 @user = User.find(1)
8 @user = User.find(1)
9 end
9 end
10
10
11 def test_create
11 def test_create
12 topics_count = @board.topics_count
12 topics_count = @board.topics_count
13 messages_count = @board.messages_count
13 messages_count = @board.messages_count
14
14
15 message = Message.new(:board => @board, :subject => 'Test message', :content => 'Test message content', :author => @user)
15 message = Message.new(:board => @board, :subject => 'Test message', :content => 'Test message content', :author => @user)
16 assert message.save
16 assert message.save
17 @board.reload
17 @board.reload
18 # topics count incremented
18 # topics count incremented
19 assert_equal topics_count+1, @board[:topics_count]
19 assert_equal topics_count+1, @board[:topics_count]
20 # messages count incremented
20 # messages count incremented
21 assert_equal messages_count+1, @board[:messages_count]
21 assert_equal messages_count+1, @board[:messages_count]
22 assert_equal message, @board.last_message
22 assert_equal message, @board.last_message
23 end
23 end
24
24
25 def test_reply
25 def test_reply
26 topics_count = @board.topics_count
26 topics_count = @board.topics_count
27 messages_count = @board.messages_count
27 messages_count = @board.messages_count
28 @message = Message.find(1)
28 @message = Message.find(1)
29 replies_count = @message.replies_count
29 replies_count = @message.replies_count
30
30
31 reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', :parent => @message, :author => @user)
31 reply = Message.new(:board => @board, :subject => 'Test reply', :content => 'Test reply content', :parent => @message, :author => @user)
32 assert reply.save
32 assert reply.save
33 @board.reload
33 @board.reload
34 # same topics count
34 # same topics count
35 assert_equal topics_count, @board[:topics_count]
35 assert_equal topics_count, @board[:topics_count]
36 # messages count incremented
36 # messages count incremented
37 assert_equal messages_count+1, @board[:messages_count]
37 assert_equal messages_count+1, @board[:messages_count]
38 assert_equal reply, @board.last_message
38 assert_equal reply, @board.last_message
39 @message.reload
39 @message.reload
40 # replies count incremented
40 # replies count incremented
41 assert_equal replies_count+1, @message[:replies_count]
41 assert_equal replies_count+1, @message[:replies_count]
42 assert_equal reply, @message.last_reply
42 assert_equal reply, @message.last_reply
43 end
43 end
44
45 def test_destroy_topic
46 message = Message.find(1)
47 board = message.board
48 topics_count, messages_count = board.topics_count, board.messages_count
49 assert message.destroy
50 board.reload
51
52 # Replies deleted
53 assert Message.find_all_by_parent_id(1).empty?
54 # Checks counters
55 assert_equal topics_count - 1, board.topics_count
56 assert_equal messages_count - 3, board.messages_count
57 end
58
59 def test_destroy_reply
60 message = Message.find(5)
61 board = message.board
62 topics_count, messages_count = board.topics_count, board.messages_count
63 assert message.destroy
64 board.reload
65
66 # Checks counters
67 assert_equal topics_count, board.topics_count
68 assert_equal messages_count - 1, board.messages_count
69 end
44 end
70 end
General Comments 0
You need to be logged in to leave comments. Login now