##// END OF EJS Templates
Contextual quick search (#3263)....
Jean-Philippe Lang -
r2829:07aa3c55bdc7
parent child
Show More
@@ -0,0 +1,55
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Search
20 module Controller
21 def self.included(base)
22 base.extend(ClassMethods)
23 end
24
25 module ClassMethods
26 @@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}
27 mattr_accessor :default_search_scopes
28
29 # Set the default search scope for a controller or specific actions
30 # Examples:
31 # * search_scope :issues # => sets the search scope to :issues for the whole controller
32 # * search_scope :issues, :only => :index
33 # * search_scope :issues, :only => [:index, :show]
34 def default_search_scope(id, options = {})
35 if actions = options[:only]
36 actions = [] << actions unless actions.is_a?(Array)
37 actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}
38 else
39 default_search_scopes[controller_name.to_sym][:default] = id.to_s
40 end
41 end
42 end
43
44 def default_search_scopes
45 self.class.default_search_scopes
46 end
47
48 # Returns the default search scope according to the current action
49 def default_search_scope
50 @default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] ||
51 default_search_scopes[controller_name.to_sym][:default]
52 end
53 end
54 end
55 end
@@ -1,251 +1,252
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 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class ApplicationController < ActionController::Base
21 class ApplicationController < ActionController::Base
22 include Redmine::I18n
22 include Redmine::I18n
23
23
24 layout 'base'
24 layout 'base'
25
25
26 before_filter :user_setup, :check_if_login_required, :set_localization
26 before_filter :user_setup, :check_if_login_required, :set_localization
27 filter_parameter_logging :password
27 filter_parameter_logging :password
28
28
29 include Redmine::Search::Controller
29 include Redmine::MenuManager::MenuController
30 include Redmine::MenuManager::MenuController
30 helper Redmine::MenuManager::MenuHelper
31 helper Redmine::MenuManager::MenuHelper
31
32
32 REDMINE_SUPPORTED_SCM.each do |scm|
33 REDMINE_SUPPORTED_SCM.each do |scm|
33 require_dependency "repository/#{scm.underscore}"
34 require_dependency "repository/#{scm.underscore}"
34 end
35 end
35
36
36 def user_setup
37 def user_setup
37 # Check the settings cache for each request
38 # Check the settings cache for each request
38 Setting.check_cache
39 Setting.check_cache
39 # Find the current user
40 # Find the current user
40 User.current = find_current_user
41 User.current = find_current_user
41 end
42 end
42
43
43 # Returns the current user or nil if no user is logged in
44 # Returns the current user or nil if no user is logged in
44 # and starts a session if needed
45 # and starts a session if needed
45 def find_current_user
46 def find_current_user
46 if session[:user_id]
47 if session[:user_id]
47 # existing session
48 # existing session
48 (User.active.find(session[:user_id]) rescue nil)
49 (User.active.find(session[:user_id]) rescue nil)
49 elsif cookies[:autologin] && Setting.autologin?
50 elsif cookies[:autologin] && Setting.autologin?
50 # auto-login feature starts a new session
51 # auto-login feature starts a new session
51 user = User.try_to_autologin(cookies[:autologin])
52 user = User.try_to_autologin(cookies[:autologin])
52 session[:user_id] = user.id if user
53 session[:user_id] = user.id if user
53 user
54 user
54 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
55 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
55 # RSS key authentication does not start a session
56 # RSS key authentication does not start a session
56 User.find_by_rss_key(params[:key])
57 User.find_by_rss_key(params[:key])
57 end
58 end
58 end
59 end
59
60
60 # Sets the logged in user
61 # Sets the logged in user
61 def logged_user=(user)
62 def logged_user=(user)
62 if user && user.is_a?(User)
63 if user && user.is_a?(User)
63 User.current = user
64 User.current = user
64 session[:user_id] = user.id
65 session[:user_id] = user.id
65 else
66 else
66 User.current = User.anonymous
67 User.current = User.anonymous
67 session[:user_id] = nil
68 session[:user_id] = nil
68 end
69 end
69 end
70 end
70
71
71 # check if login is globally required to access the application
72 # check if login is globally required to access the application
72 def check_if_login_required
73 def check_if_login_required
73 # no check needed if user is already logged in
74 # no check needed if user is already logged in
74 return true if User.current.logged?
75 return true if User.current.logged?
75 require_login if Setting.login_required?
76 require_login if Setting.login_required?
76 end
77 end
77
78
78 def set_localization
79 def set_localization
79 lang = nil
80 lang = nil
80 if User.current.logged?
81 if User.current.logged?
81 lang = find_language(User.current.language)
82 lang = find_language(User.current.language)
82 end
83 end
83 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
84 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
84 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
85 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
85 if !accept_lang.blank?
86 if !accept_lang.blank?
86 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
87 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
87 end
88 end
88 end
89 end
89 lang ||= Setting.default_language
90 lang ||= Setting.default_language
90 set_language_if_valid(lang)
91 set_language_if_valid(lang)
91 end
92 end
92
93
93 def require_login
94 def require_login
94 if !User.current.logged?
95 if !User.current.logged?
95 redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
96 redirect_to :controller => "account", :action => "login", :back_url => url_for(params)
96 return false
97 return false
97 end
98 end
98 true
99 true
99 end
100 end
100
101
101 def require_admin
102 def require_admin
102 return unless require_login
103 return unless require_login
103 if !User.current.admin?
104 if !User.current.admin?
104 render_403
105 render_403
105 return false
106 return false
106 end
107 end
107 true
108 true
108 end
109 end
109
110
110 def deny_access
111 def deny_access
111 User.current.logged? ? render_403 : require_login
112 User.current.logged? ? render_403 : require_login
112 end
113 end
113
114
114 # Authorize the user for the requested action
115 # Authorize the user for the requested action
115 def authorize(ctrl = params[:controller], action = params[:action], global = false)
116 def authorize(ctrl = params[:controller], action = params[:action], global = false)
116 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
117 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
117 allowed ? true : deny_access
118 allowed ? true : deny_access
118 end
119 end
119
120
120 # Authorize the user for the requested action outside a project
121 # Authorize the user for the requested action outside a project
121 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
122 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
122 authorize(ctrl, action, global)
123 authorize(ctrl, action, global)
123 end
124 end
124
125
125 # make sure that the user is a member of the project (or admin) if project is private
126 # make sure that the user is a member of the project (or admin) if project is private
126 # used as a before_filter for actions that do not require any particular permission on the project
127 # used as a before_filter for actions that do not require any particular permission on the project
127 def check_project_privacy
128 def check_project_privacy
128 if @project && @project.active?
129 if @project && @project.active?
129 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
130 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
130 true
131 true
131 else
132 else
132 User.current.logged? ? render_403 : require_login
133 User.current.logged? ? render_403 : require_login
133 end
134 end
134 else
135 else
135 @project = nil
136 @project = nil
136 render_404
137 render_404
137 false
138 false
138 end
139 end
139 end
140 end
140
141
141 def redirect_back_or_default(default)
142 def redirect_back_or_default(default)
142 back_url = CGI.unescape(params[:back_url].to_s)
143 back_url = CGI.unescape(params[:back_url].to_s)
143 if !back_url.blank?
144 if !back_url.blank?
144 begin
145 begin
145 uri = URI.parse(back_url)
146 uri = URI.parse(back_url)
146 # do not redirect user to another host or to the login or register page
147 # do not redirect user to another host or to the login or register page
147 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
148 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
148 redirect_to(back_url) and return
149 redirect_to(back_url) and return
149 end
150 end
150 rescue URI::InvalidURIError
151 rescue URI::InvalidURIError
151 # redirect to default
152 # redirect to default
152 end
153 end
153 end
154 end
154 redirect_to default
155 redirect_to default
155 end
156 end
156
157
157 def render_403
158 def render_403
158 @project = nil
159 @project = nil
159 render :template => "common/403", :layout => !request.xhr?, :status => 403
160 render :template => "common/403", :layout => !request.xhr?, :status => 403
160 return false
161 return false
161 end
162 end
162
163
163 def render_404
164 def render_404
164 render :template => "common/404", :layout => !request.xhr?, :status => 404
165 render :template => "common/404", :layout => !request.xhr?, :status => 404
165 return false
166 return false
166 end
167 end
167
168
168 def render_error(msg)
169 def render_error(msg)
169 flash.now[:error] = msg
170 flash.now[:error] = msg
170 render :text => '', :layout => !request.xhr?, :status => 500
171 render :text => '', :layout => !request.xhr?, :status => 500
171 end
172 end
172
173
173 def render_feed(items, options={})
174 def render_feed(items, options={})
174 @items = items || []
175 @items = items || []
175 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
176 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
176 @items = @items.slice(0, Setting.feeds_limit.to_i)
177 @items = @items.slice(0, Setting.feeds_limit.to_i)
177 @title = options[:title] || Setting.app_title
178 @title = options[:title] || Setting.app_title
178 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
179 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
179 end
180 end
180
181
181 def self.accept_key_auth(*actions)
182 def self.accept_key_auth(*actions)
182 actions = actions.flatten.map(&:to_s)
183 actions = actions.flatten.map(&:to_s)
183 write_inheritable_attribute('accept_key_auth_actions', actions)
184 write_inheritable_attribute('accept_key_auth_actions', actions)
184 end
185 end
185
186
186 def accept_key_auth_actions
187 def accept_key_auth_actions
187 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
188 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
188 end
189 end
189
190
190 # TODO: move to model
191 # TODO: move to model
191 def attach_files(obj, attachments)
192 def attach_files(obj, attachments)
192 attached = []
193 attached = []
193 unsaved = []
194 unsaved = []
194 if attachments && attachments.is_a?(Hash)
195 if attachments && attachments.is_a?(Hash)
195 attachments.each_value do |attachment|
196 attachments.each_value do |attachment|
196 file = attachment['file']
197 file = attachment['file']
197 next unless file && file.size > 0
198 next unless file && file.size > 0
198 a = Attachment.create(:container => obj,
199 a = Attachment.create(:container => obj,
199 :file => file,
200 :file => file,
200 :description => attachment['description'].to_s.strip,
201 :description => attachment['description'].to_s.strip,
201 :author => User.current)
202 :author => User.current)
202 a.new_record? ? (unsaved << a) : (attached << a)
203 a.new_record? ? (unsaved << a) : (attached << a)
203 end
204 end
204 if unsaved.any?
205 if unsaved.any?
205 flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
206 flash[:warning] = l(:warning_attachments_not_saved, unsaved.size)
206 end
207 end
207 end
208 end
208 attached
209 attached
209 end
210 end
210
211
211 # Returns the number of objects that should be displayed
212 # Returns the number of objects that should be displayed
212 # on the paginated list
213 # on the paginated list
213 def per_page_option
214 def per_page_option
214 per_page = nil
215 per_page = nil
215 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
216 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
216 per_page = params[:per_page].to_s.to_i
217 per_page = params[:per_page].to_s.to_i
217 session[:per_page] = per_page
218 session[:per_page] = per_page
218 elsif session[:per_page]
219 elsif session[:per_page]
219 per_page = session[:per_page]
220 per_page = session[:per_page]
220 else
221 else
221 per_page = Setting.per_page_options_array.first || 25
222 per_page = Setting.per_page_options_array.first || 25
222 end
223 end
223 per_page
224 per_page
224 end
225 end
225
226
226 # qvalues http header parser
227 # qvalues http header parser
227 # code taken from webrick
228 # code taken from webrick
228 def parse_qvalues(value)
229 def parse_qvalues(value)
229 tmp = []
230 tmp = []
230 if value
231 if value
231 parts = value.split(/,\s*/)
232 parts = value.split(/,\s*/)
232 parts.each {|part|
233 parts.each {|part|
233 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
234 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
234 val = m[1]
235 val = m[1]
235 q = (m[2] or 1).to_f
236 q = (m[2] or 1).to_f
236 tmp.push([val, q])
237 tmp.push([val, q])
237 end
238 end
238 }
239 }
239 tmp = tmp.sort_by{|val, q| -q}
240 tmp = tmp.sort_by{|val, q| -q}
240 tmp.collect!{|val, q| val}
241 tmp.collect!{|val, q| val}
241 end
242 end
242 return tmp
243 return tmp
243 rescue
244 rescue
244 nil
245 nil
245 end
246 end
246
247
247 # Returns a string that can be used as filename value in Content-Disposition header
248 # Returns a string that can be used as filename value in Content-Disposition header
248 def filename_for_content_disposition(name)
249 def filename_for_content_disposition(name)
249 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
250 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
250 end
251 end
251 end
252 end
@@ -1,92 +1,93
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class BoardsController < ApplicationController
18 class BoardsController < ApplicationController
19 default_search_scope :messages
19 before_filter :find_project, :authorize
20 before_filter :find_project, :authorize
20
21
21 helper :messages
22 helper :messages
22 include MessagesHelper
23 include MessagesHelper
23 helper :sort
24 helper :sort
24 include SortHelper
25 include SortHelper
25 helper :watchers
26 helper :watchers
26 include WatchersHelper
27 include WatchersHelper
27
28
28 def index
29 def index
29 @boards = @project.boards
30 @boards = @project.boards
30 # show the board if there is only one
31 # show the board if there is only one
31 if @boards.size == 1
32 if @boards.size == 1
32 @board = @boards.first
33 @board = @boards.first
33 show
34 show
34 end
35 end
35 end
36 end
36
37
37 def show
38 def show
38 respond_to do |format|
39 respond_to do |format|
39 format.html {
40 format.html {
40 sort_init 'updated_on', 'desc'
41 sort_init 'updated_on', 'desc'
41 sort_update 'created_on' => "#{Message.table_name}.created_on",
42 sort_update 'created_on' => "#{Message.table_name}.created_on",
42 'replies' => "#{Message.table_name}.replies_count",
43 'replies' => "#{Message.table_name}.replies_count",
43 'updated_on' => "#{Message.table_name}.updated_on"
44 'updated_on' => "#{Message.table_name}.updated_on"
44
45
45 @topic_count = @board.topics.count
46 @topic_count = @board.topics.count
46 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
47 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
47 @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
48 @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
48 :include => [:author, {:last_reply => :author}],
49 :include => [:author, {:last_reply => :author}],
49 :limit => @topic_pages.items_per_page,
50 :limit => @topic_pages.items_per_page,
50 :offset => @topic_pages.current.offset
51 :offset => @topic_pages.current.offset
51 @message = Message.new
52 @message = Message.new
52 render :action => 'show', :layout => !request.xhr?
53 render :action => 'show', :layout => !request.xhr?
53 }
54 }
54 format.atom {
55 format.atom {
55 @messages = @board.messages.find :all, :order => 'created_on DESC',
56 @messages = @board.messages.find :all, :order => 'created_on DESC',
56 :include => [:author, :board],
57 :include => [:author, :board],
57 :limit => Setting.feeds_limit.to_i
58 :limit => Setting.feeds_limit.to_i
58 render_feed(@messages, :title => "#{@project}: #{@board}")
59 render_feed(@messages, :title => "#{@project}: #{@board}")
59 }
60 }
60 end
61 end
61 end
62 end
62
63
63 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
64 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
64
65
65 def new
66 def new
66 @board = Board.new(params[:board])
67 @board = Board.new(params[:board])
67 @board.project = @project
68 @board.project = @project
68 if request.post? && @board.save
69 if request.post? && @board.save
69 flash[:notice] = l(:notice_successful_create)
70 flash[:notice] = l(:notice_successful_create)
70 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
71 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
71 end
72 end
72 end
73 end
73
74
74 def edit
75 def edit
75 if request.post? && @board.update_attributes(params[:board])
76 if request.post? && @board.update_attributes(params[:board])
76 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
77 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
77 end
78 end
78 end
79 end
79
80
80 def destroy
81 def destroy
81 @board.destroy
82 @board.destroy
82 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
83 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
83 end
84 end
84
85
85 private
86 private
86 def find_project
87 def find_project
87 @project = Project.find(params[:project_id])
88 @project = Project.find(params[:project_id])
88 @board = @project.boards.find(params[:id]) if params[:id]
89 @board = @project.boards.find(params[:id]) if params[:id]
89 rescue ActiveRecord::RecordNotFound
90 rescue ActiveRecord::RecordNotFound
90 render_404
91 render_404
91 end
92 end
92 end
93 end
@@ -1,87 +1,88
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 DocumentsController < ApplicationController
18 class DocumentsController < ApplicationController
19 default_search_scope :documents
19 before_filter :find_project, :only => [:index, :new]
20 before_filter :find_project, :only => [:index, :new]
20 before_filter :find_document, :except => [:index, :new]
21 before_filter :find_document, :except => [:index, :new]
21 before_filter :authorize
22 before_filter :authorize
22
23
23 helper :attachments
24 helper :attachments
24
25
25 def index
26 def index
26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 documents = @project.documents.find :all, :include => [:attachments, :category]
28 documents = @project.documents.find :all, :include => [:attachments, :category]
28 case @sort_by
29 case @sort_by
29 when 'date'
30 when 'date'
30 @grouped = documents.group_by {|d| d.created_on.to_date }
31 @grouped = documents.group_by {|d| d.created_on.to_date }
31 when 'title'
32 when 'title'
32 @grouped = documents.group_by {|d| d.title.first.upcase}
33 @grouped = documents.group_by {|d| d.title.first.upcase}
33 when 'author'
34 when 'author'
34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 else
36 else
36 @grouped = documents.group_by(&:category)
37 @grouped = documents.group_by(&:category)
37 end
38 end
38 @document = @project.documents.build
39 @document = @project.documents.build
39 render :layout => false if request.xhr?
40 render :layout => false if request.xhr?
40 end
41 end
41
42
42 def show
43 def show
43 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
44 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
44 end
45 end
45
46
46 def new
47 def new
47 @document = @project.documents.build(params[:document])
48 @document = @project.documents.build(params[:document])
48 if request.post? and @document.save
49 if request.post? and @document.save
49 attach_files(@document, params[:attachments])
50 attach_files(@document, params[:attachments])
50 flash[:notice] = l(:notice_successful_create)
51 flash[:notice] = l(:notice_successful_create)
51 redirect_to :action => 'index', :project_id => @project
52 redirect_to :action => 'index', :project_id => @project
52 end
53 end
53 end
54 end
54
55
55 def edit
56 def edit
56 @categories = DocumentCategory.all
57 @categories = DocumentCategory.all
57 if request.post? and @document.update_attributes(params[:document])
58 if request.post? and @document.update_attributes(params[:document])
58 flash[:notice] = l(:notice_successful_update)
59 flash[:notice] = l(:notice_successful_update)
59 redirect_to :action => 'show', :id => @document
60 redirect_to :action => 'show', :id => @document
60 end
61 end
61 end
62 end
62
63
63 def destroy
64 def destroy
64 @document.destroy
65 @document.destroy
65 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
66 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
66 end
67 end
67
68
68 def add_attachment
69 def add_attachment
69 attachments = attach_files(@document, params[:attachments])
70 attachments = attach_files(@document, params[:attachments])
70 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
71 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
71 redirect_to :action => 'show', :id => @document
72 redirect_to :action => 'show', :id => @document
72 end
73 end
73
74
74 private
75 private
75 def find_project
76 def find_project
76 @project = Project.find(params[:project_id])
77 @project = Project.find(params[:project_id])
77 rescue ActiveRecord::RecordNotFound
78 rescue ActiveRecord::RecordNotFound
78 render_404
79 render_404
79 end
80 end
80
81
81 def find_document
82 def find_document
82 @document = Document.find(params[:id])
83 @document = Document.find(params[:id])
83 @project = @document.project
84 @project = @document.project
84 rescue ActiveRecord::RecordNotFound
85 rescue ActiveRecord::RecordNotFound
85 render_404
86 render_404
86 end
87 end
87 end
88 end
@@ -1,508 +1,509
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20
21
21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
27
28
28 helper :journals
29 helper :journals
29 helper :projects
30 helper :projects
30 include ProjectsHelper
31 include ProjectsHelper
31 helper :custom_fields
32 helper :custom_fields
32 include CustomFieldsHelper
33 include CustomFieldsHelper
33 helper :issue_relations
34 helper :issue_relations
34 include IssueRelationsHelper
35 include IssueRelationsHelper
35 helper :watchers
36 helper :watchers
36 include WatchersHelper
37 include WatchersHelper
37 helper :attachments
38 helper :attachments
38 include AttachmentsHelper
39 include AttachmentsHelper
39 helper :queries
40 helper :queries
40 helper :sort
41 helper :sort
41 include SortHelper
42 include SortHelper
42 include IssuesHelper
43 include IssuesHelper
43 helper :timelog
44 helper :timelog
44 include Redmine::Export::PDF
45 include Redmine::Export::PDF
45
46
46 def index
47 def index
47 retrieve_query
48 retrieve_query
48 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
49 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
49 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
50
51
51 if @query.valid?
52 if @query.valid?
52 limit = per_page_option
53 limit = per_page_option
53 respond_to do |format|
54 respond_to do |format|
54 format.html { }
55 format.html { }
55 format.atom { }
56 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
59 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
62 @issues = Issue.find :all, :order => [@query.group_by_sort_order, sort_clause].compact.join(','),
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
64 :conditions => @query.statement,
64 :limit => limit,
65 :limit => limit,
65 :offset => @issue_pages.current.offset
66 :offset => @issue_pages.current.offset
66 respond_to do |format|
67 respond_to do |format|
67 format.html {
68 format.html {
68 if @query.grouped?
69 if @query.grouped?
69 # Retrieve the issue count by group
70 # Retrieve the issue count by group
70 @issue_count_by_group = begin
71 @issue_count_by_group = begin
71 Issue.count(:group => @query.group_by, :include => [:status, :project], :conditions => @query.statement)
72 Issue.count(:group => @query.group_by, :include => [:status, :project], :conditions => @query.statement)
72 # Rails will raise an (unexpected) error if there's only a nil group value
73 # Rails will raise an (unexpected) error if there's only a nil group value
73 rescue ActiveRecord::RecordNotFound
74 rescue ActiveRecord::RecordNotFound
74 {nil => @issue_count}
75 {nil => @issue_count}
75 end
76 end
76 end
77 end
77 render :template => 'issues/index.rhtml', :layout => !request.xhr?
78 render :template => 'issues/index.rhtml', :layout => !request.xhr?
78 }
79 }
79 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
81 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
81 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
82 end
83 end
83 else
84 else
84 # Send html if the query is not valid
85 # Send html if the query is not valid
85 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
86 end
87 end
87 rescue ActiveRecord::RecordNotFound
88 rescue ActiveRecord::RecordNotFound
88 render_404
89 render_404
89 end
90 end
90
91
91 def changes
92 def changes
92 retrieve_query
93 retrieve_query
93 sort_init 'id', 'desc'
94 sort_init 'id', 'desc'
94 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
95
96
96 if @query.valid?
97 if @query.valid?
97 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
98 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
98 :conditions => @query.statement,
99 :conditions => @query.statement,
99 :limit => 25,
100 :limit => 25,
100 :order => "#{Journal.table_name}.created_on DESC"
101 :order => "#{Journal.table_name}.created_on DESC"
101 end
102 end
102 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
103 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
103 render :layout => false, :content_type => 'application/atom+xml'
104 render :layout => false, :content_type => 'application/atom+xml'
104 rescue ActiveRecord::RecordNotFound
105 rescue ActiveRecord::RecordNotFound
105 render_404
106 render_404
106 end
107 end
107
108
108 def show
109 def show
109 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
110 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
110 @journals.each_with_index {|j,i| j.indice = i+1}
111 @journals.each_with_index {|j,i| j.indice = i+1}
111 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 @changesets = @issue.changesets
113 @changesets = @issue.changesets
113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
115 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
115 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
116 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
116 @priorities = IssuePriority.all
117 @priorities = IssuePriority.all
117 @time_entry = TimeEntry.new
118 @time_entry = TimeEntry.new
118 respond_to do |format|
119 respond_to do |format|
119 format.html { render :template => 'issues/show.rhtml' }
120 format.html { render :template => 'issues/show.rhtml' }
120 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
121 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
121 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
122 end
123 end
123 end
124 end
124
125
125 # Add a new issue
126 # Add a new issue
126 # The new issue will be created from an existing one if copy_from parameter is given
127 # The new issue will be created from an existing one if copy_from parameter is given
127 def new
128 def new
128 @issue = Issue.new
129 @issue = Issue.new
129 @issue.copy_from(params[:copy_from]) if params[:copy_from]
130 @issue.copy_from(params[:copy_from]) if params[:copy_from]
130 @issue.project = @project
131 @issue.project = @project
131 # Tracker must be set before custom field values
132 # Tracker must be set before custom field values
132 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
133 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
133 if @issue.tracker.nil?
134 if @issue.tracker.nil?
134 render_error l(:error_no_tracker_in_project)
135 render_error l(:error_no_tracker_in_project)
135 return
136 return
136 end
137 end
137 if params[:issue].is_a?(Hash)
138 if params[:issue].is_a?(Hash)
138 @issue.attributes = params[:issue]
139 @issue.attributes = params[:issue]
139 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
140 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
140 end
141 end
141 @issue.author = User.current
142 @issue.author = User.current
142
143
143 default_status = IssueStatus.default
144 default_status = IssueStatus.default
144 unless default_status
145 unless default_status
145 render_error l(:error_no_default_issue_status)
146 render_error l(:error_no_default_issue_status)
146 return
147 return
147 end
148 end
148 @issue.status = default_status
149 @issue.status = default_status
149 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
150 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
150
151
151 if request.get? || request.xhr?
152 if request.get? || request.xhr?
152 @issue.start_date ||= Date.today
153 @issue.start_date ||= Date.today
153 else
154 else
154 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
155 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
155 # Check that the user is allowed to apply the requested status
156 # Check that the user is allowed to apply the requested status
156 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
157 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
157 if @issue.save
158 if @issue.save
158 attach_files(@issue, params[:attachments])
159 attach_files(@issue, params[:attachments])
159 flash[:notice] = l(:notice_successful_create)
160 flash[:notice] = l(:notice_successful_create)
160 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
161 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
162 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
162 { :action => 'show', :id => @issue })
163 { :action => 'show', :id => @issue })
163 return
164 return
164 end
165 end
165 end
166 end
166 @priorities = IssuePriority.all
167 @priorities = IssuePriority.all
167 render :layout => !request.xhr?
168 render :layout => !request.xhr?
168 end
169 end
169
170
170 # Attributes that can be updated on workflow transition (without :edit permission)
171 # Attributes that can be updated on workflow transition (without :edit permission)
171 # TODO: make it configurable (at least per role)
172 # TODO: make it configurable (at least per role)
172 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
173 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
173
174
174 def edit
175 def edit
175 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
176 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
176 @priorities = IssuePriority.all
177 @priorities = IssuePriority.all
177 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
178 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
178 @time_entry = TimeEntry.new
179 @time_entry = TimeEntry.new
179
180
180 @notes = params[:notes]
181 @notes = params[:notes]
181 journal = @issue.init_journal(User.current, @notes)
182 journal = @issue.init_journal(User.current, @notes)
182 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
183 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
183 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
184 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
184 attrs = params[:issue].dup
185 attrs = params[:issue].dup
185 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
186 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
186 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
187 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
187 @issue.attributes = attrs
188 @issue.attributes = attrs
188 end
189 end
189
190
190 if request.post?
191 if request.post?
191 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
192 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
192 @time_entry.attributes = params[:time_entry]
193 @time_entry.attributes = params[:time_entry]
193 attachments = attach_files(@issue, params[:attachments])
194 attachments = attach_files(@issue, params[:attachments])
194 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
195 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
195
196
196 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
197 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
197
198
198 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
199 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
199 # Log spend time
200 # Log spend time
200 if User.current.allowed_to?(:log_time, @project)
201 if User.current.allowed_to?(:log_time, @project)
201 @time_entry.save
202 @time_entry.save
202 end
203 end
203 if !journal.new_record?
204 if !journal.new_record?
204 # Only send notification if something was actually changed
205 # Only send notification if something was actually changed
205 flash[:notice] = l(:notice_successful_update)
206 flash[:notice] = l(:notice_successful_update)
206 end
207 end
207 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
208 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
208 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
209 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
209 end
210 end
210 end
211 end
211 rescue ActiveRecord::StaleObjectError
212 rescue ActiveRecord::StaleObjectError
212 # Optimistic locking exception
213 # Optimistic locking exception
213 flash.now[:error] = l(:notice_locking_conflict)
214 flash.now[:error] = l(:notice_locking_conflict)
214 # Remove the previously added attachments if issue was not updated
215 # Remove the previously added attachments if issue was not updated
215 attachments.each(&:destroy)
216 attachments.each(&:destroy)
216 end
217 end
217
218
218 def reply
219 def reply
219 journal = Journal.find(params[:journal_id]) if params[:journal_id]
220 journal = Journal.find(params[:journal_id]) if params[:journal_id]
220 if journal
221 if journal
221 user = journal.user
222 user = journal.user
222 text = journal.notes
223 text = journal.notes
223 else
224 else
224 user = @issue.author
225 user = @issue.author
225 text = @issue.description
226 text = @issue.description
226 end
227 end
227 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
228 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
228 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
229 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
229 render(:update) { |page|
230 render(:update) { |page|
230 page.<< "$('notes').value = \"#{content}\";"
231 page.<< "$('notes').value = \"#{content}\";"
231 page.show 'update'
232 page.show 'update'
232 page << "Form.Element.focus('notes');"
233 page << "Form.Element.focus('notes');"
233 page << "Element.scrollTo('update');"
234 page << "Element.scrollTo('update');"
234 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
235 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
235 }
236 }
236 end
237 end
237
238
238 # Bulk edit a set of issues
239 # Bulk edit a set of issues
239 def bulk_edit
240 def bulk_edit
240 if request.post?
241 if request.post?
241 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
242 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
242 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
243 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
243 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
244 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
244 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
245 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
245 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
246 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
246 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
247 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
247
248
248 unsaved_issue_ids = []
249 unsaved_issue_ids = []
249 @issues.each do |issue|
250 @issues.each do |issue|
250 journal = issue.init_journal(User.current, params[:notes])
251 journal = issue.init_journal(User.current, params[:notes])
251 issue.priority = priority if priority
252 issue.priority = priority if priority
252 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
253 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
253 issue.category = category if category || params[:category_id] == 'none'
254 issue.category = category if category || params[:category_id] == 'none'
254 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
255 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
255 issue.start_date = params[:start_date] unless params[:start_date].blank?
256 issue.start_date = params[:start_date] unless params[:start_date].blank?
256 issue.due_date = params[:due_date] unless params[:due_date].blank?
257 issue.due_date = params[:due_date] unless params[:due_date].blank?
257 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
258 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
258 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
259 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
259 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
260 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
260 # Don't save any change to the issue if the user is not authorized to apply the requested status
261 # Don't save any change to the issue if the user is not authorized to apply the requested status
261 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
262 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
262 # Keep unsaved issue ids to display them in flash error
263 # Keep unsaved issue ids to display them in flash error
263 unsaved_issue_ids << issue.id
264 unsaved_issue_ids << issue.id
264 end
265 end
265 end
266 end
266 if unsaved_issue_ids.empty?
267 if unsaved_issue_ids.empty?
267 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
268 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
268 else
269 else
269 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
270 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
270 :total => @issues.size,
271 :total => @issues.size,
271 :ids => '#' + unsaved_issue_ids.join(', #'))
272 :ids => '#' + unsaved_issue_ids.join(', #'))
272 end
273 end
273 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
274 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
274 return
275 return
275 end
276 end
276 # Find potential statuses the user could be allowed to switch issues to
277 # Find potential statuses the user could be allowed to switch issues to
277 @available_statuses = Workflow.find(:all, :include => :new_status,
278 @available_statuses = Workflow.find(:all, :include => :new_status,
278 :conditions => {:role_id => User.current.roles_for_project(@project).collect(&:id)}).collect(&:new_status).compact.uniq.sort
279 :conditions => {:role_id => User.current.roles_for_project(@project).collect(&:id)}).collect(&:new_status).compact.uniq.sort
279 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
280 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
280 end
281 end
281
282
282 def move
283 def move
283 @allowed_projects = []
284 @allowed_projects = []
284 # find projects to which the user is allowed to move the issue
285 # find projects to which the user is allowed to move the issue
285 if User.current.admin?
286 if User.current.admin?
286 # admin is allowed to move issues to any active (visible) project
287 # admin is allowed to move issues to any active (visible) project
287 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
288 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
288 else
289 else
289 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
290 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
290 end
291 end
291 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
292 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
292 @target_project ||= @project
293 @target_project ||= @project
293 @trackers = @target_project.trackers
294 @trackers = @target_project.trackers
294 if request.post?
295 if request.post?
295 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
296 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
296 unsaved_issue_ids = []
297 unsaved_issue_ids = []
297 @issues.each do |issue|
298 @issues.each do |issue|
298 issue.init_journal(User.current)
299 issue.init_journal(User.current)
299 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
300 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker, params[:copy_options])
300 end
301 end
301 if unsaved_issue_ids.empty?
302 if unsaved_issue_ids.empty?
302 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
303 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
303 else
304 else
304 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
305 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
305 :total => @issues.size,
306 :total => @issues.size,
306 :ids => '#' + unsaved_issue_ids.join(', #'))
307 :ids => '#' + unsaved_issue_ids.join(', #'))
307 end
308 end
308 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
309 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
309 return
310 return
310 end
311 end
311 render :layout => false if request.xhr?
312 render :layout => false if request.xhr?
312 end
313 end
313
314
314 def destroy
315 def destroy
315 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
316 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
316 if @hours > 0
317 if @hours > 0
317 case params[:todo]
318 case params[:todo]
318 when 'destroy'
319 when 'destroy'
319 # nothing to do
320 # nothing to do
320 when 'nullify'
321 when 'nullify'
321 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
322 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
322 when 'reassign'
323 when 'reassign'
323 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
324 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
324 if reassign_to.nil?
325 if reassign_to.nil?
325 flash.now[:error] = l(:error_issue_not_found_in_project)
326 flash.now[:error] = l(:error_issue_not_found_in_project)
326 return
327 return
327 else
328 else
328 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
329 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
329 end
330 end
330 else
331 else
331 # display the destroy form
332 # display the destroy form
332 return
333 return
333 end
334 end
334 end
335 end
335 @issues.each(&:destroy)
336 @issues.each(&:destroy)
336 redirect_to :action => 'index', :project_id => @project
337 redirect_to :action => 'index', :project_id => @project
337 end
338 end
338
339
339 def gantt
340 def gantt
340 @gantt = Redmine::Helpers::Gantt.new(params)
341 @gantt = Redmine::Helpers::Gantt.new(params)
341 retrieve_query
342 retrieve_query
342 if @query.valid?
343 if @query.valid?
343 events = []
344 events = []
344 # Issues that have start and due dates
345 # Issues that have start and due dates
345 events += Issue.find(:all,
346 events += Issue.find(:all,
346 :order => "start_date, due_date",
347 :order => "start_date, due_date",
347 :include => [:tracker, :status, :assigned_to, :priority, :project],
348 :include => [:tracker, :status, :assigned_to, :priority, :project],
348 :conditions => ["(#{@query.statement}) AND (((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)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
349 :conditions => ["(#{@query.statement}) AND (((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)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
349 )
350 )
350 # Issues that don't have a due date but that are assigned to a version with a date
351 # Issues that don't have a due date but that are assigned to a version with a date
351 events += Issue.find(:all,
352 events += Issue.find(:all,
352 :order => "start_date, effective_date",
353 :order => "start_date, effective_date",
353 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
354 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
354 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
355 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
355 )
356 )
356 # Versions
357 # Versions
357 events += Version.find(:all, :include => :project,
358 events += Version.find(:all, :include => :project,
358 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
359 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
359
360
360 @gantt.events = events
361 @gantt.events = events
361 end
362 end
362
363
363 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
364 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
364
365
365 respond_to do |format|
366 respond_to do |format|
366 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
367 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
367 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
368 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
368 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
369 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
369 end
370 end
370 end
371 end
371
372
372 def calendar
373 def calendar
373 if params[:year] and params[:year].to_i > 1900
374 if params[:year] and params[:year].to_i > 1900
374 @year = params[:year].to_i
375 @year = params[:year].to_i
375 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
376 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
376 @month = params[:month].to_i
377 @month = params[:month].to_i
377 end
378 end
378 end
379 end
379 @year ||= Date.today.year
380 @year ||= Date.today.year
380 @month ||= Date.today.month
381 @month ||= Date.today.month
381
382
382 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
383 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
383 retrieve_query
384 retrieve_query
384 if @query.valid?
385 if @query.valid?
385 events = []
386 events = []
386 events += Issue.find(:all,
387 events += Issue.find(:all,
387 :include => [:tracker, :status, :assigned_to, :priority, :project],
388 :include => [:tracker, :status, :assigned_to, :priority, :project],
388 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
389 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
389 )
390 )
390 events += Version.find(:all, :include => :project,
391 events += Version.find(:all, :include => :project,
391 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
392 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
392
393
393 @calendar.events = events
394 @calendar.events = events
394 end
395 end
395
396
396 render :layout => false if request.xhr?
397 render :layout => false if request.xhr?
397 end
398 end
398
399
399 def context_menu
400 def context_menu
400 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
401 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
401 if (@issues.size == 1)
402 if (@issues.size == 1)
402 @issue = @issues.first
403 @issue = @issues.first
403 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
404 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
404 end
405 end
405 projects = @issues.collect(&:project).compact.uniq
406 projects = @issues.collect(&:project).compact.uniq
406 @project = projects.first if projects.size == 1
407 @project = projects.first if projects.size == 1
407
408
408 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
409 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
409 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
410 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
410 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
411 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
411 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
412 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
412 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
413 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
413 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
414 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
414 }
415 }
415 if @project
416 if @project
416 @assignables = @project.assignable_users
417 @assignables = @project.assignable_users
417 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
418 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
418 end
419 end
419
420
420 @priorities = IssuePriority.all.reverse
421 @priorities = IssuePriority.all.reverse
421 @statuses = IssueStatus.find(:all, :order => 'position')
422 @statuses = IssueStatus.find(:all, :order => 'position')
422 @back = request.env['HTTP_REFERER']
423 @back = request.env['HTTP_REFERER']
423
424
424 render :layout => false
425 render :layout => false
425 end
426 end
426
427
427 def update_form
428 def update_form
428 @issue = Issue.new(params[:issue])
429 @issue = Issue.new(params[:issue])
429 render :action => :new, :layout => false
430 render :action => :new, :layout => false
430 end
431 end
431
432
432 def preview
433 def preview
433 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
434 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
434 @attachements = @issue.attachments if @issue
435 @attachements = @issue.attachments if @issue
435 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
436 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
436 render :partial => 'common/preview'
437 render :partial => 'common/preview'
437 end
438 end
438
439
439 private
440 private
440 def find_issue
441 def find_issue
441 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
442 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
442 @project = @issue.project
443 @project = @issue.project
443 rescue ActiveRecord::RecordNotFound
444 rescue ActiveRecord::RecordNotFound
444 render_404
445 render_404
445 end
446 end
446
447
447 # Filter for bulk operations
448 # Filter for bulk operations
448 def find_issues
449 def find_issues
449 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
450 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
450 raise ActiveRecord::RecordNotFound if @issues.empty?
451 raise ActiveRecord::RecordNotFound if @issues.empty?
451 projects = @issues.collect(&:project).compact.uniq
452 projects = @issues.collect(&:project).compact.uniq
452 if projects.size == 1
453 if projects.size == 1
453 @project = projects.first
454 @project = projects.first
454 else
455 else
455 # TODO: let users bulk edit/move/destroy issues from different projects
456 # TODO: let users bulk edit/move/destroy issues from different projects
456 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
457 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
457 end
458 end
458 rescue ActiveRecord::RecordNotFound
459 rescue ActiveRecord::RecordNotFound
459 render_404
460 render_404
460 end
461 end
461
462
462 def find_project
463 def find_project
463 @project = Project.find(params[:project_id])
464 @project = Project.find(params[:project_id])
464 rescue ActiveRecord::RecordNotFound
465 rescue ActiveRecord::RecordNotFound
465 render_404
466 render_404
466 end
467 end
467
468
468 def find_optional_project
469 def find_optional_project
469 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
470 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
470 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
471 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
471 allowed ? true : deny_access
472 allowed ? true : deny_access
472 rescue ActiveRecord::RecordNotFound
473 rescue ActiveRecord::RecordNotFound
473 render_404
474 render_404
474 end
475 end
475
476
476 # Retrieve query from session or build a new query
477 # Retrieve query from session or build a new query
477 def retrieve_query
478 def retrieve_query
478 if !params[:query_id].blank?
479 if !params[:query_id].blank?
479 cond = "project_id IS NULL"
480 cond = "project_id IS NULL"
480 cond << " OR project_id = #{@project.id}" if @project
481 cond << " OR project_id = #{@project.id}" if @project
481 @query = Query.find(params[:query_id], :conditions => cond)
482 @query = Query.find(params[:query_id], :conditions => cond)
482 @query.project = @project
483 @query.project = @project
483 session[:query] = {:id => @query.id, :project_id => @query.project_id}
484 session[:query] = {:id => @query.id, :project_id => @query.project_id}
484 sort_clear
485 sort_clear
485 else
486 else
486 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
487 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
487 # Give it a name, required to be valid
488 # Give it a name, required to be valid
488 @query = Query.new(:name => "_")
489 @query = Query.new(:name => "_")
489 @query.project = @project
490 @query.project = @project
490 if params[:fields] and params[:fields].is_a? Array
491 if params[:fields] and params[:fields].is_a? Array
491 params[:fields].each do |field|
492 params[:fields].each do |field|
492 @query.add_filter(field, params[:operators][field], params[:values][field])
493 @query.add_filter(field, params[:operators][field], params[:values][field])
493 end
494 end
494 else
495 else
495 @query.available_filters.keys.each do |field|
496 @query.available_filters.keys.each do |field|
496 @query.add_short_filter(field, params[field]) if params[field]
497 @query.add_short_filter(field, params[field]) if params[field]
497 end
498 end
498 end
499 end
499 @query.group_by = params[:group_by]
500 @query.group_by = params[:group_by]
500 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
501 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by}
501 else
502 else
502 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
503 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
503 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
504 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by])
504 @query.project = @project
505 @query.project = @project
505 end
506 end
506 end
507 end
507 end
508 end
508 end
509 end
@@ -1,128 +1,129
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 menu_item :boards
19 menu_item :boards
20 default_search_scope :messages
20 before_filter :find_board, :only => [:new, :preview]
21 before_filter :find_board, :only => [:new, :preview]
21 before_filter :find_message, :except => [:new, :preview]
22 before_filter :find_message, :except => [:new, :preview]
22 before_filter :authorize, :except => [:preview, :edit, :destroy]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
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 verify :xhr => true, :only => :quote
26 verify :xhr => true, :only => :quote
26
27
27 helper :watchers
28 helper :watchers
28 helper :attachments
29 helper :attachments
29 include AttachmentsHelper
30 include AttachmentsHelper
30
31
31 # Show a topic and its replies
32 # Show a topic and its replies
32 def show
33 def show
33 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
34 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
34 @replies.reverse! if User.current.wants_comments_in_reverse_order?
35 @replies.reverse! if User.current.wants_comments_in_reverse_order?
35 @reply = Message.new(:subject => "RE: #{@message.subject}")
36 @reply = Message.new(:subject => "RE: #{@message.subject}")
36 render :action => "show", :layout => false if request.xhr?
37 render :action => "show", :layout => false if request.xhr?
37 end
38 end
38
39
39 # Create a new topic
40 # Create a new topic
40 def new
41 def new
41 @message = Message.new(params[:message])
42 @message = Message.new(params[:message])
42 @message.author = User.current
43 @message.author = User.current
43 @message.board = @board
44 @message.board = @board
44 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
45 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
45 @message.locked = params[:message]['locked']
46 @message.locked = params[:message]['locked']
46 @message.sticky = params[:message]['sticky']
47 @message.sticky = params[:message]['sticky']
47 end
48 end
48 if request.post? && @message.save
49 if request.post? && @message.save
49 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
50 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
50 attach_files(@message, params[:attachments])
51 attach_files(@message, params[:attachments])
51 redirect_to :action => 'show', :id => @message
52 redirect_to :action => 'show', :id => @message
52 end
53 end
53 end
54 end
54
55
55 # Reply to a topic
56 # Reply to a topic
56 def reply
57 def reply
57 @reply = Message.new(params[:reply])
58 @reply = Message.new(params[:reply])
58 @reply.author = User.current
59 @reply.author = User.current
59 @reply.board = @board
60 @reply.board = @board
60 @topic.children << @reply
61 @topic.children << @reply
61 if !@reply.new_record?
62 if !@reply.new_record?
62 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
63 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
63 attach_files(@reply, params[:attachments])
64 attach_files(@reply, params[:attachments])
64 end
65 end
65 redirect_to :action => 'show', :id => @topic
66 redirect_to :action => 'show', :id => @topic
66 end
67 end
67
68
68 # Edit a message
69 # Edit a message
69 def edit
70 def edit
70 render_403 and return false unless @message.editable_by?(User.current)
71 render_403 and return false unless @message.editable_by?(User.current)
71 if params[:message]
72 if params[:message]
72 @message.locked = params[:message]['locked']
73 @message.locked = params[:message]['locked']
73 @message.sticky = params[:message]['sticky']
74 @message.sticky = params[:message]['sticky']
74 end
75 end
75 if request.post? && @message.update_attributes(params[:message])
76 if request.post? && @message.update_attributes(params[:message])
76 attach_files(@message, params[:attachments])
77 attach_files(@message, params[:attachments])
77 flash[:notice] = l(:notice_successful_update)
78 flash[:notice] = l(:notice_successful_update)
78 @message.reload
79 @message.reload
79 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
80 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
80 end
81 end
81 end
82 end
82
83
83 # Delete a messages
84 # Delete a messages
84 def destroy
85 def destroy
85 render_403 and return false unless @message.destroyable_by?(User.current)
86 render_403 and return false unless @message.destroyable_by?(User.current)
86 @message.destroy
87 @message.destroy
87 redirect_to @message.parent.nil? ?
88 redirect_to @message.parent.nil? ?
88 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
89 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
89 { :action => 'show', :id => @message.parent }
90 { :action => 'show', :id => @message.parent }
90 end
91 end
91
92
92 def quote
93 def quote
93 user = @message.author
94 user = @message.author
94 text = @message.content
95 text = @message.content
95 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
96 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
96 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
97 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
97 render(:update) { |page|
98 render(:update) { |page|
98 page.<< "$('message_content').value = \"#{content}\";"
99 page.<< "$('message_content').value = \"#{content}\";"
99 page.show 'reply'
100 page.show 'reply'
100 page << "Form.Element.focus('message_content');"
101 page << "Form.Element.focus('message_content');"
101 page << "Element.scrollTo('reply');"
102 page << "Element.scrollTo('reply');"
102 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
103 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
103 }
104 }
104 end
105 end
105
106
106 def preview
107 def preview
107 message = @board.messages.find_by_id(params[:id])
108 message = @board.messages.find_by_id(params[:id])
108 @attachements = message.attachments if message
109 @attachements = message.attachments if message
109 @text = (params[:message] || params[:reply])[:content]
110 @text = (params[:message] || params[:reply])[:content]
110 render :partial => 'common/preview'
111 render :partial => 'common/preview'
111 end
112 end
112
113
113 private
114 private
114 def find_message
115 def find_message
115 find_board
116 find_board
116 @message = @board.messages.find(params[:id], :include => :parent)
117 @message = @board.messages.find(params[:id], :include => :parent)
117 @topic = @message.root
118 @topic = @message.root
118 rescue ActiveRecord::RecordNotFound
119 rescue ActiveRecord::RecordNotFound
119 render_404
120 render_404
120 end
121 end
121
122
122 def find_board
123 def find_board
123 @board = Board.find(params[:board_id], :include => :project)
124 @board = Board.find(params[:board_id], :include => :project)
124 @project = @board.project
125 @project = @board.project
125 rescue ActiveRecord::RecordNotFound
126 rescue ActiveRecord::RecordNotFound
126 render_404
127 render_404
127 end
128 end
128 end
129 end
@@ -1,108 +1,109
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 NewsController < ApplicationController
18 class NewsController < ApplicationController
19 default_search_scope :news
19 before_filter :find_news, :except => [:new, :index, :preview]
20 before_filter :find_news, :except => [:new, :index, :preview]
20 before_filter :find_project, :only => [:new, :preview]
21 before_filter :find_project, :only => [:new, :preview]
21 before_filter :authorize, :except => [:index, :preview]
22 before_filter :authorize, :except => [:index, :preview]
22 before_filter :find_optional_project, :only => :index
23 before_filter :find_optional_project, :only => :index
23 accept_key_auth :index
24 accept_key_auth :index
24
25
25 def index
26 def index
26 @news_pages, @newss = paginate :news,
27 @news_pages, @newss = paginate :news,
27 :per_page => 10,
28 :per_page => 10,
28 :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
29 :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
29 :include => [:author, :project],
30 :include => [:author, :project],
30 :order => "#{News.table_name}.created_on DESC"
31 :order => "#{News.table_name}.created_on DESC"
31 respond_to do |format|
32 respond_to do |format|
32 format.html { render :layout => false if request.xhr? }
33 format.html { render :layout => false if request.xhr? }
33 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
34 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
34 end
35 end
35 end
36 end
36
37
37 def show
38 def show
38 @comments = @news.comments
39 @comments = @news.comments
39 @comments.reverse! if User.current.wants_comments_in_reverse_order?
40 @comments.reverse! if User.current.wants_comments_in_reverse_order?
40 end
41 end
41
42
42 def new
43 def new
43 @news = News.new(:project => @project, :author => User.current)
44 @news = News.new(:project => @project, :author => User.current)
44 if request.post?
45 if request.post?
45 @news.attributes = params[:news]
46 @news.attributes = params[:news]
46 if @news.save
47 if @news.save
47 flash[:notice] = l(:notice_successful_create)
48 flash[:notice] = l(:notice_successful_create)
48 redirect_to :controller => 'news', :action => 'index', :project_id => @project
49 redirect_to :controller => 'news', :action => 'index', :project_id => @project
49 end
50 end
50 end
51 end
51 end
52 end
52
53
53 def edit
54 def edit
54 if request.post? and @news.update_attributes(params[:news])
55 if request.post? and @news.update_attributes(params[:news])
55 flash[:notice] = l(:notice_successful_update)
56 flash[:notice] = l(:notice_successful_update)
56 redirect_to :action => 'show', :id => @news
57 redirect_to :action => 'show', :id => @news
57 end
58 end
58 end
59 end
59
60
60 def add_comment
61 def add_comment
61 @comment = Comment.new(params[:comment])
62 @comment = Comment.new(params[:comment])
62 @comment.author = User.current
63 @comment.author = User.current
63 if @news.comments << @comment
64 if @news.comments << @comment
64 flash[:notice] = l(:label_comment_added)
65 flash[:notice] = l(:label_comment_added)
65 redirect_to :action => 'show', :id => @news
66 redirect_to :action => 'show', :id => @news
66 else
67 else
67 show
68 show
68 render :action => 'show'
69 render :action => 'show'
69 end
70 end
70 end
71 end
71
72
72 def destroy_comment
73 def destroy_comment
73 @news.comments.find(params[:comment_id]).destroy
74 @news.comments.find(params[:comment_id]).destroy
74 redirect_to :action => 'show', :id => @news
75 redirect_to :action => 'show', :id => @news
75 end
76 end
76
77
77 def destroy
78 def destroy
78 @news.destroy
79 @news.destroy
79 redirect_to :action => 'index', :project_id => @project
80 redirect_to :action => 'index', :project_id => @project
80 end
81 end
81
82
82 def preview
83 def preview
83 @text = (params[:news] ? params[:news][:description] : nil)
84 @text = (params[:news] ? params[:news][:description] : nil)
84 render :partial => 'common/preview'
85 render :partial => 'common/preview'
85 end
86 end
86
87
87 private
88 private
88 def find_news
89 def find_news
89 @news = News.find(params[:id])
90 @news = News.find(params[:id])
90 @project = @news.project
91 @project = @news.project
91 rescue ActiveRecord::RecordNotFound
92 rescue ActiveRecord::RecordNotFound
92 render_404
93 render_404
93 end
94 end
94
95
95 def find_project
96 def find_project
96 @project = Project.find(params[:project_id])
97 @project = Project.find(params[:project_id])
97 rescue ActiveRecord::RecordNotFound
98 rescue ActiveRecord::RecordNotFound
98 render_404
99 render_404
99 end
100 end
100
101
101 def find_optional_project
102 def find_optional_project
102 return true unless params[:project_id]
103 return true unless params[:project_id]
103 @project = Project.find(params[:project_id])
104 @project = Project.find(params[:project_id])
104 authorize
105 authorize
105 rescue ActiveRecord::RecordNotFound
106 rescue ActiveRecord::RecordNotFound
106 render_404
107 render_404
107 end
108 end
108 end
109 end
@@ -1,319 +1,321
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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; end
22 class ChangesetNotFound < Exception; end
23 class InvalidRevisionParam < Exception; end
23 class InvalidRevisionParam < Exception; end
24
24
25 class RepositoriesController < ApplicationController
25 class RepositoriesController < ApplicationController
26 menu_item :repository
26 menu_item :repository
27 default_search_scope :changesets
28
27 before_filter :find_repository, :except => :edit
29 before_filter :find_repository, :except => :edit
28 before_filter :find_project, :only => :edit
30 before_filter :find_project, :only => :edit
29 before_filter :authorize
31 before_filter :authorize
30 accept_key_auth :revisions
32 accept_key_auth :revisions
31
33
32 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
34 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
33
35
34 def edit
36 def edit
35 @repository = @project.repository
37 @repository = @project.repository
36 if !@repository
38 if !@repository
37 @repository = Repository.factory(params[:repository_scm])
39 @repository = Repository.factory(params[:repository_scm])
38 @repository.project = @project if @repository
40 @repository.project = @project if @repository
39 end
41 end
40 if request.post? && @repository
42 if request.post? && @repository
41 @repository.attributes = params[:repository]
43 @repository.attributes = params[:repository]
42 @repository.save
44 @repository.save
43 end
45 end
44 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
46 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
45 end
47 end
46
48
47 def committers
49 def committers
48 @committers = @repository.committers
50 @committers = @repository.committers
49 @users = @project.users
51 @users = @project.users
50 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
52 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
51 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
53 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
52 @users.compact!
54 @users.compact!
53 @users.sort!
55 @users.sort!
54 if request.post? && params[:committers].is_a?(Hash)
56 if request.post? && params[:committers].is_a?(Hash)
55 # Build a hash with repository usernames as keys and corresponding user ids as values
57 # Build a hash with repository usernames as keys and corresponding user ids as values
56 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
58 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
57 flash[:notice] = l(:notice_successful_update)
59 flash[:notice] = l(:notice_successful_update)
58 redirect_to :action => 'committers', :id => @project
60 redirect_to :action => 'committers', :id => @project
59 end
61 end
60 end
62 end
61
63
62 def destroy
64 def destroy
63 @repository.destroy
65 @repository.destroy
64 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
66 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
65 end
67 end
66
68
67 def show
69 def show
68 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
70 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
69
71
70 @entries = @repository.entries(@path, @rev)
72 @entries = @repository.entries(@path, @rev)
71 if request.xhr?
73 if request.xhr?
72 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
74 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
73 else
75 else
74 show_error_not_found and return unless @entries
76 show_error_not_found and return unless @entries
75 @changesets = @repository.latest_changesets(@path, @rev)
77 @changesets = @repository.latest_changesets(@path, @rev)
76 @properties = @repository.properties(@path, @rev)
78 @properties = @repository.properties(@path, @rev)
77 render :action => 'show'
79 render :action => 'show'
78 end
80 end
79 end
81 end
80
82
81 alias_method :browse, :show
83 alias_method :browse, :show
82
84
83 def changes
85 def changes
84 @entry = @repository.entry(@path, @rev)
86 @entry = @repository.entry(@path, @rev)
85 show_error_not_found and return unless @entry
87 show_error_not_found and return unless @entry
86 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
88 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
87 @properties = @repository.properties(@path, @rev)
89 @properties = @repository.properties(@path, @rev)
88 end
90 end
89
91
90 def revisions
92 def revisions
91 @changeset_count = @repository.changesets.count
93 @changeset_count = @repository.changesets.count
92 @changeset_pages = Paginator.new self, @changeset_count,
94 @changeset_pages = Paginator.new self, @changeset_count,
93 per_page_option,
95 per_page_option,
94 params['page']
96 params['page']
95 @changesets = @repository.changesets.find(:all,
97 @changesets = @repository.changesets.find(:all,
96 :limit => @changeset_pages.items_per_page,
98 :limit => @changeset_pages.items_per_page,
97 :offset => @changeset_pages.current.offset,
99 :offset => @changeset_pages.current.offset,
98 :include => [:user, :repository])
100 :include => [:user, :repository])
99
101
100 respond_to do |format|
102 respond_to do |format|
101 format.html { render :layout => false if request.xhr? }
103 format.html { render :layout => false if request.xhr? }
102 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
104 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
103 end
105 end
104 end
106 end
105
107
106 def entry
108 def entry
107 @entry = @repository.entry(@path, @rev)
109 @entry = @repository.entry(@path, @rev)
108 show_error_not_found and return unless @entry
110 show_error_not_found and return unless @entry
109
111
110 # If the entry is a dir, show the browser
112 # If the entry is a dir, show the browser
111 show and return if @entry.is_dir?
113 show and return if @entry.is_dir?
112
114
113 @content = @repository.cat(@path, @rev)
115 @content = @repository.cat(@path, @rev)
114 show_error_not_found and return unless @content
116 show_error_not_found and return unless @content
115 if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
117 if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
116 # Force the download
118 # Force the download
117 send_data @content, :filename => @path.split('/').last
119 send_data @content, :filename => @path.split('/').last
118 else
120 else
119 # Prevent empty lines when displaying a file with Windows style eol
121 # Prevent empty lines when displaying a file with Windows style eol
120 @content.gsub!("\r\n", "\n")
122 @content.gsub!("\r\n", "\n")
121 end
123 end
122 end
124 end
123
125
124 def annotate
126 def annotate
125 @entry = @repository.entry(@path, @rev)
127 @entry = @repository.entry(@path, @rev)
126 show_error_not_found and return unless @entry
128 show_error_not_found and return unless @entry
127
129
128 @annotate = @repository.scm.annotate(@path, @rev)
130 @annotate = @repository.scm.annotate(@path, @rev)
129 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
131 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
130 end
132 end
131
133
132 def revision
134 def revision
133 @changeset = @repository.find_changeset_by_name(@rev)
135 @changeset = @repository.find_changeset_by_name(@rev)
134 raise ChangesetNotFound unless @changeset
136 raise ChangesetNotFound unless @changeset
135
137
136 respond_to do |format|
138 respond_to do |format|
137 format.html
139 format.html
138 format.js {render :layout => false}
140 format.js {render :layout => false}
139 end
141 end
140 rescue ChangesetNotFound
142 rescue ChangesetNotFound
141 show_error_not_found
143 show_error_not_found
142 end
144 end
143
145
144 def diff
146 def diff
145 if params[:format] == 'diff'
147 if params[:format] == 'diff'
146 @diff = @repository.diff(@path, @rev, @rev_to)
148 @diff = @repository.diff(@path, @rev, @rev_to)
147 show_error_not_found and return unless @diff
149 show_error_not_found and return unless @diff
148 filename = "changeset_r#{@rev}"
150 filename = "changeset_r#{@rev}"
149 filename << "_r#{@rev_to}" if @rev_to
151 filename << "_r#{@rev_to}" if @rev_to
150 send_data @diff.join, :filename => "#{filename}.diff",
152 send_data @diff.join, :filename => "#{filename}.diff",
151 :type => 'text/x-patch',
153 :type => 'text/x-patch',
152 :disposition => 'attachment'
154 :disposition => 'attachment'
153 else
155 else
154 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
156 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
155 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
157 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
156
158
157 # Save diff type as user preference
159 # Save diff type as user preference
158 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
160 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
159 User.current.pref[:diff_type] = @diff_type
161 User.current.pref[:diff_type] = @diff_type
160 User.current.preference.save
162 User.current.preference.save
161 end
163 end
162
164
163 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
165 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
164 unless read_fragment(@cache_key)
166 unless read_fragment(@cache_key)
165 @diff = @repository.diff(@path, @rev, @rev_to)
167 @diff = @repository.diff(@path, @rev, @rev_to)
166 show_error_not_found unless @diff
168 show_error_not_found unless @diff
167 end
169 end
168 end
170 end
169 end
171 end
170
172
171 def stats
173 def stats
172 end
174 end
173
175
174 def graph
176 def graph
175 data = nil
177 data = nil
176 case params[:graph]
178 case params[:graph]
177 when "commits_per_month"
179 when "commits_per_month"
178 data = graph_commits_per_month(@repository)
180 data = graph_commits_per_month(@repository)
179 when "commits_per_author"
181 when "commits_per_author"
180 data = graph_commits_per_author(@repository)
182 data = graph_commits_per_author(@repository)
181 end
183 end
182 if data
184 if data
183 headers["Content-Type"] = "image/svg+xml"
185 headers["Content-Type"] = "image/svg+xml"
184 send_data(data, :type => "image/svg+xml", :disposition => "inline")
186 send_data(data, :type => "image/svg+xml", :disposition => "inline")
185 else
187 else
186 render_404
188 render_404
187 end
189 end
188 end
190 end
189
191
190 private
192 private
191 def find_project
193 def find_project
192 @project = Project.find(params[:id])
194 @project = Project.find(params[:id])
193 rescue ActiveRecord::RecordNotFound
195 rescue ActiveRecord::RecordNotFound
194 render_404
196 render_404
195 end
197 end
196
198
197 def find_repository
199 def find_repository
198 @project = Project.find(params[:id])
200 @project = Project.find(params[:id])
199 @repository = @project.repository
201 @repository = @project.repository
200 render_404 and return false unless @repository
202 render_404 and return false unless @repository
201 @path = params[:path].join('/') unless params[:path].nil?
203 @path = params[:path].join('/') unless params[:path].nil?
202 @path ||= ''
204 @path ||= ''
203 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
205 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
204 @rev_to = params[:rev_to]
206 @rev_to = params[:rev_to]
205 rescue ActiveRecord::RecordNotFound
207 rescue ActiveRecord::RecordNotFound
206 render_404
208 render_404
207 rescue InvalidRevisionParam
209 rescue InvalidRevisionParam
208 show_error_not_found
210 show_error_not_found
209 end
211 end
210
212
211 def show_error_not_found
213 def show_error_not_found
212 render_error l(:error_scm_not_found)
214 render_error l(:error_scm_not_found)
213 end
215 end
214
216
215 # Handler for Redmine::Scm::Adapters::CommandFailed exception
217 # Handler for Redmine::Scm::Adapters::CommandFailed exception
216 def show_error_command_failed(exception)
218 def show_error_command_failed(exception)
217 render_error l(:error_scm_command_failed, exception.message)
219 render_error l(:error_scm_command_failed, exception.message)
218 end
220 end
219
221
220 def graph_commits_per_month(repository)
222 def graph_commits_per_month(repository)
221 @date_to = Date.today
223 @date_to = Date.today
222 @date_from = @date_to << 11
224 @date_from = @date_to << 11
223 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
225 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
224 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
226 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
225 commits_by_month = [0] * 12
227 commits_by_month = [0] * 12
226 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
228 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
227
229
228 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
230 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
229 changes_by_month = [0] * 12
231 changes_by_month = [0] * 12
230 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
232 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
231
233
232 fields = []
234 fields = []
233 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
235 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
234
236
235 graph = SVG::Graph::Bar.new(
237 graph = SVG::Graph::Bar.new(
236 :height => 300,
238 :height => 300,
237 :width => 800,
239 :width => 800,
238 :fields => fields.reverse,
240 :fields => fields.reverse,
239 :stack => :side,
241 :stack => :side,
240 :scale_integers => true,
242 :scale_integers => true,
241 :step_x_labels => 2,
243 :step_x_labels => 2,
242 :show_data_values => false,
244 :show_data_values => false,
243 :graph_title => l(:label_commits_per_month),
245 :graph_title => l(:label_commits_per_month),
244 :show_graph_title => true
246 :show_graph_title => true
245 )
247 )
246
248
247 graph.add_data(
249 graph.add_data(
248 :data => commits_by_month[0..11].reverse,
250 :data => commits_by_month[0..11].reverse,
249 :title => l(:label_revision_plural)
251 :title => l(:label_revision_plural)
250 )
252 )
251
253
252 graph.add_data(
254 graph.add_data(
253 :data => changes_by_month[0..11].reverse,
255 :data => changes_by_month[0..11].reverse,
254 :title => l(:label_change_plural)
256 :title => l(:label_change_plural)
255 )
257 )
256
258
257 graph.burn
259 graph.burn
258 end
260 end
259
261
260 def graph_commits_per_author(repository)
262 def graph_commits_per_author(repository)
261 commits_by_author = repository.changesets.count(:all, :group => :committer)
263 commits_by_author = repository.changesets.count(:all, :group => :committer)
262 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
264 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
263
265
264 changes_by_author = repository.changes.count(:all, :group => :committer)
266 changes_by_author = repository.changes.count(:all, :group => :committer)
265 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
267 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
266
268
267 fields = commits_by_author.collect {|r| r.first}
269 fields = commits_by_author.collect {|r| r.first}
268 commits_data = commits_by_author.collect {|r| r.last}
270 commits_data = commits_by_author.collect {|r| r.last}
269 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
271 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
270
272
271 fields = fields + [""]*(10 - fields.length) if fields.length<10
273 fields = fields + [""]*(10 - fields.length) if fields.length<10
272 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
274 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
273 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
275 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
274
276
275 # Remove email adress in usernames
277 # Remove email adress in usernames
276 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
278 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
277
279
278 graph = SVG::Graph::BarHorizontal.new(
280 graph = SVG::Graph::BarHorizontal.new(
279 :height => 400,
281 :height => 400,
280 :width => 800,
282 :width => 800,
281 :fields => fields,
283 :fields => fields,
282 :stack => :side,
284 :stack => :side,
283 :scale_integers => true,
285 :scale_integers => true,
284 :show_data_values => false,
286 :show_data_values => false,
285 :rotate_y_labels => false,
287 :rotate_y_labels => false,
286 :graph_title => l(:label_commits_per_author),
288 :graph_title => l(:label_commits_per_author),
287 :show_graph_title => true
289 :show_graph_title => true
288 )
290 )
289
291
290 graph.add_data(
292 graph.add_data(
291 :data => commits_data,
293 :data => commits_data,
292 :title => l(:label_revision_plural)
294 :title => l(:label_revision_plural)
293 )
295 )
294
296
295 graph.add_data(
297 graph.add_data(
296 :data => changes_data,
298 :data => changes_data,
297 :title => l(:label_change_plural)
299 :title => l(:label_change_plural)
298 )
300 )
299
301
300 graph.burn
302 graph.burn
301 end
303 end
302
304
303 end
305 end
304
306
305 class Date
307 class Date
306 def months_ago(date = Date.today)
308 def months_ago(date = Date.today)
307 (date.year - self.year)*12 + (date.month - self.month)
309 (date.year - self.year)*12 + (date.month - self.month)
308 end
310 end
309
311
310 def weeks_ago(date = Date.today)
312 def weeks_ago(date = Date.today)
311 (date.year - self.year)*52 + (date.cweek - self.cweek)
313 (date.year - self.year)*52 + (date.cweek - self.cweek)
312 end
314 end
313 end
315 end
314
316
315 class String
317 class String
316 def with_leading_slash
318 def with_leading_slash
317 starts_with?('/') ? self : "/#{self}"
319 starts_with?('/') ? self : "/#{self}"
318 end
320 end
319 end
321 end
@@ -1,235 +1,236
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 'diff'
18 require 'diff'
19
19
20 class WikiController < ApplicationController
20 class WikiController < ApplicationController
21 default_search_scope :wiki_pages
21 before_filter :find_wiki, :authorize
22 before_filter :find_wiki, :authorize
22 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
23 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
23
24
24 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
25 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
25
26
26 helper :attachments
27 helper :attachments
27 include AttachmentsHelper
28 include AttachmentsHelper
28 helper :watchers
29 helper :watchers
29
30
30 # display a page (in editing mode if it doesn't exist)
31 # display a page (in editing mode if it doesn't exist)
31 def index
32 def index
32 page_title = params[:page]
33 page_title = params[:page]
33 @page = @wiki.find_or_new_page(page_title)
34 @page = @wiki.find_or_new_page(page_title)
34 if @page.new_record?
35 if @page.new_record?
35 if User.current.allowed_to?(:edit_wiki_pages, @project)
36 if User.current.allowed_to?(:edit_wiki_pages, @project)
36 edit
37 edit
37 render :action => 'edit'
38 render :action => 'edit'
38 else
39 else
39 render_404
40 render_404
40 end
41 end
41 return
42 return
42 end
43 end
43 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
44 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
44 # Redirects user to the current version if he's not allowed to view previous versions
45 # Redirects user to the current version if he's not allowed to view previous versions
45 redirect_to :version => nil
46 redirect_to :version => nil
46 return
47 return
47 end
48 end
48 @content = @page.content_for_version(params[:version])
49 @content = @page.content_for_version(params[:version])
49 if params[:format] == 'html'
50 if params[:format] == 'html'
50 export = render_to_string :action => 'export', :layout => false
51 export = render_to_string :action => 'export', :layout => false
51 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
52 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
52 return
53 return
53 elsif params[:format] == 'txt'
54 elsif params[:format] == 'txt'
54 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
55 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
55 return
56 return
56 end
57 end
57 @editable = editable?
58 @editable = editable?
58 render :action => 'show'
59 render :action => 'show'
59 end
60 end
60
61
61 # edit an existing page or a new one
62 # edit an existing page or a new one
62 def edit
63 def edit
63 @page = @wiki.find_or_new_page(params[:page])
64 @page = @wiki.find_or_new_page(params[:page])
64 return render_403 unless editable?
65 return render_403 unless editable?
65 @page.content = WikiContent.new(:page => @page) if @page.new_record?
66 @page.content = WikiContent.new(:page => @page) if @page.new_record?
66
67
67 @content = @page.content_for_version(params[:version])
68 @content = @page.content_for_version(params[:version])
68 @content.text = initial_page_content(@page) if @content.text.blank?
69 @content.text = initial_page_content(@page) if @content.text.blank?
69 # don't keep previous comment
70 # don't keep previous comment
70 @content.comments = nil
71 @content.comments = nil
71 if request.get?
72 if request.get?
72 # To prevent StaleObjectError exception when reverting to a previous version
73 # To prevent StaleObjectError exception when reverting to a previous version
73 @content.version = @page.content.version
74 @content.version = @page.content.version
74 else
75 else
75 if !@page.new_record? && @content.text == params[:content][:text]
76 if !@page.new_record? && @content.text == params[:content][:text]
76 # don't save if text wasn't changed
77 # don't save if text wasn't changed
77 redirect_to :action => 'index', :id => @project, :page => @page.title
78 redirect_to :action => 'index', :id => @project, :page => @page.title
78 return
79 return
79 end
80 end
80 #@content.text = params[:content][:text]
81 #@content.text = params[:content][:text]
81 #@content.comments = params[:content][:comments]
82 #@content.comments = params[:content][:comments]
82 @content.attributes = params[:content]
83 @content.attributes = params[:content]
83 @content.author = User.current
84 @content.author = User.current
84 # if page is new @page.save will also save content, but not if page isn't a new record
85 # if page is new @page.save will also save content, but not if page isn't a new record
85 if (@page.new_record? ? @page.save : @content.save)
86 if (@page.new_record? ? @page.save : @content.save)
86 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
87 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
87 redirect_to :action => 'index', :id => @project, :page => @page.title
88 redirect_to :action => 'index', :id => @project, :page => @page.title
88 end
89 end
89 end
90 end
90 rescue ActiveRecord::StaleObjectError
91 rescue ActiveRecord::StaleObjectError
91 # Optimistic locking exception
92 # Optimistic locking exception
92 flash[:error] = l(:notice_locking_conflict)
93 flash[:error] = l(:notice_locking_conflict)
93 end
94 end
94
95
95 # rename a page
96 # rename a page
96 def rename
97 def rename
97 return render_403 unless editable?
98 return render_403 unless editable?
98 @page.redirect_existing_links = true
99 @page.redirect_existing_links = true
99 # used to display the *original* title if some AR validation errors occur
100 # used to display the *original* title if some AR validation errors occur
100 @original_title = @page.pretty_title
101 @original_title = @page.pretty_title
101 if request.post? && @page.update_attributes(params[:wiki_page])
102 if request.post? && @page.update_attributes(params[:wiki_page])
102 flash[:notice] = l(:notice_successful_update)
103 flash[:notice] = l(:notice_successful_update)
103 redirect_to :action => 'index', :id => @project, :page => @page.title
104 redirect_to :action => 'index', :id => @project, :page => @page.title
104 end
105 end
105 end
106 end
106
107
107 def protect
108 def protect
108 @page.update_attribute :protected, params[:protected]
109 @page.update_attribute :protected, params[:protected]
109 redirect_to :action => 'index', :id => @project, :page => @page.title
110 redirect_to :action => 'index', :id => @project, :page => @page.title
110 end
111 end
111
112
112 # show page history
113 # show page history
113 def history
114 def history
114 @version_count = @page.content.versions.count
115 @version_count = @page.content.versions.count
115 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
116 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
116 # don't load text
117 # don't load text
117 @versions = @page.content.versions.find :all,
118 @versions = @page.content.versions.find :all,
118 :select => "id, author_id, comments, updated_on, version",
119 :select => "id, author_id, comments, updated_on, version",
119 :order => 'version DESC',
120 :order => 'version DESC',
120 :limit => @version_pages.items_per_page + 1,
121 :limit => @version_pages.items_per_page + 1,
121 :offset => @version_pages.current.offset
122 :offset => @version_pages.current.offset
122
123
123 render :layout => false if request.xhr?
124 render :layout => false if request.xhr?
124 end
125 end
125
126
126 def diff
127 def diff
127 @diff = @page.diff(params[:version], params[:version_from])
128 @diff = @page.diff(params[:version], params[:version_from])
128 render_404 unless @diff
129 render_404 unless @diff
129 end
130 end
130
131
131 def annotate
132 def annotate
132 @annotate = @page.annotate(params[:version])
133 @annotate = @page.annotate(params[:version])
133 render_404 unless @annotate
134 render_404 unless @annotate
134 end
135 end
135
136
136 # Removes a wiki page and its history
137 # Removes a wiki page and its history
137 # Children can be either set as root pages, removed or reassigned to another parent page
138 # Children can be either set as root pages, removed or reassigned to another parent page
138 def destroy
139 def destroy
139 return render_403 unless editable?
140 return render_403 unless editable?
140
141
141 @descendants_count = @page.descendants.size
142 @descendants_count = @page.descendants.size
142 if @descendants_count > 0
143 if @descendants_count > 0
143 case params[:todo]
144 case params[:todo]
144 when 'nullify'
145 when 'nullify'
145 # Nothing to do
146 # Nothing to do
146 when 'destroy'
147 when 'destroy'
147 # Removes all its descendants
148 # Removes all its descendants
148 @page.descendants.each(&:destroy)
149 @page.descendants.each(&:destroy)
149 when 'reassign'
150 when 'reassign'
150 # Reassign children to another parent page
151 # Reassign children to another parent page
151 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
152 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
152 return unless reassign_to
153 return unless reassign_to
153 @page.children.each do |child|
154 @page.children.each do |child|
154 child.update_attribute(:parent, reassign_to)
155 child.update_attribute(:parent, reassign_to)
155 end
156 end
156 else
157 else
157 @reassignable_to = @wiki.pages - @page.self_and_descendants
158 @reassignable_to = @wiki.pages - @page.self_and_descendants
158 return
159 return
159 end
160 end
160 end
161 end
161 @page.destroy
162 @page.destroy
162 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
163 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
163 end
164 end
164
165
165 # display special pages
166 # display special pages
166 def special
167 def special
167 page_title = params[:page].downcase
168 page_title = params[:page].downcase
168 case page_title
169 case page_title
169 # show pages index, sorted by title
170 # show pages index, sorted by title
170 when 'page_index', 'date_index'
171 when 'page_index', 'date_index'
171 # eager load information about last updates, without loading text
172 # eager load information about last updates, without loading text
172 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
173 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
173 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
174 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
174 :order => 'title'
175 :order => 'title'
175 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
176 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
176 @pages_by_parent_id = @pages.group_by(&:parent_id)
177 @pages_by_parent_id = @pages.group_by(&:parent_id)
177 # export wiki to a single html file
178 # export wiki to a single html file
178 when 'export'
179 when 'export'
179 @pages = @wiki.pages.find :all, :order => 'title'
180 @pages = @wiki.pages.find :all, :order => 'title'
180 export = render_to_string :action => 'export_multiple', :layout => false
181 export = render_to_string :action => 'export_multiple', :layout => false
181 send_data(export, :type => 'text/html', :filename => "wiki.html")
182 send_data(export, :type => 'text/html', :filename => "wiki.html")
182 return
183 return
183 else
184 else
184 # requested special page doesn't exist, redirect to default page
185 # requested special page doesn't exist, redirect to default page
185 redirect_to :action => 'index', :id => @project, :page => nil and return
186 redirect_to :action => 'index', :id => @project, :page => nil and return
186 end
187 end
187 render :action => "special_#{page_title}"
188 render :action => "special_#{page_title}"
188 end
189 end
189
190
190 def preview
191 def preview
191 page = @wiki.find_page(params[:page])
192 page = @wiki.find_page(params[:page])
192 # page is nil when previewing a new page
193 # page is nil when previewing a new page
193 return render_403 unless page.nil? || editable?(page)
194 return render_403 unless page.nil? || editable?(page)
194 if page
195 if page
195 @attachements = page.attachments
196 @attachements = page.attachments
196 @previewed = page.content
197 @previewed = page.content
197 end
198 end
198 @text = params[:content][:text]
199 @text = params[:content][:text]
199 render :partial => 'common/preview'
200 render :partial => 'common/preview'
200 end
201 end
201
202
202 def add_attachment
203 def add_attachment
203 return render_403 unless editable?
204 return render_403 unless editable?
204 attach_files(@page, params[:attachments])
205 attach_files(@page, params[:attachments])
205 redirect_to :action => 'index', :page => @page.title
206 redirect_to :action => 'index', :page => @page.title
206 end
207 end
207
208
208 private
209 private
209
210
210 def find_wiki
211 def find_wiki
211 @project = Project.find(params[:id])
212 @project = Project.find(params[:id])
212 @wiki = @project.wiki
213 @wiki = @project.wiki
213 render_404 unless @wiki
214 render_404 unless @wiki
214 rescue ActiveRecord::RecordNotFound
215 rescue ActiveRecord::RecordNotFound
215 render_404
216 render_404
216 end
217 end
217
218
218 # Finds the requested page and returns a 404 error if it doesn't exist
219 # Finds the requested page and returns a 404 error if it doesn't exist
219 def find_existing_page
220 def find_existing_page
220 @page = @wiki.find_page(params[:page])
221 @page = @wiki.find_page(params[:page])
221 render_404 if @page.nil?
222 render_404 if @page.nil?
222 end
223 end
223
224
224 # Returns true if the current user is allowed to edit the page, otherwise false
225 # Returns true if the current user is allowed to edit the page, otherwise false
225 def editable?(page = @page)
226 def editable?(page = @page)
226 page.editable_by?(User.current)
227 page.editable_by?(User.current)
227 end
228 end
228
229
229 # Returns the default content of a new wiki page
230 # Returns the default content of a new wiki page
230 def initial_page_content(page)
231 def initial_page_content(page)
231 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
232 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
232 extend helper unless self.instance_of?(helper)
233 extend helper unless self.instance_of?(helper)
233 helper.instance_method(:initial_page_content).bind(self).call(page)
234 helper.instance_method(:initial_page_content).bind(self).call(page)
234 end
235 end
235 end
236 end
@@ -1,69 +1,70
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 <%= heads_for_wiki_formatter %>
10 <%= heads_for_wiki_formatter %>
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 <%= call_hook :view_layouts_base_html_head %>
17 <%= call_hook :view_layouts_base_html_head %>
18 <!-- page specific tags -->
18 <!-- page specific tags -->
19 <%= yield :header_tags -%>
19 <%= yield :header_tags -%>
20 </head>
20 </head>
21 <body>
21 <body>
22 <div id="wrapper">
22 <div id="wrapper">
23 <div id="top-menu">
23 <div id="top-menu">
24 <div id="account">
24 <div id="account">
25 <%= render_menu :account_menu -%>
25 <%= render_menu :account_menu -%>
26 </div>
26 </div>
27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
28 <%= render_menu :top_menu -%>
28 <%= render_menu :top_menu -%>
29 </div>
29 </div>
30
30
31 <div id="header">
31 <div id="header">
32 <div id="quick-search">
32 <div id="quick-search">
33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
34 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
34 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
35 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
35 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
36 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
36 <% end %>
37 <% end %>
37 <%= render_project_jump_box %>
38 <%= render_project_jump_box %>
38 </div>
39 </div>
39
40
40 <h1><%= page_header_title %></h1>
41 <h1><%= page_header_title %></h1>
41
42
42 <div id="main-menu">
43 <div id="main-menu">
43 <%= render_main_menu(@project) %>
44 <%= render_main_menu(@project) %>
44 </div>
45 </div>
45 </div>
46 </div>
46
47
47 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
48 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
48 <div id="sidebar">
49 <div id="sidebar">
49 <%= yield :sidebar %>
50 <%= yield :sidebar %>
50 <%= call_hook :view_layouts_base_sidebar %>
51 <%= call_hook :view_layouts_base_sidebar %>
51 </div>
52 </div>
52
53
53 <div id="content">
54 <div id="content">
54 <%= render_flash_messages %>
55 <%= render_flash_messages %>
55 <%= yield %>
56 <%= yield %>
56 <%= call_hook :view_layouts_base_content %>
57 <%= call_hook :view_layouts_base_content %>
57 <div style="clear:both;"></div>
58 <div style="clear:both;"></div>
58 </div>
59 </div>
59 </div>
60 </div>
60
61
61 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
62 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
62
63
63 <div id="footer">
64 <div id="footer">
64 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2009 Jean-Philippe Lang
65 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2009 Jean-Philippe Lang
65 </div>
66 </div>
66 </div>
67 </div>
67 <%= call_hook :view_layouts_base_body_bottom %>
68 <%= call_hook :view_layouts_base_body_bottom %>
68 </body>
69 </body>
69 </html>
70 </html>
@@ -1,1096 +1,1103
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < ActionController::TestCase
24 class IssuesControllerTest < ActionController::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :member_roles,
29 :member_roles,
30 :issues,
30 :issues,
31 :issue_statuses,
31 :issue_statuses,
32 :versions,
32 :versions,
33 :trackers,
33 :trackers,
34 :projects_trackers,
34 :projects_trackers,
35 :issue_categories,
35 :issue_categories,
36 :enabled_modules,
36 :enabled_modules,
37 :enumerations,
37 :enumerations,
38 :attachments,
38 :attachments,
39 :workflows,
39 :workflows,
40 :custom_fields,
40 :custom_fields,
41 :custom_values,
41 :custom_values,
42 :custom_fields_trackers,
42 :custom_fields_trackers,
43 :time_entries,
43 :time_entries,
44 :journals,
44 :journals,
45 :journal_details,
45 :journal_details,
46 :queries
46 :queries
47
47
48 def setup
48 def setup
49 @controller = IssuesController.new
49 @controller = IssuesController.new
50 @request = ActionController::TestRequest.new
50 @request = ActionController::TestRequest.new
51 @response = ActionController::TestResponse.new
51 @response = ActionController::TestResponse.new
52 User.current = nil
52 User.current = nil
53 end
53 end
54
54
55 def test_index_routing
55 def test_index_routing
56 assert_routing(
56 assert_routing(
57 {:method => :get, :path => '/issues'},
57 {:method => :get, :path => '/issues'},
58 :controller => 'issues', :action => 'index'
58 :controller => 'issues', :action => 'index'
59 )
59 )
60 end
60 end
61
61
62 def test_index
62 def test_index
63 Setting.default_language = 'en'
63 Setting.default_language = 'en'
64
64
65 get :index
65 get :index
66 assert_response :success
66 assert_response :success
67 assert_template 'index.rhtml'
67 assert_template 'index.rhtml'
68 assert_not_nil assigns(:issues)
68 assert_not_nil assigns(:issues)
69 assert_nil assigns(:project)
69 assert_nil assigns(:project)
70 assert_tag :tag => 'a', :content => /Can't print recipes/
70 assert_tag :tag => 'a', :content => /Can't print recipes/
71 assert_tag :tag => 'a', :content => /Subproject issue/
71 assert_tag :tag => 'a', :content => /Subproject issue/
72 # private projects hidden
72 # private projects hidden
73 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
73 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
74 assert_no_tag :tag => 'a', :content => /Issue on project 2/
74 assert_no_tag :tag => 'a', :content => /Issue on project 2/
75 # project column
75 # project column
76 assert_tag :tag => 'th', :content => /Project/
76 assert_tag :tag => 'th', :content => /Project/
77 end
77 end
78
78
79 def test_index_should_not_list_issues_when_module_disabled
79 def test_index_should_not_list_issues_when_module_disabled
80 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
80 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
81 get :index
81 get :index
82 assert_response :success
82 assert_response :success
83 assert_template 'index.rhtml'
83 assert_template 'index.rhtml'
84 assert_not_nil assigns(:issues)
84 assert_not_nil assigns(:issues)
85 assert_nil assigns(:project)
85 assert_nil assigns(:project)
86 assert_no_tag :tag => 'a', :content => /Can't print recipes/
86 assert_no_tag :tag => 'a', :content => /Can't print recipes/
87 assert_tag :tag => 'a', :content => /Subproject issue/
87 assert_tag :tag => 'a', :content => /Subproject issue/
88 end
88 end
89
89
90 def test_index_with_project_routing
90 def test_index_with_project_routing
91 assert_routing(
91 assert_routing(
92 {:method => :get, :path => '/projects/23/issues'},
92 {:method => :get, :path => '/projects/23/issues'},
93 :controller => 'issues', :action => 'index', :project_id => '23'
93 :controller => 'issues', :action => 'index', :project_id => '23'
94 )
94 )
95 end
95 end
96
96
97 def test_index_should_not_list_issues_when_module_disabled
97 def test_index_should_not_list_issues_when_module_disabled
98 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
98 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
99 get :index
99 get :index
100 assert_response :success
100 assert_response :success
101 assert_template 'index.rhtml'
101 assert_template 'index.rhtml'
102 assert_not_nil assigns(:issues)
102 assert_not_nil assigns(:issues)
103 assert_nil assigns(:project)
103 assert_nil assigns(:project)
104 assert_no_tag :tag => 'a', :content => /Can't print recipes/
104 assert_no_tag :tag => 'a', :content => /Can't print recipes/
105 assert_tag :tag => 'a', :content => /Subproject issue/
105 assert_tag :tag => 'a', :content => /Subproject issue/
106 end
106 end
107
107
108 def test_index_with_project_routing
108 def test_index_with_project_routing
109 assert_routing(
109 assert_routing(
110 {:method => :get, :path => 'projects/23/issues'},
110 {:method => :get, :path => 'projects/23/issues'},
111 :controller => 'issues', :action => 'index', :project_id => '23'
111 :controller => 'issues', :action => 'index', :project_id => '23'
112 )
112 )
113 end
113 end
114
114
115 def test_index_with_project
115 def test_index_with_project
116 Setting.display_subprojects_issues = 0
116 Setting.display_subprojects_issues = 0
117 get :index, :project_id => 1
117 get :index, :project_id => 1
118 assert_response :success
118 assert_response :success
119 assert_template 'index.rhtml'
119 assert_template 'index.rhtml'
120 assert_not_nil assigns(:issues)
120 assert_not_nil assigns(:issues)
121 assert_tag :tag => 'a', :content => /Can't print recipes/
121 assert_tag :tag => 'a', :content => /Can't print recipes/
122 assert_no_tag :tag => 'a', :content => /Subproject issue/
122 assert_no_tag :tag => 'a', :content => /Subproject issue/
123 end
123 end
124
124
125 def test_index_with_project_and_subprojects
125 def test_index_with_project_and_subprojects
126 Setting.display_subprojects_issues = 1
126 Setting.display_subprojects_issues = 1
127 get :index, :project_id => 1
127 get :index, :project_id => 1
128 assert_response :success
128 assert_response :success
129 assert_template 'index.rhtml'
129 assert_template 'index.rhtml'
130 assert_not_nil assigns(:issues)
130 assert_not_nil assigns(:issues)
131 assert_tag :tag => 'a', :content => /Can't print recipes/
131 assert_tag :tag => 'a', :content => /Can't print recipes/
132 assert_tag :tag => 'a', :content => /Subproject issue/
132 assert_tag :tag => 'a', :content => /Subproject issue/
133 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
133 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
134 end
134 end
135
135
136 def test_index_with_project_and_subprojects_should_show_private_subprojects
136 def test_index_with_project_and_subprojects_should_show_private_subprojects
137 @request.session[:user_id] = 2
137 @request.session[:user_id] = 2
138 Setting.display_subprojects_issues = 1
138 Setting.display_subprojects_issues = 1
139 get :index, :project_id => 1
139 get :index, :project_id => 1
140 assert_response :success
140 assert_response :success
141 assert_template 'index.rhtml'
141 assert_template 'index.rhtml'
142 assert_not_nil assigns(:issues)
142 assert_not_nil assigns(:issues)
143 assert_tag :tag => 'a', :content => /Can't print recipes/
143 assert_tag :tag => 'a', :content => /Can't print recipes/
144 assert_tag :tag => 'a', :content => /Subproject issue/
144 assert_tag :tag => 'a', :content => /Subproject issue/
145 assert_tag :tag => 'a', :content => /Issue of a private subproject/
145 assert_tag :tag => 'a', :content => /Issue of a private subproject/
146 end
146 end
147
147
148 def test_index_with_project_routing_formatted
148 def test_index_with_project_routing_formatted
149 assert_routing(
149 assert_routing(
150 {:method => :get, :path => 'projects/23/issues.pdf'},
150 {:method => :get, :path => 'projects/23/issues.pdf'},
151 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
151 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
152 )
152 )
153 assert_routing(
153 assert_routing(
154 {:method => :get, :path => 'projects/23/issues.atom'},
154 {:method => :get, :path => 'projects/23/issues.atom'},
155 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
155 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
156 )
156 )
157 end
157 end
158
158
159 def test_index_with_project_and_filter
159 def test_index_with_project_and_filter
160 get :index, :project_id => 1, :set_filter => 1
160 get :index, :project_id => 1, :set_filter => 1
161 assert_response :success
161 assert_response :success
162 assert_template 'index.rhtml'
162 assert_template 'index.rhtml'
163 assert_not_nil assigns(:issues)
163 assert_not_nil assigns(:issues)
164 end
164 end
165
165
166 def test_index_with_query
166 def test_index_with_query
167 get :index, :project_id => 1, :query_id => 5
167 get :index, :project_id => 1, :query_id => 5
168 assert_response :success
168 assert_response :success
169 assert_template 'index.rhtml'
169 assert_template 'index.rhtml'
170 assert_not_nil assigns(:issues)
170 assert_not_nil assigns(:issues)
171 assert_nil assigns(:issue_count_by_group)
171 assert_nil assigns(:issue_count_by_group)
172 end
172 end
173
173
174 def test_index_with_grouped_query
174 def test_index_with_grouped_query
175 get :index, :project_id => 1, :query_id => 6
175 get :index, :project_id => 1, :query_id => 6
176 assert_response :success
176 assert_response :success
177 assert_template 'index.rhtml'
177 assert_template 'index.rhtml'
178 assert_not_nil assigns(:issues)
178 assert_not_nil assigns(:issues)
179 assert_not_nil assigns(:issue_count_by_group)
179 assert_not_nil assigns(:issue_count_by_group)
180 end
180 end
181
181
182 def test_index_csv_with_project
182 def test_index_csv_with_project
183 get :index, :format => 'csv'
183 get :index, :format => 'csv'
184 assert_response :success
184 assert_response :success
185 assert_not_nil assigns(:issues)
185 assert_not_nil assigns(:issues)
186 assert_equal 'text/csv', @response.content_type
186 assert_equal 'text/csv', @response.content_type
187
187
188 get :index, :project_id => 1, :format => 'csv'
188 get :index, :project_id => 1, :format => 'csv'
189 assert_response :success
189 assert_response :success
190 assert_not_nil assigns(:issues)
190 assert_not_nil assigns(:issues)
191 assert_equal 'text/csv', @response.content_type
191 assert_equal 'text/csv', @response.content_type
192 end
192 end
193
193
194 def test_index_formatted
194 def test_index_formatted
195 assert_routing(
195 assert_routing(
196 {:method => :get, :path => 'issues.pdf'},
196 {:method => :get, :path => 'issues.pdf'},
197 :controller => 'issues', :action => 'index', :format => 'pdf'
197 :controller => 'issues', :action => 'index', :format => 'pdf'
198 )
198 )
199 assert_routing(
199 assert_routing(
200 {:method => :get, :path => 'issues.atom'},
200 {:method => :get, :path => 'issues.atom'},
201 :controller => 'issues', :action => 'index', :format => 'atom'
201 :controller => 'issues', :action => 'index', :format => 'atom'
202 )
202 )
203 end
203 end
204
204
205 def test_index_pdf
205 def test_index_pdf
206 get :index, :format => 'pdf'
206 get :index, :format => 'pdf'
207 assert_response :success
207 assert_response :success
208 assert_not_nil assigns(:issues)
208 assert_not_nil assigns(:issues)
209 assert_equal 'application/pdf', @response.content_type
209 assert_equal 'application/pdf', @response.content_type
210
210
211 get :index, :project_id => 1, :format => 'pdf'
211 get :index, :project_id => 1, :format => 'pdf'
212 assert_response :success
212 assert_response :success
213 assert_not_nil assigns(:issues)
213 assert_not_nil assigns(:issues)
214 assert_equal 'application/pdf', @response.content_type
214 assert_equal 'application/pdf', @response.content_type
215
215
216 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
216 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
217 assert_response :success
217 assert_response :success
218 assert_not_nil assigns(:issues)
218 assert_not_nil assigns(:issues)
219 assert_equal 'application/pdf', @response.content_type
219 assert_equal 'application/pdf', @response.content_type
220 end
220 end
221
221
222 def test_index_sort
222 def test_index_sort
223 get :index, :sort => 'tracker,id:desc'
223 get :index, :sort => 'tracker,id:desc'
224 assert_response :success
224 assert_response :success
225
225
226 sort_params = @request.session['issues_index_sort']
226 sort_params = @request.session['issues_index_sort']
227 assert sort_params.is_a?(String)
227 assert sort_params.is_a?(String)
228 assert_equal 'tracker,id:desc', sort_params
228 assert_equal 'tracker,id:desc', sort_params
229
229
230 issues = assigns(:issues)
230 issues = assigns(:issues)
231 assert_not_nil issues
231 assert_not_nil issues
232 assert !issues.empty?
232 assert !issues.empty?
233 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
233 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
234 end
234 end
235
235
236 def test_gantt
236 def test_gantt
237 get :gantt, :project_id => 1
237 get :gantt, :project_id => 1
238 assert_response :success
238 assert_response :success
239 assert_template 'gantt.rhtml'
239 assert_template 'gantt.rhtml'
240 assert_not_nil assigns(:gantt)
240 assert_not_nil assigns(:gantt)
241 events = assigns(:gantt).events
241 events = assigns(:gantt).events
242 assert_not_nil events
242 assert_not_nil events
243 # Issue with start and due dates
243 # Issue with start and due dates
244 i = Issue.find(1)
244 i = Issue.find(1)
245 assert_not_nil i.due_date
245 assert_not_nil i.due_date
246 assert events.include?(Issue.find(1))
246 assert events.include?(Issue.find(1))
247 # Issue with without due date but targeted to a version with date
247 # Issue with without due date but targeted to a version with date
248 i = Issue.find(2)
248 i = Issue.find(2)
249 assert_nil i.due_date
249 assert_nil i.due_date
250 assert events.include?(i)
250 assert events.include?(i)
251 end
251 end
252
252
253 def test_cross_project_gantt
253 def test_cross_project_gantt
254 get :gantt
254 get :gantt
255 assert_response :success
255 assert_response :success
256 assert_template 'gantt.rhtml'
256 assert_template 'gantt.rhtml'
257 assert_not_nil assigns(:gantt)
257 assert_not_nil assigns(:gantt)
258 events = assigns(:gantt).events
258 events = assigns(:gantt).events
259 assert_not_nil events
259 assert_not_nil events
260 end
260 end
261
261
262 def test_gantt_export_to_pdf
262 def test_gantt_export_to_pdf
263 get :gantt, :project_id => 1, :format => 'pdf'
263 get :gantt, :project_id => 1, :format => 'pdf'
264 assert_response :success
264 assert_response :success
265 assert_equal 'application/pdf', @response.content_type
265 assert_equal 'application/pdf', @response.content_type
266 assert @response.body.starts_with?('%PDF')
266 assert @response.body.starts_with?('%PDF')
267 assert_not_nil assigns(:gantt)
267 assert_not_nil assigns(:gantt)
268 end
268 end
269
269
270 def test_cross_project_gantt_export_to_pdf
270 def test_cross_project_gantt_export_to_pdf
271 get :gantt, :format => 'pdf'
271 get :gantt, :format => 'pdf'
272 assert_response :success
272 assert_response :success
273 assert_equal 'application/pdf', @response.content_type
273 assert_equal 'application/pdf', @response.content_type
274 assert @response.body.starts_with?('%PDF')
274 assert @response.body.starts_with?('%PDF')
275 assert_not_nil assigns(:gantt)
275 assert_not_nil assigns(:gantt)
276 end
276 end
277
277
278 if Object.const_defined?(:Magick)
278 if Object.const_defined?(:Magick)
279 def test_gantt_image
279 def test_gantt_image
280 get :gantt, :project_id => 1, :format => 'png'
280 get :gantt, :project_id => 1, :format => 'png'
281 assert_response :success
281 assert_response :success
282 assert_equal 'image/png', @response.content_type
282 assert_equal 'image/png', @response.content_type
283 end
283 end
284 else
284 else
285 puts "RMagick not installed. Skipping tests !!!"
285 puts "RMagick not installed. Skipping tests !!!"
286 end
286 end
287
287
288 def test_calendar
288 def test_calendar
289 get :calendar, :project_id => 1
289 get :calendar, :project_id => 1
290 assert_response :success
290 assert_response :success
291 assert_template 'calendar'
291 assert_template 'calendar'
292 assert_not_nil assigns(:calendar)
292 assert_not_nil assigns(:calendar)
293 end
293 end
294
294
295 def test_cross_project_calendar
295 def test_cross_project_calendar
296 get :calendar
296 get :calendar
297 assert_response :success
297 assert_response :success
298 assert_template 'calendar'
298 assert_template 'calendar'
299 assert_not_nil assigns(:calendar)
299 assert_not_nil assigns(:calendar)
300 end
300 end
301
301
302 def test_changes
302 def test_changes
303 get :changes, :project_id => 1
303 get :changes, :project_id => 1
304 assert_response :success
304 assert_response :success
305 assert_not_nil assigns(:journals)
305 assert_not_nil assigns(:journals)
306 assert_equal 'application/atom+xml', @response.content_type
306 assert_equal 'application/atom+xml', @response.content_type
307 end
307 end
308
308
309 def test_show_routing
309 def test_show_routing
310 assert_routing(
310 assert_routing(
311 {:method => :get, :path => '/issues/64'},
311 {:method => :get, :path => '/issues/64'},
312 :controller => 'issues', :action => 'show', :id => '64'
312 :controller => 'issues', :action => 'show', :id => '64'
313 )
313 )
314 end
314 end
315
315
316 def test_show_routing_formatted
316 def test_show_routing_formatted
317 assert_routing(
317 assert_routing(
318 {:method => :get, :path => '/issues/2332.pdf'},
318 {:method => :get, :path => '/issues/2332.pdf'},
319 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
319 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
320 )
320 )
321 assert_routing(
321 assert_routing(
322 {:method => :get, :path => '/issues/23123.atom'},
322 {:method => :get, :path => '/issues/23123.atom'},
323 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
323 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
324 )
324 )
325 end
325 end
326
326
327 def test_show_by_anonymous
327 def test_show_by_anonymous
328 get :show, :id => 1
328 get :show, :id => 1
329 assert_response :success
329 assert_response :success
330 assert_template 'show.rhtml'
330 assert_template 'show.rhtml'
331 assert_not_nil assigns(:issue)
331 assert_not_nil assigns(:issue)
332 assert_equal Issue.find(1), assigns(:issue)
332 assert_equal Issue.find(1), assigns(:issue)
333
333
334 # anonymous role is allowed to add a note
334 # anonymous role is allowed to add a note
335 assert_tag :tag => 'form',
335 assert_tag :tag => 'form',
336 :descendant => { :tag => 'fieldset',
336 :descendant => { :tag => 'fieldset',
337 :child => { :tag => 'legend',
337 :child => { :tag => 'legend',
338 :content => /Notes/ } }
338 :content => /Notes/ } }
339 end
339 end
340
340
341 def test_show_by_manager
341 def test_show_by_manager
342 @request.session[:user_id] = 2
342 @request.session[:user_id] = 2
343 get :show, :id => 1
343 get :show, :id => 1
344 assert_response :success
344 assert_response :success
345
345
346 assert_tag :tag => 'form',
346 assert_tag :tag => 'form',
347 :descendant => { :tag => 'fieldset',
347 :descendant => { :tag => 'fieldset',
348 :child => { :tag => 'legend',
348 :child => { :tag => 'legend',
349 :content => /Change properties/ } },
349 :content => /Change properties/ } },
350 :descendant => { :tag => 'fieldset',
350 :descendant => { :tag => 'fieldset',
351 :child => { :tag => 'legend',
351 :child => { :tag => 'legend',
352 :content => /Log time/ } },
352 :content => /Log time/ } },
353 :descendant => { :tag => 'fieldset',
353 :descendant => { :tag => 'fieldset',
354 :child => { :tag => 'legend',
354 :child => { :tag => 'legend',
355 :content => /Notes/ } }
355 :content => /Notes/ } }
356 end
356 end
357
357
358 def test_show_should_not_disclose_relations_to_invisible_issues
358 def test_show_should_not_disclose_relations_to_invisible_issues
359 Setting.cross_project_issue_relations = '1'
359 Setting.cross_project_issue_relations = '1'
360 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
360 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
361 # Relation to a private project issue
361 # Relation to a private project issue
362 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
362 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
363
363
364 get :show, :id => 1
364 get :show, :id => 1
365 assert_response :success
365 assert_response :success
366
366
367 assert_tag :div, :attributes => { :id => 'relations' },
367 assert_tag :div, :attributes => { :id => 'relations' },
368 :descendant => { :tag => 'a', :content => /#2$/ }
368 :descendant => { :tag => 'a', :content => /#2$/ }
369 assert_no_tag :div, :attributes => { :id => 'relations' },
369 assert_no_tag :div, :attributes => { :id => 'relations' },
370 :descendant => { :tag => 'a', :content => /#4$/ }
370 :descendant => { :tag => 'a', :content => /#4$/ }
371 end
371 end
372
372
373 def test_show_atom
373 def test_show_atom
374 get :show, :id => 2, :format => 'atom'
374 get :show, :id => 2, :format => 'atom'
375 assert_response :success
375 assert_response :success
376 assert_template 'changes.rxml'
376 assert_template 'changes.rxml'
377 # Inline image
377 # Inline image
378 assert @response.body.include?("&lt;img src=&quot;http://test.host/attachments/download/10&quot; alt=&quot;&quot; /&gt;")
378 assert @response.body.include?("&lt;img src=&quot;http://test.host/attachments/download/10&quot; alt=&quot;&quot; /&gt;")
379 end
379 end
380
380
381 def test_new_routing
381 def test_new_routing
382 assert_routing(
382 assert_routing(
383 {:method => :get, :path => '/projects/1/issues/new'},
383 {:method => :get, :path => '/projects/1/issues/new'},
384 :controller => 'issues', :action => 'new', :project_id => '1'
384 :controller => 'issues', :action => 'new', :project_id => '1'
385 )
385 )
386 assert_recognizes(
386 assert_recognizes(
387 {:controller => 'issues', :action => 'new', :project_id => '1'},
387 {:controller => 'issues', :action => 'new', :project_id => '1'},
388 {:method => :post, :path => '/projects/1/issues'}
388 {:method => :post, :path => '/projects/1/issues'}
389 )
389 )
390 end
390 end
391
391
392 def test_show_export_to_pdf
392 def test_show_export_to_pdf
393 get :show, :id => 3, :format => 'pdf'
393 get :show, :id => 3, :format => 'pdf'
394 assert_response :success
394 assert_response :success
395 assert_equal 'application/pdf', @response.content_type
395 assert_equal 'application/pdf', @response.content_type
396 assert @response.body.starts_with?('%PDF')
396 assert @response.body.starts_with?('%PDF')
397 assert_not_nil assigns(:issue)
397 assert_not_nil assigns(:issue)
398 end
398 end
399
399
400 def test_get_new
400 def test_get_new
401 @request.session[:user_id] = 2
401 @request.session[:user_id] = 2
402 get :new, :project_id => 1, :tracker_id => 1
402 get :new, :project_id => 1, :tracker_id => 1
403 assert_response :success
403 assert_response :success
404 assert_template 'new'
404 assert_template 'new'
405
405
406 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
406 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
407 :value => 'Default string' }
407 :value => 'Default string' }
408 end
408 end
409
409
410 def test_get_new_without_tracker_id
410 def test_get_new_without_tracker_id
411 @request.session[:user_id] = 2
411 @request.session[:user_id] = 2
412 get :new, :project_id => 1
412 get :new, :project_id => 1
413 assert_response :success
413 assert_response :success
414 assert_template 'new'
414 assert_template 'new'
415
415
416 issue = assigns(:issue)
416 issue = assigns(:issue)
417 assert_not_nil issue
417 assert_not_nil issue
418 assert_equal Project.find(1).trackers.first, issue.tracker
418 assert_equal Project.find(1).trackers.first, issue.tracker
419 end
419 end
420
420
421 def test_get_new_with_no_default_status_should_display_an_error
421 def test_get_new_with_no_default_status_should_display_an_error
422 @request.session[:user_id] = 2
422 @request.session[:user_id] = 2
423 IssueStatus.delete_all
423 IssueStatus.delete_all
424
424
425 get :new, :project_id => 1
425 get :new, :project_id => 1
426 assert_response 500
426 assert_response 500
427 assert_not_nil flash[:error]
427 assert_not_nil flash[:error]
428 assert_tag :tag => 'div', :attributes => { :class => /error/ },
428 assert_tag :tag => 'div', :attributes => { :class => /error/ },
429 :content => /No default issue/
429 :content => /No default issue/
430 end
430 end
431
431
432 def test_get_new_with_no_tracker_should_display_an_error
432 def test_get_new_with_no_tracker_should_display_an_error
433 @request.session[:user_id] = 2
433 @request.session[:user_id] = 2
434 Tracker.delete_all
434 Tracker.delete_all
435
435
436 get :new, :project_id => 1
436 get :new, :project_id => 1
437 assert_response 500
437 assert_response 500
438 assert_not_nil flash[:error]
438 assert_not_nil flash[:error]
439 assert_tag :tag => 'div', :attributes => { :class => /error/ },
439 assert_tag :tag => 'div', :attributes => { :class => /error/ },
440 :content => /No tracker/
440 :content => /No tracker/
441 end
441 end
442
442
443 def test_update_new_form
443 def test_update_new_form
444 @request.session[:user_id] = 2
444 @request.session[:user_id] = 2
445 xhr :post, :new, :project_id => 1,
445 xhr :post, :new, :project_id => 1,
446 :issue => {:tracker_id => 2,
446 :issue => {:tracker_id => 2,
447 :subject => 'This is the test_new issue',
447 :subject => 'This is the test_new issue',
448 :description => 'This is the description',
448 :description => 'This is the description',
449 :priority_id => 5}
449 :priority_id => 5}
450 assert_response :success
450 assert_response :success
451 assert_template 'new'
451 assert_template 'new'
452 end
452 end
453
453
454 def test_post_new
454 def test_post_new
455 @request.session[:user_id] = 2
455 @request.session[:user_id] = 2
456 assert_difference 'Issue.count' do
456 assert_difference 'Issue.count' do
457 post :new, :project_id => 1,
457 post :new, :project_id => 1,
458 :issue => {:tracker_id => 3,
458 :issue => {:tracker_id => 3,
459 :subject => 'This is the test_new issue',
459 :subject => 'This is the test_new issue',
460 :description => 'This is the description',
460 :description => 'This is the description',
461 :priority_id => 5,
461 :priority_id => 5,
462 :estimated_hours => '',
462 :estimated_hours => '',
463 :custom_field_values => {'2' => 'Value for field 2'}}
463 :custom_field_values => {'2' => 'Value for field 2'}}
464 end
464 end
465 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
465 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
466
466
467 issue = Issue.find_by_subject('This is the test_new issue')
467 issue = Issue.find_by_subject('This is the test_new issue')
468 assert_not_nil issue
468 assert_not_nil issue
469 assert_equal 2, issue.author_id
469 assert_equal 2, issue.author_id
470 assert_equal 3, issue.tracker_id
470 assert_equal 3, issue.tracker_id
471 assert_nil issue.estimated_hours
471 assert_nil issue.estimated_hours
472 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
472 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
473 assert_not_nil v
473 assert_not_nil v
474 assert_equal 'Value for field 2', v.value
474 assert_equal 'Value for field 2', v.value
475 end
475 end
476
476
477 def test_post_new_and_continue
477 def test_post_new_and_continue
478 @request.session[:user_id] = 2
478 @request.session[:user_id] = 2
479 post :new, :project_id => 1,
479 post :new, :project_id => 1,
480 :issue => {:tracker_id => 3,
480 :issue => {:tracker_id => 3,
481 :subject => 'This is first issue',
481 :subject => 'This is first issue',
482 :priority_id => 5},
482 :priority_id => 5},
483 :continue => ''
483 :continue => ''
484 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
484 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
485 end
485 end
486
486
487 def test_post_new_without_custom_fields_param
487 def test_post_new_without_custom_fields_param
488 @request.session[:user_id] = 2
488 @request.session[:user_id] = 2
489 assert_difference 'Issue.count' do
489 assert_difference 'Issue.count' do
490 post :new, :project_id => 1,
490 post :new, :project_id => 1,
491 :issue => {:tracker_id => 1,
491 :issue => {:tracker_id => 1,
492 :subject => 'This is the test_new issue',
492 :subject => 'This is the test_new issue',
493 :description => 'This is the description',
493 :description => 'This is the description',
494 :priority_id => 5}
494 :priority_id => 5}
495 end
495 end
496 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
496 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
497 end
497 end
498
498
499 def test_post_new_with_required_custom_field_and_without_custom_fields_param
499 def test_post_new_with_required_custom_field_and_without_custom_fields_param
500 field = IssueCustomField.find_by_name('Database')
500 field = IssueCustomField.find_by_name('Database')
501 field.update_attribute(:is_required, true)
501 field.update_attribute(:is_required, true)
502
502
503 @request.session[:user_id] = 2
503 @request.session[:user_id] = 2
504 post :new, :project_id => 1,
504 post :new, :project_id => 1,
505 :issue => {:tracker_id => 1,
505 :issue => {:tracker_id => 1,
506 :subject => 'This is the test_new issue',
506 :subject => 'This is the test_new issue',
507 :description => 'This is the description',
507 :description => 'This is the description',
508 :priority_id => 5}
508 :priority_id => 5}
509 assert_response :success
509 assert_response :success
510 assert_template 'new'
510 assert_template 'new'
511 issue = assigns(:issue)
511 issue = assigns(:issue)
512 assert_not_nil issue
512 assert_not_nil issue
513 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
513 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
514 end
514 end
515
515
516 def test_post_new_with_watchers
516 def test_post_new_with_watchers
517 @request.session[:user_id] = 2
517 @request.session[:user_id] = 2
518 ActionMailer::Base.deliveries.clear
518 ActionMailer::Base.deliveries.clear
519
519
520 assert_difference 'Watcher.count', 2 do
520 assert_difference 'Watcher.count', 2 do
521 post :new, :project_id => 1,
521 post :new, :project_id => 1,
522 :issue => {:tracker_id => 1,
522 :issue => {:tracker_id => 1,
523 :subject => 'This is a new issue with watchers',
523 :subject => 'This is a new issue with watchers',
524 :description => 'This is the description',
524 :description => 'This is the description',
525 :priority_id => 5,
525 :priority_id => 5,
526 :watcher_user_ids => ['2', '3']}
526 :watcher_user_ids => ['2', '3']}
527 end
527 end
528 issue = Issue.find_by_subject('This is a new issue with watchers')
528 issue = Issue.find_by_subject('This is a new issue with watchers')
529 assert_not_nil issue
529 assert_not_nil issue
530 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
530 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
531
531
532 # Watchers added
532 # Watchers added
533 assert_equal [2, 3], issue.watcher_user_ids.sort
533 assert_equal [2, 3], issue.watcher_user_ids.sort
534 assert issue.watched_by?(User.find(3))
534 assert issue.watched_by?(User.find(3))
535 # Watchers notified
535 # Watchers notified
536 mail = ActionMailer::Base.deliveries.last
536 mail = ActionMailer::Base.deliveries.last
537 assert_kind_of TMail::Mail, mail
537 assert_kind_of TMail::Mail, mail
538 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
538 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
539 end
539 end
540
540
541 def test_post_new_should_send_a_notification
541 def test_post_new_should_send_a_notification
542 ActionMailer::Base.deliveries.clear
542 ActionMailer::Base.deliveries.clear
543 @request.session[:user_id] = 2
543 @request.session[:user_id] = 2
544 assert_difference 'Issue.count' do
544 assert_difference 'Issue.count' do
545 post :new, :project_id => 1,
545 post :new, :project_id => 1,
546 :issue => {:tracker_id => 3,
546 :issue => {:tracker_id => 3,
547 :subject => 'This is the test_new issue',
547 :subject => 'This is the test_new issue',
548 :description => 'This is the description',
548 :description => 'This is the description',
549 :priority_id => 5,
549 :priority_id => 5,
550 :estimated_hours => '',
550 :estimated_hours => '',
551 :custom_field_values => {'2' => 'Value for field 2'}}
551 :custom_field_values => {'2' => 'Value for field 2'}}
552 end
552 end
553 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
553 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
554
554
555 assert_equal 1, ActionMailer::Base.deliveries.size
555 assert_equal 1, ActionMailer::Base.deliveries.size
556 end
556 end
557
557
558 def test_post_should_preserve_fields_values_on_validation_failure
558 def test_post_should_preserve_fields_values_on_validation_failure
559 @request.session[:user_id] = 2
559 @request.session[:user_id] = 2
560 post :new, :project_id => 1,
560 post :new, :project_id => 1,
561 :issue => {:tracker_id => 1,
561 :issue => {:tracker_id => 1,
562 # empty subject
562 # empty subject
563 :subject => '',
563 :subject => '',
564 :description => 'This is a description',
564 :description => 'This is a description',
565 :priority_id => 6,
565 :priority_id => 6,
566 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
566 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
567 assert_response :success
567 assert_response :success
568 assert_template 'new'
568 assert_template 'new'
569
569
570 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
570 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
571 :content => 'This is a description'
571 :content => 'This is a description'
572 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
572 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
573 :child => { :tag => 'option', :attributes => { :selected => 'selected',
573 :child => { :tag => 'option', :attributes => { :selected => 'selected',
574 :value => '6' },
574 :value => '6' },
575 :content => 'High' }
575 :content => 'High' }
576 # Custom fields
576 # Custom fields
577 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
577 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
578 :child => { :tag => 'option', :attributes => { :selected => 'selected',
578 :child => { :tag => 'option', :attributes => { :selected => 'selected',
579 :value => 'Oracle' },
579 :value => 'Oracle' },
580 :content => 'Oracle' }
580 :content => 'Oracle' }
581 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
581 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
582 :value => 'Value for field 2'}
582 :value => 'Value for field 2'}
583 end
583 end
584
584
585 def test_copy_routing
585 def test_copy_routing
586 assert_routing(
586 assert_routing(
587 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
587 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
588 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
588 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
589 )
589 )
590 end
590 end
591
591
592 def test_copy_issue
592 def test_copy_issue
593 @request.session[:user_id] = 2
593 @request.session[:user_id] = 2
594 get :new, :project_id => 1, :copy_from => 1
594 get :new, :project_id => 1, :copy_from => 1
595 assert_template 'new'
595 assert_template 'new'
596 assert_not_nil assigns(:issue)
596 assert_not_nil assigns(:issue)
597 orig = Issue.find(1)
597 orig = Issue.find(1)
598 assert_equal orig.subject, assigns(:issue).subject
598 assert_equal orig.subject, assigns(:issue).subject
599 end
599 end
600
600
601 def test_edit_routing
601 def test_edit_routing
602 assert_routing(
602 assert_routing(
603 {:method => :get, :path => '/issues/1/edit'},
603 {:method => :get, :path => '/issues/1/edit'},
604 :controller => 'issues', :action => 'edit', :id => '1'
604 :controller => 'issues', :action => 'edit', :id => '1'
605 )
605 )
606 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
606 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
607 {:controller => 'issues', :action => 'edit', :id => '1'},
607 {:controller => 'issues', :action => 'edit', :id => '1'},
608 {:method => :post, :path => '/issues/1/edit'}
608 {:method => :post, :path => '/issues/1/edit'}
609 )
609 )
610 end
610 end
611
611
612 def test_get_edit
612 def test_get_edit
613 @request.session[:user_id] = 2
613 @request.session[:user_id] = 2
614 get :edit, :id => 1
614 get :edit, :id => 1
615 assert_response :success
615 assert_response :success
616 assert_template 'edit'
616 assert_template 'edit'
617 assert_not_nil assigns(:issue)
617 assert_not_nil assigns(:issue)
618 assert_equal Issue.find(1), assigns(:issue)
618 assert_equal Issue.find(1), assigns(:issue)
619 end
619 end
620
620
621 def test_get_edit_with_params
621 def test_get_edit_with_params
622 @request.session[:user_id] = 2
622 @request.session[:user_id] = 2
623 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
623 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
624 assert_response :success
624 assert_response :success
625 assert_template 'edit'
625 assert_template 'edit'
626
626
627 issue = assigns(:issue)
627 issue = assigns(:issue)
628 assert_not_nil issue
628 assert_not_nil issue
629
629
630 assert_equal 5, issue.status_id
630 assert_equal 5, issue.status_id
631 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
631 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
632 :child => { :tag => 'option',
632 :child => { :tag => 'option',
633 :content => 'Closed',
633 :content => 'Closed',
634 :attributes => { :selected => 'selected' } }
634 :attributes => { :selected => 'selected' } }
635
635
636 assert_equal 7, issue.priority_id
636 assert_equal 7, issue.priority_id
637 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
637 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
638 :child => { :tag => 'option',
638 :child => { :tag => 'option',
639 :content => 'Urgent',
639 :content => 'Urgent',
640 :attributes => { :selected => 'selected' } }
640 :attributes => { :selected => 'selected' } }
641 end
641 end
642
642
643 def test_reply_routing
643 def test_reply_routing
644 assert_routing(
644 assert_routing(
645 {:method => :post, :path => '/issues/1/quoted'},
645 {:method => :post, :path => '/issues/1/quoted'},
646 :controller => 'issues', :action => 'reply', :id => '1'
646 :controller => 'issues', :action => 'reply', :id => '1'
647 )
647 )
648 end
648 end
649
649
650 def test_reply_to_issue
650 def test_reply_to_issue
651 @request.session[:user_id] = 2
651 @request.session[:user_id] = 2
652 get :reply, :id => 1
652 get :reply, :id => 1
653 assert_response :success
653 assert_response :success
654 assert_select_rjs :show, "update"
654 assert_select_rjs :show, "update"
655 end
655 end
656
656
657 def test_reply_to_note
657 def test_reply_to_note
658 @request.session[:user_id] = 2
658 @request.session[:user_id] = 2
659 get :reply, :id => 1, :journal_id => 2
659 get :reply, :id => 1, :journal_id => 2
660 assert_response :success
660 assert_response :success
661 assert_select_rjs :show, "update"
661 assert_select_rjs :show, "update"
662 end
662 end
663
663
664 def test_post_edit_without_custom_fields_param
664 def test_post_edit_without_custom_fields_param
665 @request.session[:user_id] = 2
665 @request.session[:user_id] = 2
666 ActionMailer::Base.deliveries.clear
666 ActionMailer::Base.deliveries.clear
667
667
668 issue = Issue.find(1)
668 issue = Issue.find(1)
669 assert_equal '125', issue.custom_value_for(2).value
669 assert_equal '125', issue.custom_value_for(2).value
670 old_subject = issue.subject
670 old_subject = issue.subject
671 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
671 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
672
672
673 assert_difference('Journal.count') do
673 assert_difference('Journal.count') do
674 assert_difference('JournalDetail.count', 2) do
674 assert_difference('JournalDetail.count', 2) do
675 post :edit, :id => 1, :issue => {:subject => new_subject,
675 post :edit, :id => 1, :issue => {:subject => new_subject,
676 :priority_id => '6',
676 :priority_id => '6',
677 :category_id => '1' # no change
677 :category_id => '1' # no change
678 }
678 }
679 end
679 end
680 end
680 end
681 assert_redirected_to :action => 'show', :id => '1'
681 assert_redirected_to :action => 'show', :id => '1'
682 issue.reload
682 issue.reload
683 assert_equal new_subject, issue.subject
683 assert_equal new_subject, issue.subject
684 # Make sure custom fields were not cleared
684 # Make sure custom fields were not cleared
685 assert_equal '125', issue.custom_value_for(2).value
685 assert_equal '125', issue.custom_value_for(2).value
686
686
687 mail = ActionMailer::Base.deliveries.last
687 mail = ActionMailer::Base.deliveries.last
688 assert_kind_of TMail::Mail, mail
688 assert_kind_of TMail::Mail, mail
689 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
689 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
690 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
690 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
691 end
691 end
692
692
693 def test_post_edit_with_custom_field_change
693 def test_post_edit_with_custom_field_change
694 @request.session[:user_id] = 2
694 @request.session[:user_id] = 2
695 issue = Issue.find(1)
695 issue = Issue.find(1)
696 assert_equal '125', issue.custom_value_for(2).value
696 assert_equal '125', issue.custom_value_for(2).value
697
697
698 assert_difference('Journal.count') do
698 assert_difference('Journal.count') do
699 assert_difference('JournalDetail.count', 3) do
699 assert_difference('JournalDetail.count', 3) do
700 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
700 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
701 :priority_id => '6',
701 :priority_id => '6',
702 :category_id => '1', # no change
702 :category_id => '1', # no change
703 :custom_field_values => { '2' => 'New custom value' }
703 :custom_field_values => { '2' => 'New custom value' }
704 }
704 }
705 end
705 end
706 end
706 end
707 assert_redirected_to :action => 'show', :id => '1'
707 assert_redirected_to :action => 'show', :id => '1'
708 issue.reload
708 issue.reload
709 assert_equal 'New custom value', issue.custom_value_for(2).value
709 assert_equal 'New custom value', issue.custom_value_for(2).value
710
710
711 mail = ActionMailer::Base.deliveries.last
711 mail = ActionMailer::Base.deliveries.last
712 assert_kind_of TMail::Mail, mail
712 assert_kind_of TMail::Mail, mail
713 assert mail.body.include?("Searchable field changed from 125 to New custom value")
713 assert mail.body.include?("Searchable field changed from 125 to New custom value")
714 end
714 end
715
715
716 def test_post_edit_with_status_and_assignee_change
716 def test_post_edit_with_status_and_assignee_change
717 issue = Issue.find(1)
717 issue = Issue.find(1)
718 assert_equal 1, issue.status_id
718 assert_equal 1, issue.status_id
719 @request.session[:user_id] = 2
719 @request.session[:user_id] = 2
720 assert_difference('TimeEntry.count', 0) do
720 assert_difference('TimeEntry.count', 0) do
721 post :edit,
721 post :edit,
722 :id => 1,
722 :id => 1,
723 :issue => { :status_id => 2, :assigned_to_id => 3 },
723 :issue => { :status_id => 2, :assigned_to_id => 3 },
724 :notes => 'Assigned to dlopper',
724 :notes => 'Assigned to dlopper',
725 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
725 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
726 end
726 end
727 assert_redirected_to :action => 'show', :id => '1'
727 assert_redirected_to :action => 'show', :id => '1'
728 issue.reload
728 issue.reload
729 assert_equal 2, issue.status_id
729 assert_equal 2, issue.status_id
730 j = issue.journals.find(:first, :order => 'id DESC')
730 j = issue.journals.find(:first, :order => 'id DESC')
731 assert_equal 'Assigned to dlopper', j.notes
731 assert_equal 'Assigned to dlopper', j.notes
732 assert_equal 2, j.details.size
732 assert_equal 2, j.details.size
733
733
734 mail = ActionMailer::Base.deliveries.last
734 mail = ActionMailer::Base.deliveries.last
735 assert mail.body.include?("Status changed from New to Assigned")
735 assert mail.body.include?("Status changed from New to Assigned")
736 # subject should contain the new status
736 # subject should contain the new status
737 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
737 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
738 end
738 end
739
739
740 def test_post_edit_with_note_only
740 def test_post_edit_with_note_only
741 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
741 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
742 # anonymous user
742 # anonymous user
743 post :edit,
743 post :edit,
744 :id => 1,
744 :id => 1,
745 :notes => notes
745 :notes => notes
746 assert_redirected_to :action => 'show', :id => '1'
746 assert_redirected_to :action => 'show', :id => '1'
747 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
747 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
748 assert_equal notes, j.notes
748 assert_equal notes, j.notes
749 assert_equal 0, j.details.size
749 assert_equal 0, j.details.size
750 assert_equal User.anonymous, j.user
750 assert_equal User.anonymous, j.user
751
751
752 mail = ActionMailer::Base.deliveries.last
752 mail = ActionMailer::Base.deliveries.last
753 assert mail.body.include?(notes)
753 assert mail.body.include?(notes)
754 end
754 end
755
755
756 def test_post_edit_with_note_and_spent_time
756 def test_post_edit_with_note_and_spent_time
757 @request.session[:user_id] = 2
757 @request.session[:user_id] = 2
758 spent_hours_before = Issue.find(1).spent_hours
758 spent_hours_before = Issue.find(1).spent_hours
759 assert_difference('TimeEntry.count') do
759 assert_difference('TimeEntry.count') do
760 post :edit,
760 post :edit,
761 :id => 1,
761 :id => 1,
762 :notes => '2.5 hours added',
762 :notes => '2.5 hours added',
763 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
763 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
764 end
764 end
765 assert_redirected_to :action => 'show', :id => '1'
765 assert_redirected_to :action => 'show', :id => '1'
766
766
767 issue = Issue.find(1)
767 issue = Issue.find(1)
768
768
769 j = issue.journals.find(:first, :order => 'id DESC')
769 j = issue.journals.find(:first, :order => 'id DESC')
770 assert_equal '2.5 hours added', j.notes
770 assert_equal '2.5 hours added', j.notes
771 assert_equal 0, j.details.size
771 assert_equal 0, j.details.size
772
772
773 t = issue.time_entries.find(:first, :order => 'id DESC')
773 t = issue.time_entries.find(:first, :order => 'id DESC')
774 assert_not_nil t
774 assert_not_nil t
775 assert_equal 2.5, t.hours
775 assert_equal 2.5, t.hours
776 assert_equal spent_hours_before + 2.5, issue.spent_hours
776 assert_equal spent_hours_before + 2.5, issue.spent_hours
777 end
777 end
778
778
779 def test_post_edit_with_attachment_only
779 def test_post_edit_with_attachment_only
780 set_tmp_attachments_directory
780 set_tmp_attachments_directory
781
781
782 # Delete all fixtured journals, a race condition can occur causing the wrong
782 # Delete all fixtured journals, a race condition can occur causing the wrong
783 # journal to get fetched in the next find.
783 # journal to get fetched in the next find.
784 Journal.delete_all
784 Journal.delete_all
785
785
786 # anonymous user
786 # anonymous user
787 post :edit,
787 post :edit,
788 :id => 1,
788 :id => 1,
789 :notes => '',
789 :notes => '',
790 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
790 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
791 assert_redirected_to :action => 'show', :id => '1'
791 assert_redirected_to :action => 'show', :id => '1'
792 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
792 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
793 assert j.notes.blank?
793 assert j.notes.blank?
794 assert_equal 1, j.details.size
794 assert_equal 1, j.details.size
795 assert_equal 'testfile.txt', j.details.first.value
795 assert_equal 'testfile.txt', j.details.first.value
796 assert_equal User.anonymous, j.user
796 assert_equal User.anonymous, j.user
797
797
798 mail = ActionMailer::Base.deliveries.last
798 mail = ActionMailer::Base.deliveries.last
799 assert mail.body.include?('testfile.txt')
799 assert mail.body.include?('testfile.txt')
800 end
800 end
801
801
802 def test_post_edit_with_no_change
802 def test_post_edit_with_no_change
803 issue = Issue.find(1)
803 issue = Issue.find(1)
804 issue.journals.clear
804 issue.journals.clear
805 ActionMailer::Base.deliveries.clear
805 ActionMailer::Base.deliveries.clear
806
806
807 post :edit,
807 post :edit,
808 :id => 1,
808 :id => 1,
809 :notes => ''
809 :notes => ''
810 assert_redirected_to :action => 'show', :id => '1'
810 assert_redirected_to :action => 'show', :id => '1'
811
811
812 issue.reload
812 issue.reload
813 assert issue.journals.empty?
813 assert issue.journals.empty?
814 # No email should be sent
814 # No email should be sent
815 assert ActionMailer::Base.deliveries.empty?
815 assert ActionMailer::Base.deliveries.empty?
816 end
816 end
817
817
818 def test_post_edit_should_send_a_notification
818 def test_post_edit_should_send_a_notification
819 @request.session[:user_id] = 2
819 @request.session[:user_id] = 2
820 ActionMailer::Base.deliveries.clear
820 ActionMailer::Base.deliveries.clear
821 issue = Issue.find(1)
821 issue = Issue.find(1)
822 old_subject = issue.subject
822 old_subject = issue.subject
823 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
823 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
824
824
825 post :edit, :id => 1, :issue => {:subject => new_subject,
825 post :edit, :id => 1, :issue => {:subject => new_subject,
826 :priority_id => '6',
826 :priority_id => '6',
827 :category_id => '1' # no change
827 :category_id => '1' # no change
828 }
828 }
829 assert_equal 1, ActionMailer::Base.deliveries.size
829 assert_equal 1, ActionMailer::Base.deliveries.size
830 end
830 end
831
831
832 def test_post_edit_with_invalid_spent_time
832 def test_post_edit_with_invalid_spent_time
833 @request.session[:user_id] = 2
833 @request.session[:user_id] = 2
834 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
834 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
835
835
836 assert_no_difference('Journal.count') do
836 assert_no_difference('Journal.count') do
837 post :edit,
837 post :edit,
838 :id => 1,
838 :id => 1,
839 :notes => notes,
839 :notes => notes,
840 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
840 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
841 end
841 end
842 assert_response :success
842 assert_response :success
843 assert_template 'edit'
843 assert_template 'edit'
844
844
845 assert_tag :textarea, :attributes => { :name => 'notes' },
845 assert_tag :textarea, :attributes => { :name => 'notes' },
846 :content => notes
846 :content => notes
847 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
847 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
848 end
848 end
849
849
850 def test_get_bulk_edit
850 def test_get_bulk_edit
851 @request.session[:user_id] = 2
851 @request.session[:user_id] = 2
852 get :bulk_edit, :ids => [1, 2]
852 get :bulk_edit, :ids => [1, 2]
853 assert_response :success
853 assert_response :success
854 assert_template 'bulk_edit'
854 assert_template 'bulk_edit'
855 end
855 end
856
856
857 def test_bulk_edit
857 def test_bulk_edit
858 @request.session[:user_id] = 2
858 @request.session[:user_id] = 2
859 # update issues priority
859 # update issues priority
860 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
860 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
861 :assigned_to_id => '',
861 :assigned_to_id => '',
862 :custom_field_values => {'2' => ''},
862 :custom_field_values => {'2' => ''},
863 :notes => 'Bulk editing'
863 :notes => 'Bulk editing'
864 assert_response 302
864 assert_response 302
865 # check that the issues were updated
865 # check that the issues were updated
866 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
866 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
867
867
868 issue = Issue.find(1)
868 issue = Issue.find(1)
869 journal = issue.journals.find(:first, :order => 'created_on DESC')
869 journal = issue.journals.find(:first, :order => 'created_on DESC')
870 assert_equal '125', issue.custom_value_for(2).value
870 assert_equal '125', issue.custom_value_for(2).value
871 assert_equal 'Bulk editing', journal.notes
871 assert_equal 'Bulk editing', journal.notes
872 assert_equal 1, journal.details.size
872 assert_equal 1, journal.details.size
873 end
873 end
874
874
875 def test_bullk_edit_should_send_a_notification
875 def test_bullk_edit_should_send_a_notification
876 @request.session[:user_id] = 2
876 @request.session[:user_id] = 2
877 ActionMailer::Base.deliveries.clear
877 ActionMailer::Base.deliveries.clear
878 post(:bulk_edit,
878 post(:bulk_edit,
879 {
879 {
880 :ids => [1, 2],
880 :ids => [1, 2],
881 :priority_id => 7,
881 :priority_id => 7,
882 :assigned_to_id => '',
882 :assigned_to_id => '',
883 :custom_field_values => {'2' => ''},
883 :custom_field_values => {'2' => ''},
884 :notes => 'Bulk editing'
884 :notes => 'Bulk editing'
885 })
885 })
886
886
887 assert_response 302
887 assert_response 302
888 assert_equal 2, ActionMailer::Base.deliveries.size
888 assert_equal 2, ActionMailer::Base.deliveries.size
889 end
889 end
890
890
891 def test_bulk_edit_status
891 def test_bulk_edit_status
892 @request.session[:user_id] = 2
892 @request.session[:user_id] = 2
893 # update issues priority
893 # update issues priority
894 post :bulk_edit, :ids => [1, 2], :priority_id => '',
894 post :bulk_edit, :ids => [1, 2], :priority_id => '',
895 :assigned_to_id => '',
895 :assigned_to_id => '',
896 :status_id => '5',
896 :status_id => '5',
897 :notes => 'Bulk editing status'
897 :notes => 'Bulk editing status'
898 assert_response 302
898 assert_response 302
899 issue = Issue.find(1)
899 issue = Issue.find(1)
900 assert issue.closed?
900 assert issue.closed?
901 end
901 end
902
902
903 def test_bulk_edit_custom_field
903 def test_bulk_edit_custom_field
904 @request.session[:user_id] = 2
904 @request.session[:user_id] = 2
905 # update issues priority
905 # update issues priority
906 post :bulk_edit, :ids => [1, 2], :priority_id => '',
906 post :bulk_edit, :ids => [1, 2], :priority_id => '',
907 :assigned_to_id => '',
907 :assigned_to_id => '',
908 :custom_field_values => {'2' => '777'},
908 :custom_field_values => {'2' => '777'},
909 :notes => 'Bulk editing custom field'
909 :notes => 'Bulk editing custom field'
910 assert_response 302
910 assert_response 302
911
911
912 issue = Issue.find(1)
912 issue = Issue.find(1)
913 journal = issue.journals.find(:first, :order => 'created_on DESC')
913 journal = issue.journals.find(:first, :order => 'created_on DESC')
914 assert_equal '777', issue.custom_value_for(2).value
914 assert_equal '777', issue.custom_value_for(2).value
915 assert_equal 1, journal.details.size
915 assert_equal 1, journal.details.size
916 assert_equal '125', journal.details.first.old_value
916 assert_equal '125', journal.details.first.old_value
917 assert_equal '777', journal.details.first.value
917 assert_equal '777', journal.details.first.value
918 end
918 end
919
919
920 def test_bulk_unassign
920 def test_bulk_unassign
921 assert_not_nil Issue.find(2).assigned_to
921 assert_not_nil Issue.find(2).assigned_to
922 @request.session[:user_id] = 2
922 @request.session[:user_id] = 2
923 # unassign issues
923 # unassign issues
924 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
924 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
925 assert_response 302
925 assert_response 302
926 # check that the issues were updated
926 # check that the issues were updated
927 assert_nil Issue.find(2).assigned_to
927 assert_nil Issue.find(2).assigned_to
928 end
928 end
929
929
930 def test_move_routing
930 def test_move_routing
931 assert_routing(
931 assert_routing(
932 {:method => :get, :path => '/issues/1/move'},
932 {:method => :get, :path => '/issues/1/move'},
933 :controller => 'issues', :action => 'move', :id => '1'
933 :controller => 'issues', :action => 'move', :id => '1'
934 )
934 )
935 assert_recognizes(
935 assert_recognizes(
936 {:controller => 'issues', :action => 'move', :id => '1'},
936 {:controller => 'issues', :action => 'move', :id => '1'},
937 {:method => :post, :path => '/issues/1/move'}
937 {:method => :post, :path => '/issues/1/move'}
938 )
938 )
939 end
939 end
940
940
941 def test_move_one_issue_to_another_project
941 def test_move_one_issue_to_another_project
942 @request.session[:user_id] = 2
942 @request.session[:user_id] = 2
943 post :move, :id => 1, :new_project_id => 2
943 post :move, :id => 1, :new_project_id => 2
944 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
944 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
945 assert_equal 2, Issue.find(1).project_id
945 assert_equal 2, Issue.find(1).project_id
946 end
946 end
947
947
948 def test_bulk_move_to_another_project
948 def test_bulk_move_to_another_project
949 @request.session[:user_id] = 2
949 @request.session[:user_id] = 2
950 post :move, :ids => [1, 2], :new_project_id => 2
950 post :move, :ids => [1, 2], :new_project_id => 2
951 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
951 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
952 # Issues moved to project 2
952 # Issues moved to project 2
953 assert_equal 2, Issue.find(1).project_id
953 assert_equal 2, Issue.find(1).project_id
954 assert_equal 2, Issue.find(2).project_id
954 assert_equal 2, Issue.find(2).project_id
955 # No tracker change
955 # No tracker change
956 assert_equal 1, Issue.find(1).tracker_id
956 assert_equal 1, Issue.find(1).tracker_id
957 assert_equal 2, Issue.find(2).tracker_id
957 assert_equal 2, Issue.find(2).tracker_id
958 end
958 end
959
959
960 def test_bulk_move_to_another_tracker
960 def test_bulk_move_to_another_tracker
961 @request.session[:user_id] = 2
961 @request.session[:user_id] = 2
962 post :move, :ids => [1, 2], :new_tracker_id => 2
962 post :move, :ids => [1, 2], :new_tracker_id => 2
963 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
963 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
964 assert_equal 2, Issue.find(1).tracker_id
964 assert_equal 2, Issue.find(1).tracker_id
965 assert_equal 2, Issue.find(2).tracker_id
965 assert_equal 2, Issue.find(2).tracker_id
966 end
966 end
967
967
968 def test_bulk_copy_to_another_project
968 def test_bulk_copy_to_another_project
969 @request.session[:user_id] = 2
969 @request.session[:user_id] = 2
970 assert_difference 'Issue.count', 2 do
970 assert_difference 'Issue.count', 2 do
971 assert_no_difference 'Project.find(1).issues.count' do
971 assert_no_difference 'Project.find(1).issues.count' do
972 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
972 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
973 end
973 end
974 end
974 end
975 assert_redirected_to 'projects/ecookbook/issues'
975 assert_redirected_to 'projects/ecookbook/issues'
976 end
976 end
977
977
978 def test_context_menu_one_issue
978 def test_context_menu_one_issue
979 @request.session[:user_id] = 2
979 @request.session[:user_id] = 2
980 get :context_menu, :ids => [1]
980 get :context_menu, :ids => [1]
981 assert_response :success
981 assert_response :success
982 assert_template 'context_menu'
982 assert_template 'context_menu'
983 assert_tag :tag => 'a', :content => 'Edit',
983 assert_tag :tag => 'a', :content => 'Edit',
984 :attributes => { :href => '/issues/1/edit',
984 :attributes => { :href => '/issues/1/edit',
985 :class => 'icon-edit' }
985 :class => 'icon-edit' }
986 assert_tag :tag => 'a', :content => 'Closed',
986 assert_tag :tag => 'a', :content => 'Closed',
987 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
987 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
988 :class => '' }
988 :class => '' }
989 assert_tag :tag => 'a', :content => 'Immediate',
989 assert_tag :tag => 'a', :content => 'Immediate',
990 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
990 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
991 :class => '' }
991 :class => '' }
992 assert_tag :tag => 'a', :content => 'Dave Lopper',
992 assert_tag :tag => 'a', :content => 'Dave Lopper',
993 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
993 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
994 :class => '' }
994 :class => '' }
995 assert_tag :tag => 'a', :content => 'Copy',
995 assert_tag :tag => 'a', :content => 'Copy',
996 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
996 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
997 :class => 'icon-copy' }
997 :class => 'icon-copy' }
998 assert_tag :tag => 'a', :content => 'Move',
998 assert_tag :tag => 'a', :content => 'Move',
999 :attributes => { :href => '/issues/move?ids%5B%5D=1',
999 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1000 :class => 'icon-move' }
1000 :class => 'icon-move' }
1001 assert_tag :tag => 'a', :content => 'Delete',
1001 assert_tag :tag => 'a', :content => 'Delete',
1002 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1002 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1003 :class => 'icon-del' }
1003 :class => 'icon-del' }
1004 end
1004 end
1005
1005
1006 def test_context_menu_one_issue_by_anonymous
1006 def test_context_menu_one_issue_by_anonymous
1007 get :context_menu, :ids => [1]
1007 get :context_menu, :ids => [1]
1008 assert_response :success
1008 assert_response :success
1009 assert_template 'context_menu'
1009 assert_template 'context_menu'
1010 assert_tag :tag => 'a', :content => 'Delete',
1010 assert_tag :tag => 'a', :content => 'Delete',
1011 :attributes => { :href => '#',
1011 :attributes => { :href => '#',
1012 :class => 'icon-del disabled' }
1012 :class => 'icon-del disabled' }
1013 end
1013 end
1014
1014
1015 def test_context_menu_multiple_issues_of_same_project
1015 def test_context_menu_multiple_issues_of_same_project
1016 @request.session[:user_id] = 2
1016 @request.session[:user_id] = 2
1017 get :context_menu, :ids => [1, 2]
1017 get :context_menu, :ids => [1, 2]
1018 assert_response :success
1018 assert_response :success
1019 assert_template 'context_menu'
1019 assert_template 'context_menu'
1020 assert_tag :tag => 'a', :content => 'Edit',
1020 assert_tag :tag => 'a', :content => 'Edit',
1021 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1021 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1022 :class => 'icon-edit' }
1022 :class => 'icon-edit' }
1023 assert_tag :tag => 'a', :content => 'Immediate',
1023 assert_tag :tag => 'a', :content => 'Immediate',
1024 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1024 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1025 :class => '' }
1025 :class => '' }
1026 assert_tag :tag => 'a', :content => 'Dave Lopper',
1026 assert_tag :tag => 'a', :content => 'Dave Lopper',
1027 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1027 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1028 :class => '' }
1028 :class => '' }
1029 assert_tag :tag => 'a', :content => 'Move',
1029 assert_tag :tag => 'a', :content => 'Move',
1030 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1030 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1031 :class => 'icon-move' }
1031 :class => 'icon-move' }
1032 assert_tag :tag => 'a', :content => 'Delete',
1032 assert_tag :tag => 'a', :content => 'Delete',
1033 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1033 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1034 :class => 'icon-del' }
1034 :class => 'icon-del' }
1035 end
1035 end
1036
1036
1037 def test_context_menu_multiple_issues_of_different_project
1037 def test_context_menu_multiple_issues_of_different_project
1038 @request.session[:user_id] = 2
1038 @request.session[:user_id] = 2
1039 get :context_menu, :ids => [1, 2, 4]
1039 get :context_menu, :ids => [1, 2, 4]
1040 assert_response :success
1040 assert_response :success
1041 assert_template 'context_menu'
1041 assert_template 'context_menu'
1042 assert_tag :tag => 'a', :content => 'Delete',
1042 assert_tag :tag => 'a', :content => 'Delete',
1043 :attributes => { :href => '#',
1043 :attributes => { :href => '#',
1044 :class => 'icon-del disabled' }
1044 :class => 'icon-del disabled' }
1045 end
1045 end
1046
1046
1047 def test_destroy_routing
1047 def test_destroy_routing
1048 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1048 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1049 {:controller => 'issues', :action => 'destroy', :id => '1'},
1049 {:controller => 'issues', :action => 'destroy', :id => '1'},
1050 {:method => :post, :path => '/issues/1/destroy'}
1050 {:method => :post, :path => '/issues/1/destroy'}
1051 )
1051 )
1052 end
1052 end
1053
1053
1054 def test_destroy_issue_with_no_time_entries
1054 def test_destroy_issue_with_no_time_entries
1055 assert_nil TimeEntry.find_by_issue_id(2)
1055 assert_nil TimeEntry.find_by_issue_id(2)
1056 @request.session[:user_id] = 2
1056 @request.session[:user_id] = 2
1057 post :destroy, :id => 2
1057 post :destroy, :id => 2
1058 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1058 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1059 assert_nil Issue.find_by_id(2)
1059 assert_nil Issue.find_by_id(2)
1060 end
1060 end
1061
1061
1062 def test_destroy_issues_with_time_entries
1062 def test_destroy_issues_with_time_entries
1063 @request.session[:user_id] = 2
1063 @request.session[:user_id] = 2
1064 post :destroy, :ids => [1, 3]
1064 post :destroy, :ids => [1, 3]
1065 assert_response :success
1065 assert_response :success
1066 assert_template 'destroy'
1066 assert_template 'destroy'
1067 assert_not_nil assigns(:hours)
1067 assert_not_nil assigns(:hours)
1068 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1068 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1069 end
1069 end
1070
1070
1071 def test_destroy_issues_and_destroy_time_entries
1071 def test_destroy_issues_and_destroy_time_entries
1072 @request.session[:user_id] = 2
1072 @request.session[:user_id] = 2
1073 post :destroy, :ids => [1, 3], :todo => 'destroy'
1073 post :destroy, :ids => [1, 3], :todo => 'destroy'
1074 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1074 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1075 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1075 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1076 assert_nil TimeEntry.find_by_id([1, 2])
1076 assert_nil TimeEntry.find_by_id([1, 2])
1077 end
1077 end
1078
1078
1079 def test_destroy_issues_and_assign_time_entries_to_project
1079 def test_destroy_issues_and_assign_time_entries_to_project
1080 @request.session[:user_id] = 2
1080 @request.session[:user_id] = 2
1081 post :destroy, :ids => [1, 3], :todo => 'nullify'
1081 post :destroy, :ids => [1, 3], :todo => 'nullify'
1082 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1082 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1083 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1083 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1084 assert_nil TimeEntry.find(1).issue_id
1084 assert_nil TimeEntry.find(1).issue_id
1085 assert_nil TimeEntry.find(2).issue_id
1085 assert_nil TimeEntry.find(2).issue_id
1086 end
1086 end
1087
1087
1088 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1088 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1089 @request.session[:user_id] = 2
1089 @request.session[:user_id] = 2
1090 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1090 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1091 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1091 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1092 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1092 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1093 assert_equal 2, TimeEntry.find(1).issue_id
1093 assert_equal 2, TimeEntry.find(1).issue_id
1094 assert_equal 2, TimeEntry.find(2).issue_id
1094 assert_equal 2, TimeEntry.find(2).issue_id
1095 end
1095 end
1096
1097 def test_default_search_scope
1098 get :index
1099 assert_tag :div, :attributes => {:id => 'quick-search'},
1100 :child => {:tag => 'form',
1101 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1102 end
1096 end
1103 end
General Comments 0
You need to be logged in to leave comments. Login now