##// END OF EJS Templates
Highlight the current item of the main menu....
Jean-Philippe Lang -
r1062:0faa4568a0ab
parent child
Show More
@@ -1,204 +1,207
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 ApplicationController < ActionController::Base
18 class ApplicationController < ActionController::Base
19 before_filter :user_setup, :check_if_login_required, :set_localization
19 before_filter :user_setup, :check_if_login_required, :set_localization
20 filter_parameter_logging :password
20 filter_parameter_logging :password
21
21
22 include Redmine::MenuManager::MenuController
23 helper Redmine::MenuManager::MenuHelper
24
22 REDMINE_SUPPORTED_SCM.each do |scm|
25 REDMINE_SUPPORTED_SCM.each do |scm|
23 require_dependency "repository/#{scm.underscore}"
26 require_dependency "repository/#{scm.underscore}"
24 end
27 end
25
28
26 def current_role
29 def current_role
27 @current_role ||= User.current.role_for_project(@project)
30 @current_role ||= User.current.role_for_project(@project)
28 end
31 end
29
32
30 def user_setup
33 def user_setup
31 # Check the settings cache for each request
34 # Check the settings cache for each request
32 Setting.check_cache
35 Setting.check_cache
33 # Find the current user
36 # Find the current user
34 User.current = find_current_user
37 User.current = find_current_user
35 end
38 end
36
39
37 # Returns the current user or nil if no user is logged in
40 # Returns the current user or nil if no user is logged in
38 def find_current_user
41 def find_current_user
39 if session[:user_id]
42 if session[:user_id]
40 # existing session
43 # existing session
41 (User.find_active(session[:user_id]) rescue nil)
44 (User.find_active(session[:user_id]) rescue nil)
42 elsif cookies[:autologin] && Setting.autologin?
45 elsif cookies[:autologin] && Setting.autologin?
43 # auto-login feature
46 # auto-login feature
44 User.find_by_autologin_key(cookies[:autologin])
47 User.find_by_autologin_key(cookies[:autologin])
45 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
48 elsif params[:key] && accept_key_auth_actions.include?(params[:action])
46 # RSS key authentication
49 # RSS key authentication
47 User.find_by_rss_key(params[:key])
50 User.find_by_rss_key(params[:key])
48 end
51 end
49 end
52 end
50
53
51 # check if login is globally required to access the application
54 # check if login is globally required to access the application
52 def check_if_login_required
55 def check_if_login_required
53 # no check needed if user is already logged in
56 # no check needed if user is already logged in
54 return true if User.current.logged?
57 return true if User.current.logged?
55 require_login if Setting.login_required?
58 require_login if Setting.login_required?
56 end
59 end
57
60
58 def set_localization
61 def set_localization
59 lang = begin
62 lang = begin
60 if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym
63 if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym
61 User.current.language
64 User.current.language
62 elsif request.env['HTTP_ACCEPT_LANGUAGE']
65 elsif request.env['HTTP_ACCEPT_LANGUAGE']
63 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first
66 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first
64 if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym
67 if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym
65 accept_lang
68 accept_lang
66 end
69 end
67 end
70 end
68 rescue
71 rescue
69 nil
72 nil
70 end || Setting.default_language
73 end || Setting.default_language
71 set_language_if_valid(lang)
74 set_language_if_valid(lang)
72 end
75 end
73
76
74 def require_login
77 def require_login
75 if !User.current.logged?
78 if !User.current.logged?
76 store_location
79 store_location
77 redirect_to :controller => "account", :action => "login"
80 redirect_to :controller => "account", :action => "login"
78 return false
81 return false
79 end
82 end
80 true
83 true
81 end
84 end
82
85
83 def require_admin
86 def require_admin
84 return unless require_login
87 return unless require_login
85 if !User.current.admin?
88 if !User.current.admin?
86 render_403
89 render_403
87 return false
90 return false
88 end
91 end
89 true
92 true
90 end
93 end
91
94
92 # Authorize the user for the requested action
95 # Authorize the user for the requested action
93 def authorize(ctrl = params[:controller], action = params[:action])
96 def authorize(ctrl = params[:controller], action = params[:action])
94 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
97 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project)
95 allowed ? true : (User.current.logged? ? render_403 : require_login)
98 allowed ? true : (User.current.logged? ? render_403 : require_login)
96 end
99 end
97
100
98 # make sure that the user is a member of the project (or admin) if project is private
101 # make sure that the user is a member of the project (or admin) if project is private
99 # used as a before_filter for actions that do not require any particular permission on the project
102 # used as a before_filter for actions that do not require any particular permission on the project
100 def check_project_privacy
103 def check_project_privacy
101 unless @project.active?
104 unless @project.active?
102 @project = nil
105 @project = nil
103 render_404
106 render_404
104 return false
107 return false
105 end
108 end
106 return true if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
109 return true if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
107 User.current.logged? ? render_403 : require_login
110 User.current.logged? ? render_403 : require_login
108 end
111 end
109
112
110 # store current uri in session.
113 # store current uri in session.
111 # return to this location by calling redirect_back_or_default
114 # return to this location by calling redirect_back_or_default
112 def store_location
115 def store_location
113 session[:return_to_params] = params
116 session[:return_to_params] = params
114 end
117 end
115
118
116 # move to the last store_location call or to the passed default one
119 # move to the last store_location call or to the passed default one
117 def redirect_back_or_default(default)
120 def redirect_back_or_default(default)
118 if session[:return_to_params].nil?
121 if session[:return_to_params].nil?
119 redirect_to default
122 redirect_to default
120 else
123 else
121 redirect_to session[:return_to_params]
124 redirect_to session[:return_to_params]
122 session[:return_to_params] = nil
125 session[:return_to_params] = nil
123 end
126 end
124 end
127 end
125
128
126 def render_403
129 def render_403
127 @project = nil
130 @project = nil
128 render :template => "common/403", :layout => !request.xhr?, :status => 403
131 render :template => "common/403", :layout => !request.xhr?, :status => 403
129 return false
132 return false
130 end
133 end
131
134
132 def render_404
135 def render_404
133 render :template => "common/404", :layout => !request.xhr?, :status => 404
136 render :template => "common/404", :layout => !request.xhr?, :status => 404
134 return false
137 return false
135 end
138 end
136
139
137 def render_feed(items, options={})
140 def render_feed(items, options={})
138 @items = items || []
141 @items = items || []
139 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
142 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
140 @title = options[:title] || Setting.app_title
143 @title = options[:title] || Setting.app_title
141 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
144 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
142 end
145 end
143
146
144 def self.accept_key_auth(*actions)
147 def self.accept_key_auth(*actions)
145 actions = actions.flatten.map(&:to_s)
148 actions = actions.flatten.map(&:to_s)
146 write_inheritable_attribute('accept_key_auth_actions', actions)
149 write_inheritable_attribute('accept_key_auth_actions', actions)
147 end
150 end
148
151
149 def accept_key_auth_actions
152 def accept_key_auth_actions
150 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
153 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
151 end
154 end
152
155
153 # TODO: move to model
156 # TODO: move to model
154 def attach_files(obj, files)
157 def attach_files(obj, files)
155 attachments = []
158 attachments = []
156 if files && files.is_a?(Array)
159 if files && files.is_a?(Array)
157 files.each do |file|
160 files.each do |file|
158 next unless file.size > 0
161 next unless file.size > 0
159 a = Attachment.create(:container => obj, :file => file, :author => User.current)
162 a = Attachment.create(:container => obj, :file => file, :author => User.current)
160 attachments << a unless a.new_record?
163 attachments << a unless a.new_record?
161 end
164 end
162 end
165 end
163 attachments
166 attachments
164 end
167 end
165
168
166 # Returns the number of objects that should be displayed
169 # Returns the number of objects that should be displayed
167 # on the paginated list
170 # on the paginated list
168 def per_page_option
171 def per_page_option
169 per_page = nil
172 per_page = nil
170 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
173 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
171 per_page = params[:per_page].to_s.to_i
174 per_page = params[:per_page].to_s.to_i
172 session[:per_page] = per_page
175 session[:per_page] = per_page
173 elsif session[:per_page]
176 elsif session[:per_page]
174 per_page = session[:per_page]
177 per_page = session[:per_page]
175 else
178 else
176 per_page = Setting.per_page_options_array.first || 25
179 per_page = Setting.per_page_options_array.first || 25
177 end
180 end
178 per_page
181 per_page
179 end
182 end
180
183
181 # qvalues http header parser
184 # qvalues http header parser
182 # code taken from webrick
185 # code taken from webrick
183 def parse_qvalues(value)
186 def parse_qvalues(value)
184 tmp = []
187 tmp = []
185 if value
188 if value
186 parts = value.split(/,\s*/)
189 parts = value.split(/,\s*/)
187 parts.each {|part|
190 parts.each {|part|
188 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
191 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
189 val = m[1]
192 val = m[1]
190 q = (m[2] or 1).to_f
193 q = (m[2] or 1).to_f
191 tmp.push([val, q])
194 tmp.push([val, q])
192 end
195 end
193 }
196 }
194 tmp = tmp.sort_by{|val, q| -q}
197 tmp = tmp.sort_by{|val, q| -q}
195 tmp.collect!{|val, q| val}
198 tmp.collect!{|val, q| val}
196 end
199 end
197 return tmp
200 return tmp
198 end
201 end
199
202
200 # Returns a string that can be used as filename value in Content-Disposition header
203 # Returns a string that can be used as filename value in Content-Disposition header
201 def filename_for_content_disposition(name)
204 def filename_for_content_disposition(name)
202 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
205 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
203 end
206 end
204 end
207 end
@@ -1,52 +1,53
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 IssueCategoriesController < ApplicationController
18 class IssueCategoriesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :settings
20 before_filter :find_project, :authorize
21 before_filter :find_project, :authorize
21
22
22 verify :method => :post, :only => :destroy
23 verify :method => :post, :only => :destroy
23
24
24 def edit
25 def edit
25 if request.post? and @category.update_attributes(params[:category])
26 if request.post? and @category.update_attributes(params[:category])
26 flash[:notice] = l(:notice_successful_update)
27 flash[:notice] = l(:notice_successful_update)
27 redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
28 redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
28 end
29 end
29 end
30 end
30
31
31 def destroy
32 def destroy
32 @issue_count = @category.issues.size
33 @issue_count = @category.issues.size
33 if @issue_count == 0
34 if @issue_count == 0
34 # No issue assigned to this category
35 # No issue assigned to this category
35 @category.destroy
36 @category.destroy
36 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
37 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
37 elsif params[:todo]
38 elsif params[:todo]
38 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign'
39 reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id]) if params[:todo] == 'reassign'
39 @category.destroy(reassign_to)
40 @category.destroy(reassign_to)
40 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
41 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories'
41 end
42 end
42 @categories = @project.issue_categories - [@category]
43 @categories = @project.issue_categories - [@category]
43 end
44 end
44
45
45 private
46 private
46 def find_project
47 def find_project
47 @category = IssueCategory.find(params[:id])
48 @category = IssueCategory.find(params[:id])
48 @project = @category.project
49 @project = @category.project
49 rescue ActiveRecord::RecordNotFound
50 rescue ActiveRecord::RecordNotFound
50 render_404
51 render_404
51 end
52 end
52 end
53 end
@@ -1,98 +1,99
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 menu_item :boards
20 before_filter :find_board, :only => :new
21 before_filter :find_board, :only => :new
21 before_filter :find_message, :except => :new
22 before_filter :find_message, :except => :new
22 before_filter :authorize
23 before_filter :authorize
23
24
24 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
25 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
25
26
26 helper :attachments
27 helper :attachments
27 include AttachmentsHelper
28 include AttachmentsHelper
28
29
29 # Show a topic and its replies
30 # Show a topic and its replies
30 def show
31 def show
31 @reply = Message.new(:subject => "RE: #{@message.subject}")
32 @reply = Message.new(:subject => "RE: #{@message.subject}")
32 render :action => "show", :layout => false if request.xhr?
33 render :action => "show", :layout => false if request.xhr?
33 end
34 end
34
35
35 # Create a new topic
36 # Create a new topic
36 def new
37 def new
37 @message = Message.new(params[:message])
38 @message = Message.new(params[:message])
38 @message.author = User.current
39 @message.author = User.current
39 @message.board = @board
40 @message.board = @board
40 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
41 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
41 @message.locked = params[:message]['locked']
42 @message.locked = params[:message]['locked']
42 @message.sticky = params[:message]['sticky']
43 @message.sticky = params[:message]['sticky']
43 end
44 end
44 if request.post? && @message.save
45 if request.post? && @message.save
45 attach_files(@message, params[:attachments])
46 attach_files(@message, params[:attachments])
46 redirect_to :action => 'show', :id => @message
47 redirect_to :action => 'show', :id => @message
47 end
48 end
48 end
49 end
49
50
50 # Reply to a topic
51 # Reply to a topic
51 def reply
52 def reply
52 @reply = Message.new(params[:reply])
53 @reply = Message.new(params[:reply])
53 @reply.author = User.current
54 @reply.author = User.current
54 @reply.board = @board
55 @reply.board = @board
55 @topic.children << @reply
56 @topic.children << @reply
56 if !@reply.new_record?
57 if !@reply.new_record?
57 attach_files(@reply, params[:attachments])
58 attach_files(@reply, params[:attachments])
58 end
59 end
59 redirect_to :action => 'show', :id => @topic
60 redirect_to :action => 'show', :id => @topic
60 end
61 end
61
62
62 # Edit a message
63 # Edit a message
63 def edit
64 def edit
64 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
65 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
65 @message.locked = params[:message]['locked']
66 @message.locked = params[:message]['locked']
66 @message.sticky = params[:message]['sticky']
67 @message.sticky = params[:message]['sticky']
67 end
68 end
68 if request.post? && @message.update_attributes(params[:message])
69 if request.post? && @message.update_attributes(params[:message])
69 attach_files(@message, params[:attachments])
70 attach_files(@message, params[:attachments])
70 flash[:notice] = l(:notice_successful_update)
71 flash[:notice] = l(:notice_successful_update)
71 redirect_to :action => 'show', :id => @topic
72 redirect_to :action => 'show', :id => @topic
72 end
73 end
73 end
74 end
74
75
75 # Delete a messages
76 # Delete a messages
76 def destroy
77 def destroy
77 @message.destroy
78 @message.destroy
78 redirect_to @message.parent.nil? ?
79 redirect_to @message.parent.nil? ?
79 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
80 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
80 { :action => 'show', :id => @message.parent }
81 { :action => 'show', :id => @message.parent }
81 end
82 end
82
83
83 private
84 private
84 def find_message
85 def find_message
85 find_board
86 find_board
86 @message = @board.messages.find(params[:id], :include => :parent)
87 @message = @board.messages.find(params[:id], :include => :parent)
87 @topic = @message.root
88 @topic = @message.root
88 rescue ActiveRecord::RecordNotFound
89 rescue ActiveRecord::RecordNotFound
89 render_404
90 render_404
90 end
91 end
91
92
92 def find_board
93 def find_board
93 @board = Board.find(params[:board_id], :include => :project)
94 @board = Board.find(params[:board_id], :include => :project)
94 @project = @board.project
95 @project = @board.project
95 rescue ActiveRecord::RecordNotFound
96 rescue ActiveRecord::RecordNotFound
96 render_404
97 render_404
97 end
98 end
98 end
99 end
@@ -1,518 +1,525
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 ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :overview
21 menu_item :activity, :only => :activity
22 menu_item :roadmap, :only => :roadmap
23 menu_item :files, :only => [:list_files, :add_file]
24 menu_item :settings, :only => :settings
25 menu_item :issues, :only => [:add_issue, :bulk_edit_issues, :changelog, :move_issues]
26
20 before_filter :find_project, :except => [ :index, :list, :add ]
27 before_filter :find_project, :except => [ :index, :list, :add ]
21 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy ]
22 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
23 accept_key_auth :activity, :calendar
30 accept_key_auth :activity, :calendar
24
31
25 cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
32 cache_sweeper :project_sweeper, :only => [ :add, :edit, :archive, :unarchive, :destroy ]
26 cache_sweeper :issue_sweeper, :only => [ :add_issue ]
33 cache_sweeper :issue_sweeper, :only => [ :add_issue ]
27 cache_sweeper :version_sweeper, :only => [ :add_version ]
34 cache_sweeper :version_sweeper, :only => [ :add_version ]
28
35
29 helper :sort
36 helper :sort
30 include SortHelper
37 include SortHelper
31 helper :custom_fields
38 helper :custom_fields
32 include CustomFieldsHelper
39 include CustomFieldsHelper
33 helper :ifpdf
40 helper :ifpdf
34 include IfpdfHelper
41 include IfpdfHelper
35 helper :issues
42 helper :issues
36 helper IssuesHelper
43 helper IssuesHelper
37 helper :queries
44 helper :queries
38 include QueriesHelper
45 include QueriesHelper
39 helper :repositories
46 helper :repositories
40 include RepositoriesHelper
47 include RepositoriesHelper
41 include ProjectsHelper
48 include ProjectsHelper
42
49
43 def index
50 def index
44 list
51 list
45 render :action => 'list' unless request.xhr?
52 render :action => 'list' unless request.xhr?
46 end
53 end
47
54
48 # Lists visible projects
55 # Lists visible projects
49 def list
56 def list
50 projects = Project.find :all,
57 projects = Project.find :all,
51 :conditions => Project.visible_by(User.current),
58 :conditions => Project.visible_by(User.current),
52 :include => :parent
59 :include => :parent
53 @project_tree = projects.group_by {|p| p.parent || p}
60 @project_tree = projects.group_by {|p| p.parent || p}
54 @project_tree.each_key {|p| @project_tree[p] -= [p]}
61 @project_tree.each_key {|p| @project_tree[p] -= [p]}
55 end
62 end
56
63
57 # Add a new project
64 # Add a new project
58 def add
65 def add
59 @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
60 @trackers = Tracker.all
67 @trackers = Tracker.all
61 @root_projects = Project.find(:all,
68 @root_projects = Project.find(:all,
62 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
69 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
63 :order => 'name')
70 :order => 'name')
64 @project = Project.new(params[:project])
71 @project = Project.new(params[:project])
65 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
72 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
66 if request.get?
73 if request.get?
67 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
74 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
68 @project.trackers = Tracker.all
75 @project.trackers = Tracker.all
69 else
76 else
70 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
77 @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
71 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
78 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
72 @project.custom_values = @custom_values
79 @project.custom_values = @custom_values
73 if @project.save
80 if @project.save
74 @project.enabled_module_names = params[:enabled_modules]
81 @project.enabled_module_names = params[:enabled_modules]
75 flash[:notice] = l(:notice_successful_create)
82 flash[:notice] = l(:notice_successful_create)
76 redirect_to :controller => 'admin', :action => 'projects'
83 redirect_to :controller => 'admin', :action => 'projects'
77 end
84 end
78 end
85 end
79 end
86 end
80
87
81 # Show @project
88 # Show @project
82 def show
89 def show
83 @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
90 @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
84 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
91 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
85 @subprojects = @project.active_children
92 @subprojects = @project.active_children
86 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
93 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
87 @trackers = @project.trackers
94 @trackers = @project.trackers
88 @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
95 @open_issues_by_tracker = Issue.count(:group => :tracker, :joins => "INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id", :conditions => ["project_id=? and #{IssueStatus.table_name}.is_closed=?", @project.id, false])
89 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
96 @total_issues_by_tracker = Issue.count(:group => :tracker, :conditions => ["project_id=?", @project.id])
90 @total_hours = @project.time_entries.sum(:hours)
97 @total_hours = @project.time_entries.sum(:hours)
91 @key = User.current.rss_key
98 @key = User.current.rss_key
92 end
99 end
93
100
94 def settings
101 def settings
95 @root_projects = Project.find(:all,
102 @root_projects = Project.find(:all,
96 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
103 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
97 :order => 'name')
104 :order => 'name')
98 @custom_fields = IssueCustomField.find(:all)
105 @custom_fields = IssueCustomField.find(:all)
99 @issue_category ||= IssueCategory.new
106 @issue_category ||= IssueCategory.new
100 @member ||= @project.members.new
107 @member ||= @project.members.new
101 @trackers = Tracker.all
108 @trackers = Tracker.all
102 @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
109 @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
103 @repository ||= @project.repository
110 @repository ||= @project.repository
104 @wiki ||= @project.wiki
111 @wiki ||= @project.wiki
105 end
112 end
106
113
107 # Edit @project
114 # Edit @project
108 def edit
115 def edit
109 if request.post?
116 if request.post?
110 @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
117 @project.custom_fields = IssueCustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
111 if params[:custom_fields]
118 if params[:custom_fields]
112 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
119 @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
113 @project.custom_values = @custom_values
120 @project.custom_values = @custom_values
114 end
121 end
115 @project.attributes = params[:project]
122 @project.attributes = params[:project]
116 if @project.save
123 if @project.save
117 flash[:notice] = l(:notice_successful_update)
124 flash[:notice] = l(:notice_successful_update)
118 redirect_to :action => 'settings', :id => @project
125 redirect_to :action => 'settings', :id => @project
119 else
126 else
120 settings
127 settings
121 render :action => 'settings'
128 render :action => 'settings'
122 end
129 end
123 end
130 end
124 end
131 end
125
132
126 def modules
133 def modules
127 @project.enabled_module_names = params[:enabled_modules]
134 @project.enabled_module_names = params[:enabled_modules]
128 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
135 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
129 end
136 end
130
137
131 def archive
138 def archive
132 @project.archive if request.post? && @project.active?
139 @project.archive if request.post? && @project.active?
133 redirect_to :controller => 'admin', :action => 'projects'
140 redirect_to :controller => 'admin', :action => 'projects'
134 end
141 end
135
142
136 def unarchive
143 def unarchive
137 @project.unarchive if request.post? && !@project.active?
144 @project.unarchive if request.post? && !@project.active?
138 redirect_to :controller => 'admin', :action => 'projects'
145 redirect_to :controller => 'admin', :action => 'projects'
139 end
146 end
140
147
141 # Delete @project
148 # Delete @project
142 def destroy
149 def destroy
143 @project_to_destroy = @project
150 @project_to_destroy = @project
144 if request.post? and params[:confirm]
151 if request.post? and params[:confirm]
145 @project_to_destroy.destroy
152 @project_to_destroy.destroy
146 redirect_to :controller => 'admin', :action => 'projects'
153 redirect_to :controller => 'admin', :action => 'projects'
147 end
154 end
148 # hide project in layout
155 # hide project in layout
149 @project = nil
156 @project = nil
150 end
157 end
151
158
152 # Add a new issue category to @project
159 # Add a new issue category to @project
153 def add_issue_category
160 def add_issue_category
154 @category = @project.issue_categories.build(params[:category])
161 @category = @project.issue_categories.build(params[:category])
155 if request.post? and @category.save
162 if request.post? and @category.save
156 respond_to do |format|
163 respond_to do |format|
157 format.html do
164 format.html do
158 flash[:notice] = l(:notice_successful_create)
165 flash[:notice] = l(:notice_successful_create)
159 redirect_to :action => 'settings', :tab => 'categories', :id => @project
166 redirect_to :action => 'settings', :tab => 'categories', :id => @project
160 end
167 end
161 format.js do
168 format.js do
162 # IE doesn't support the replace_html rjs method for select box options
169 # IE doesn't support the replace_html rjs method for select box options
163 render(:update) {|page| page.replace "issue_category_id",
170 render(:update) {|page| page.replace "issue_category_id",
164 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
171 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
165 }
172 }
166 end
173 end
167 end
174 end
168 end
175 end
169 end
176 end
170
177
171 # Add a new version to @project
178 # Add a new version to @project
172 def add_version
179 def add_version
173 @version = @project.versions.build(params[:version])
180 @version = @project.versions.build(params[:version])
174 if request.post? and @version.save
181 if request.post? and @version.save
175 flash[:notice] = l(:notice_successful_create)
182 flash[:notice] = l(:notice_successful_create)
176 redirect_to :action => 'settings', :tab => 'versions', :id => @project
183 redirect_to :action => 'settings', :tab => 'versions', :id => @project
177 end
184 end
178 end
185 end
179
186
180 # Add a new issue to @project
187 # Add a new issue to @project
181 # The new issue will be created from an existing one if copy_from parameter is given
188 # The new issue will be created from an existing one if copy_from parameter is given
182 def add_issue
189 def add_issue
183 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
190 @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
184 @issue.project = @project
191 @issue.project = @project
185 @issue.author = User.current
192 @issue.author = User.current
186 @issue.tracker ||= @project.trackers.find(params[:tracker_id])
193 @issue.tracker ||= @project.trackers.find(params[:tracker_id])
187
194
188 default_status = IssueStatus.default
195 default_status = IssueStatus.default
189 unless default_status
196 unless default_status
190 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
197 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
191 render :nothing => true, :layout => true
198 render :nothing => true, :layout => true
192 return
199 return
193 end
200 end
194 @issue.status = default_status
201 @issue.status = default_status
195 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker))
202 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker))
196
203
197 if request.get?
204 if request.get?
198 @issue.start_date ||= Date.today
205 @issue.start_date ||= Date.today
199 @custom_values = @issue.custom_values.empty? ?
206 @custom_values = @issue.custom_values.empty? ?
200 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
207 @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
201 @issue.custom_values
208 @issue.custom_values
202 else
209 else
203 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
210 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
204 # Check that the user is allowed to apply the requested status
211 # Check that the user is allowed to apply the requested status
205 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
212 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
206 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
213 @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
207 @issue.custom_values = @custom_values
214 @issue.custom_values = @custom_values
208 if @issue.save
215 if @issue.save
209 attach_files(@issue, params[:attachments])
216 attach_files(@issue, params[:attachments])
210 flash[:notice] = l(:notice_successful_create)
217 flash[:notice] = l(:notice_successful_create)
211 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
218 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
212 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
219 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
213 return
220 return
214 end
221 end
215 end
222 end
216 @priorities = Enumeration::get_values('IPRI')
223 @priorities = Enumeration::get_values('IPRI')
217 end
224 end
218
225
219 # Bulk edit issues
226 # Bulk edit issues
220 def bulk_edit_issues
227 def bulk_edit_issues
221 if request.post?
228 if request.post?
222 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
229 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
223 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
230 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
224 assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
231 assigned_to = params[:assigned_to_id].blank? ? nil : User.find_by_id(params[:assigned_to_id])
225 category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
232 category = params[:category_id].blank? ? nil : @project.issue_categories.find_by_id(params[:category_id])
226 fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
233 fixed_version = params[:fixed_version_id].blank? ? nil : @project.versions.find_by_id(params[:fixed_version_id])
227 issues = @project.issues.find_all_by_id(params[:issue_ids])
234 issues = @project.issues.find_all_by_id(params[:issue_ids])
228 unsaved_issue_ids = []
235 unsaved_issue_ids = []
229 issues.each do |issue|
236 issues.each do |issue|
230 journal = issue.init_journal(User.current, params[:notes])
237 journal = issue.init_journal(User.current, params[:notes])
231 issue.priority = priority if priority
238 issue.priority = priority if priority
232 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
239 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
233 issue.category = category if category
240 issue.category = category if category
234 issue.fixed_version = fixed_version if fixed_version
241 issue.fixed_version = fixed_version if fixed_version
235 issue.start_date = params[:start_date] unless params[:start_date].blank?
242 issue.start_date = params[:start_date] unless params[:start_date].blank?
236 issue.due_date = params[:due_date] unless params[:due_date].blank?
243 issue.due_date = params[:due_date] unless params[:due_date].blank?
237 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
244 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
238 # Don't save any change to the issue if the user is not authorized to apply the requested status
245 # Don't save any change to the issue if the user is not authorized to apply the requested status
239 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
246 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
240 # Send notification for each issue (if changed)
247 # Send notification for each issue (if changed)
241 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
248 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
242 else
249 else
243 # Keep unsaved issue ids to display them in flash error
250 # Keep unsaved issue ids to display them in flash error
244 unsaved_issue_ids << issue.id
251 unsaved_issue_ids << issue.id
245 end
252 end
246 end
253 end
247 if unsaved_issue_ids.empty?
254 if unsaved_issue_ids.empty?
248 flash[:notice] = l(:notice_successful_update) unless issues.empty?
255 flash[:notice] = l(:notice_successful_update) unless issues.empty?
249 else
256 else
250 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
257 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, issues.size, '#' + unsaved_issue_ids.join(', #'))
251 end
258 end
252 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
259 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
253 return
260 return
254 end
261 end
255 # Find potential statuses the user could be allowed to switch issues to
262 # Find potential statuses the user could be allowed to switch issues to
256 @available_statuses = Workflow.find(:all, :include => :new_status,
263 @available_statuses = Workflow.find(:all, :include => :new_status,
257 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
264 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq
258 render :update do |page|
265 render :update do |page|
259 page.hide 'query_form'
266 page.hide 'query_form'
260 page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
267 page.replace_html 'bulk-edit', :partial => 'issues/bulk_edit_form'
261 end
268 end
262 end
269 end
263
270
264 def move_issues
271 def move_issues
265 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
272 @issues = @project.issues.find(params[:issue_ids]) if params[:issue_ids]
266 redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
273 redirect_to :controller => 'issues', :action => 'index', :project_id => @project and return unless @issues
267
274
268 @projects = []
275 @projects = []
269 # find projects to which the user is allowed to move the issue
276 # find projects to which the user is allowed to move the issue
270 if User.current.admin?
277 if User.current.admin?
271 # admin is allowed to move issues to any active (visible) project
278 # admin is allowed to move issues to any active (visible) project
272 @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
279 @projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
273 else
280 else
274 User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
281 User.current.memberships.each {|m| @projects << m.project if m.role.allowed_to?(:move_issues)}
275 end
282 end
276 @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
283 @target_project = @projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
277 @target_project ||= @project
284 @target_project ||= @project
278 @trackers = @target_project.trackers
285 @trackers = @target_project.trackers
279 if request.post?
286 if request.post?
280 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
287 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
281 unsaved_issue_ids = []
288 unsaved_issue_ids = []
282 @issues.each do |issue|
289 @issues.each do |issue|
283 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
290 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
284 end
291 end
285 if unsaved_issue_ids.empty?
292 if unsaved_issue_ids.empty?
286 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
293 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
287 else
294 else
288 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
295 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
289 end
296 end
290 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
297 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
291 return
298 return
292 end
299 end
293 render :layout => false if request.xhr?
300 render :layout => false if request.xhr?
294 end
301 end
295
302
296 # Add a news to @project
303 # Add a news to @project
297 def add_news
304 def add_news
298 @news = News.new(:project => @project, :author => User.current)
305 @news = News.new(:project => @project, :author => User.current)
299 if request.post?
306 if request.post?
300 @news.attributes = params[:news]
307 @news.attributes = params[:news]
301 if @news.save
308 if @news.save
302 flash[:notice] = l(:notice_successful_create)
309 flash[:notice] = l(:notice_successful_create)
303 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
310 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
304 redirect_to :controller => 'news', :action => 'index', :project_id => @project
311 redirect_to :controller => 'news', :action => 'index', :project_id => @project
305 end
312 end
306 end
313 end
307 end
314 end
308
315
309 def add_file
316 def add_file
310 if request.post?
317 if request.post?
311 @version = @project.versions.find_by_id(params[:version_id])
318 @version = @project.versions.find_by_id(params[:version_id])
312 attachments = attach_files(@version, params[:attachments])
319 attachments = attach_files(@version, params[:attachments])
313 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
320 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
314 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
321 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
315 end
322 end
316 @versions = @project.versions.sort
323 @versions = @project.versions.sort
317 end
324 end
318
325
319 def list_files
326 def list_files
320 @versions = @project.versions.sort
327 @versions = @project.versions.sort
321 end
328 end
322
329
323 # Show changelog for @project
330 # Show changelog for @project
324 def changelog
331 def changelog
325 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
332 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
326 retrieve_selected_tracker_ids(@trackers)
333 retrieve_selected_tracker_ids(@trackers)
327 @versions = @project.versions.sort
334 @versions = @project.versions.sort
328 end
335 end
329
336
330 def roadmap
337 def roadmap
331 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
338 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
332 retrieve_selected_tracker_ids(@trackers)
339 retrieve_selected_tracker_ids(@trackers)
333 @versions = @project.versions.sort
340 @versions = @project.versions.sort
334 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
341 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
335 end
342 end
336
343
337 def activity
344 def activity
338 if params[:year] and params[:year].to_i > 1900
345 if params[:year] and params[:year].to_i > 1900
339 @year = params[:year].to_i
346 @year = params[:year].to_i
340 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
347 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
341 @month = params[:month].to_i
348 @month = params[:month].to_i
342 end
349 end
343 end
350 end
344 @year ||= Date.today.year
351 @year ||= Date.today.year
345 @month ||= Date.today.month
352 @month ||= Date.today.month
346
353
347 case params[:format]
354 case params[:format]
348 when 'atom'
355 when 'atom'
349 # 30 last days
356 # 30 last days
350 @date_from = Date.today - 30
357 @date_from = Date.today - 30
351 @date_to = Date.today + 1
358 @date_to = Date.today + 1
352 else
359 else
353 # current month
360 # current month
354 @date_from = Date.civil(@year, @month, 1)
361 @date_from = Date.civil(@year, @month, 1)
355 @date_to = @date_from >> 1
362 @date_to = @date_from >> 1
356 end
363 end
357
364
358 @event_types = %w(issues news files documents changesets wiki_pages messages)
365 @event_types = %w(issues news files documents changesets wiki_pages messages)
359 @event_types.delete('wiki_pages') unless @project.wiki
366 @event_types.delete('wiki_pages') unless @project.wiki
360 @event_types.delete('changesets') unless @project.repository
367 @event_types.delete('changesets') unless @project.repository
361 @event_types.delete('messages') unless @project.boards.any?
368 @event_types.delete('messages') unless @project.boards.any?
362 # only show what the user is allowed to view
369 # only show what the user is allowed to view
363 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
370 @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
364
371
365 @scope = @event_types.select {|t| params["show_#{t}"]}
372 @scope = @event_types.select {|t| params["show_#{t}"]}
366 # default events if none is specified in parameters
373 # default events if none is specified in parameters
367 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
374 @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
368
375
369 @events = []
376 @events = []
370
377
371 if @scope.include?('issues')
378 if @scope.include?('issues')
372 @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
379 @events += @project.issues.find(:all, :include => [:author, :tracker], :conditions => ["#{Issue.table_name}.created_on>=? and #{Issue.table_name}.created_on<=?", @date_from, @date_to] )
373 @events += @project.issues_status_changes(@date_from, @date_to)
380 @events += @project.issues_status_changes(@date_from, @date_to)
374 end
381 end
375
382
376 if @scope.include?('news')
383 if @scope.include?('news')
377 @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
384 @events += @project.news.find(:all, :conditions => ["#{News.table_name}.created_on>=? and #{News.table_name}.created_on<=?", @date_from, @date_to], :include => :author )
378 end
385 end
379
386
380 if @scope.include?('files')
387 if @scope.include?('files')
381 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
388 @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*", :joins => "LEFT JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Version' and #{Version.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
382 end
389 end
383
390
384 if @scope.include?('documents')
391 if @scope.include?('documents')
385 @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
392 @events += @project.documents.find(:all, :conditions => ["#{Document.table_name}.created_on>=? and #{Document.table_name}.created_on<=?", @date_from, @date_to] )
386 @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
393 @events += Attachment.find(:all, :select => "attachments.*", :joins => "LEFT JOIN #{Document.table_name} ON #{Document.table_name}.id = #{Attachment.table_name}.container_id", :conditions => ["#{Attachment.table_name}.container_type='Document' and #{Document.table_name}.project_id=? and #{Attachment.table_name}.created_on>=? and #{Attachment.table_name}.created_on<=?", @project.id, @date_from, @date_to], :include => :author )
387 end
394 end
388
395
389 if @scope.include?('wiki_pages')
396 if @scope.include?('wiki_pages')
390 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
397 select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
391 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
398 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
392 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
399 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
393 "#{WikiContent.versioned_table_name}.id"
400 "#{WikiContent.versioned_table_name}.id"
394 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
401 joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
395 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
402 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id "
396 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
403 conditions = ["#{Wiki.table_name}.project_id = ? AND #{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?",
397 @project.id, @date_from, @date_to]
404 @project.id, @date_from, @date_to]
398
405
399 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
406 @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => conditions)
400 end
407 end
401
408
402 if @scope.include?('changesets')
409 if @scope.include?('changesets')
403 @events += Changeset.find(:all, :include => :repository, :conditions => ["#{Repository.table_name}.project_id = ? AND #{Changeset.table_name}.committed_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
410 @events += Changeset.find(:all, :include => :repository, :conditions => ["#{Repository.table_name}.project_id = ? AND #{Changeset.table_name}.committed_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
404 end
411 end
405
412
406 if @scope.include?('messages')
413 if @scope.include?('messages')
407 @events += Message.find(:all,
414 @events += Message.find(:all,
408 :include => [:board, :author],
415 :include => [:board, :author],
409 :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
416 :conditions => ["#{Board.table_name}.project_id=? AND #{Message.table_name}.parent_id IS NULL AND #{Message.table_name}.created_on BETWEEN ? AND ?", @project.id, @date_from, @date_to])
410 end
417 end
411
418
412 @events_by_day = @events.group_by(&:event_date)
419 @events_by_day = @events.group_by(&:event_date)
413
420
414 respond_to do |format|
421 respond_to do |format|
415 format.html { render :layout => false if request.xhr? }
422 format.html { render :layout => false if request.xhr? }
416 format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
423 format.atom { render_feed(@events, :title => "#{@project.name}: #{l(:label_activity)}") }
417 end
424 end
418 end
425 end
419
426
420 def calendar
427 def calendar
421 @trackers = @project.rolled_up_trackers
428 @trackers = @project.rolled_up_trackers
422 retrieve_selected_tracker_ids(@trackers)
429 retrieve_selected_tracker_ids(@trackers)
423
430
424 if params[:year] and params[:year].to_i > 1900
431 if params[:year] and params[:year].to_i > 1900
425 @year = params[:year].to_i
432 @year = params[:year].to_i
426 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
433 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
427 @month = params[:month].to_i
434 @month = params[:month].to_i
428 end
435 end
429 end
436 end
430 @year ||= Date.today.year
437 @year ||= Date.today.year
431 @month ||= Date.today.month
438 @month ||= Date.today.month
432 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
439 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
433
440
434 events = []
441 events = []
435 @project.issues_with_subprojects(params[:with_subprojects]) do
442 @project.issues_with_subprojects(params[:with_subprojects]) do
436 events += Issue.find(:all,
443 events += Issue.find(:all,
437 :include => [:tracker, :status, :assigned_to, :priority, :project],
444 :include => [:tracker, :status, :assigned_to, :priority, :project],
438 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
445 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?)) AND #{Issue.table_name}.tracker_id IN (#{@selected_tracker_ids.join(',')})", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
439 ) unless @selected_tracker_ids.empty?
446 ) unless @selected_tracker_ids.empty?
440 end
447 end
441 events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
448 events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
442 @calendar.events = events
449 @calendar.events = events
443
450
444 render :layout => false if request.xhr?
451 render :layout => false if request.xhr?
445 end
452 end
446
453
447 def gantt
454 def gantt
448 @trackers = @project.rolled_up_trackers
455 @trackers = @project.rolled_up_trackers
449 retrieve_selected_tracker_ids(@trackers)
456 retrieve_selected_tracker_ids(@trackers)
450
457
451 if params[:year] and params[:year].to_i >0
458 if params[:year] and params[:year].to_i >0
452 @year_from = params[:year].to_i
459 @year_from = params[:year].to_i
453 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
460 if params[:month] and params[:month].to_i >=1 and params[:month].to_i <= 12
454 @month_from = params[:month].to_i
461 @month_from = params[:month].to_i
455 else
462 else
456 @month_from = 1
463 @month_from = 1
457 end
464 end
458 else
465 else
459 @month_from ||= Date.today.month
466 @month_from ||= Date.today.month
460 @year_from ||= Date.today.year
467 @year_from ||= Date.today.year
461 end
468 end
462
469
463 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
470 zoom = (params[:zoom] || User.current.pref[:gantt_zoom]).to_i
464 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
471 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
465 months = (params[:months] || User.current.pref[:gantt_months]).to_i
472 months = (params[:months] || User.current.pref[:gantt_months]).to_i
466 @months = (months > 0 && months < 25) ? months : 6
473 @months = (months > 0 && months < 25) ? months : 6
467
474
468 # Save gantt paramters as user preference (zoom and months count)
475 # Save gantt paramters as user preference (zoom and months count)
469 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
476 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
470 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
477 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
471 User.current.preference.save
478 User.current.preference.save
472 end
479 end
473
480
474 @date_from = Date.civil(@year_from, @month_from, 1)
481 @date_from = Date.civil(@year_from, @month_from, 1)
475 @date_to = (@date_from >> @months) - 1
482 @date_to = (@date_from >> @months) - 1
476
483
477 @events = []
484 @events = []
478 @project.issues_with_subprojects(params[:with_subprojects]) do
485 @project.issues_with_subprojects(params[:with_subprojects]) do
479 @events += Issue.find(:all,
486 @events += Issue.find(:all,
480 :order => "start_date, due_date",
487 :order => "start_date, due_date",
481 :include => [:tracker, :status, :assigned_to, :priority, :project],
488 :include => [:tracker, :status, :assigned_to, :priority, :project],
482 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
489 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
483 ) unless @selected_tracker_ids.empty?
490 ) unless @selected_tracker_ids.empty?
484 end
491 end
485 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
492 @events += @project.versions.find(:all, :conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
486 @events.sort! {|x,y| x.start_date <=> y.start_date }
493 @events.sort! {|x,y| x.start_date <=> y.start_date }
487
494
488 if params[:format]=='pdf'
495 if params[:format]=='pdf'
489 @options_for_rfpdf ||= {}
496 @options_for_rfpdf ||= {}
490 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
497 @options_for_rfpdf[:file_name] = "#{@project.identifier}-gantt.pdf"
491 render :template => "projects/gantt.rfpdf", :layout => false
498 render :template => "projects/gantt.rfpdf", :layout => false
492 elsif params[:format]=='png' && respond_to?('gantt_image')
499 elsif params[:format]=='png' && respond_to?('gantt_image')
493 image = gantt_image(@events, @date_from, @months, @zoom)
500 image = gantt_image(@events, @date_from, @months, @zoom)
494 image.format = 'PNG'
501 image.format = 'PNG'
495 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
502 send_data(image.to_blob, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png")
496 else
503 else
497 render :template => "projects/gantt.rhtml"
504 render :template => "projects/gantt.rhtml"
498 end
505 end
499 end
506 end
500
507
501 private
508 private
502 # Find project of id params[:id]
509 # Find project of id params[:id]
503 # if not found, redirect to project list
510 # if not found, redirect to project list
504 # Used as a before_filter
511 # Used as a before_filter
505 def find_project
512 def find_project
506 @project = Project.find(params[:id])
513 @project = Project.find(params[:id])
507 rescue ActiveRecord::RecordNotFound
514 rescue ActiveRecord::RecordNotFound
508 render_404
515 render_404
509 end
516 end
510
517
511 def retrieve_selected_tracker_ids(selectable_trackers)
518 def retrieve_selected_tracker_ids(selectable_trackers)
512 if ids = params[:tracker_ids]
519 if ids = params[:tracker_ids]
513 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
520 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
514 else
521 else
515 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
522 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
516 end
523 end
517 end
524 end
518 end
525 end
@@ -1,81 +1,82
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 QueriesController < ApplicationController
18 class QueriesController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :issues
20 before_filter :find_project, :authorize
21 before_filter :find_project, :authorize
21
22
22 def index
23 def index
23 @queries = @project.queries.find(:all,
24 @queries = @project.queries.find(:all,
24 :order => "name ASC",
25 :order => "name ASC",
25 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
26 :conditions => ["is_public = ? or user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
26 end
27 end
27
28
28 def new
29 def new
29 @query = Query.new(params[:query])
30 @query = Query.new(params[:query])
30 @query.project = @project
31 @query.project = @project
31 @query.user = User.current
32 @query.user = User.current
32 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
33 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
33 @query.column_names = nil if params[:default_columns]
34 @query.column_names = nil if params[:default_columns]
34
35
35 params[:fields].each do |field|
36 params[:fields].each do |field|
36 @query.add_filter(field, params[:operators][field], params[:values][field])
37 @query.add_filter(field, params[:operators][field], params[:values][field])
37 end if params[:fields]
38 end if params[:fields]
38
39
39 if request.post? && params[:confirm] && @query.save
40 if request.post? && params[:confirm] && @query.save
40 flash[:notice] = l(:notice_successful_create)
41 flash[:notice] = l(:notice_successful_create)
41 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
42 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
42 return
43 return
43 end
44 end
44 render :layout => false if request.xhr?
45 render :layout => false if request.xhr?
45 end
46 end
46
47
47 def edit
48 def edit
48 if request.post?
49 if request.post?
49 @query.filters = {}
50 @query.filters = {}
50 params[:fields].each do |field|
51 params[:fields].each do |field|
51 @query.add_filter(field, params[:operators][field], params[:values][field])
52 @query.add_filter(field, params[:operators][field], params[:values][field])
52 end if params[:fields]
53 end if params[:fields]
53 @query.attributes = params[:query]
54 @query.attributes = params[:query]
54 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
55 @query.is_public = false unless current_role.allowed_to?(:manage_public_queries)
55 @query.column_names = nil if params[:default_columns]
56 @query.column_names = nil if params[:default_columns]
56
57
57 if @query.save
58 if @query.save
58 flash[:notice] = l(:notice_successful_update)
59 flash[:notice] = l(:notice_successful_update)
59 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
60 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
60 end
61 end
61 end
62 end
62 end
63 end
63
64
64 def destroy
65 def destroy
65 @query.destroy if request.post?
66 @query.destroy if request.post?
66 redirect_to :controller => 'queries', :project_id => @project
67 redirect_to :controller => 'queries', :project_id => @project
67 end
68 end
68
69
69 private
70 private
70 def find_project
71 def find_project
71 if params[:id]
72 if params[:id]
72 @query = Query.find(params[:id])
73 @query = Query.find(params[:id])
73 @project = @query.project
74 @project = @query.project
74 render_403 unless @query.editable_by?(User.current)
75 render_403 unless @query.editable_by?(User.current)
75 else
76 else
76 @project = Project.find(params[:project_id])
77 @project = Project.find(params[:project_id])
77 end
78 end
78 rescue ActiveRecord::RecordNotFound
79 rescue ActiveRecord::RecordNotFound
79 render_404
80 render_404
80 end
81 end
81 end
82 end
@@ -1,236 +1,237
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 ReportsController < ApplicationController
18 class ReportsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :issues
20 before_filter :find_project, :authorize
21 before_filter :find_project, :authorize
21
22
22 def issue_report
23 def issue_report
23 @statuses = IssueStatus.find(:all, :order => 'position')
24 @statuses = IssueStatus.find(:all, :order => 'position')
24
25
25 case params[:detail]
26 case params[:detail]
26 when "tracker"
27 when "tracker"
27 @field = "tracker_id"
28 @field = "tracker_id"
28 @rows = @project.trackers
29 @rows = @project.trackers
29 @data = issues_by_tracker
30 @data = issues_by_tracker
30 @report_title = l(:field_tracker)
31 @report_title = l(:field_tracker)
31 render :template => "reports/issue_report_details"
32 render :template => "reports/issue_report_details"
32 when "version"
33 when "version"
33 @field = "fixed_version_id"
34 @field = "fixed_version_id"
34 @rows = @project.versions.sort
35 @rows = @project.versions.sort
35 @data = issues_by_version
36 @data = issues_by_version
36 @report_title = l(:field_version)
37 @report_title = l(:field_version)
37 render :template => "reports/issue_report_details"
38 render :template => "reports/issue_report_details"
38 when "priority"
39 when "priority"
39 @field = "priority_id"
40 @field = "priority_id"
40 @rows = Enumeration::get_values('IPRI')
41 @rows = Enumeration::get_values('IPRI')
41 @data = issues_by_priority
42 @data = issues_by_priority
42 @report_title = l(:field_priority)
43 @report_title = l(:field_priority)
43 render :template => "reports/issue_report_details"
44 render :template => "reports/issue_report_details"
44 when "category"
45 when "category"
45 @field = "category_id"
46 @field = "category_id"
46 @rows = @project.issue_categories
47 @rows = @project.issue_categories
47 @data = issues_by_category
48 @data = issues_by_category
48 @report_title = l(:field_category)
49 @report_title = l(:field_category)
49 render :template => "reports/issue_report_details"
50 render :template => "reports/issue_report_details"
50 when "assigned_to"
51 when "assigned_to"
51 @field = "assigned_to_id"
52 @field = "assigned_to_id"
52 @rows = @project.members.collect { |m| m.user }
53 @rows = @project.members.collect { |m| m.user }
53 @data = issues_by_assigned_to
54 @data = issues_by_assigned_to
54 @report_title = l(:field_assigned_to)
55 @report_title = l(:field_assigned_to)
55 render :template => "reports/issue_report_details"
56 render :template => "reports/issue_report_details"
56 when "author"
57 when "author"
57 @field = "author_id"
58 @field = "author_id"
58 @rows = @project.members.collect { |m| m.user }
59 @rows = @project.members.collect { |m| m.user }
59 @data = issues_by_author
60 @data = issues_by_author
60 @report_title = l(:field_author)
61 @report_title = l(:field_author)
61 render :template => "reports/issue_report_details"
62 render :template => "reports/issue_report_details"
62 when "subproject"
63 when "subproject"
63 @field = "project_id"
64 @field = "project_id"
64 @rows = @project.active_children
65 @rows = @project.active_children
65 @data = issues_by_subproject
66 @data = issues_by_subproject
66 @report_title = l(:field_subproject)
67 @report_title = l(:field_subproject)
67 render :template => "reports/issue_report_details"
68 render :template => "reports/issue_report_details"
68 else
69 else
69 @trackers = @project.trackers
70 @trackers = @project.trackers
70 @versions = @project.versions.sort
71 @versions = @project.versions.sort
71 @priorities = Enumeration::get_values('IPRI')
72 @priorities = Enumeration::get_values('IPRI')
72 @categories = @project.issue_categories
73 @categories = @project.issue_categories
73 @assignees = @project.members.collect { |m| m.user }
74 @assignees = @project.members.collect { |m| m.user }
74 @authors = @project.members.collect { |m| m.user }
75 @authors = @project.members.collect { |m| m.user }
75 @subprojects = @project.active_children
76 @subprojects = @project.active_children
76 issues_by_tracker
77 issues_by_tracker
77 issues_by_version
78 issues_by_version
78 issues_by_priority
79 issues_by_priority
79 issues_by_category
80 issues_by_category
80 issues_by_assigned_to
81 issues_by_assigned_to
81 issues_by_author
82 issues_by_author
82 issues_by_subproject
83 issues_by_subproject
83
84
84 render :template => "reports/issue_report"
85 render :template => "reports/issue_report"
85 end
86 end
86 end
87 end
87
88
88 def delays
89 def delays
89 @trackers = Tracker.find(:all)
90 @trackers = Tracker.find(:all)
90 if request.get?
91 if request.get?
91 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
92 @selected_tracker_ids = @trackers.collect {|t| t.id.to_s }
92 else
93 else
93 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
94 @selected_tracker_ids = params[:tracker_ids].collect { |id| id.to_i.to_s } if params[:tracker_ids] and params[:tracker_ids].is_a? Array
94 end
95 end
95 @selected_tracker_ids ||= []
96 @selected_tracker_ids ||= []
96 @raw =
97 @raw =
97 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
98 ActiveRecord::Base.connection.select_all("SELECT datediff( a.created_on, b.created_on ) as delay, count(a.id) as total
98 FROM issue_histories a, issue_histories b, issues i
99 FROM issue_histories a, issue_histories b, issues i
99 WHERE a.status_id =5
100 WHERE a.status_id =5
100 AND a.issue_id = b.issue_id
101 AND a.issue_id = b.issue_id
101 AND a.issue_id = i.id
102 AND a.issue_id = i.id
102 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
103 AND i.tracker_id in (#{@selected_tracker_ids.join(',')})
103 AND b.id = (
104 AND b.id = (
104 SELECT min( c.id )
105 SELECT min( c.id )
105 FROM issue_histories c
106 FROM issue_histories c
106 WHERE b.issue_id = c.issue_id )
107 WHERE b.issue_id = c.issue_id )
107 GROUP BY delay") unless @selected_tracker_ids.empty?
108 GROUP BY delay") unless @selected_tracker_ids.empty?
108 @raw ||=[]
109 @raw ||=[]
109
110
110 @x_from = 0
111 @x_from = 0
111 @x_to = 0
112 @x_to = 0
112 @y_from = 0
113 @y_from = 0
113 @y_to = 0
114 @y_to = 0
114 @sum_total = 0
115 @sum_total = 0
115 @sum_delay = 0
116 @sum_delay = 0
116 @raw.each do |r|
117 @raw.each do |r|
117 @x_to = [r['delay'].to_i, @x_to].max
118 @x_to = [r['delay'].to_i, @x_to].max
118 @y_to = [r['total'].to_i, @y_to].max
119 @y_to = [r['total'].to_i, @y_to].max
119 @sum_total = @sum_total + r['total'].to_i
120 @sum_total = @sum_total + r['total'].to_i
120 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
121 @sum_delay = @sum_delay + r['total'].to_i * r['delay'].to_i
121 end
122 end
122 end
123 end
123
124
124 private
125 private
125 # Find project of id params[:id]
126 # Find project of id params[:id]
126 def find_project
127 def find_project
127 @project = Project.find(params[:id])
128 @project = Project.find(params[:id])
128 rescue ActiveRecord::RecordNotFound
129 rescue ActiveRecord::RecordNotFound
129 render_404
130 render_404
130 end
131 end
131
132
132 def issues_by_tracker
133 def issues_by_tracker
133 @issues_by_tracker ||=
134 @issues_by_tracker ||=
134 ActiveRecord::Base.connection.select_all("select s.id as status_id,
135 ActiveRecord::Base.connection.select_all("select s.id as status_id,
135 s.is_closed as closed,
136 s.is_closed as closed,
136 t.id as tracker_id,
137 t.id as tracker_id,
137 count(i.id) as total
138 count(i.id) as total
138 from
139 from
139 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
140 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Tracker.table_name} t
140 where
141 where
141 i.status_id=s.id
142 i.status_id=s.id
142 and i.tracker_id=t.id
143 and i.tracker_id=t.id
143 and i.project_id=#{@project.id}
144 and i.project_id=#{@project.id}
144 group by s.id, s.is_closed, t.id")
145 group by s.id, s.is_closed, t.id")
145 end
146 end
146
147
147 def issues_by_version
148 def issues_by_version
148 @issues_by_version ||=
149 @issues_by_version ||=
149 ActiveRecord::Base.connection.select_all("select s.id as status_id,
150 ActiveRecord::Base.connection.select_all("select s.id as status_id,
150 s.is_closed as closed,
151 s.is_closed as closed,
151 v.id as fixed_version_id,
152 v.id as fixed_version_id,
152 count(i.id) as total
153 count(i.id) as total
153 from
154 from
154 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
155 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Version.table_name} v
155 where
156 where
156 i.status_id=s.id
157 i.status_id=s.id
157 and i.fixed_version_id=v.id
158 and i.fixed_version_id=v.id
158 and i.project_id=#{@project.id}
159 and i.project_id=#{@project.id}
159 group by s.id, s.is_closed, v.id")
160 group by s.id, s.is_closed, v.id")
160 end
161 end
161
162
162 def issues_by_priority
163 def issues_by_priority
163 @issues_by_priority ||=
164 @issues_by_priority ||=
164 ActiveRecord::Base.connection.select_all("select s.id as status_id,
165 ActiveRecord::Base.connection.select_all("select s.id as status_id,
165 s.is_closed as closed,
166 s.is_closed as closed,
166 p.id as priority_id,
167 p.id as priority_id,
167 count(i.id) as total
168 count(i.id) as total
168 from
169 from
169 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
170 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{Enumeration.table_name} p
170 where
171 where
171 i.status_id=s.id
172 i.status_id=s.id
172 and i.priority_id=p.id
173 and i.priority_id=p.id
173 and i.project_id=#{@project.id}
174 and i.project_id=#{@project.id}
174 group by s.id, s.is_closed, p.id")
175 group by s.id, s.is_closed, p.id")
175 end
176 end
176
177
177 def issues_by_category
178 def issues_by_category
178 @issues_by_category ||=
179 @issues_by_category ||=
179 ActiveRecord::Base.connection.select_all("select s.id as status_id,
180 ActiveRecord::Base.connection.select_all("select s.id as status_id,
180 s.is_closed as closed,
181 s.is_closed as closed,
181 c.id as category_id,
182 c.id as category_id,
182 count(i.id) as total
183 count(i.id) as total
183 from
184 from
184 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
185 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{IssueCategory.table_name} c
185 where
186 where
186 i.status_id=s.id
187 i.status_id=s.id
187 and i.category_id=c.id
188 and i.category_id=c.id
188 and i.project_id=#{@project.id}
189 and i.project_id=#{@project.id}
189 group by s.id, s.is_closed, c.id")
190 group by s.id, s.is_closed, c.id")
190 end
191 end
191
192
192 def issues_by_assigned_to
193 def issues_by_assigned_to
193 @issues_by_assigned_to ||=
194 @issues_by_assigned_to ||=
194 ActiveRecord::Base.connection.select_all("select s.id as status_id,
195 ActiveRecord::Base.connection.select_all("select s.id as status_id,
195 s.is_closed as closed,
196 s.is_closed as closed,
196 a.id as assigned_to_id,
197 a.id as assigned_to_id,
197 count(i.id) as total
198 count(i.id) as total
198 from
199 from
199 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
200 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
200 where
201 where
201 i.status_id=s.id
202 i.status_id=s.id
202 and i.assigned_to_id=a.id
203 and i.assigned_to_id=a.id
203 and i.project_id=#{@project.id}
204 and i.project_id=#{@project.id}
204 group by s.id, s.is_closed, a.id")
205 group by s.id, s.is_closed, a.id")
205 end
206 end
206
207
207 def issues_by_author
208 def issues_by_author
208 @issues_by_author ||=
209 @issues_by_author ||=
209 ActiveRecord::Base.connection.select_all("select s.id as status_id,
210 ActiveRecord::Base.connection.select_all("select s.id as status_id,
210 s.is_closed as closed,
211 s.is_closed as closed,
211 a.id as author_id,
212 a.id as author_id,
212 count(i.id) as total
213 count(i.id) as total
213 from
214 from
214 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
215 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{User.table_name} a
215 where
216 where
216 i.status_id=s.id
217 i.status_id=s.id
217 and i.author_id=a.id
218 and i.author_id=a.id
218 and i.project_id=#{@project.id}
219 and i.project_id=#{@project.id}
219 group by s.id, s.is_closed, a.id")
220 group by s.id, s.is_closed, a.id")
220 end
221 end
221
222
222 def issues_by_subproject
223 def issues_by_subproject
223 @issues_by_subproject ||=
224 @issues_by_subproject ||=
224 ActiveRecord::Base.connection.select_all("select s.id as status_id,
225 ActiveRecord::Base.connection.select_all("select s.id as status_id,
225 s.is_closed as closed,
226 s.is_closed as closed,
226 i.project_id as project_id,
227 i.project_id as project_id,
227 count(i.id) as total
228 count(i.id) as total
228 from
229 from
229 #{Issue.table_name} i, #{IssueStatus.table_name} s
230 #{Issue.table_name} i, #{IssueStatus.table_name} s
230 where
231 where
231 i.status_id=s.id
232 i.status_id=s.id
232 and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
233 and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')})
233 group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
234 group by s.id, s.is_closed, i.project_id") if @project.active_children.any?
234 @issues_by_subproject ||= []
235 @issues_by_subproject ||= []
235 end
236 end
236 end
237 end
@@ -1,280 +1,281
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 require 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21
21
22 class ChangesetNotFound < Exception
22 class ChangesetNotFound < Exception
23 end
23 end
24
24
25 class RepositoriesController < ApplicationController
25 class RepositoriesController < ApplicationController
26 layout 'base'
26 layout 'base'
27 menu_item :repository
27 before_filter :find_repository, :except => :edit
28 before_filter :find_repository, :except => :edit
28 before_filter :find_project, :only => :edit
29 before_filter :find_project, :only => :edit
29 before_filter :authorize
30 before_filter :authorize
30 accept_key_auth :revisions
31 accept_key_auth :revisions
31
32
32 def edit
33 def edit
33 @repository = @project.repository
34 @repository = @project.repository
34 if !@repository
35 if !@repository
35 @repository = Repository.factory(params[:repository_scm])
36 @repository = Repository.factory(params[:repository_scm])
36 @repository.project = @project
37 @repository.project = @project
37 end
38 end
38 if request.post?
39 if request.post?
39 @repository.attributes = params[:repository]
40 @repository.attributes = params[:repository]
40 @repository.save
41 @repository.save
41 end
42 end
42 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
43 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
43 end
44 end
44
45
45 def destroy
46 def destroy
46 @repository.destroy
47 @repository.destroy
47 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
48 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
48 end
49 end
49
50
50 def show
51 def show
51 # check if new revisions have been committed in the repository
52 # check if new revisions have been committed in the repository
52 @repository.fetch_changesets if Setting.autofetch_changesets?
53 @repository.fetch_changesets if Setting.autofetch_changesets?
53 # get entries for the browse frame
54 # get entries for the browse frame
54 @entries = @repository.entries('')
55 @entries = @repository.entries('')
55 # latest changesets
56 # latest changesets
56 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
57 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
57 show_error and return unless @entries || @changesets.any?
58 show_error and return unless @entries || @changesets.any?
58 end
59 end
59
60
60 def browse
61 def browse
61 @entries = @repository.entries(@path, @rev)
62 @entries = @repository.entries(@path, @rev)
62 if request.xhr?
63 if request.xhr?
63 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
64 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
64 else
65 else
65 show_error unless @entries
66 show_error unless @entries
66 end
67 end
67 end
68 end
68
69
69 def changes
70 def changes
70 @entry = @repository.scm.entry(@path, @rev)
71 @entry = @repository.scm.entry(@path, @rev)
71 show_error and return unless @entry
72 show_error and return unless @entry
72 @changesets = @repository.changesets_for_path(@path)
73 @changesets = @repository.changesets_for_path(@path)
73 end
74 end
74
75
75 def revisions
76 def revisions
76 @changeset_count = @repository.changesets.count
77 @changeset_count = @repository.changesets.count
77 @changeset_pages = Paginator.new self, @changeset_count,
78 @changeset_pages = Paginator.new self, @changeset_count,
78 per_page_option,
79 per_page_option,
79 params['page']
80 params['page']
80 @changesets = @repository.changesets.find(:all,
81 @changesets = @repository.changesets.find(:all,
81 :limit => @changeset_pages.items_per_page,
82 :limit => @changeset_pages.items_per_page,
82 :offset => @changeset_pages.current.offset)
83 :offset => @changeset_pages.current.offset)
83
84
84 respond_to do |format|
85 respond_to do |format|
85 format.html { render :layout => false if request.xhr? }
86 format.html { render :layout => false if request.xhr? }
86 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
87 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
87 end
88 end
88 end
89 end
89
90
90 def entry
91 def entry
91 @content = @repository.scm.cat(@path, @rev)
92 @content = @repository.scm.cat(@path, @rev)
92 show_error and return unless @content
93 show_error and return unless @content
93 if 'raw' == params[:format]
94 if 'raw' == params[:format]
94 send_data @content, :filename => @path.split('/').last
95 send_data @content, :filename => @path.split('/').last
95 else
96 else
96 # Prevent empty lines when displaying a file with Windows style eol
97 # Prevent empty lines when displaying a file with Windows style eol
97 @content.gsub!("\r\n", "\n")
98 @content.gsub!("\r\n", "\n")
98 end
99 end
99 end
100 end
100
101
101 def annotate
102 def annotate
102 @annotate = @repository.scm.annotate(@path, @rev)
103 @annotate = @repository.scm.annotate(@path, @rev)
103 show_error and return if @annotate.nil? || @annotate.empty?
104 show_error and return if @annotate.nil? || @annotate.empty?
104 end
105 end
105
106
106 def revision
107 def revision
107 @changeset = @repository.changesets.find_by_revision(@rev)
108 @changeset = @repository.changesets.find_by_revision(@rev)
108 raise ChangesetNotFound unless @changeset
109 raise ChangesetNotFound unless @changeset
109 @changes_count = @changeset.changes.size
110 @changes_count = @changeset.changes.size
110 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
111 @changes_pages = Paginator.new self, @changes_count, 150, params['page']
111 @changes = @changeset.changes.find(:all,
112 @changes = @changeset.changes.find(:all,
112 :limit => @changes_pages.items_per_page,
113 :limit => @changes_pages.items_per_page,
113 :offset => @changes_pages.current.offset)
114 :offset => @changes_pages.current.offset)
114
115
115 respond_to do |format|
116 respond_to do |format|
116 format.html
117 format.html
117 format.js {render :layout => false}
118 format.js {render :layout => false}
118 end
119 end
119 rescue ChangesetNotFound
120 rescue ChangesetNotFound
120 show_error
121 show_error
121 end
122 end
122
123
123 def diff
124 def diff
124 @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
125 @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
125 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
126 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
126 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
127 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
127
128
128 # Save diff type as user preference
129 # Save diff type as user preference
129 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
130 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
130 User.current.pref[:diff_type] = @diff_type
131 User.current.pref[:diff_type] = @diff_type
131 User.current.preference.save
132 User.current.preference.save
132 end
133 end
133
134
134 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
135 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
135 unless read_fragment(@cache_key)
136 unless read_fragment(@cache_key)
136 @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
137 @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
137 show_error and return unless @diff
138 show_error and return unless @diff
138 end
139 end
139 end
140 end
140
141
141 def stats
142 def stats
142 end
143 end
143
144
144 def graph
145 def graph
145 data = nil
146 data = nil
146 case params[:graph]
147 case params[:graph]
147 when "commits_per_month"
148 when "commits_per_month"
148 data = graph_commits_per_month(@repository)
149 data = graph_commits_per_month(@repository)
149 when "commits_per_author"
150 when "commits_per_author"
150 data = graph_commits_per_author(@repository)
151 data = graph_commits_per_author(@repository)
151 end
152 end
152 if data
153 if data
153 headers["Content-Type"] = "image/svg+xml"
154 headers["Content-Type"] = "image/svg+xml"
154 send_data(data, :type => "image/svg+xml", :disposition => "inline")
155 send_data(data, :type => "image/svg+xml", :disposition => "inline")
155 else
156 else
156 render_404
157 render_404
157 end
158 end
158 end
159 end
159
160
160 private
161 private
161 def find_project
162 def find_project
162 @project = Project.find(params[:id])
163 @project = Project.find(params[:id])
163 rescue ActiveRecord::RecordNotFound
164 rescue ActiveRecord::RecordNotFound
164 render_404
165 render_404
165 end
166 end
166
167
167 def find_repository
168 def find_repository
168 @project = Project.find(params[:id])
169 @project = Project.find(params[:id])
169 @repository = @project.repository
170 @repository = @project.repository
170 render_404 and return false unless @repository
171 render_404 and return false unless @repository
171 @path = params[:path].join('/') unless params[:path].nil?
172 @path = params[:path].join('/') unless params[:path].nil?
172 @path ||= ''
173 @path ||= ''
173 @rev = params[:rev].to_i if params[:rev]
174 @rev = params[:rev].to_i if params[:rev]
174 rescue ActiveRecord::RecordNotFound
175 rescue ActiveRecord::RecordNotFound
175 render_404
176 render_404
176 end
177 end
177
178
178 def show_error
179 def show_error
179 flash.now[:error] = l(:notice_scm_error)
180 flash.now[:error] = l(:notice_scm_error)
180 render :nothing => true, :layout => true
181 render :nothing => true, :layout => true
181 end
182 end
182
183
183 def graph_commits_per_month(repository)
184 def graph_commits_per_month(repository)
184 @date_to = Date.today
185 @date_to = Date.today
185 @date_from = @date_to << 11
186 @date_from = @date_to << 11
186 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
187 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
187 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
188 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
188 commits_by_month = [0] * 12
189 commits_by_month = [0] * 12
189 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
190 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
190
191
191 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
192 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
192 changes_by_month = [0] * 12
193 changes_by_month = [0] * 12
193 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
194 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
194
195
195 fields = []
196 fields = []
196 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
197 month_names = l(:actionview_datehelper_select_month_names_abbr).split(',')
197 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
198 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]}
198
199
199 graph = SVG::Graph::Bar.new(
200 graph = SVG::Graph::Bar.new(
200 :height => 300,
201 :height => 300,
201 :width => 500,
202 :width => 500,
202 :fields => fields.reverse,
203 :fields => fields.reverse,
203 :stack => :side,
204 :stack => :side,
204 :scale_integers => true,
205 :scale_integers => true,
205 :step_x_labels => 2,
206 :step_x_labels => 2,
206 :show_data_values => false,
207 :show_data_values => false,
207 :graph_title => l(:label_commits_per_month),
208 :graph_title => l(:label_commits_per_month),
208 :show_graph_title => true
209 :show_graph_title => true
209 )
210 )
210
211
211 graph.add_data(
212 graph.add_data(
212 :data => commits_by_month[0..11].reverse,
213 :data => commits_by_month[0..11].reverse,
213 :title => l(:label_revision_plural)
214 :title => l(:label_revision_plural)
214 )
215 )
215
216
216 graph.add_data(
217 graph.add_data(
217 :data => changes_by_month[0..11].reverse,
218 :data => changes_by_month[0..11].reverse,
218 :title => l(:label_change_plural)
219 :title => l(:label_change_plural)
219 )
220 )
220
221
221 graph.burn
222 graph.burn
222 end
223 end
223
224
224 def graph_commits_per_author(repository)
225 def graph_commits_per_author(repository)
225 commits_by_author = repository.changesets.count(:all, :group => :committer)
226 commits_by_author = repository.changesets.count(:all, :group => :committer)
226 commits_by_author.sort! {|x, y| x.last <=> y.last}
227 commits_by_author.sort! {|x, y| x.last <=> y.last}
227
228
228 changes_by_author = repository.changes.count(:all, :group => :committer)
229 changes_by_author = repository.changes.count(:all, :group => :committer)
229 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
230 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
230
231
231 fields = commits_by_author.collect {|r| r.first}
232 fields = commits_by_author.collect {|r| r.first}
232 commits_data = commits_by_author.collect {|r| r.last}
233 commits_data = commits_by_author.collect {|r| r.last}
233 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
234 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
234
235
235 fields = fields + [""]*(10 - fields.length) if fields.length<10
236 fields = fields + [""]*(10 - fields.length) if fields.length<10
236 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
237 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
237 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
238 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
238
239
239 graph = SVG::Graph::BarHorizontal.new(
240 graph = SVG::Graph::BarHorizontal.new(
240 :height => 300,
241 :height => 300,
241 :width => 500,
242 :width => 500,
242 :fields => fields,
243 :fields => fields,
243 :stack => :side,
244 :stack => :side,
244 :scale_integers => true,
245 :scale_integers => true,
245 :show_data_values => false,
246 :show_data_values => false,
246 :rotate_y_labels => false,
247 :rotate_y_labels => false,
247 :graph_title => l(:label_commits_per_author),
248 :graph_title => l(:label_commits_per_author),
248 :show_graph_title => true
249 :show_graph_title => true
249 )
250 )
250
251
251 graph.add_data(
252 graph.add_data(
252 :data => commits_data,
253 :data => commits_data,
253 :title => l(:label_revision_plural)
254 :title => l(:label_revision_plural)
254 )
255 )
255
256
256 graph.add_data(
257 graph.add_data(
257 :data => changes_data,
258 :data => changes_data,
258 :title => l(:label_change_plural)
259 :title => l(:label_change_plural)
259 )
260 )
260
261
261 graph.burn
262 graph.burn
262 end
263 end
263
264
264 end
265 end
265
266
266 class Date
267 class Date
267 def months_ago(date = Date.today)
268 def months_ago(date = Date.today)
268 (date.year - self.year)*12 + (date.month - self.month)
269 (date.year - self.year)*12 + (date.month - self.month)
269 end
270 end
270
271
271 def weeks_ago(date = Date.today)
272 def weeks_ago(date = Date.today)
272 (date.year - self.year)*52 + (date.cweek - self.cweek)
273 (date.year - self.year)*52 + (date.cweek - self.cweek)
273 end
274 end
274 end
275 end
275
276
276 class String
277 class String
277 def with_leading_slash
278 def with_leading_slash
278 starts_with?('/') ? self : "/#{self}"
279 starts_with?('/') ? self : "/#{self}"
279 end
280 end
280 end
281 end
@@ -1,172 +1,173
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 TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :issues
20 before_filter :find_project, :authorize
21 before_filter :find_project, :authorize
21
22
22 helper :sort
23 helper :sort
23 include SortHelper
24 include SortHelper
24 helper :issues
25 helper :issues
25
26
26 def report
27 def report
27 @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
28 @available_criterias = { 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
28 :values => @project.versions,
29 :values => @project.versions,
29 :label => :label_version},
30 :label => :label_version},
30 'category' => {:sql => "#{Issue.table_name}.category_id",
31 'category' => {:sql => "#{Issue.table_name}.category_id",
31 :values => @project.issue_categories,
32 :values => @project.issue_categories,
32 :label => :field_category},
33 :label => :field_category},
33 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
34 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
34 :values => @project.users,
35 :values => @project.users,
35 :label => :label_member},
36 :label => :label_member},
36 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
37 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
37 :values => Tracker.find(:all),
38 :values => Tracker.find(:all),
38 :label => :label_tracker},
39 :label => :label_tracker},
39 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
40 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
40 :values => Enumeration::get_values('ACTI'),
41 :values => Enumeration::get_values('ACTI'),
41 :label => :label_activity}
42 :label => :label_activity}
42 }
43 }
43
44
44 @criterias = params[:criterias] || []
45 @criterias = params[:criterias] || []
45 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
46 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
46 @criterias.uniq!
47 @criterias.uniq!
47
48
48 @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
49 @columns = (params[:period] && %w(year month week).include?(params[:period])) ? params[:period] : 'month'
49
50
50 if params[:date_from]
51 if params[:date_from]
51 begin; @date_from = params[:date_from].to_date; rescue; end
52 begin; @date_from = params[:date_from].to_date; rescue; end
52 end
53 end
53 if params[:date_to]
54 if params[:date_to]
54 begin; @date_to = params[:date_to].to_date; rescue; end
55 begin; @date_to = params[:date_to].to_date; rescue; end
55 end
56 end
56 @date_from ||= Date.civil(Date.today.year, 1, 1)
57 @date_from ||= Date.civil(Date.today.year, 1, 1)
57 @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1
58 @date_to ||= (Date.civil(Date.today.year, Date.today.month, 1) >> 1) - 1
58
59
59 unless @criterias.empty?
60 unless @criterias.empty?
60 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
61 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
61 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
62 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
62
63
63 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
64 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, SUM(hours) AS hours"
64 sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
65 sql << " FROM #{TimeEntry.table_name} LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
65 sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id
66 sql << " WHERE #{TimeEntry.table_name}.project_id = %s" % @project.id
66 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
67 sql << " AND spent_on BETWEEN '%s' AND '%s'" % [ActiveRecord::Base.connection.quoted_date(@date_from.to_time), ActiveRecord::Base.connection.quoted_date(@date_to.to_time)]
67 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
68 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek"
68
69
69 @hours = ActiveRecord::Base.connection.select_all(sql)
70 @hours = ActiveRecord::Base.connection.select_all(sql)
70
71
71 @hours.each do |row|
72 @hours.each do |row|
72 case @columns
73 case @columns
73 when 'year'
74 when 'year'
74 row['year'] = row['tyear']
75 row['year'] = row['tyear']
75 when 'month'
76 when 'month'
76 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
77 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
77 when 'week'
78 when 'week'
78 row['week'] = "#{row['tyear']}-#{row['tweek']}"
79 row['week'] = "#{row['tyear']}-#{row['tweek']}"
79 end
80 end
80 end
81 end
81 end
82 end
82
83
83 @periods = []
84 @periods = []
84 date_from = @date_from
85 date_from = @date_from
85 # 100 columns max
86 # 100 columns max
86 while date_from < @date_to && @periods.length < 100
87 while date_from < @date_to && @periods.length < 100
87 case @columns
88 case @columns
88 when 'year'
89 when 'year'
89 @periods << "#{date_from.year}"
90 @periods << "#{date_from.year}"
90 date_from = date_from >> 12
91 date_from = date_from >> 12
91 when 'month'
92 when 'month'
92 @periods << "#{date_from.year}-#{date_from.month}"
93 @periods << "#{date_from.year}-#{date_from.month}"
93 date_from = date_from >> 1
94 date_from = date_from >> 1
94 when 'week'
95 when 'week'
95 @periods << "#{date_from.year}-#{date_from.cweek}"
96 @periods << "#{date_from.year}-#{date_from.cweek}"
96 date_from = date_from + 7
97 date_from = date_from + 7
97 end
98 end
98 end
99 end
99
100
100 render :layout => false if request.xhr?
101 render :layout => false if request.xhr?
101 end
102 end
102
103
103 def details
104 def details
104 sort_init 'spent_on', 'desc'
105 sort_init 'spent_on', 'desc'
105 sort_update
106 sort_update
106
107
107 @entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause)
108 @entries = (@issue ? @issue : @project).time_entries.find(:all, :include => [:activity, :user, {:issue => [:tracker, :assigned_to, :priority]}], :order => sort_clause)
108
109
109 @total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours }
110 @total_hours = @entries.inject(0) { |sum,entry| sum + entry.hours }
110 @owner_id = User.current.id
111 @owner_id = User.current.id
111
112
112 send_csv and return if 'csv' == params[:export]
113 send_csv and return if 'csv' == params[:export]
113 render :action => 'details', :layout => false if request.xhr?
114 render :action => 'details', :layout => false if request.xhr?
114 end
115 end
115
116
116 def edit
117 def edit
117 render_404 and return if @time_entry && @time_entry.user != User.current
118 render_404 and return if @time_entry && @time_entry.user != User.current
118 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
119 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
119 @time_entry.attributes = params[:time_entry]
120 @time_entry.attributes = params[:time_entry]
120 if request.post? and @time_entry.save
121 if request.post? and @time_entry.save
121 flash[:notice] = l(:notice_successful_update)
122 flash[:notice] = l(:notice_successful_update)
122 redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
123 redirect_to :action => 'details', :project_id => @time_entry.project, :issue_id => @time_entry.issue
123 return
124 return
124 end
125 end
125 @activities = Enumeration::get_values('ACTI')
126 @activities = Enumeration::get_values('ACTI')
126 end
127 end
127
128
128 private
129 private
129 def find_project
130 def find_project
130 if params[:id]
131 if params[:id]
131 @time_entry = TimeEntry.find(params[:id])
132 @time_entry = TimeEntry.find(params[:id])
132 @project = @time_entry.project
133 @project = @time_entry.project
133 elsif params[:issue_id]
134 elsif params[:issue_id]
134 @issue = Issue.find(params[:issue_id])
135 @issue = Issue.find(params[:issue_id])
135 @project = @issue.project
136 @project = @issue.project
136 elsif params[:project_id]
137 elsif params[:project_id]
137 @project = Project.find(params[:project_id])
138 @project = Project.find(params[:project_id])
138 else
139 else
139 render_404
140 render_404
140 return false
141 return false
141 end
142 end
142 end
143 end
143
144
144 def send_csv
145 def send_csv
145 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
146 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
146 export = StringIO.new
147 export = StringIO.new
147 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
148 CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
148 # csv header fields
149 # csv header fields
149 headers = [l(:field_spent_on),
150 headers = [l(:field_spent_on),
150 l(:field_user),
151 l(:field_user),
151 l(:field_activity),
152 l(:field_activity),
152 l(:field_issue),
153 l(:field_issue),
153 l(:field_hours),
154 l(:field_hours),
154 l(:field_comments)
155 l(:field_comments)
155 ]
156 ]
156 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
157 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
157 # csv lines
158 # csv lines
158 @entries.each do |entry|
159 @entries.each do |entry|
159 fields = [l_date(entry.spent_on),
160 fields = [l_date(entry.spent_on),
160 entry.user.name,
161 entry.user.name,
161 entry.activity.name,
162 entry.activity.name,
162 (entry.issue ? entry.issue.id : nil),
163 (entry.issue ? entry.issue.id : nil),
163 entry.hours,
164 entry.hours,
164 entry.comments
165 entry.comments
165 ]
166 ]
166 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
167 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
167 end
168 end
168 end
169 end
169 export.rewind
170 export.rewind
170 send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
171 send_data(export.read, :type => 'text/csv; header=present', :filename => 'export.csv')
171 end
172 end
172 end
173 end
@@ -1,71 +1,72
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 VersionsController < ApplicationController
18 class VersionsController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :roadmap
20 before_filter :find_project, :authorize
21 before_filter :find_project, :authorize
21
22
22 cache_sweeper :version_sweeper, :only => [ :edit, :destroy ]
23 cache_sweeper :version_sweeper, :only => [ :edit, :destroy ]
23
24
24 def show
25 def show
25 end
26 end
26
27
27 def edit
28 def edit
28 if request.post? and @version.update_attributes(params[:version])
29 if request.post? and @version.update_attributes(params[:version])
29 flash[:notice] = l(:notice_successful_update)
30 flash[:notice] = l(:notice_successful_update)
30 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
31 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
31 end
32 end
32 end
33 end
33
34
34 def destroy
35 def destroy
35 @version.destroy
36 @version.destroy
36 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
37 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
37 rescue
38 rescue
38 flash[:error] = "Unable to delete version"
39 flash[:error] = "Unable to delete version"
39 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
40 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
40 end
41 end
41
42
42 def download
43 def download
43 @attachment = @version.attachments.find(params[:attachment_id])
44 @attachment = @version.attachments.find(params[:attachment_id])
44 @attachment.increment_download
45 @attachment.increment_download
45 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
46 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
46 :type => @attachment.content_type
47 :type => @attachment.content_type
47 rescue
48 rescue
48 render_404
49 render_404
49 end
50 end
50
51
51 def destroy_file
52 def destroy_file
52 @version.attachments.find(params[:attachment_id]).destroy
53 @version.attachments.find(params[:attachment_id]).destroy
53 flash[:notice] = l(:notice_successful_delete)
54 flash[:notice] = l(:notice_successful_delete)
54 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
55 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
55 end
56 end
56
57
57 def status_by
58 def status_by
58 respond_to do |format|
59 respond_to do |format|
59 format.html { render :action => 'show' }
60 format.html { render :action => 'show' }
60 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
61 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
61 end
62 end
62 end
63 end
63
64
64 private
65 private
65 def find_project
66 def find_project
66 @version = Version.find(params[:id])
67 @version = Version.find(params[:id])
67 @project = @version.project
68 @project = @version.project
68 rescue ActiveRecord::RecordNotFound
69 rescue ActiveRecord::RecordNotFound
69 render_404
70 render_404
70 end
71 end
71 end
72 end
@@ -1,44 +1,45
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 WikisController < ApplicationController
18 class WikisController < ApplicationController
19 layout 'base'
19 layout 'base'
20 menu_item :settings
20 before_filter :find_project, :authorize
21 before_filter :find_project, :authorize
21
22
22 # Create or update a project's wiki
23 # Create or update a project's wiki
23 def edit
24 def edit
24 @wiki = @project.wiki || Wiki.new(:project => @project)
25 @wiki = @project.wiki || Wiki.new(:project => @project)
25 @wiki.attributes = params[:wiki]
26 @wiki.attributes = params[:wiki]
26 @wiki.save if request.post?
27 @wiki.save if request.post?
27 render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
28 render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
28 end
29 end
29
30
30 # Delete a project's wiki
31 # Delete a project's wiki
31 def destroy
32 def destroy
32 if request.post? && params[:confirm] && @project.wiki
33 if request.post? && params[:confirm] && @project.wiki
33 @project.wiki.destroy
34 @project.wiki.destroy
34 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
35 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
35 end
36 end
36 end
37 end
37
38
38 private
39 private
39 def find_project
40 def find_project
40 @project = Project.find(params[:id])
41 @project = Project.find(params[:id])
41 rescue ActiveRecord::RecordNotFound
42 rescue ActiveRecord::RecordNotFound
42 render_404
43 render_404
43 end
44 end
44 end
45 end
@@ -1,81 +1,75
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
3 <head>
4 <title><%=h html_title %></title>
4 <title><%=h html_title %></title>
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 <meta name="keywords" content="issue,bug,tracker" />
7 <meta name="keywords" content="issue,bug,tracker" />
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 <%= javascript_include_tag :defaults %>
9 <%= javascript_include_tag :defaults %>
10 <%= stylesheet_link_tag 'jstoolbar' %>
10 <%= stylesheet_link_tag 'jstoolbar' %>
11 <!--[if IE]>
11 <!--[if IE]>
12 <style type="text/css">
12 <style type="text/css">
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
15 </style>
15 </style>
16 <![endif]-->
16 <![endif]-->
17
17
18 <!-- page specific tags --><%= yield :header_tags %>
18 <!-- page specific tags --><%= yield :header_tags %>
19 </head>
19 </head>
20 <body>
20 <body>
21 <div id="wrapper">
21 <div id="wrapper">
22 <div id="top-menu">
22 <div id="top-menu">
23 <div id="account">
23 <div id="account">
24 <% if User.current.logged? %>
24 <% if User.current.logged? %>
25 <%=l(:label_logged_as)%> <%= User.current.login %> -
25 <%=l(:label_logged_as)%> <%= User.current.login %> -
26 <%= link_to l(:label_my_account), { :controller => 'my', :action => 'account' }, :class => 'myaccount' %>
26 <%= link_to l(:label_my_account), { :controller => 'my', :action => 'account' }, :class => 'myaccount' %>
27 <%= link_to_signout %>
27 <%= link_to_signout %>
28 <% else %>
28 <% else %>
29 <%= link_to_signin %>
29 <%= link_to_signin %>
30 <%= link_to(l(:label_register), { :controller => 'account',:action => 'register' }, :class => 'register') if Setting.self_registration? %>
30 <%= link_to(l(:label_register), { :controller => 'account',:action => 'register' }, :class => 'register') if Setting.self_registration? %>
31 <% end %>
31 <% end %>
32 </div>
32 </div>
33 <%= link_to l(:label_home), home_url, :class => 'home' %>
33 <%= link_to l(:label_home), home_url, :class => 'home' %>
34 <%= link_to l(:label_my_page), { :controller => 'my', :action => 'page'}, :class => 'mypage' if User.current.logged? %>
34 <%= link_to l(:label_my_page), { :controller => 'my', :action => 'page'}, :class => 'mypage' if User.current.logged? %>
35 <%= link_to l(:label_project_plural), { :controller => 'projects' }, :class => 'projects' %>
35 <%= link_to l(:label_project_plural), { :controller => 'projects' }, :class => 'projects' %>
36 <%= link_to l(:label_administration), { :controller => 'admin' }, :class => 'admin' if User.current.admin? %>
36 <%= link_to l(:label_administration), { :controller => 'admin' }, :class => 'admin' if User.current.admin? %>
37 <%= link_to l(:label_help), Redmine::Info.help_url, :class => 'help' %>
37 <%= link_to l(:label_help), Redmine::Info.help_url, :class => 'help' %>
38 </div>
38 </div>
39
39
40 <div id="header">
40 <div id="header">
41 <div id="quick-search">
41 <div id="quick-search">
42 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
42 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
43 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
43 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
44 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
45 <% end %>
45 <% end %>
46 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
46 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
47 </div>
47 </div>
48
48
49 <h1><%= h(@project ? @project.name : Setting.app_title) %></h1>
49 <h1><%= h(@project ? @project.name : Setting.app_title) %></h1>
50
50
51 <div id="main-menu">
51 <div id="main-menu">
52 <ul>
52 <%= render_main_menu(@project) %>
53 <% Redmine::MenuManager.allowed_items(:project_menu, User.current, @project).each do |item| %>
54 <% unless item.condition && !item.condition.call(@project) %>
55 <li><%= link_to l(item.name), {item.param => @project}.merge(item.url) %></li>
56 <% end %>
57 <% end if @project && !@project.new_record? %>
58 </ul>
59 </div>
53 </div>
60 </div>
54 </div>
61
55
62 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
56 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
63 <div id="sidebar">
57 <div id="sidebar">
64 <%= yield :sidebar %>
58 <%= yield :sidebar %>
65 </div>
59 </div>
66
60
67 <div id="content">
61 <div id="content">
68 <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %>
62 <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %>
69 <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %>
63 <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %>
70 <%= yield %>
64 <%= yield %>
71 </div>
65 </div>
72 </div>
66 </div>
73
67
74 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
68 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
75
69
76 <div id="footer">
70 <div id="footer">
77 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> <%= Redmine::VERSION %> &copy; 2006-2007 Jean-Philippe Lang
71 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> <%= Redmine::VERSION %> &copy; 2006-2007 Jean-Philippe Lang
78 </div>
72 </div>
79 </div>
73 </div>
80 </body>
74 </body>
81 </html>
75 </html>
@@ -1,105 +1,108
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 Bazaar )
13 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar )
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 :versions => [:show, :status_by],
30 :versions => [:show, :status_by],
31 :queries => :index,
31 :queries => :index,
32 :reports => :issue_report}, :public => true
32 :reports => :issue_report}, :public => true
33 map.permission :add_issues, {:projects => :add_issue}
33 map.permission :add_issues, {:projects => :add_issue}
34 map.permission :edit_issues, {:projects => :bulk_edit_issues,
34 map.permission :edit_issues, {:projects => :bulk_edit_issues,
35 :issues => [:edit, :update, :destroy_attachment]}
35 :issues => [:edit, :update, :destroy_attachment]}
36 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
36 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
37 map.permission :add_issue_notes, {:issues => :update}
37 map.permission :add_issue_notes, {:issues => :update}
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, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
60 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
61 map.permission :view_documents, :documents => [:index, :show, :download]
61 map.permission :view_documents, :documents => [:index, :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, :annotate, :special]
73 map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :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, :annotate, :changes, :diff, :stats, :graph]
79 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :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
87 map.permission :edit_messages, {:messages => :edit}, :require => :member
88 map.permission :delete_messages, {:messages => :destroy}, :require => :member
88 map.permission :delete_messages, {:messages => :destroy}, :require => :member
89 end
89 end
90 end
90 end
91
91
92 # Project menu configuration
92 # Project menu configuration
93 Redmine::MenuManager.map :project_menu do |menu|
93 Redmine::MenuManager.map :project_menu do |menu|
94 menu.push :label_overview, :controller => 'projects', :action => 'show'
94 menu.push :overview, { :controller => 'projects', :action => 'show' }, :caption => :label_overview
95 menu.push :label_activity, :controller => 'projects', :action => 'activity'
95 menu.push :activity, { :controller => 'projects', :action => 'activity' }, :caption => :label_activity
96 menu.push :label_roadmap, :controller => 'projects', :action => 'roadmap'
96 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' }, :caption => :label_roadmap
97 menu.push :label_issue_plural, { :controller => 'issues', :action => 'index' }, :param => :project_id
97 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
98 menu.push :label_news_plural, { :controller => 'news', :action => 'index' }, :param => :project_id
98 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
99 menu.push :label_document_plural, { :controller => 'documents', :action => 'index' }, :param => :project_id
99 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
100 menu.push :label_wiki, { :controller => 'wiki', :action => 'index', :page => nil }, :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
100 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
101 menu.push :label_board_plural, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id, :if => Proc.new { |p| p.boards.any? }
101 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }, :caption => :label_wiki
102 menu.push :label_attachment_plural, :controller => 'projects', :action => 'list_files'
102 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
103 menu.push :label_repository, { :controller => 'repositories', :action => 'show' }, :if => Proc.new { |p| p.repository && !p.repository.new_record? }
103 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
104 menu.push :label_settings, :controller => 'projects', :action => 'settings'
104 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
105 menu.push :repository, { :controller => 'repositories', :action => 'show' },
106 :if => Proc.new { |p| p.repository && !p.repository.new_record? }, :caption => :label_repository
107 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :caption => :label_settings
105 end
108 end
@@ -1,61 +1,119
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 Redmine
18 module Redmine
19 module MenuManager
19 module MenuManager
20 module MenuController
21 def self.included(base)
22 base.extend(ClassMethods)
23 end
24
25 module ClassMethods
26 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
27 mattr_accessor :menu_items
28
29 # Set the menu item name for a controller or specific actions
30 # Examples:
31 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
32 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
33 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
34 #
35 # The default menu item name for a controller is controller_name by default
36 # Eg. the default menu item name for ProjectsController is :projects
37 def menu_item(id, options = {})
38 if actions = options[:only]
39 actions = [] << actions unless actions.is_a?(Array)
40 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
41 else
42 menu_items[controller_name.to_sym][:default] = id
43 end
44 end
45 end
46
47 def menu_items
48 self.class.menu_items
49 end
50
51 # Returns the menu item name according to the current action
52 def current_menu_item
53 menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
54 menu_items[controller_name.to_sym][:default]
55 end
56 end
57
58 module MenuHelper
59 # Returns the current menu item name
60 def current_menu_item
61 @controller.current_menu_item
62 end
63
64 # Renders the application main menu as a ul element
65 def render_main_menu(project)
66 links = []
67 Redmine::MenuManager.allowed_items(:project_menu, User.current, project).each do |item|
68 unless item.condition && !item.condition.call(project)
69 links << content_tag('li',
70 link_to(l(item.caption), {item.param => project}.merge(item.url),
71 :class => (current_menu_item == item.name ? 'selected' : nil)))
72 end
73 end if project && !project.new_record?
74 links.empty? ? nil : content_tag('ul', links.join("\n"))
75 end
76 end
20
77
21 class << self
78 class << self
22 def map(menu_name)
79 def map(menu_name)
23 mapper = Mapper.new
80 mapper = Mapper.new
24 yield mapper
81 yield mapper
25 @items ||= {}
82 @items ||= {}
26 @items[menu_name.to_sym] ||= []
83 @items[menu_name.to_sym] ||= []
27 @items[menu_name.to_sym] += mapper.items
84 @items[menu_name.to_sym] += mapper.items
28 end
85 end
29
86
30 def items(menu_name)
87 def items(menu_name)
31 @items[menu_name.to_sym] || []
88 @items[menu_name.to_sym] || []
32 end
89 end
33
90
34 def allowed_items(menu_name, user, project)
91 def allowed_items(menu_name, user, project)
35 items(menu_name).select {|item| user && user.allowed_to?(item.url, project)}
92 items(menu_name).select {|item| user && user.allowed_to?(item.url, project)}
36 end
93 end
37 end
94 end
38
95
39 class Mapper
96 class Mapper
40 def push(name, url, options={})
97 def push(name, url, options={})
41 @items ||= []
98 @items ||= []
42 @items << MenuItem.new(name, url, options)
99 @items << MenuItem.new(name, url, options)
43 end
100 end
44
101
45 def items
102 def items
46 @items
103 @items
47 end
104 end
48 end
105 end
49
106
50 class MenuItem
107 class MenuItem
51 attr_reader :name, :url, :param, :condition
108 attr_reader :name, :url, :param, :condition, :caption
52
109
53 def initialize(name, url, options)
110 def initialize(name, url, options)
54 @name = name
111 @name = name
55 @url = url
112 @url = url
56 @condition = options[:if]
113 @condition = options[:if]
57 @param = options[:param] || :id
114 @param = options[:param] || :id
115 @caption = options[:caption] || name.to_s.humanize
58 end
116 end
59 end
117 end
60 end
118 end
61 end
119 end
@@ -1,520 +1,520
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 #wrapper {background: white;}
10 #wrapper {background: white;}
11
11
12 #top-menu {background: #2C4056;color: #fff;height:1.5em; padding: 2px 6px 0px 6px;}
12 #top-menu {background: #2C4056;color: #fff;height:1.5em; padding: 2px 6px 0px 6px;}
13 #top-menu a {color: #fff; padding-right: 4px;}
13 #top-menu a {color: #fff; padding-right: 4px;}
14 #account {float:right;}
14 #account {float:right;}
15
15
16 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
16 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
17 #header a {color:#f8f8f8;}
17 #header a {color:#f8f8f8;}
18 #quick-search {float:right;}
18 #quick-search {float:right;}
19
19
20 #main-menu {position: absolute; bottom: 0px; left:6px;}
20 #main-menu {position: absolute; bottom: 0px; left:6px;}
21 #main-menu ul {margin: 0; padding: 0;}
21 #main-menu ul {margin: 0; padding: 0;}
22 #main-menu li {
22 #main-menu li {
23 float:left;
23 float:left;
24 list-style-type:none;
24 list-style-type:none;
25 margin: 0px 10px 0px 0px;
25 margin: 0px 10px 0px 0px;
26 padding: 0px 0px 0px 0px;
26 padding: 0px 0px 0px 0px;
27 white-space:nowrap;
27 white-space:nowrap;
28 }
28 }
29 #main-menu li a {
29 #main-menu li a {
30 display: block;
30 display: block;
31 color: #fff;
31 color: #fff;
32 text-decoration: none;
32 text-decoration: none;
33 margin: 0;
33 margin: 0;
34 padding: 4px 4px 4px 4px;
34 padding: 4px 4px 4px 4px;
35 background: #2C4056;
35 background: #2C4056;
36 }
36 }
37 #main-menu li a:hover {background:#759FCF;}
37 #main-menu li a:hover, #main-menu li a.selected {background:#759FCF;}
38
38
39 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
39 #main {background: url(../images/mainbg.png) repeat-x; background-color:#EEEEEE;}
40
40
41 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
41 #sidebar{ float: right; width: 17%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;}
42 * html #sidebar{ width: 17%; }
42 * html #sidebar{ width: 17%; }
43 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
43 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
44 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
44 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
45 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
45 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
46
46
47 #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;}
47 #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;}
48 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
48 * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
49 html>body #content {
49 html>body #content {
50 height: auto;
50 height: auto;
51 min-height: 600px;
51 min-height: 600px;
52 }
52 }
53
53
54 #main.nosidebar #sidebar{ display: none; }
54 #main.nosidebar #sidebar{ display: none; }
55 #main.nosidebar #content{ width: auto; border-right: 0; }
55 #main.nosidebar #content{ width: auto; border-right: 0; }
56
56
57 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
57 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
58
58
59 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
59 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
60 #login-form table td {padding: 6px;}
60 #login-form table td {padding: 6px;}
61 #login-form label {font-weight: bold;}
61 #login-form label {font-weight: bold;}
62
62
63 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
63 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
64
64
65 /***** Links *****/
65 /***** Links *****/
66 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
66 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
67 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
67 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
68 a img{ border: 0; }
68 a img{ border: 0; }
69
69
70 /***** Tables *****/
70 /***** Tables *****/
71 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
71 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
72 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
72 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
73 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
73 table.list td { overflow: hidden; text-overflow: ellipsis; vertical-align: top;}
74 table.list td.id { width: 2%; text-align: center;}
74 table.list td.id { width: 2%; text-align: center;}
75 table.list td.checkbox { width: 15px; padding: 0px;}
75 table.list td.checkbox { width: 15px; padding: 0px;}
76
76
77 tr.issue { text-align: center; white-space: nowrap; }
77 tr.issue { text-align: center; white-space: nowrap; }
78 tr.issue td.subject, tr.issue td.category { white-space: normal; }
78 tr.issue td.subject, tr.issue td.category { white-space: normal; }
79 tr.issue td.subject { text-align: left; }
79 tr.issue td.subject { text-align: left; }
80 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
80 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
81
81
82 tr.entry { border: 1px solid #f8f8f8; }
82 tr.entry { border: 1px solid #f8f8f8; }
83 tr.entry td { white-space: nowrap; }
83 tr.entry td { white-space: nowrap; }
84 tr.entry td.filename { width: 30%; }
84 tr.entry td.filename { width: 30%; }
85 tr.entry td.size { text-align: right; font-size: 90%; }
85 tr.entry td.size { text-align: right; font-size: 90%; }
86 tr.entry td.revision, tr.entry td.author { text-align: center; }
86 tr.entry td.revision, tr.entry td.author { text-align: center; }
87 tr.entry td.age { text-align: right; }
87 tr.entry td.age { text-align: right; }
88
88
89 tr.changeset td.author { text-align: center; width: 15%; }
89 tr.changeset td.author { text-align: center; width: 15%; }
90 tr.changeset td.committed_on { text-align: center; width: 15%; }
90 tr.changeset td.committed_on { text-align: center; width: 15%; }
91
91
92 tr.message { height: 2.6em; }
92 tr.message { height: 2.6em; }
93 tr.message td.last_message { font-size: 80%; }
93 tr.message td.last_message { font-size: 80%; }
94 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
94 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
95 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
95 tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
96
96
97 table.list tbody tr:hover { background-color:#ffffdd; }
97 table.list tbody tr:hover { background-color:#ffffdd; }
98 table td {padding:2px;}
98 table td {padding:2px;}
99 table p {margin:0;}
99 table p {margin:0;}
100 .odd {background-color:#f6f7f8;}
100 .odd {background-color:#f6f7f8;}
101 .even {background-color: #fff;}
101 .even {background-color: #fff;}
102
102
103 .highlight { background-color: #FCFD8D;}
103 .highlight { background-color: #FCFD8D;}
104 .highlight.token-1 { background-color: #faa;}
104 .highlight.token-1 { background-color: #faa;}
105 .highlight.token-2 { background-color: #afa;}
105 .highlight.token-2 { background-color: #afa;}
106 .highlight.token-3 { background-color: #aaf;}
106 .highlight.token-3 { background-color: #aaf;}
107
107
108 .box{
108 .box{
109 padding:6px;
109 padding:6px;
110 margin-bottom: 10px;
110 margin-bottom: 10px;
111 background-color:#f6f6f6;
111 background-color:#f6f6f6;
112 color:#505050;
112 color:#505050;
113 line-height:1.5em;
113 line-height:1.5em;
114 border: 1px solid #e4e4e4;
114 border: 1px solid #e4e4e4;
115 }
115 }
116
116
117 div.square {
117 div.square {
118 border: 1px solid #999;
118 border: 1px solid #999;
119 float: left;
119 float: left;
120 margin: .3em .4em 0 .4em;
120 margin: .3em .4em 0 .4em;
121 overflow: hidden;
121 overflow: hidden;
122 width: .6em; height: .6em;
122 width: .6em; height: .6em;
123 }
123 }
124
124
125 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;}
125 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px;font-size:0.9em;}
126 .contextual input {font-size:0.9em;}
126 .contextual input {font-size:0.9em;}
127
127
128 .splitcontentleft{float:left; width:49%;}
128 .splitcontentleft{float:left; width:49%;}
129 .splitcontentright{float:right; width:49%;}
129 .splitcontentright{float:right; width:49%;}
130 form {display: inline;}
130 form {display: inline;}
131 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
131 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
132 fieldset {border: 1px solid #e4e4e4; margin:0;}
132 fieldset {border: 1px solid #e4e4e4; margin:0;}
133 legend {color: #484848;}
133 legend {color: #484848;}
134 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
134 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
135 textarea.wiki-edit { width: 99%; }
135 textarea.wiki-edit { width: 99%; }
136 li p {margin-top: 0;}
136 li p {margin-top: 0;}
137 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
137 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
138 .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;}
138 .autoscroll {overflow-x: auto; padding:1px; width:100%; margin-bottom: 1.2em;}
139 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
139 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
140
140
141 .pagination {font-size: 90%}
141 .pagination {font-size: 90%}
142 p.pagination {margin-top:8px;}
142 p.pagination {margin-top:8px;}
143
143
144 /***** Tabular forms ******/
144 /***** Tabular forms ******/
145 .tabular p{
145 .tabular p{
146 margin: 0;
146 margin: 0;
147 padding: 5px 0 8px 0;
147 padding: 5px 0 8px 0;
148 padding-left: 180px; /*width of left column containing the label elements*/
148 padding-left: 180px; /*width of left column containing the label elements*/
149 height: 1%;
149 height: 1%;
150 clear:left;
150 clear:left;
151 }
151 }
152
152
153 .tabular label{
153 .tabular label{
154 font-weight: bold;
154 font-weight: bold;
155 float: left;
155 float: left;
156 text-align: right;
156 text-align: right;
157 margin-left: -180px; /*width of left column*/
157 margin-left: -180px; /*width of left column*/
158 width: 175px; /*width of labels. Should be smaller than left column to create some right
158 width: 175px; /*width of labels. Should be smaller than left column to create some right
159 margin*/
159 margin*/
160 }
160 }
161
161
162 .tabular label.floating{
162 .tabular label.floating{
163 font-weight: normal;
163 font-weight: normal;
164 margin-left: 0px;
164 margin-left: 0px;
165 text-align: left;
165 text-align: left;
166 width: 200px;
166 width: 200px;
167 }
167 }
168
168
169 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
169 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
170
170
171 .tabular.settings p{ padding-left: 300px; }
171 .tabular.settings p{ padding-left: 300px; }
172 .tabular.settings label{ margin-left: -300px; width: 295px; }
172 .tabular.settings label{ margin-left: -300px; width: 295px; }
173
173
174 .required {color: #bb0000;}
174 .required {color: #bb0000;}
175 .summary {font-style: italic;}
175 .summary {font-style: italic;}
176
176
177 div.attachments p { margin:4px 0 2px 0; }
177 div.attachments p { margin:4px 0 2px 0; }
178
178
179 /***** Flash & error messages ****/
179 /***** Flash & error messages ****/
180 #errorExplanation, div.flash, .nodata {
180 #errorExplanation, div.flash, .nodata {
181 padding: 4px 4px 4px 30px;
181 padding: 4px 4px 4px 30px;
182 margin-bottom: 12px;
182 margin-bottom: 12px;
183 font-size: 1.1em;
183 font-size: 1.1em;
184 border: 2px solid;
184 border: 2px solid;
185 }
185 }
186
186
187 div.flash {margin-top: 8px;}
187 div.flash {margin-top: 8px;}
188
188
189 div.flash.error, #errorExplanation {
189 div.flash.error, #errorExplanation {
190 background: url(../images/false.png) 8px 5px no-repeat;
190 background: url(../images/false.png) 8px 5px no-repeat;
191 background-color: #ffe3e3;
191 background-color: #ffe3e3;
192 border-color: #dd0000;
192 border-color: #dd0000;
193 color: #550000;
193 color: #550000;
194 }
194 }
195
195
196 div.flash.notice {
196 div.flash.notice {
197 background: url(../images/true.png) 8px 5px no-repeat;
197 background: url(../images/true.png) 8px 5px no-repeat;
198 background-color: #dfffdf;
198 background-color: #dfffdf;
199 border-color: #9fcf9f;
199 border-color: #9fcf9f;
200 color: #005f00;
200 color: #005f00;
201 }
201 }
202
202
203 .nodata {
203 .nodata {
204 text-align: center;
204 text-align: center;
205 background-color: #FFEBC1;
205 background-color: #FFEBC1;
206 border-color: #FDBF3B;
206 border-color: #FDBF3B;
207 color: #A6750C;
207 color: #A6750C;
208 }
208 }
209
209
210 #errorExplanation ul { font-size: 0.9em;}
210 #errorExplanation ul { font-size: 0.9em;}
211
211
212 /***** Ajax indicator ******/
212 /***** Ajax indicator ******/
213 #ajax-indicator {
213 #ajax-indicator {
214 position: absolute; /* fixed not supported by IE */
214 position: absolute; /* fixed not supported by IE */
215 background-color:#eee;
215 background-color:#eee;
216 border: 1px solid #bbb;
216 border: 1px solid #bbb;
217 top:35%;
217 top:35%;
218 left:40%;
218 left:40%;
219 width:20%;
219 width:20%;
220 font-weight:bold;
220 font-weight:bold;
221 text-align:center;
221 text-align:center;
222 padding:0.6em;
222 padding:0.6em;
223 z-index:100;
223 z-index:100;
224 filter:alpha(opacity=50);
224 filter:alpha(opacity=50);
225 -moz-opacity:0.5;
225 -moz-opacity:0.5;
226 opacity: 0.5;
226 opacity: 0.5;
227 -khtml-opacity: 0.5;
227 -khtml-opacity: 0.5;
228 }
228 }
229
229
230 html>body #ajax-indicator { position: fixed; }
230 html>body #ajax-indicator { position: fixed; }
231
231
232 #ajax-indicator span {
232 #ajax-indicator span {
233 background-position: 0% 40%;
233 background-position: 0% 40%;
234 background-repeat: no-repeat;
234 background-repeat: no-repeat;
235 background-image: url(../images/loading.gif);
235 background-image: url(../images/loading.gif);
236 padding-left: 26px;
236 padding-left: 26px;
237 vertical-align: bottom;
237 vertical-align: bottom;
238 }
238 }
239
239
240 /***** Calendar *****/
240 /***** Calendar *****/
241 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
241 table.cal {border-collapse: collapse; width: 100%; margin: 8px 0 6px 0;border: 1px solid #d7d7d7;}
242 table.cal thead th {width: 14%;}
242 table.cal thead th {width: 14%;}
243 table.cal tbody tr {height: 100px;}
243 table.cal tbody tr {height: 100px;}
244 table.cal th { background-color:#EEEEEE; padding: 4px; }
244 table.cal th { background-color:#EEEEEE; padding: 4px; }
245 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
245 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
246 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
246 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
247 table.cal td.odd p.day-num {color: #bbb;}
247 table.cal td.odd p.day-num {color: #bbb;}
248 table.cal td.today {background:#ffffdd;}
248 table.cal td.today {background:#ffffdd;}
249 table.cal td.today p.day-num {font-weight: bold;}
249 table.cal td.today p.day-num {font-weight: bold;}
250
250
251 /***** Tooltips ******/
251 /***** Tooltips ******/
252 .tooltip{position:relative;z-index:24;}
252 .tooltip{position:relative;z-index:24;}
253 .tooltip:hover{z-index:25;color:#000;}
253 .tooltip:hover{z-index:25;color:#000;}
254 .tooltip span.tip{display: none; text-align:left;}
254 .tooltip span.tip{display: none; text-align:left;}
255
255
256 div.tooltip:hover span.tip{
256 div.tooltip:hover span.tip{
257 display:block;
257 display:block;
258 position:absolute;
258 position:absolute;
259 top:12px; left:24px; width:270px;
259 top:12px; left:24px; width:270px;
260 border:1px solid #555;
260 border:1px solid #555;
261 background-color:#fff;
261 background-color:#fff;
262 padding: 4px;
262 padding: 4px;
263 font-size: 0.8em;
263 font-size: 0.8em;
264 color:#505050;
264 color:#505050;
265 }
265 }
266
266
267 /***** Progress bar *****/
267 /***** Progress bar *****/
268 table.progress {
268 table.progress {
269 border: 1px solid #D7D7D7;
269 border: 1px solid #D7D7D7;
270 border-collapse: collapse;
270 border-collapse: collapse;
271 border-spacing: 0pt;
271 border-spacing: 0pt;
272 empty-cells: show;
272 empty-cells: show;
273 text-align: center;
273 text-align: center;
274 float:left;
274 float:left;
275 margin: 1px 6px 1px 0px;
275 margin: 1px 6px 1px 0px;
276 }
276 }
277
277
278 table.progress td { height: 0.9em; }
278 table.progress td { height: 0.9em; }
279 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
279 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
280 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
280 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
281 table.progress td.open { background: #FFF none repeat scroll 0%; }
281 table.progress td.open { background: #FFF none repeat scroll 0%; }
282 p.pourcent {font-size: 80%;}
282 p.pourcent {font-size: 80%;}
283 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
283 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
284
284
285 div#status_by { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; }
285 div#status_by { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; }
286
286
287 /***** Tabs *****/
287 /***** Tabs *****/
288 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
288 #content .tabs {height: 2.6em; border-bottom: 1px solid #bbbbbb; margin-bottom:1.2em; position:relative;}
289 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
289 #content .tabs ul {margin:0; position:absolute; bottom:-2px; padding-left:1em;}
290 #content .tabs>ul { bottom:-1px; } /* others */
290 #content .tabs>ul { bottom:-1px; } /* others */
291 #content .tabs ul li {
291 #content .tabs ul li {
292 float:left;
292 float:left;
293 list-style-type:none;
293 list-style-type:none;
294 white-space:nowrap;
294 white-space:nowrap;
295 margin-right:8px;
295 margin-right:8px;
296 background:#fff;
296 background:#fff;
297 }
297 }
298 #content .tabs ul li a{
298 #content .tabs ul li a{
299 display:block;
299 display:block;
300 font-size: 0.9em;
300 font-size: 0.9em;
301 text-decoration:none;
301 text-decoration:none;
302 line-height:1.3em;
302 line-height:1.3em;
303 padding:4px 6px 4px 6px;
303 padding:4px 6px 4px 6px;
304 border: 1px solid #ccc;
304 border: 1px solid #ccc;
305 border-bottom: 1px solid #bbbbbb;
305 border-bottom: 1px solid #bbbbbb;
306 background-color: #eeeeee;
306 background-color: #eeeeee;
307 color:#777;
307 color:#777;
308 font-weight:bold;
308 font-weight:bold;
309 }
309 }
310
310
311 #content .tabs ul li a:hover {
311 #content .tabs ul li a:hover {
312 background-color: #ffffdd;
312 background-color: #ffffdd;
313 text-decoration:none;
313 text-decoration:none;
314 }
314 }
315
315
316 #content .tabs ul li a.selected {
316 #content .tabs ul li a.selected {
317 background-color: #fff;
317 background-color: #fff;
318 border: 1px solid #bbbbbb;
318 border: 1px solid #bbbbbb;
319 border-bottom: 1px solid #fff;
319 border-bottom: 1px solid #fff;
320 }
320 }
321
321
322 #content .tabs ul li a.selected:hover {
322 #content .tabs ul li a.selected:hover {
323 background-color: #fff;
323 background-color: #fff;
324 }
324 }
325
325
326 /***** Diff *****/
326 /***** Diff *****/
327 .diff_out { background: #fcc; }
327 .diff_out { background: #fcc; }
328 .diff_in { background: #cfc; }
328 .diff_in { background: #cfc; }
329
329
330 /***** Wiki *****/
330 /***** Wiki *****/
331 div.wiki table {
331 div.wiki table {
332 border: 1px solid #505050;
332 border: 1px solid #505050;
333 border-collapse: collapse;
333 border-collapse: collapse;
334 }
334 }
335
335
336 div.wiki table, div.wiki td, div.wiki th {
336 div.wiki table, div.wiki td, div.wiki th {
337 border: 1px solid #bbb;
337 border: 1px solid #bbb;
338 padding: 4px;
338 padding: 4px;
339 }
339 }
340
340
341 div.wiki .external {
341 div.wiki .external {
342 background-position: 0% 60%;
342 background-position: 0% 60%;
343 background-repeat: no-repeat;
343 background-repeat: no-repeat;
344 padding-left: 12px;
344 padding-left: 12px;
345 background-image: url(../images/external.png);
345 background-image: url(../images/external.png);
346 }
346 }
347
347
348 div.wiki a.new {
348 div.wiki a.new {
349 color: #b73535;
349 color: #b73535;
350 }
350 }
351
351
352 div.wiki pre {
352 div.wiki pre {
353 margin: 1em 1em 1em 1.6em;
353 margin: 1em 1em 1em 1.6em;
354 padding: 2px;
354 padding: 2px;
355 background-color: #fafafa;
355 background-color: #fafafa;
356 border: 1px solid #dadada;
356 border: 1px solid #dadada;
357 width:95%;
357 width:95%;
358 overflow-x: auto;
358 overflow-x: auto;
359 }
359 }
360
360
361 div.wiki div.toc {
361 div.wiki div.toc {
362 background-color: #ffffdd;
362 background-color: #ffffdd;
363 border: 1px solid #e4e4e4;
363 border: 1px solid #e4e4e4;
364 padding: 4px;
364 padding: 4px;
365 line-height: 1.2em;
365 line-height: 1.2em;
366 margin-bottom: 12px;
366 margin-bottom: 12px;
367 margin-right: 12px;
367 margin-right: 12px;
368 display: table
368 display: table
369 }
369 }
370 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
370 * html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
371
371
372 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
372 div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
373 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
373 div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
374
374
375 div.wiki div.toc a {
375 div.wiki div.toc a {
376 display: block;
376 display: block;
377 font-size: 0.9em;
377 font-size: 0.9em;
378 font-weight: normal;
378 font-weight: normal;
379 text-decoration: none;
379 text-decoration: none;
380 color: #606060;
380 color: #606060;
381 }
381 }
382 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
382 div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
383
383
384 div.wiki div.toc a.heading2 { margin-left: 6px; }
384 div.wiki div.toc a.heading2 { margin-left: 6px; }
385 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
385 div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
386
386
387 /***** My page layout *****/
387 /***** My page layout *****/
388 .block-receiver {
388 .block-receiver {
389 border:1px dashed #c0c0c0;
389 border:1px dashed #c0c0c0;
390 margin-bottom: 20px;
390 margin-bottom: 20px;
391 padding: 15px 0 15px 0;
391 padding: 15px 0 15px 0;
392 }
392 }
393
393
394 .mypage-box {
394 .mypage-box {
395 margin:0 0 20px 0;
395 margin:0 0 20px 0;
396 color:#505050;
396 color:#505050;
397 line-height:1.5em;
397 line-height:1.5em;
398 }
398 }
399
399
400 .handle {
400 .handle {
401 cursor: move;
401 cursor: move;
402 }
402 }
403
403
404 a.close-icon {
404 a.close-icon {
405 display:block;
405 display:block;
406 margin-top:3px;
406 margin-top:3px;
407 overflow:hidden;
407 overflow:hidden;
408 width:12px;
408 width:12px;
409 height:12px;
409 height:12px;
410 background-repeat: no-repeat;
410 background-repeat: no-repeat;
411 cursor:pointer;
411 cursor:pointer;
412 background-image:url('../images/close.png');
412 background-image:url('../images/close.png');
413 }
413 }
414
414
415 a.close-icon:hover {
415 a.close-icon:hover {
416 background-image:url('../images/close_hl.png');
416 background-image:url('../images/close_hl.png');
417 }
417 }
418
418
419 /***** Gantt chart *****/
419 /***** Gantt chart *****/
420 .gantt_hdr {
420 .gantt_hdr {
421 position:absolute;
421 position:absolute;
422 top:0;
422 top:0;
423 height:16px;
423 height:16px;
424 border-top: 1px solid #c0c0c0;
424 border-top: 1px solid #c0c0c0;
425 border-bottom: 1px solid #c0c0c0;
425 border-bottom: 1px solid #c0c0c0;
426 border-right: 1px solid #c0c0c0;
426 border-right: 1px solid #c0c0c0;
427 text-align: center;
427 text-align: center;
428 overflow: hidden;
428 overflow: hidden;
429 }
429 }
430
430
431 .task {
431 .task {
432 position: absolute;
432 position: absolute;
433 height:8px;
433 height:8px;
434 font-size:0.8em;
434 font-size:0.8em;
435 color:#888;
435 color:#888;
436 padding:0;
436 padding:0;
437 margin:0;
437 margin:0;
438 line-height:0.8em;
438 line-height:0.8em;
439 }
439 }
440
440
441 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
441 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
442 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
442 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
443 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
443 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
444 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
444 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
445
445
446 /***** Icons *****/
446 /***** Icons *****/
447 .icon {
447 .icon {
448 background-position: 0% 40%;
448 background-position: 0% 40%;
449 background-repeat: no-repeat;
449 background-repeat: no-repeat;
450 padding-left: 20px;
450 padding-left: 20px;
451 padding-top: 2px;
451 padding-top: 2px;
452 padding-bottom: 3px;
452 padding-bottom: 3px;
453 }
453 }
454
454
455 .icon22 {
455 .icon22 {
456 background-position: 0% 40%;
456 background-position: 0% 40%;
457 background-repeat: no-repeat;
457 background-repeat: no-repeat;
458 padding-left: 26px;
458 padding-left: 26px;
459 line-height: 22px;
459 line-height: 22px;
460 vertical-align: middle;
460 vertical-align: middle;
461 }
461 }
462
462
463 .icon-add { background-image: url(../images/add.png); }
463 .icon-add { background-image: url(../images/add.png); }
464 .icon-edit { background-image: url(../images/edit.png); }
464 .icon-edit { background-image: url(../images/edit.png); }
465 .icon-copy { background-image: url(../images/copy.png); }
465 .icon-copy { background-image: url(../images/copy.png); }
466 .icon-del { background-image: url(../images/delete.png); }
466 .icon-del { background-image: url(../images/delete.png); }
467 .icon-move { background-image: url(../images/move.png); }
467 .icon-move { background-image: url(../images/move.png); }
468 .icon-save { background-image: url(../images/save.png); }
468 .icon-save { background-image: url(../images/save.png); }
469 .icon-cancel { background-image: url(../images/cancel.png); }
469 .icon-cancel { background-image: url(../images/cancel.png); }
470 .icon-pdf { background-image: url(../images/pdf.png); }
470 .icon-pdf { background-image: url(../images/pdf.png); }
471 .icon-csv { background-image: url(../images/csv.png); }
471 .icon-csv { background-image: url(../images/csv.png); }
472 .icon-html { background-image: url(../images/html.png); }
472 .icon-html { background-image: url(../images/html.png); }
473 .icon-image { background-image: url(../images/image.png); }
473 .icon-image { background-image: url(../images/image.png); }
474 .icon-txt { background-image: url(../images/txt.png); }
474 .icon-txt { background-image: url(../images/txt.png); }
475 .icon-file { background-image: url(../images/file.png); }
475 .icon-file { background-image: url(../images/file.png); }
476 .icon-folder { background-image: url(../images/folder.png); }
476 .icon-folder { background-image: url(../images/folder.png); }
477 .open .icon-folder { background-image: url(../images/folder_open.png); }
477 .open .icon-folder { background-image: url(../images/folder_open.png); }
478 .icon-package { background-image: url(../images/package.png); }
478 .icon-package { background-image: url(../images/package.png); }
479 .icon-home { background-image: url(../images/home.png); }
479 .icon-home { background-image: url(../images/home.png); }
480 .icon-user { background-image: url(../images/user.png); }
480 .icon-user { background-image: url(../images/user.png); }
481 .icon-mypage { background-image: url(../images/user_page.png); }
481 .icon-mypage { background-image: url(../images/user_page.png); }
482 .icon-admin { background-image: url(../images/admin.png); }
482 .icon-admin { background-image: url(../images/admin.png); }
483 .icon-projects { background-image: url(../images/projects.png); }
483 .icon-projects { background-image: url(../images/projects.png); }
484 .icon-logout { background-image: url(../images/logout.png); }
484 .icon-logout { background-image: url(../images/logout.png); }
485 .icon-help { background-image: url(../images/help.png); }
485 .icon-help { background-image: url(../images/help.png); }
486 .icon-attachment { background-image: url(../images/attachment.png); }
486 .icon-attachment { background-image: url(../images/attachment.png); }
487 .icon-index { background-image: url(../images/index.png); }
487 .icon-index { background-image: url(../images/index.png); }
488 .icon-history { background-image: url(../images/history.png); }
488 .icon-history { background-image: url(../images/history.png); }
489 .icon-feed { background-image: url(../images/feed.png); }
489 .icon-feed { background-image: url(../images/feed.png); }
490 .icon-time { background-image: url(../images/time.png); }
490 .icon-time { background-image: url(../images/time.png); }
491 .icon-stats { background-image: url(../images/stats.png); }
491 .icon-stats { background-image: url(../images/stats.png); }
492 .icon-warning { background-image: url(../images/warning.png); }
492 .icon-warning { background-image: url(../images/warning.png); }
493 .icon-fav { background-image: url(../images/fav.png); }
493 .icon-fav { background-image: url(../images/fav.png); }
494 .icon-fav-off { background-image: url(../images/fav_off.png); }
494 .icon-fav-off { background-image: url(../images/fav_off.png); }
495 .icon-reload { background-image: url(../images/reload.png); }
495 .icon-reload { background-image: url(../images/reload.png); }
496 .icon-lock { background-image: url(../images/locked.png); }
496 .icon-lock { background-image: url(../images/locked.png); }
497 .icon-unlock { background-image: url(../images/unlock.png); }
497 .icon-unlock { background-image: url(../images/unlock.png); }
498 .icon-note { background-image: url(../images/note.png); }
498 .icon-note { background-image: url(../images/note.png); }
499 .icon-checked { background-image: url(../images/true.png); }
499 .icon-checked { background-image: url(../images/true.png); }
500
500
501 .icon22-projects { background-image: url(../images/22x22/projects.png); }
501 .icon22-projects { background-image: url(../images/22x22/projects.png); }
502 .icon22-users { background-image: url(../images/22x22/users.png); }
502 .icon22-users { background-image: url(../images/22x22/users.png); }
503 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
503 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
504 .icon22-role { background-image: url(../images/22x22/role.png); }
504 .icon22-role { background-image: url(../images/22x22/role.png); }
505 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
505 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
506 .icon22-options { background-image: url(../images/22x22/options.png); }
506 .icon22-options { background-image: url(../images/22x22/options.png); }
507 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
507 .icon22-notifications { background-image: url(../images/22x22/notifications.png); }
508 .icon22-authent { background-image: url(../images/22x22/authent.png); }
508 .icon22-authent { background-image: url(../images/22x22/authent.png); }
509 .icon22-info { background-image: url(../images/22x22/info.png); }
509 .icon22-info { background-image: url(../images/22x22/info.png); }
510 .icon22-comment { background-image: url(../images/22x22/comment.png); }
510 .icon22-comment { background-image: url(../images/22x22/comment.png); }
511 .icon22-package { background-image: url(../images/22x22/package.png); }
511 .icon22-package { background-image: url(../images/22x22/package.png); }
512 .icon22-settings { background-image: url(../images/22x22/settings.png); }
512 .icon22-settings { background-image: url(../images/22x22/settings.png); }
513 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
513 .icon22-plugin { background-image: url(../images/22x22/plugin.png); }
514
514
515 /***** Media print specific styles *****/
515 /***** Media print specific styles *****/
516 @media print {
516 @media print {
517 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
517 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual { display:none; }
518 #main { background: #fff; }
518 #main { background: #fff; }
519 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
519 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; }
520 }
520 }
General Comments 0
You need to be logged in to leave comments. Login now