##// END OF EJS Templates
Restores object count and adds offset/limit attributes to API responses for paginated collections (#6140)....
Jean-Philippe Lang -
r4375:00d50157d3d6
parent child
Show More
@@ -1,449 +1,466
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 exempt_from_layout 'builder', 'rsb'
25 exempt_from_layout 'builder', 'rsb'
26
26
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
29 # TODO: remove it when Rails is fixed
29 # TODO: remove it when Rails is fixed
30 before_filter :delete_broken_cookies
30 before_filter :delete_broken_cookies
31 def delete_broken_cookies
31 def delete_broken_cookies
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
33 cookies.delete '_redmine_session'
33 cookies.delete '_redmine_session'
34 redirect_to home_path
34 redirect_to home_path
35 return false
35 return false
36 end
36 end
37 end
37 end
38
38
39 before_filter :user_setup, :check_if_login_required, :set_localization
39 before_filter :user_setup, :check_if_login_required, :set_localization
40 filter_parameter_logging :password
40 filter_parameter_logging :password
41 protect_from_forgery
41 protect_from_forgery
42
42
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
44
44
45 include Redmine::Search::Controller
45 include Redmine::Search::Controller
46 include Redmine::MenuManager::MenuController
46 include Redmine::MenuManager::MenuController
47 helper Redmine::MenuManager::MenuHelper
47 helper Redmine::MenuManager::MenuHelper
48
48
49 Redmine::Scm::Base.all.each do |scm|
49 Redmine::Scm::Base.all.each do |scm|
50 require_dependency "repository/#{scm.underscore}"
50 require_dependency "repository/#{scm.underscore}"
51 end
51 end
52
52
53 def user_setup
53 def user_setup
54 # Check the settings cache for each request
54 # Check the settings cache for each request
55 Setting.check_cache
55 Setting.check_cache
56 # Find the current user
56 # Find the current user
57 User.current = find_current_user
57 User.current = find_current_user
58 end
58 end
59
59
60 # Returns the current user or nil if no user is logged in
60 # Returns the current user or nil if no user is logged in
61 # and starts a session if needed
61 # and starts a session if needed
62 def find_current_user
62 def find_current_user
63 if session[:user_id]
63 if session[:user_id]
64 # existing session
64 # existing session
65 (User.active.find(session[:user_id]) rescue nil)
65 (User.active.find(session[:user_id]) rescue nil)
66 elsif cookies[:autologin] && Setting.autologin?
66 elsif cookies[:autologin] && Setting.autologin?
67 # auto-login feature starts a new session
67 # auto-login feature starts a new session
68 user = User.try_to_autologin(cookies[:autologin])
68 user = User.try_to_autologin(cookies[:autologin])
69 session[:user_id] = user.id if user
69 session[:user_id] = user.id if user
70 user
70 user
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
72 # RSS key authentication does not start a session
72 # RSS key authentication does not start a session
73 User.find_by_rss_key(params[:key])
73 User.find_by_rss_key(params[:key])
74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
76 # Use API key
76 # Use API key
77 User.find_by_api_key(params[:key])
77 User.find_by_api_key(params[:key])
78 else
78 else
79 # HTTP Basic, either username/password or API key/random
79 # HTTP Basic, either username/password or API key/random
80 authenticate_with_http_basic do |username, password|
80 authenticate_with_http_basic do |username, password|
81 User.try_to_login(username, password) || User.find_by_api_key(username)
81 User.try_to_login(username, password) || User.find_by_api_key(username)
82 end
82 end
83 end
83 end
84 end
84 end
85 end
85 end
86
86
87 # Sets the logged in user
87 # Sets the logged in user
88 def logged_user=(user)
88 def logged_user=(user)
89 reset_session
89 reset_session
90 if user && user.is_a?(User)
90 if user && user.is_a?(User)
91 User.current = user
91 User.current = user
92 session[:user_id] = user.id
92 session[:user_id] = user.id
93 else
93 else
94 User.current = User.anonymous
94 User.current = User.anonymous
95 end
95 end
96 end
96 end
97
97
98 # check if login is globally required to access the application
98 # check if login is globally required to access the application
99 def check_if_login_required
99 def check_if_login_required
100 # no check needed if user is already logged in
100 # no check needed if user is already logged in
101 return true if User.current.logged?
101 return true if User.current.logged?
102 require_login if Setting.login_required?
102 require_login if Setting.login_required?
103 end
103 end
104
104
105 def set_localization
105 def set_localization
106 lang = nil
106 lang = nil
107 if User.current.logged?
107 if User.current.logged?
108 lang = find_language(User.current.language)
108 lang = find_language(User.current.language)
109 end
109 end
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
112 if !accept_lang.blank?
112 if !accept_lang.blank?
113 accept_lang = accept_lang.downcase
113 accept_lang = accept_lang.downcase
114 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
114 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
115 end
115 end
116 end
116 end
117 lang ||= Setting.default_language
117 lang ||= Setting.default_language
118 set_language_if_valid(lang)
118 set_language_if_valid(lang)
119 end
119 end
120
120
121 def require_login
121 def require_login
122 if !User.current.logged?
122 if !User.current.logged?
123 # Extract only the basic url parameters on non-GET requests
123 # Extract only the basic url parameters on non-GET requests
124 if request.get?
124 if request.get?
125 url = url_for(params)
125 url = url_for(params)
126 else
126 else
127 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
127 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
128 end
128 end
129 respond_to do |format|
129 respond_to do |format|
130 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
130 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
131 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
131 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
132 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
132 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
134 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
134 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
135 end
135 end
136 return false
136 return false
137 end
137 end
138 true
138 true
139 end
139 end
140
140
141 def require_admin
141 def require_admin
142 return unless require_login
142 return unless require_login
143 if !User.current.admin?
143 if !User.current.admin?
144 render_403
144 render_403
145 return false
145 return false
146 end
146 end
147 true
147 true
148 end
148 end
149
149
150 def deny_access
150 def deny_access
151 User.current.logged? ? render_403 : require_login
151 User.current.logged? ? render_403 : require_login
152 end
152 end
153
153
154 # Authorize the user for the requested action
154 # Authorize the user for the requested action
155 def authorize(ctrl = params[:controller], action = params[:action], global = false)
155 def authorize(ctrl = params[:controller], action = params[:action], global = false)
156 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
156 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
157 if allowed
157 if allowed
158 true
158 true
159 else
159 else
160 if @project && @project.archived?
160 if @project && @project.archived?
161 render_403 :message => :notice_not_authorized_archived_project
161 render_403 :message => :notice_not_authorized_archived_project
162 else
162 else
163 deny_access
163 deny_access
164 end
164 end
165 end
165 end
166 end
166 end
167
167
168 # Authorize the user for the requested action outside a project
168 # Authorize the user for the requested action outside a project
169 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
169 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
170 authorize(ctrl, action, global)
170 authorize(ctrl, action, global)
171 end
171 end
172
172
173 # Find project of id params[:id]
173 # Find project of id params[:id]
174 def find_project
174 def find_project
175 @project = Project.find(params[:id])
175 @project = Project.find(params[:id])
176 rescue ActiveRecord::RecordNotFound
176 rescue ActiveRecord::RecordNotFound
177 render_404
177 render_404
178 end
178 end
179
179
180 # Find project of id params[:project_id]
180 # Find project of id params[:project_id]
181 def find_project_by_project_id
181 def find_project_by_project_id
182 @project = Project.find(params[:project_id])
182 @project = Project.find(params[:project_id])
183 rescue ActiveRecord::RecordNotFound
183 rescue ActiveRecord::RecordNotFound
184 render_404
184 render_404
185 end
185 end
186
186
187 # Find a project based on params[:project_id]
187 # Find a project based on params[:project_id]
188 # TODO: some subclasses override this, see about merging their logic
188 # TODO: some subclasses override this, see about merging their logic
189 def find_optional_project
189 def find_optional_project
190 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
190 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
191 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
191 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
192 allowed ? true : deny_access
192 allowed ? true : deny_access
193 rescue ActiveRecord::RecordNotFound
193 rescue ActiveRecord::RecordNotFound
194 render_404
194 render_404
195 end
195 end
196
196
197 # Finds and sets @project based on @object.project
197 # Finds and sets @project based on @object.project
198 def find_project_from_association
198 def find_project_from_association
199 render_404 unless @object.present?
199 render_404 unless @object.present?
200
200
201 @project = @object.project
201 @project = @object.project
202 rescue ActiveRecord::RecordNotFound
202 rescue ActiveRecord::RecordNotFound
203 render_404
203 render_404
204 end
204 end
205
205
206 def find_model_object
206 def find_model_object
207 model = self.class.read_inheritable_attribute('model_object')
207 model = self.class.read_inheritable_attribute('model_object')
208 if model
208 if model
209 @object = model.find(params[:id])
209 @object = model.find(params[:id])
210 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
210 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
211 end
211 end
212 rescue ActiveRecord::RecordNotFound
212 rescue ActiveRecord::RecordNotFound
213 render_404
213 render_404
214 end
214 end
215
215
216 def self.model_object(model)
216 def self.model_object(model)
217 write_inheritable_attribute('model_object', model)
217 write_inheritable_attribute('model_object', model)
218 end
218 end
219
219
220 # Filter for bulk issue operations
220 # Filter for bulk issue operations
221 def find_issues
221 def find_issues
222 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
222 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
223 raise ActiveRecord::RecordNotFound if @issues.empty?
223 raise ActiveRecord::RecordNotFound if @issues.empty?
224 @projects = @issues.collect(&:project).compact.uniq
224 @projects = @issues.collect(&:project).compact.uniq
225 @project = @projects.first if @projects.size == 1
225 @project = @projects.first if @projects.size == 1
226 rescue ActiveRecord::RecordNotFound
226 rescue ActiveRecord::RecordNotFound
227 render_404
227 render_404
228 end
228 end
229
229
230 # Check if project is unique before bulk operations
230 # Check if project is unique before bulk operations
231 def check_project_uniqueness
231 def check_project_uniqueness
232 unless @project
232 unless @project
233 # TODO: let users bulk edit/move/destroy issues from different projects
233 # TODO: let users bulk edit/move/destroy issues from different projects
234 render_error 'Can not bulk edit/move/destroy issues from different projects'
234 render_error 'Can not bulk edit/move/destroy issues from different projects'
235 return false
235 return false
236 end
236 end
237 end
237 end
238
238
239 # make sure that the user is a member of the project (or admin) if project is private
239 # make sure that the user is a member of the project (or admin) if project is private
240 # used as a before_filter for actions that do not require any particular permission on the project
240 # used as a before_filter for actions that do not require any particular permission on the project
241 def check_project_privacy
241 def check_project_privacy
242 if @project && @project.active?
242 if @project && @project.active?
243 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
243 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
244 true
244 true
245 else
245 else
246 User.current.logged? ? render_403 : require_login
246 User.current.logged? ? render_403 : require_login
247 end
247 end
248 else
248 else
249 @project = nil
249 @project = nil
250 render_404
250 render_404
251 false
251 false
252 end
252 end
253 end
253 end
254
254
255 def back_url
255 def back_url
256 params[:back_url] || request.env['HTTP_REFERER']
256 params[:back_url] || request.env['HTTP_REFERER']
257 end
257 end
258
258
259 def redirect_back_or_default(default)
259 def redirect_back_or_default(default)
260 back_url = CGI.unescape(params[:back_url].to_s)
260 back_url = CGI.unescape(params[:back_url].to_s)
261 if !back_url.blank?
261 if !back_url.blank?
262 begin
262 begin
263 uri = URI.parse(back_url)
263 uri = URI.parse(back_url)
264 # do not redirect user to another host or to the login or register page
264 # do not redirect user to another host or to the login or register page
265 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
265 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
266 redirect_to(back_url)
266 redirect_to(back_url)
267 return
267 return
268 end
268 end
269 rescue URI::InvalidURIError
269 rescue URI::InvalidURIError
270 # redirect to default
270 # redirect to default
271 end
271 end
272 end
272 end
273 redirect_to default
273 redirect_to default
274 end
274 end
275
275
276 def render_403(options={})
276 def render_403(options={})
277 @project = nil
277 @project = nil
278 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
278 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
279 return false
279 return false
280 end
280 end
281
281
282 def render_404(options={})
282 def render_404(options={})
283 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
283 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
284 return false
284 return false
285 end
285 end
286
286
287 # Renders an error response
287 # Renders an error response
288 def render_error(arg)
288 def render_error(arg)
289 arg = {:message => arg} unless arg.is_a?(Hash)
289 arg = {:message => arg} unless arg.is_a?(Hash)
290
290
291 @message = arg[:message]
291 @message = arg[:message]
292 @message = l(@message) if @message.is_a?(Symbol)
292 @message = l(@message) if @message.is_a?(Symbol)
293 @status = arg[:status] || 500
293 @status = arg[:status] || 500
294
294
295 respond_to do |format|
295 respond_to do |format|
296 format.html {
296 format.html {
297 render :template => 'common/error', :layout => use_layout, :status => @status
297 render :template => 'common/error', :layout => use_layout, :status => @status
298 }
298 }
299 format.atom { head @status }
299 format.atom { head @status }
300 format.xml { head @status }
300 format.xml { head @status }
301 format.js { head @status }
301 format.js { head @status }
302 format.json { head @status }
302 format.json { head @status }
303 end
303 end
304 end
304 end
305
305
306 # Picks which layout to use based on the request
306 # Picks which layout to use based on the request
307 #
307 #
308 # @return [boolean, string] name of the layout to use or false for no layout
308 # @return [boolean, string] name of the layout to use or false for no layout
309 def use_layout
309 def use_layout
310 request.xhr? ? false : 'base'
310 request.xhr? ? false : 'base'
311 end
311 end
312
312
313 def invalid_authenticity_token
313 def invalid_authenticity_token
314 if api_request?
314 if api_request?
315 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
315 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
316 end
316 end
317 render_error "Invalid form authenticity token."
317 render_error "Invalid form authenticity token."
318 end
318 end
319
319
320 def render_feed(items, options={})
320 def render_feed(items, options={})
321 @items = items || []
321 @items = items || []
322 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
322 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
323 @items = @items.slice(0, Setting.feeds_limit.to_i)
323 @items = @items.slice(0, Setting.feeds_limit.to_i)
324 @title = options[:title] || Setting.app_title
324 @title = options[:title] || Setting.app_title
325 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
325 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
326 end
326 end
327
327
328 def self.accept_key_auth(*actions)
328 def self.accept_key_auth(*actions)
329 actions = actions.flatten.map(&:to_s)
329 actions = actions.flatten.map(&:to_s)
330 write_inheritable_attribute('accept_key_auth_actions', actions)
330 write_inheritable_attribute('accept_key_auth_actions', actions)
331 end
331 end
332
332
333 def accept_key_auth_actions
333 def accept_key_auth_actions
334 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
334 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
335 end
335 end
336
336
337 # Returns the number of objects that should be displayed
337 # Returns the number of objects that should be displayed
338 # on the paginated list
338 # on the paginated list
339 def per_page_option
339 def per_page_option
340 per_page = nil
340 per_page = nil
341 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
341 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
342 per_page = params[:per_page].to_s.to_i
342 per_page = params[:per_page].to_s.to_i
343 session[:per_page] = per_page
343 session[:per_page] = per_page
344 elsif session[:per_page]
344 elsif session[:per_page]
345 per_page = session[:per_page]
345 per_page = session[:per_page]
346 else
346 else
347 per_page = Setting.per_page_options_array.first || 25
347 per_page = Setting.per_page_options_array.first || 25
348 end
348 end
349 per_page
349 per_page
350 end
350 end
351
351
352 def api_offset_and_limit
353 offset = nil
354 if params[:offset].present?
355 offset = params[:offset].to_i
356 if offset < 0
357 offset = 0
358 end
359 end
360 limit = params[:limit].to_i
361 if limit < 1
362 limit = 25
363 elsif limit > 100
364 limit = 100
365 end
366 [offset, limit]
367 end
368
352 # qvalues http header parser
369 # qvalues http header parser
353 # code taken from webrick
370 # code taken from webrick
354 def parse_qvalues(value)
371 def parse_qvalues(value)
355 tmp = []
372 tmp = []
356 if value
373 if value
357 parts = value.split(/,\s*/)
374 parts = value.split(/,\s*/)
358 parts.each {|part|
375 parts.each {|part|
359 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
376 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
360 val = m[1]
377 val = m[1]
361 q = (m[2] or 1).to_f
378 q = (m[2] or 1).to_f
362 tmp.push([val, q])
379 tmp.push([val, q])
363 end
380 end
364 }
381 }
365 tmp = tmp.sort_by{|val, q| -q}
382 tmp = tmp.sort_by{|val, q| -q}
366 tmp.collect!{|val, q| val}
383 tmp.collect!{|val, q| val}
367 end
384 end
368 return tmp
385 return tmp
369 rescue
386 rescue
370 nil
387 nil
371 end
388 end
372
389
373 # Returns a string that can be used as filename value in Content-Disposition header
390 # Returns a string that can be used as filename value in Content-Disposition header
374 def filename_for_content_disposition(name)
391 def filename_for_content_disposition(name)
375 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
392 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
376 end
393 end
377
394
378 def api_request?
395 def api_request?
379 %w(xml json).include? params[:format]
396 %w(xml json).include? params[:format]
380 end
397 end
381
398
382 # Renders a warning flash if obj has unsaved attachments
399 # Renders a warning flash if obj has unsaved attachments
383 def render_attachment_warning_if_needed(obj)
400 def render_attachment_warning_if_needed(obj)
384 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
401 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
385 end
402 end
386
403
387 # Sets the `flash` notice or error based the number of issues that did not save
404 # Sets the `flash` notice or error based the number of issues that did not save
388 #
405 #
389 # @param [Array, Issue] issues all of the saved and unsaved Issues
406 # @param [Array, Issue] issues all of the saved and unsaved Issues
390 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
407 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
391 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
408 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
392 if unsaved_issue_ids.empty?
409 if unsaved_issue_ids.empty?
393 flash[:notice] = l(:notice_successful_update) unless issues.empty?
410 flash[:notice] = l(:notice_successful_update) unless issues.empty?
394 else
411 else
395 flash[:error] = l(:notice_failed_to_save_issues,
412 flash[:error] = l(:notice_failed_to_save_issues,
396 :count => unsaved_issue_ids.size,
413 :count => unsaved_issue_ids.size,
397 :total => issues.size,
414 :total => issues.size,
398 :ids => '#' + unsaved_issue_ids.join(', #'))
415 :ids => '#' + unsaved_issue_ids.join(', #'))
399 end
416 end
400 end
417 end
401
418
402 # Rescues an invalid query statement. Just in case...
419 # Rescues an invalid query statement. Just in case...
403 def query_statement_invalid(exception)
420 def query_statement_invalid(exception)
404 logger.error "Query::StatementInvalid: #{exception.message}" if logger
421 logger.error "Query::StatementInvalid: #{exception.message}" if logger
405 session.delete(:query)
422 session.delete(:query)
406 sort_clear if respond_to?(:sort_clear)
423 sort_clear if respond_to?(:sort_clear)
407 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
424 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
408 end
425 end
409
426
410 # Converts the errors on an ActiveRecord object into a common JSON format
427 # Converts the errors on an ActiveRecord object into a common JSON format
411 def object_errors_to_json(object)
428 def object_errors_to_json(object)
412 object.errors.collect do |attribute, error|
429 object.errors.collect do |attribute, error|
413 { attribute => error }
430 { attribute => error }
414 end.to_json
431 end.to_json
415 end
432 end
416
433
417 # Renders API response on validation failure
434 # Renders API response on validation failure
418 def render_validation_errors(object)
435 def render_validation_errors(object)
419 options = { :status => :unprocessable_entity, :layout => false }
436 options = { :status => :unprocessable_entity, :layout => false }
420 options.merge!(case params[:format]
437 options.merge!(case params[:format]
421 when 'xml'; { :xml => object.errors }
438 when 'xml'; { :xml => object.errors }
422 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
439 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
423 else
440 else
424 raise "Unknown format #{params[:format]} in #render_validation_errors"
441 raise "Unknown format #{params[:format]} in #render_validation_errors"
425 end
442 end
426 )
443 )
427 render options
444 render options
428 end
445 end
429
446
430 # Overrides #default_template so that the api template
447 # Overrides #default_template so that the api template
431 # is used automatically if it exists
448 # is used automatically if it exists
432 def default_template(action_name = self.action_name)
449 def default_template(action_name = self.action_name)
433 if api_request?
450 if api_request?
434 begin
451 begin
435 return self.view_paths.find_template(default_template_name(action_name), 'api')
452 return self.view_paths.find_template(default_template_name(action_name), 'api')
436 rescue ::ActionView::MissingTemplate
453 rescue ::ActionView::MissingTemplate
437 # the api template was not found
454 # the api template was not found
438 # fallback to the default behaviour
455 # fallback to the default behaviour
439 end
456 end
440 end
457 end
441 super
458 super
442 end
459 end
443
460
444 # Overrides #pick_layout so that #render with no arguments
461 # Overrides #pick_layout so that #render with no arguments
445 # doesn't use the layout for api requests
462 # doesn't use the layout for api requests
446 def pick_layout(*args)
463 def pick_layout(*args)
447 api_request? ? nil : super
464 api_request? ? nil : super
448 end
465 end
449 end
466 end
@@ -1,313 +1,316
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, :create]
19 menu_item :new_issue, :only => [:new, :create]
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issue, :only => [:show, :edit, :update]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 before_filter :find_project, :only => [:new, :create]
25 before_filter :find_project, :only => [:new, :create]
26 before_filter :authorize, :except => [:index]
26 before_filter :authorize, :except => [:index]
27 before_filter :find_optional_project, :only => [:index]
27 before_filter :find_optional_project, :only => [:index]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 accept_key_auth :index, :show, :create, :update, :destroy
30 accept_key_auth :index, :show, :create, :update, :destroy
31
31
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33
33
34 helper :journals
34 helper :journals
35 helper :projects
35 helper :projects
36 include ProjectsHelper
36 include ProjectsHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :issue_relations
39 helper :issue_relations
40 include IssueRelationsHelper
40 include IssueRelationsHelper
41 helper :watchers
41 helper :watchers
42 include WatchersHelper
42 include WatchersHelper
43 helper :attachments
43 helper :attachments
44 include AttachmentsHelper
44 include AttachmentsHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :sort
47 helper :sort
48 include SortHelper
48 include SortHelper
49 include IssuesHelper
49 include IssuesHelper
50 helper :timelog
50 helper :timelog
51 helper :gantt
51 helper :gantt
52 include Redmine::Export::PDF
52 include Redmine::Export::PDF
53
53
54 verify :method => [:post, :delete],
54 verify :method => [:post, :delete],
55 :only => :destroy,
55 :only => :destroy,
56 :render => { :nothing => true, :status => :method_not_allowed }
56 :render => { :nothing => true, :status => :method_not_allowed }
57
57
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
61
61
62 def index
62 def index
63 retrieve_query
63 retrieve_query
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
65 sort_update(@query.sortable_columns)
65 sort_update(@query.sortable_columns)
66
66
67 if @query.valid?
67 if @query.valid?
68 limit = case params[:format]
68 case params[:format]
69 when 'csv', 'pdf'
69 when 'csv', 'pdf'
70 Setting.issues_export_limit.to_i
70 @limit = Setting.issues_export_limit.to_i
71 when 'atom'
71 when 'atom'
72 Setting.feeds_limit.to_i
72 @limit = Setting.feeds_limit.to_i
73 when 'xml', 'json'
74 @offset, @limit = api_offset_and_limit
73 else
75 else
74 per_page_option
76 @limit = per_page_option
75 end
77 end
76
78
77 @issue_count = @query.issue_count
79 @issue_count = @query.issue_count
78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
80 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
81 @offset ||= @issue_pages.current.offset
79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
82 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 :order => sort_clause,
83 :order => sort_clause,
81 :offset => @issue_pages.current.offset,
84 :offset => @offset,
82 :limit => limit)
85 :limit => @limit)
83 @issue_count_by_group = @query.issue_count_by_group
86 @issue_count_by_group = @query.issue_count_by_group
84
87
85 respond_to do |format|
88 respond_to do |format|
86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
89 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
87 format.api
90 format.api
88 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
91 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
89 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
92 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
90 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
93 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
91 end
94 end
92 else
95 else
93 # Send html if the query is not valid
96 # Send html if the query is not valid
94 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
97 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
95 end
98 end
96 rescue ActiveRecord::RecordNotFound
99 rescue ActiveRecord::RecordNotFound
97 render_404
100 render_404
98 end
101 end
99
102
100 def show
103 def show
101 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
104 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
102 @journals.each_with_index {|j,i| j.indice = i+1}
105 @journals.each_with_index {|j,i| j.indice = i+1}
103 @journals.reverse! if User.current.wants_comments_in_reverse_order?
106 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104 @changesets = @issue.changesets.visible.all
107 @changesets = @issue.changesets.visible.all
105 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
108 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
106 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
109 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
107 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
110 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
108 @priorities = IssuePriority.all
111 @priorities = IssuePriority.all
109 @time_entry = TimeEntry.new
112 @time_entry = TimeEntry.new
110 respond_to do |format|
113 respond_to do |format|
111 format.html { render :template => 'issues/show.rhtml' }
114 format.html { render :template => 'issues/show.rhtml' }
112 format.api
115 format.api
113 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
116 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
114 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
115 end
118 end
116 end
119 end
117
120
118 # Add a new issue
121 # Add a new issue
119 # The new issue will be created from an existing one if copy_from parameter is given
122 # The new issue will be created from an existing one if copy_from parameter is given
120 def new
123 def new
121 respond_to do |format|
124 respond_to do |format|
122 format.html { render :action => 'new', :layout => !request.xhr? }
125 format.html { render :action => 'new', :layout => !request.xhr? }
123 format.js { render :partial => 'attributes' }
126 format.js { render :partial => 'attributes' }
124 end
127 end
125 end
128 end
126
129
127 def create
130 def create
128 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
131 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
129 if @issue.save
132 if @issue.save
130 attachments = Attachment.attach_files(@issue, params[:attachments])
133 attachments = Attachment.attach_files(@issue, params[:attachments])
131 render_attachment_warning_if_needed(@issue)
134 render_attachment_warning_if_needed(@issue)
132 flash[:notice] = l(:notice_successful_create)
135 flash[:notice] = l(:notice_successful_create)
133 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
136 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
134 respond_to do |format|
137 respond_to do |format|
135 format.html {
138 format.html {
136 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
139 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
137 { :action => 'show', :id => @issue })
140 { :action => 'show', :id => @issue })
138 }
141 }
139 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
142 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
140 end
143 end
141 return
144 return
142 else
145 else
143 respond_to do |format|
146 respond_to do |format|
144 format.html { render :action => 'new' }
147 format.html { render :action => 'new' }
145 format.api { render_validation_errors(@issue) }
148 format.api { render_validation_errors(@issue) }
146 end
149 end
147 end
150 end
148 end
151 end
149
152
150 def edit
153 def edit
151 update_issue_from_params
154 update_issue_from_params
152
155
153 @journal = @issue.current_journal
156 @journal = @issue.current_journal
154
157
155 respond_to do |format|
158 respond_to do |format|
156 format.html { }
159 format.html { }
157 format.xml { }
160 format.xml { }
158 end
161 end
159 end
162 end
160
163
161 def update
164 def update
162 update_issue_from_params
165 update_issue_from_params
163
166
164 if @issue.save_issue_with_child_records(params, @time_entry)
167 if @issue.save_issue_with_child_records(params, @time_entry)
165 render_attachment_warning_if_needed(@issue)
168 render_attachment_warning_if_needed(@issue)
166 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
169 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
167
170
168 respond_to do |format|
171 respond_to do |format|
169 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
172 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
170 format.api { head :ok }
173 format.api { head :ok }
171 end
174 end
172 else
175 else
173 render_attachment_warning_if_needed(@issue)
176 render_attachment_warning_if_needed(@issue)
174 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
177 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
175 @journal = @issue.current_journal
178 @journal = @issue.current_journal
176
179
177 respond_to do |format|
180 respond_to do |format|
178 format.html { render :action => 'edit' }
181 format.html { render :action => 'edit' }
179 format.api { render_validation_errors(@issue) }
182 format.api { render_validation_errors(@issue) }
180 end
183 end
181 end
184 end
182 end
185 end
183
186
184 # Bulk edit a set of issues
187 # Bulk edit a set of issues
185 def bulk_edit
188 def bulk_edit
186 @issues.sort!
189 @issues.sort!
187 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
190 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
188 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
191 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
189 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
192 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
190 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
193 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
191 end
194 end
192
195
193 def bulk_update
196 def bulk_update
194 @issues.sort!
197 @issues.sort!
195 attributes = parse_params_for_bulk_issue_attributes(params)
198 attributes = parse_params_for_bulk_issue_attributes(params)
196
199
197 unsaved_issue_ids = []
200 unsaved_issue_ids = []
198 @issues.each do |issue|
201 @issues.each do |issue|
199 issue.reload
202 issue.reload
200 journal = issue.init_journal(User.current, params[:notes])
203 journal = issue.init_journal(User.current, params[:notes])
201 issue.safe_attributes = attributes
204 issue.safe_attributes = attributes
202 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
205 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
203 unless issue.save
206 unless issue.save
204 # Keep unsaved issue ids to display them in flash error
207 # Keep unsaved issue ids to display them in flash error
205 unsaved_issue_ids << issue.id
208 unsaved_issue_ids << issue.id
206 end
209 end
207 end
210 end
208 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
211 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
209 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
212 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
210 end
213 end
211
214
212 def destroy
215 def destroy
213 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
216 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
214 if @hours > 0
217 if @hours > 0
215 case params[:todo]
218 case params[:todo]
216 when 'destroy'
219 when 'destroy'
217 # nothing to do
220 # nothing to do
218 when 'nullify'
221 when 'nullify'
219 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
222 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
220 when 'reassign'
223 when 'reassign'
221 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
224 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
222 if reassign_to.nil?
225 if reassign_to.nil?
223 flash.now[:error] = l(:error_issue_not_found_in_project)
226 flash.now[:error] = l(:error_issue_not_found_in_project)
224 return
227 return
225 else
228 else
226 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
229 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
227 end
230 end
228 else
231 else
229 # display the destroy form if it's a user request
232 # display the destroy form if it's a user request
230 return unless api_request?
233 return unless api_request?
231 end
234 end
232 end
235 end
233 @issues.each(&:destroy)
236 @issues.each(&:destroy)
234 respond_to do |format|
237 respond_to do |format|
235 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
238 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
236 format.api { head :ok }
239 format.api { head :ok }
237 end
240 end
238 end
241 end
239
242
240 private
243 private
241 def find_issue
244 def find_issue
242 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
245 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
243 @project = @issue.project
246 @project = @issue.project
244 rescue ActiveRecord::RecordNotFound
247 rescue ActiveRecord::RecordNotFound
245 render_404
248 render_404
246 end
249 end
247
250
248 def find_project
251 def find_project
249 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
252 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
250 @project = Project.find(project_id)
253 @project = Project.find(project_id)
251 rescue ActiveRecord::RecordNotFound
254 rescue ActiveRecord::RecordNotFound
252 render_404
255 render_404
253 end
256 end
254
257
255 # Used by #edit and #update to set some common instance variables
258 # Used by #edit and #update to set some common instance variables
256 # from the params
259 # from the params
257 # TODO: Refactor, not everything in here is needed by #edit
260 # TODO: Refactor, not everything in here is needed by #edit
258 def update_issue_from_params
261 def update_issue_from_params
259 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
262 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
260 @priorities = IssuePriority.all
263 @priorities = IssuePriority.all
261 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
264 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
262 @time_entry = TimeEntry.new
265 @time_entry = TimeEntry.new
263 @time_entry.attributes = params[:time_entry]
266 @time_entry.attributes = params[:time_entry]
264
267
265 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
268 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
266 @issue.init_journal(User.current, @notes)
269 @issue.init_journal(User.current, @notes)
267 @issue.safe_attributes = params[:issue]
270 @issue.safe_attributes = params[:issue]
268 end
271 end
269
272
270 # TODO: Refactor, lots of extra code in here
273 # TODO: Refactor, lots of extra code in here
271 # TODO: Changing tracker on an existing issue should not trigger this
274 # TODO: Changing tracker on an existing issue should not trigger this
272 def build_new_issue_from_params
275 def build_new_issue_from_params
273 if params[:id].blank?
276 if params[:id].blank?
274 @issue = Issue.new
277 @issue = Issue.new
275 @issue.copy_from(params[:copy_from]) if params[:copy_from]
278 @issue.copy_from(params[:copy_from]) if params[:copy_from]
276 @issue.project = @project
279 @issue.project = @project
277 else
280 else
278 @issue = @project.issues.visible.find(params[:id])
281 @issue = @project.issues.visible.find(params[:id])
279 end
282 end
280
283
281 @issue.project = @project
284 @issue.project = @project
282 # Tracker must be set before custom field values
285 # Tracker must be set before custom field values
283 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
286 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
284 if @issue.tracker.nil?
287 if @issue.tracker.nil?
285 render_error l(:error_no_tracker_in_project)
288 render_error l(:error_no_tracker_in_project)
286 return false
289 return false
287 end
290 end
288 @issue.start_date ||= Date.today
291 @issue.start_date ||= Date.today
289 if params[:issue].is_a?(Hash)
292 if params[:issue].is_a?(Hash)
290 @issue.safe_attributes = params[:issue]
293 @issue.safe_attributes = params[:issue]
291 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
294 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
292 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
295 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
293 end
296 end
294 end
297 end
295 @issue.author = User.current
298 @issue.author = User.current
296 @priorities = IssuePriority.all
299 @priorities = IssuePriority.all
297 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
300 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
298 end
301 end
299
302
300 def check_for_default_issue_status
303 def check_for_default_issue_status
301 if IssueStatus.default.nil?
304 if IssueStatus.default.nil?
302 render_error l(:error_no_default_issue_status)
305 render_error l(:error_no_default_issue_status)
303 return false
306 return false
304 end
307 end
305 end
308 end
306
309
307 def parse_params_for_bulk_issue_attributes(params)
310 def parse_params_for_bulk_issue_attributes(params)
308 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
311 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
309 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
312 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
310 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
313 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
311 attributes
314 attributes
312 end
315 end
313 end
316 end
@@ -1,223 +1,230
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 UsersController < ApplicationController
18 class UsersController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :show
21 before_filter :require_admin, :except => :show
22 accept_key_auth :index, :show, :create, :update
22 accept_key_auth :index, :show, :create, :update
23
23
24 helper :sort
24 helper :sort
25 include SortHelper
25 include SortHelper
26 helper :custom_fields
26 helper :custom_fields
27 include CustomFieldsHelper
27 include CustomFieldsHelper
28
28
29 def index
29 def index
30 sort_init 'login', 'asc'
30 sort_init 'login', 'asc'
31 sort_update %w(login firstname lastname mail admin created_on last_login_on)
31 sort_update %w(login firstname lastname mail admin created_on last_login_on)
32
32
33 case params[:format]
34 when 'xml', 'json'
35 @offset, @limit = api_offset_and_limit
36 else
37 @limit = per_page_option
38 end
39
33 @status = params[:status] ? params[:status].to_i : 1
40 @status = params[:status] ? params[:status].to_i : 1
34 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
41 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
35
42
36 unless params[:name].blank?
43 unless params[:name].blank?
37 name = "%#{params[:name].strip.downcase}%"
44 name = "%#{params[:name].strip.downcase}%"
38 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
45 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
39 end
46 end
40
47
41 @user_count = User.count(:conditions => c.conditions)
48 @user_count = User.count(:conditions => c.conditions)
42 @user_pages = Paginator.new self, @user_count,
49 @user_pages = Paginator.new self, @user_count, @limit, params['page']
43 per_page_option,
50 @offset ||= @user_pages.current.offset
44 params['page']
51 @users = User.find :all,
45 @users = User.find :all,:order => sort_clause,
52 :order => sort_clause,
46 :conditions => c.conditions,
53 :conditions => c.conditions,
47 :limit => @user_pages.items_per_page,
54 :limit => @limit,
48 :offset => @user_pages.current.offset
55 :offset => @offset
49
56
50 respond_to do |format|
57 respond_to do |format|
51 format.html { render :layout => !request.xhr? }
58 format.html { render :layout => !request.xhr? }
52 format.api
59 format.api
53 end
60 end
54 end
61 end
55
62
56 def show
63 def show
57 @user = User.find(params[:id])
64 @user = User.find(params[:id])
58
65
59 # show projects based on current user visibility
66 # show projects based on current user visibility
60 @memberships = @user.memberships.all(:conditions => Project.visible_by(User.current))
67 @memberships = @user.memberships.all(:conditions => Project.visible_by(User.current))
61
68
62 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
69 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
63 @events_by_day = events.group_by(&:event_date)
70 @events_by_day = events.group_by(&:event_date)
64
71
65 unless User.current.admin?
72 unless User.current.admin?
66 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
73 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
67 render_404
74 render_404
68 return
75 return
69 end
76 end
70 end
77 end
71
78
72 respond_to do |format|
79 respond_to do |format|
73 format.html { render :layout => 'base' }
80 format.html { render :layout => 'base' }
74 format.api
81 format.api
75 end
82 end
76 rescue ActiveRecord::RecordNotFound
83 rescue ActiveRecord::RecordNotFound
77 render_404
84 render_404
78 end
85 end
79
86
80 def new
87 def new
81 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
88 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
82 @notification_option = Setting.default_notification_option
89 @notification_option = Setting.default_notification_option
83
90
84 @user = User.new(:language => Setting.default_language)
91 @user = User.new(:language => Setting.default_language)
85 @auth_sources = AuthSource.find(:all)
92 @auth_sources = AuthSource.find(:all)
86 end
93 end
87
94
88 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
95 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
89 def create
96 def create
90 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
97 @notification_options = User::MAIL_NOTIFICATION_OPTIONS
91 @notification_option = Setting.default_notification_option
98 @notification_option = Setting.default_notification_option
92
99
93 @user = User.new(params[:user])
100 @user = User.new(params[:user])
94 @user.admin = params[:user][:admin] || false
101 @user.admin = params[:user][:admin] || false
95 @user.login = params[:user][:login]
102 @user.login = params[:user][:login]
96 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
103 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
97
104
98 # TODO: Similar to My#account
105 # TODO: Similar to My#account
99 @user.mail_notification = params[:notification_option] || 'only_my_events'
106 @user.mail_notification = params[:notification_option] || 'only_my_events'
100 @user.pref.attributes = params[:pref]
107 @user.pref.attributes = params[:pref]
101 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
108 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
102
109
103 if @user.save
110 if @user.save
104 @user.pref.save
111 @user.pref.save
105 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
112 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
106
113
107 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
114 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
108
115
109 respond_to do |format|
116 respond_to do |format|
110 format.html {
117 format.html {
111 flash[:notice] = l(:notice_successful_create)
118 flash[:notice] = l(:notice_successful_create)
112 redirect_to(params[:continue] ?
119 redirect_to(params[:continue] ?
113 {:controller => 'users', :action => 'new'} :
120 {:controller => 'users', :action => 'new'} :
114 {:controller => 'users', :action => 'edit', :id => @user}
121 {:controller => 'users', :action => 'edit', :id => @user}
115 )
122 )
116 }
123 }
117 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
124 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
118 end
125 end
119 else
126 else
120 @auth_sources = AuthSource.find(:all)
127 @auth_sources = AuthSource.find(:all)
121 @notification_option = @user.mail_notification
128 @notification_option = @user.mail_notification
122
129
123 respond_to do |format|
130 respond_to do |format|
124 format.html { render :action => 'new' }
131 format.html { render :action => 'new' }
125 format.api { render_validation_errors(@user) }
132 format.api { render_validation_errors(@user) }
126 end
133 end
127 end
134 end
128 end
135 end
129
136
130 def edit
137 def edit
131 @user = User.find(params[:id])
138 @user = User.find(params[:id])
132 @notification_options = @user.valid_notification_options
139 @notification_options = @user.valid_notification_options
133 @notification_option = @user.mail_notification
140 @notification_option = @user.mail_notification
134
141
135 @auth_sources = AuthSource.find(:all)
142 @auth_sources = AuthSource.find(:all)
136 @membership ||= Member.new
143 @membership ||= Member.new
137 end
144 end
138
145
139 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
146 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
140 def update
147 def update
141 @user = User.find(params[:id])
148 @user = User.find(params[:id])
142 @notification_options = @user.valid_notification_options
149 @notification_options = @user.valid_notification_options
143 @notification_option = @user.mail_notification
150 @notification_option = @user.mail_notification
144
151
145 @user.admin = params[:user][:admin] if params[:user][:admin]
152 @user.admin = params[:user][:admin] if params[:user][:admin]
146 @user.login = params[:user][:login] if params[:user][:login]
153 @user.login = params[:user][:login] if params[:user][:login]
147 if params[:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
154 if params[:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
148 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
155 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
149 end
156 end
150 @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
157 @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
151 @user.attributes = params[:user]
158 @user.attributes = params[:user]
152 # Was the account actived ? (do it before User#save clears the change)
159 # Was the account actived ? (do it before User#save clears the change)
153 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
160 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
154 # TODO: Similar to My#account
161 # TODO: Similar to My#account
155 @user.mail_notification = params[:notification_option] || 'only_my_events'
162 @user.mail_notification = params[:notification_option] || 'only_my_events'
156 @user.pref.attributes = params[:pref]
163 @user.pref.attributes = params[:pref]
157 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
164 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
158
165
159 if @user.save
166 if @user.save
160 @user.pref.save
167 @user.pref.save
161 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
168 @user.notified_project_ids = (params[:notification_option] == 'selected' ? params[:notified_project_ids] : [])
162
169
163 if was_activated
170 if was_activated
164 Mailer.deliver_account_activated(@user)
171 Mailer.deliver_account_activated(@user)
165 elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
172 elsif @user.active? && params[:send_information] && !params[:password].blank? && @user.auth_source_id.nil?
166 Mailer.deliver_account_information(@user, params[:password])
173 Mailer.deliver_account_information(@user, params[:password])
167 end
174 end
168
175
169 respond_to do |format|
176 respond_to do |format|
170 format.html {
177 format.html {
171 flash[:notice] = l(:notice_successful_update)
178 flash[:notice] = l(:notice_successful_update)
172 redirect_to :back
179 redirect_to :back
173 }
180 }
174 format.api { head :ok }
181 format.api { head :ok }
175 end
182 end
176 else
183 else
177 @auth_sources = AuthSource.find(:all)
184 @auth_sources = AuthSource.find(:all)
178 @membership ||= Member.new
185 @membership ||= Member.new
179
186
180 respond_to do |format|
187 respond_to do |format|
181 format.html { render :action => :edit }
188 format.html { render :action => :edit }
182 format.api { render_validation_errors(@user) }
189 format.api { render_validation_errors(@user) }
183 end
190 end
184 end
191 end
185 rescue ::ActionController::RedirectBackError
192 rescue ::ActionController::RedirectBackError
186 redirect_to :controller => 'users', :action => 'edit', :id => @user
193 redirect_to :controller => 'users', :action => 'edit', :id => @user
187 end
194 end
188
195
189 def edit_membership
196 def edit_membership
190 @user = User.find(params[:id])
197 @user = User.find(params[:id])
191 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
198 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
192 @membership.save if request.post?
199 @membership.save if request.post?
193 respond_to do |format|
200 respond_to do |format|
194 if @membership.valid?
201 if @membership.valid?
195 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
202 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
196 format.js {
203 format.js {
197 render(:update) {|page|
204 render(:update) {|page|
198 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
205 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
199 page.visual_effect(:highlight, "member-#{@membership.id}")
206 page.visual_effect(:highlight, "member-#{@membership.id}")
200 }
207 }
201 }
208 }
202 else
209 else
203 format.js {
210 format.js {
204 render(:update) {|page|
211 render(:update) {|page|
205 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
212 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
206 }
213 }
207 }
214 }
208 end
215 end
209 end
216 end
210 end
217 end
211
218
212 def destroy_membership
219 def destroy_membership
213 @user = User.find(params[:id])
220 @user = User.find(params[:id])
214 @membership = Member.find(params[:membership_id])
221 @membership = Member.find(params[:membership_id])
215 if request.post? && @membership.deletable?
222 if request.post? && @membership.deletable?
216 @membership.destroy
223 @membership.destroy
217 end
224 end
218 respond_to do |format|
225 respond_to do |format|
219 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
226 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
220 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
227 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
221 end
228 end
222 end
229 end
223 end
230 end
@@ -1,895 +1,907
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 'forwardable'
18 require 'forwardable'
19 require 'cgi'
19 require 'cgi'
20
20
21 module ApplicationHelper
21 module ApplicationHelper
22 include Redmine::WikiFormatting::Macros::Definitions
22 include Redmine::WikiFormatting::Macros::Definitions
23 include Redmine::I18n
23 include Redmine::I18n
24 include GravatarHelper::PublicMethods
24 include GravatarHelper::PublicMethods
25
25
26 extend Forwardable
26 extend Forwardable
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28
28
29 # Return true if user is authorized for controller/action, otherwise false
29 # Return true if user is authorized for controller/action, otherwise false
30 def authorize_for(controller, action)
30 def authorize_for(controller, action)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 end
32 end
33
33
34 # Display a link if user is authorized
34 # Display a link if user is authorized
35 #
35 #
36 # @param [String] name Anchor text (passed to link_to)
36 # @param [String] name Anchor text (passed to link_to)
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 # @param [optional, Hash] html_options Options passed to link_to
38 # @param [optional, Hash] html_options Options passed to link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
42 end
43
43
44 # Display a link to remote if user is authorized
44 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
46 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active?
54 if user.active?
55 link_to name, :controller => 'users', :action => 'show', :id => user
55 link_to name, :controller => 'users', :action => 'show', :id => user
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 #
71 #
72 def link_to_issue(issue, options={})
72 def link_to_issue(issue, options={})
73 title = nil
73 title = nil
74 subject = nil
74 subject = nil
75 if options[:subject] == false
75 if options[:subject] == false
76 title = truncate(issue.subject, :length => 60)
76 title = truncate(issue.subject, :length => 60)
77 else
77 else
78 subject = issue.subject
78 subject = issue.subject
79 if options[:truncate]
79 if options[:truncate]
80 subject = truncate(subject, :length => options[:truncate])
80 subject = truncate(subject, :length => options[:truncate])
81 end
81 end
82 end
82 end
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 :class => issue.css_classes,
84 :class => issue.css_classes,
85 :title => title
85 :title => title
86 s << ": #{h subject}" if subject
86 s << ": #{h subject}" if subject
87 s = "#{h issue.project} - " + s if options[:project]
87 s = "#{h issue.project} - " + s if options[:project]
88 s
88 s
89 end
89 end
90
90
91 # Generates a link to an attachment.
91 # Generates a link to an attachment.
92 # Options:
92 # Options:
93 # * :text - Link text (default to attachment filename)
93 # * :text - Link text (default to attachment filename)
94 # * :download - Force download (default: false)
94 # * :download - Force download (default: false)
95 def link_to_attachment(attachment, options={})
95 def link_to_attachment(attachment, options={})
96 text = options.delete(:text) || attachment.filename
96 text = options.delete(:text) || attachment.filename
97 action = options.delete(:download) ? 'download' : 'show'
97 action = options.delete(:download) ? 'download' : 'show'
98
98
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 end
100 end
101
101
102 # Generates a link to a SCM revision
102 # Generates a link to a SCM revision
103 # Options:
103 # Options:
104 # * :text - Link text (default to the formatted revision)
104 # * :text - Link text (default to the formatted revision)
105 def link_to_revision(revision, project, options={})
105 def link_to_revision(revision, project, options={})
106 text = options.delete(:text) || format_revision(revision)
106 text = options.delete(:text) || format_revision(revision)
107
107
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 end
109 end
110
110
111 # Generates a link to a project if active
111 # Generates a link to a project if active
112 # Examples:
112 # Examples:
113 #
113 #
114 # link_to_project(project) # => link to the specified project overview
114 # link_to_project(project) # => link to the specified project overview
115 # link_to_project(project, :action=>'settings') # => link to project settings
115 # link_to_project(project, :action=>'settings') # => link to project settings
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 #
118 #
119 def link_to_project(project, options={}, html_options = nil)
119 def link_to_project(project, options={}, html_options = nil)
120 if project.active?
120 if project.active?
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 link_to(h(project), url, html_options)
122 link_to(h(project), url, html_options)
123 else
123 else
124 h(project)
124 h(project)
125 end
125 end
126 end
126 end
127
127
128 def toggle_link(name, id, options={})
128 def toggle_link(name, id, options={})
129 onclick = "Element.toggle('#{id}'); "
129 onclick = "Element.toggle('#{id}'); "
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 onclick << "return false;"
131 onclick << "return false;"
132 link_to(name, "#", :onclick => onclick)
132 link_to(name, "#", :onclick => onclick)
133 end
133 end
134
134
135 def image_to_function(name, function, html_options = {})
135 def image_to_function(name, function, html_options = {})
136 html_options.symbolize_keys!
136 html_options.symbolize_keys!
137 tag(:input, html_options.merge({
137 tag(:input, html_options.merge({
138 :type => "image", :src => image_path(name),
138 :type => "image", :src => image_path(name),
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 }))
140 }))
141 end
141 end
142
142
143 def prompt_to_remote(name, text, param, url, html_options = {})
143 def prompt_to_remote(name, text, param, url, html_options = {})
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 link_to name, {}, html_options
145 link_to name, {}, html_options
146 end
146 end
147
147
148 def format_activity_title(text)
148 def format_activity_title(text)
149 h(truncate_single_line(text, :length => 100))
149 h(truncate_single_line(text, :length => 100))
150 end
150 end
151
151
152 def format_activity_day(date)
152 def format_activity_day(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 end
154 end
155
155
156 def format_activity_description(text)
156 def format_activity_description(text)
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 end
158 end
159
159
160 def format_version_name(version)
160 def format_version_name(version)
161 if version.project == @project
161 if version.project == @project
162 h(version)
162 h(version)
163 else
163 else
164 h("#{version.project} - #{version}")
164 h("#{version.project} - #{version}")
165 end
165 end
166 end
166 end
167
167
168 def due_date_distance_in_words(date)
168 def due_date_distance_in_words(date)
169 if date
169 if date
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 end
171 end
172 end
172 end
173
173
174 def render_page_hierarchy(pages, node=nil)
174 def render_page_hierarchy(pages, node=nil)
175 content = ''
175 content = ''
176 if pages[node]
176 if pages[node]
177 content << "<ul class=\"pages-hierarchy\">\n"
177 content << "<ul class=\"pages-hierarchy\">\n"
178 pages[node].each do |page|
178 pages[node].each do |page|
179 content << "<li>"
179 content << "<li>"
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 content << "</li>\n"
183 content << "</li>\n"
184 end
184 end
185 content << "</ul>\n"
185 content << "</ul>\n"
186 end
186 end
187 content
187 content
188 end
188 end
189
189
190 # Renders flash messages
190 # Renders flash messages
191 def render_flash_messages
191 def render_flash_messages
192 s = ''
192 s = ''
193 flash.each do |k,v|
193 flash.each do |k,v|
194 s << content_tag('div', v, :class => "flash #{k}")
194 s << content_tag('div', v, :class => "flash #{k}")
195 end
195 end
196 s
196 s
197 end
197 end
198
198
199 # Renders tabs and their content
199 # Renders tabs and their content
200 def render_tabs(tabs)
200 def render_tabs(tabs)
201 if tabs.any?
201 if tabs.any?
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 else
203 else
204 content_tag 'p', l(:label_no_data), :class => "nodata"
204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 end
205 end
206 end
206 end
207
207
208 # Renders the project quick-jump box
208 # Renders the project quick-jump box
209 def render_project_jump_box
209 def render_project_jump_box
210 # Retrieve them now to avoid a COUNT query
210 # Retrieve them now to avoid a COUNT query
211 projects = User.current.projects.all
211 projects = User.current.projects.all
212 if projects.any?
212 if projects.any?
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 '<option value="" disabled="disabled">---</option>'
215 '<option value="" disabled="disabled">---</option>'
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 end
218 end
219 s << '</select>'
219 s << '</select>'
220 s
220 s
221 end
221 end
222 end
222 end
223
223
224 def project_tree_options_for_select(projects, options = {})
224 def project_tree_options_for_select(projects, options = {})
225 s = ''
225 s = ''
226 project_tree(projects) do |project, level|
226 project_tree(projects) do |project, level|
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 tag_options = {:value => project.id}
228 tag_options = {:value => project.id}
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 tag_options[:selected] = 'selected'
230 tag_options[:selected] = 'selected'
231 else
231 else
232 tag_options[:selected] = nil
232 tag_options[:selected] = nil
233 end
233 end
234 tag_options.merge!(yield(project)) if block_given?
234 tag_options.merge!(yield(project)) if block_given?
235 s << content_tag('option', name_prefix + h(project), tag_options)
235 s << content_tag('option', name_prefix + h(project), tag_options)
236 end
236 end
237 s
237 s
238 end
238 end
239
239
240 # Yields the given block for each project with its level in the tree
240 # Yields the given block for each project with its level in the tree
241 #
241 #
242 # Wrapper for Project#project_tree
242 # Wrapper for Project#project_tree
243 def project_tree(projects, &block)
243 def project_tree(projects, &block)
244 Project.project_tree(projects, &block)
244 Project.project_tree(projects, &block)
245 end
245 end
246
246
247 def project_nested_ul(projects, &block)
247 def project_nested_ul(projects, &block)
248 s = ''
248 s = ''
249 if projects.any?
249 if projects.any?
250 ancestors = []
250 ancestors = []
251 projects.sort_by(&:lft).each do |project|
251 projects.sort_by(&:lft).each do |project|
252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 s << "<ul>\n"
253 s << "<ul>\n"
254 else
254 else
255 ancestors.pop
255 ancestors.pop
256 s << "</li>"
256 s << "</li>"
257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 ancestors.pop
258 ancestors.pop
259 s << "</ul></li>\n"
259 s << "</ul></li>\n"
260 end
260 end
261 end
261 end
262 s << "<li>"
262 s << "<li>"
263 s << yield(project).to_s
263 s << yield(project).to_s
264 ancestors << project
264 ancestors << project
265 end
265 end
266 s << ("</li></ul>\n" * ancestors.size)
266 s << ("</li></ul>\n" * ancestors.size)
267 end
267 end
268 s
268 s
269 end
269 end
270
270
271 def principals_check_box_tags(name, principals)
271 def principals_check_box_tags(name, principals)
272 s = ''
272 s = ''
273 principals.sort.each do |principal|
273 principals.sort.each do |principal|
274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 end
275 end
276 s
276 s
277 end
277 end
278
278
279 # Truncates and returns the string as a single line
279 # Truncates and returns the string as a single line
280 def truncate_single_line(string, *args)
280 def truncate_single_line(string, *args)
281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 end
282 end
283
283
284 # Truncates at line break after 250 characters or options[:length]
284 # Truncates at line break after 250 characters or options[:length]
285 def truncate_lines(string, options={})
285 def truncate_lines(string, options={})
286 length = options[:length] || 250
286 length = options[:length] || 250
287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 "#{$1}..."
288 "#{$1}..."
289 else
289 else
290 string
290 string
291 end
291 end
292 end
292 end
293
293
294 def html_hours(text)
294 def html_hours(text)
295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 end
296 end
297
297
298 def authoring(created, author, options={})
298 def authoring(created, author, options={})
299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 end
300 end
301
301
302 def time_tag(time)
302 def time_tag(time)
303 text = distance_of_time_in_words(Time.now, time)
303 text = distance_of_time_in_words(Time.now, time)
304 if @project
304 if @project
305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 else
306 else
307 content_tag('acronym', text, :title => format_time(time))
307 content_tag('acronym', text, :title => format_time(time))
308 end
308 end
309 end
309 end
310
310
311 def syntax_highlight(name, content)
311 def syntax_highlight(name, content)
312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 end
313 end
314
314
315 def to_path_param(path)
315 def to_path_param(path)
316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 end
317 end
318
318
319 def pagination_links_full(paginator, count=nil, options={})
319 def pagination_links_full(paginator, count=nil, options={})
320 page_param = options.delete(:page_param) || :page
320 page_param = options.delete(:page_param) || :page
321 per_page_links = options.delete(:per_page_links)
321 per_page_links = options.delete(:per_page_links)
322 url_param = params.dup
322 url_param = params.dup
323 # don't reuse query params if filters are present
323 # don't reuse query params if filters are present
324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325
325
326 html = ''
326 html = ''
327 if paginator.current.previous
327 if paginator.current.previous
328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 end
329 end
330
330
331 html << (pagination_links_each(paginator, options) do |n|
331 html << (pagination_links_each(paginator, options) do |n|
332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 end || '')
333 end || '')
334
334
335 if paginator.current.next
335 if paginator.current.next
336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 end
337 end
338
338
339 unless count.nil?
339 unless count.nil?
340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 html << " | #{links}"
342 html << " | #{links}"
343 end
343 end
344 end
344 end
345
345
346 html
346 html
347 end
347 end
348
348
349 def per_page_links(selected=nil)
349 def per_page_links(selected=nil)
350 url_param = params.dup
350 url_param = params.dup
351 url_param.clear if url_param.has_key?(:set_filter)
351 url_param.clear if url_param.has_key?(:set_filter)
352
352
353 links = Setting.per_page_options_array.collect do |n|
353 links = Setting.per_page_options_array.collect do |n|
354 n == selected ? n : link_to_remote(n, {:update => "content",
354 n == selected ? n : link_to_remote(n, {:update => "content",
355 :url => params.dup.merge(:per_page => n),
355 :url => params.dup.merge(:per_page => n),
356 :method => :get},
356 :method => :get},
357 {:href => url_for(url_param.merge(:per_page => n))})
357 {:href => url_for(url_param.merge(:per_page => n))})
358 end
358 end
359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 end
360 end
361
361
362 def reorder_links(name, url)
362 def reorder_links(name, url)
363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
367 end
367 end
368
368
369 def breadcrumb(*args)
369 def breadcrumb(*args)
370 elements = args.flatten
370 elements = args.flatten
371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 end
372 end
373
373
374 def other_formats_links(&block)
374 def other_formats_links(&block)
375 concat('<p class="other-formats">' + l(:label_export_to))
375 concat('<p class="other-formats">' + l(:label_export_to))
376 yield Redmine::Views::OtherFormatsBuilder.new(self)
376 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 concat('</p>')
377 concat('</p>')
378 end
378 end
379
379
380 def page_header_title
380 def page_header_title
381 if @project.nil? || @project.new_record?
381 if @project.nil? || @project.new_record?
382 h(Setting.app_title)
382 h(Setting.app_title)
383 else
383 else
384 b = []
384 b = []
385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 if ancestors.any?
386 if ancestors.any?
387 root = ancestors.shift
387 root = ancestors.shift
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 if ancestors.size > 2
389 if ancestors.size > 2
390 b << '&#8230;'
390 b << '&#8230;'
391 ancestors = ancestors[-2, 2]
391 ancestors = ancestors[-2, 2]
392 end
392 end
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 end
394 end
395 b << h(@project)
395 b << h(@project)
396 b.join(' &#187; ')
396 b.join(' &#187; ')
397 end
397 end
398 end
398 end
399
399
400 def html_title(*args)
400 def html_title(*args)
401 if args.empty?
401 if args.empty?
402 title = []
402 title = []
403 title << @project.name if @project
403 title << @project.name if @project
404 title += @html_title if @html_title
404 title += @html_title if @html_title
405 title << Setting.app_title
405 title << Setting.app_title
406 title.select {|t| !t.blank? }.join(' - ')
406 title.select {|t| !t.blank? }.join(' - ')
407 else
407 else
408 @html_title ||= []
408 @html_title ||= []
409 @html_title += args
409 @html_title += args
410 end
410 end
411 end
411 end
412
412
413 # Returns the theme, controller name, and action as css classes for the
413 # Returns the theme, controller name, and action as css classes for the
414 # HTML body.
414 # HTML body.
415 def body_css_classes
415 def body_css_classes
416 css = []
416 css = []
417 if theme = Redmine::Themes.theme(Setting.ui_theme)
417 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 css << 'theme-' + theme.name
418 css << 'theme-' + theme.name
419 end
419 end
420
420
421 css << 'controller-' + params[:controller]
421 css << 'controller-' + params[:controller]
422 css << 'action-' + params[:action]
422 css << 'action-' + params[:action]
423 css.join(' ')
423 css.join(' ')
424 end
424 end
425
425
426 def accesskey(s)
426 def accesskey(s)
427 Redmine::AccessKeys.key_for s
427 Redmine::AccessKeys.key_for s
428 end
428 end
429
429
430 # Formats text according to system settings.
430 # Formats text according to system settings.
431 # 2 ways to call this method:
431 # 2 ways to call this method:
432 # * with a String: textilizable(text, options)
432 # * with a String: textilizable(text, options)
433 # * with an object and one of its attribute: textilizable(issue, :description, options)
433 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 def textilizable(*args)
434 def textilizable(*args)
435 options = args.last.is_a?(Hash) ? args.pop : {}
435 options = args.last.is_a?(Hash) ? args.pop : {}
436 case args.size
436 case args.size
437 when 1
437 when 1
438 obj = options[:object]
438 obj = options[:object]
439 text = args.shift
439 text = args.shift
440 when 2
440 when 2
441 obj = args.shift
441 obj = args.shift
442 attr = args.shift
442 attr = args.shift
443 text = obj.send(attr).to_s
443 text = obj.send(attr).to_s
444 else
444 else
445 raise ArgumentError, 'invalid arguments to textilizable'
445 raise ArgumentError, 'invalid arguments to textilizable'
446 end
446 end
447 return '' if text.blank?
447 return '' if text.blank?
448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 only_path = options.delete(:only_path) == false ? false : true
449 only_path = options.delete(:only_path) == false ? false : true
450
450
451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452
452
453 parse_non_pre_blocks(text) do |text|
453 parse_non_pre_blocks(text) do |text|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
455 send method_name, text, project, obj, attr, only_path, options
455 send method_name, text, project, obj, attr, only_path, options
456 end
456 end
457 end
457 end
458 end
458 end
459
459
460 def parse_non_pre_blocks(text)
460 def parse_non_pre_blocks(text)
461 s = StringScanner.new(text)
461 s = StringScanner.new(text)
462 tags = []
462 tags = []
463 parsed = ''
463 parsed = ''
464 while !s.eos?
464 while !s.eos?
465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
467 if tags.empty?
467 if tags.empty?
468 yield text
468 yield text
469 end
469 end
470 parsed << text
470 parsed << text
471 if tag
471 if tag
472 if closing
472 if closing
473 if tags.last == tag.downcase
473 if tags.last == tag.downcase
474 tags.pop
474 tags.pop
475 end
475 end
476 else
476 else
477 tags << tag.downcase
477 tags << tag.downcase
478 end
478 end
479 parsed << full_tag
479 parsed << full_tag
480 end
480 end
481 end
481 end
482 # Close any non closing tags
482 # Close any non closing tags
483 while tag = tags.pop
483 while tag = tags.pop
484 parsed << "</#{tag}>"
484 parsed << "</#{tag}>"
485 end
485 end
486 parsed
486 parsed
487 end
487 end
488
488
489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
490 # when using an image link, try to use an attachment, if possible
490 # when using an image link, try to use an attachment, if possible
491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
492 attachments = nil
492 attachments = nil
493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
496 # search for the picture in attachments
496 # search for the picture in attachments
497 if found = attachments.detect { |att| att.filename.downcase == filename }
497 if found = attachments.detect { |att| att.filename.downcase == filename }
498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
499 desc = found.description.to_s.gsub('"', '')
499 desc = found.description.to_s.gsub('"', '')
500 if !desc.blank? && alttext.blank?
500 if !desc.blank? && alttext.blank?
501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
502 end
502 end
503 "src=\"#{image_url}\"#{alt}"
503 "src=\"#{image_url}\"#{alt}"
504 else
504 else
505 m
505 m
506 end
506 end
507 end
507 end
508 end
508 end
509 end
509 end
510
510
511 # Wiki links
511 # Wiki links
512 #
512 #
513 # Examples:
513 # Examples:
514 # [[mypage]]
514 # [[mypage]]
515 # [[mypage|mytext]]
515 # [[mypage|mytext]]
516 # wiki links can refer other project wikis, using project name or identifier:
516 # wiki links can refer other project wikis, using project name or identifier:
517 # [[project:]] -> wiki starting page
517 # [[project:]] -> wiki starting page
518 # [[project:|mytext]]
518 # [[project:|mytext]]
519 # [[project:mypage]]
519 # [[project:mypage]]
520 # [[project:mypage|mytext]]
520 # [[project:mypage|mytext]]
521 def parse_wiki_links(text, project, obj, attr, only_path, options)
521 def parse_wiki_links(text, project, obj, attr, only_path, options)
522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
523 link_project = project
523 link_project = project
524 esc, all, page, title = $1, $2, $3, $5
524 esc, all, page, title = $1, $2, $3, $5
525 if esc.nil?
525 if esc.nil?
526 if page =~ /^([^\:]+)\:(.*)$/
526 if page =~ /^([^\:]+)\:(.*)$/
527 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
527 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
528 page = $2
528 page = $2
529 title ||= $1 if page.blank?
529 title ||= $1 if page.blank?
530 end
530 end
531
531
532 if link_project && link_project.wiki
532 if link_project && link_project.wiki
533 # extract anchor
533 # extract anchor
534 anchor = nil
534 anchor = nil
535 if page =~ /^(.+?)\#(.+)$/
535 if page =~ /^(.+?)\#(.+)$/
536 page, anchor = $1, $2
536 page, anchor = $1, $2
537 end
537 end
538 # check if page exists
538 # check if page exists
539 wiki_page = link_project.wiki.find_page(page)
539 wiki_page = link_project.wiki.find_page(page)
540 url = case options[:wiki_links]
540 url = case options[:wiki_links]
541 when :local; "#{title}.html"
541 when :local; "#{title}.html"
542 when :anchor; "##{title}" # used for single-file wiki export
542 when :anchor; "##{title}" # used for single-file wiki export
543 else
543 else
544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
546 end
546 end
547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
548 else
548 else
549 # project or wiki doesn't exist
549 # project or wiki doesn't exist
550 all
550 all
551 end
551 end
552 else
552 else
553 all
553 all
554 end
554 end
555 end
555 end
556 end
556 end
557
557
558 # Redmine links
558 # Redmine links
559 #
559 #
560 # Examples:
560 # Examples:
561 # Issues:
561 # Issues:
562 # #52 -> Link to issue #52
562 # #52 -> Link to issue #52
563 # Changesets:
563 # Changesets:
564 # r52 -> Link to revision 52
564 # r52 -> Link to revision 52
565 # commit:a85130f -> Link to scmid starting with a85130f
565 # commit:a85130f -> Link to scmid starting with a85130f
566 # Documents:
566 # Documents:
567 # document#17 -> Link to document with id 17
567 # document#17 -> Link to document with id 17
568 # document:Greetings -> Link to the document with title "Greetings"
568 # document:Greetings -> Link to the document with title "Greetings"
569 # document:"Some document" -> Link to the document with title "Some document"
569 # document:"Some document" -> Link to the document with title "Some document"
570 # Versions:
570 # Versions:
571 # version#3 -> Link to version with id 3
571 # version#3 -> Link to version with id 3
572 # version:1.0.0 -> Link to version named "1.0.0"
572 # version:1.0.0 -> Link to version named "1.0.0"
573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
574 # Attachments:
574 # Attachments:
575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
576 # Source files:
576 # Source files:
577 # source:some/file -> Link to the file located at /some/file in the project's repository
577 # source:some/file -> Link to the file located at /some/file in the project's repository
578 # source:some/file@52 -> Link to the file's revision 52
578 # source:some/file@52 -> Link to the file's revision 52
579 # source:some/file#L120 -> Link to line 120 of the file
579 # source:some/file#L120 -> Link to line 120 of the file
580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
581 # export:some/file -> Force the download of the file
581 # export:some/file -> Force the download of the file
582 # Forum messages:
582 # Forum messages:
583 # message#1218 -> Link to message with id 1218
583 # message#1218 -> Link to message with id 1218
584 def parse_redmine_links(text, project, obj, attr, only_path, options)
584 def parse_redmine_links(text, project, obj, attr, only_path, options)
585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
587 link = nil
587 link = nil
588 if esc.nil?
588 if esc.nil?
589 if prefix.nil? && sep == 'r'
589 if prefix.nil? && sep == 'r'
590 if project && (changeset = project.changesets.find_by_revision(identifier))
590 if project && (changeset = project.changesets.find_by_revision(identifier))
591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
592 :class => 'changeset',
592 :class => 'changeset',
593 :title => truncate_single_line(changeset.comments, :length => 100))
593 :title => truncate_single_line(changeset.comments, :length => 100))
594 end
594 end
595 elsif sep == '#'
595 elsif sep == '#'
596 oid = identifier.to_i
596 oid = identifier.to_i
597 case prefix
597 case prefix
598 when nil
598 when nil
599 if issue = Issue.visible.find_by_id(oid, :include => :status)
599 if issue = Issue.visible.find_by_id(oid, :include => :status)
600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
601 :class => issue.css_classes,
601 :class => issue.css_classes,
602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
603 end
603 end
604 when 'document'
604 when 'document'
605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
607 :class => 'document'
607 :class => 'document'
608 end
608 end
609 when 'version'
609 when 'version'
610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
612 :class => 'version'
612 :class => 'version'
613 end
613 end
614 when 'message'
614 when 'message'
615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
617 :controller => 'messages',
617 :controller => 'messages',
618 :action => 'show',
618 :action => 'show',
619 :board_id => message.board,
619 :board_id => message.board,
620 :id => message.root,
620 :id => message.root,
621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
622 :class => 'message'
622 :class => 'message'
623 end
623 end
624 when 'project'
624 when 'project'
625 if p = Project.visible.find_by_id(oid)
625 if p = Project.visible.find_by_id(oid)
626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
627 end
627 end
628 end
628 end
629 elsif sep == ':'
629 elsif sep == ':'
630 # removes the double quotes if any
630 # removes the double quotes if any
631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
632 case prefix
632 case prefix
633 when 'document'
633 when 'document'
634 if project && document = project.documents.find_by_title(name)
634 if project && document = project.documents.find_by_title(name)
635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 :class => 'document'
636 :class => 'document'
637 end
637 end
638 when 'version'
638 when 'version'
639 if project && version = project.versions.find_by_name(name)
639 if project && version = project.versions.find_by_name(name)
640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 :class => 'version'
641 :class => 'version'
642 end
642 end
643 when 'commit'
643 when 'commit'
644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
646 :class => 'changeset',
646 :class => 'changeset',
647 :title => truncate_single_line(changeset.comments, :length => 100)
647 :title => truncate_single_line(changeset.comments, :length => 100)
648 end
648 end
649 when 'source', 'export'
649 when 'source', 'export'
650 if project && project.repository
650 if project && project.repository
651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
652 path, rev, anchor = $1, $3, $5
652 path, rev, anchor = $1, $3, $5
653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
654 :path => to_path_param(path),
654 :path => to_path_param(path),
655 :rev => rev,
655 :rev => rev,
656 :anchor => anchor,
656 :anchor => anchor,
657 :format => (prefix == 'export' ? 'raw' : nil)},
657 :format => (prefix == 'export' ? 'raw' : nil)},
658 :class => (prefix == 'export' ? 'source download' : 'source')
658 :class => (prefix == 'export' ? 'source download' : 'source')
659 end
659 end
660 when 'attachment'
660 when 'attachment'
661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
662 if attachments && attachment = attachments.detect {|a| a.filename == name }
662 if attachments && attachment = attachments.detect {|a| a.filename == name }
663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
664 :class => 'attachment'
664 :class => 'attachment'
665 end
665 end
666 when 'project'
666 when 'project'
667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
669 end
669 end
670 end
670 end
671 end
671 end
672 end
672 end
673 leading + (link || "#{prefix}#{sep}#{identifier}")
673 leading + (link || "#{prefix}#{sep}#{identifier}")
674 end
674 end
675 end
675 end
676
676
677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
678 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
678 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
679
679
680 # Headings and TOC
680 # Headings and TOC
681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
682 def parse_headings(text, project, obj, attr, only_path, options)
682 def parse_headings(text, project, obj, attr, only_path, options)
683 headings = []
683 headings = []
684 text.gsub!(HEADING_RE) do
684 text.gsub!(HEADING_RE) do
685 level, attrs, content = $1.to_i, $2, $3
685 level, attrs, content = $1.to_i, $2, $3
686 item = strip_tags(content).strip
686 item = strip_tags(content).strip
687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
688 headings << [level, anchor, item]
688 headings << [level, anchor, item]
689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
690 end unless options[:headings] == false
690 end unless options[:headings] == false
691
691
692 text.gsub!(TOC_RE) do
692 text.gsub!(TOC_RE) do
693 if headings.empty?
693 if headings.empty?
694 ''
694 ''
695 else
695 else
696 div_class = 'toc'
696 div_class = 'toc'
697 div_class << ' right' if $1 == '>'
697 div_class << ' right' if $1 == '>'
698 div_class << ' left' if $1 == '<'
698 div_class << ' left' if $1 == '<'
699 out = "<ul class=\"#{div_class}\"><li>"
699 out = "<ul class=\"#{div_class}\"><li>"
700 root = headings.map(&:first).min
700 root = headings.map(&:first).min
701 current = root
701 current = root
702 started = false
702 started = false
703 headings.each do |level, anchor, item|
703 headings.each do |level, anchor, item|
704 if level > current
704 if level > current
705 out << '<ul><li>' * (level - current)
705 out << '<ul><li>' * (level - current)
706 elsif level < current
706 elsif level < current
707 out << "</li></ul>\n" * (current - level) + "</li><li>"
707 out << "</li></ul>\n" * (current - level) + "</li><li>"
708 elsif started
708 elsif started
709 out << '</li><li>'
709 out << '</li><li>'
710 end
710 end
711 out << "<a href=\"##{anchor}\">#{item}</a>"
711 out << "<a href=\"##{anchor}\">#{item}</a>"
712 current = level
712 current = level
713 started = true
713 started = true
714 end
714 end
715 out << '</li></ul>' * (current - root)
715 out << '</li></ul>' * (current - root)
716 out << '</li></ul>'
716 out << '</li></ul>'
717 end
717 end
718 end
718 end
719 end
719 end
720
720
721 # Same as Rails' simple_format helper without using paragraphs
721 # Same as Rails' simple_format helper without using paragraphs
722 def simple_format_without_paragraph(text)
722 def simple_format_without_paragraph(text)
723 text.to_s.
723 text.to_s.
724 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
724 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
725 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
725 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
726 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
726 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
727 end
727 end
728
728
729 def lang_options_for_select(blank=true)
729 def lang_options_for_select(blank=true)
730 (blank ? [["(auto)", ""]] : []) +
730 (blank ? [["(auto)", ""]] : []) +
731 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
731 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
732 end
732 end
733
733
734 def label_tag_for(name, option_tags = nil, options = {})
734 def label_tag_for(name, option_tags = nil, options = {})
735 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
735 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
736 content_tag("label", label_text)
736 content_tag("label", label_text)
737 end
737 end
738
738
739 def labelled_tabular_form_for(name, object, options, &proc)
739 def labelled_tabular_form_for(name, object, options, &proc)
740 options[:html] ||= {}
740 options[:html] ||= {}
741 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
741 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
742 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
742 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
743 end
743 end
744
744
745 def back_url_hidden_field_tag
745 def back_url_hidden_field_tag
746 back_url = params[:back_url] || request.env['HTTP_REFERER']
746 back_url = params[:back_url] || request.env['HTTP_REFERER']
747 back_url = CGI.unescape(back_url.to_s)
747 back_url = CGI.unescape(back_url.to_s)
748 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
748 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
749 end
749 end
750
750
751 def check_all_links(form_name)
751 def check_all_links(form_name)
752 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
752 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
753 " | " +
753 " | " +
754 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
754 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
755 end
755 end
756
756
757 def progress_bar(pcts, options={})
757 def progress_bar(pcts, options={})
758 pcts = [pcts, pcts] unless pcts.is_a?(Array)
758 pcts = [pcts, pcts] unless pcts.is_a?(Array)
759 pcts = pcts.collect(&:round)
759 pcts = pcts.collect(&:round)
760 pcts[1] = pcts[1] - pcts[0]
760 pcts[1] = pcts[1] - pcts[0]
761 pcts << (100 - pcts[1] - pcts[0])
761 pcts << (100 - pcts[1] - pcts[0])
762 width = options[:width] || '100px;'
762 width = options[:width] || '100px;'
763 legend = options[:legend] || ''
763 legend = options[:legend] || ''
764 content_tag('table',
764 content_tag('table',
765 content_tag('tr',
765 content_tag('tr',
766 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
766 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
767 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
767 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
768 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
768 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
769 ), :class => 'progress', :style => "width: #{width};") +
769 ), :class => 'progress', :style => "width: #{width};") +
770 content_tag('p', legend, :class => 'pourcent')
770 content_tag('p', legend, :class => 'pourcent')
771 end
771 end
772
772
773 def checked_image(checked=true)
773 def checked_image(checked=true)
774 if checked
774 if checked
775 image_tag 'toggle_check.png'
775 image_tag 'toggle_check.png'
776 end
776 end
777 end
777 end
778
778
779 def context_menu(url)
779 def context_menu(url)
780 unless @context_menu_included
780 unless @context_menu_included
781 content_for :header_tags do
781 content_for :header_tags do
782 javascript_include_tag('context_menu') +
782 javascript_include_tag('context_menu') +
783 stylesheet_link_tag('context_menu')
783 stylesheet_link_tag('context_menu')
784 end
784 end
785 if l(:direction) == 'rtl'
785 if l(:direction) == 'rtl'
786 content_for :header_tags do
786 content_for :header_tags do
787 stylesheet_link_tag('context_menu_rtl')
787 stylesheet_link_tag('context_menu_rtl')
788 end
788 end
789 end
789 end
790 @context_menu_included = true
790 @context_menu_included = true
791 end
791 end
792 javascript_tag "new ContextMenu('#{ url_for(url) }')"
792 javascript_tag "new ContextMenu('#{ url_for(url) }')"
793 end
793 end
794
794
795 def context_menu_link(name, url, options={})
795 def context_menu_link(name, url, options={})
796 options[:class] ||= ''
796 options[:class] ||= ''
797 if options.delete(:selected)
797 if options.delete(:selected)
798 options[:class] << ' icon-checked disabled'
798 options[:class] << ' icon-checked disabled'
799 options[:disabled] = true
799 options[:disabled] = true
800 end
800 end
801 if options.delete(:disabled)
801 if options.delete(:disabled)
802 options.delete(:method)
802 options.delete(:method)
803 options.delete(:confirm)
803 options.delete(:confirm)
804 options.delete(:onclick)
804 options.delete(:onclick)
805 options[:class] << ' disabled'
805 options[:class] << ' disabled'
806 url = '#'
806 url = '#'
807 end
807 end
808 link_to name, url, options
808 link_to name, url, options
809 end
809 end
810
810
811 def calendar_for(field_id)
811 def calendar_for(field_id)
812 include_calendar_headers_tags
812 include_calendar_headers_tags
813 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
813 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
814 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
814 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
815 end
815 end
816
816
817 def include_calendar_headers_tags
817 def include_calendar_headers_tags
818 unless @calendar_headers_tags_included
818 unless @calendar_headers_tags_included
819 @calendar_headers_tags_included = true
819 @calendar_headers_tags_included = true
820 content_for :header_tags do
820 content_for :header_tags do
821 start_of_week = case Setting.start_of_week.to_i
821 start_of_week = case Setting.start_of_week.to_i
822 when 1
822 when 1
823 'Calendar._FD = 1;' # Monday
823 'Calendar._FD = 1;' # Monday
824 when 7
824 when 7
825 'Calendar._FD = 0;' # Sunday
825 'Calendar._FD = 0;' # Sunday
826 else
826 else
827 '' # use language
827 '' # use language
828 end
828 end
829
829
830 javascript_include_tag('calendar/calendar') +
830 javascript_include_tag('calendar/calendar') +
831 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
831 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
832 javascript_tag(start_of_week) +
832 javascript_tag(start_of_week) +
833 javascript_include_tag('calendar/calendar-setup') +
833 javascript_include_tag('calendar/calendar-setup') +
834 stylesheet_link_tag('calendar')
834 stylesheet_link_tag('calendar')
835 end
835 end
836 end
836 end
837 end
837 end
838
838
839 def content_for(name, content = nil, &block)
839 def content_for(name, content = nil, &block)
840 @has_content ||= {}
840 @has_content ||= {}
841 @has_content[name] = true
841 @has_content[name] = true
842 super(name, content, &block)
842 super(name, content, &block)
843 end
843 end
844
844
845 def has_content?(name)
845 def has_content?(name)
846 (@has_content && @has_content[name]) || false
846 (@has_content && @has_content[name]) || false
847 end
847 end
848
848
849 # Returns the avatar image tag for the given +user+ if avatars are enabled
849 # Returns the avatar image tag for the given +user+ if avatars are enabled
850 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
850 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
851 def avatar(user, options = { })
851 def avatar(user, options = { })
852 if Setting.gravatar_enabled?
852 if Setting.gravatar_enabled?
853 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
853 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
854 email = nil
854 email = nil
855 if user.respond_to?(:mail)
855 if user.respond_to?(:mail)
856 email = user.mail
856 email = user.mail
857 elsif user.to_s =~ %r{<(.+?)>}
857 elsif user.to_s =~ %r{<(.+?)>}
858 email = $1
858 email = $1
859 end
859 end
860 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
860 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
861 else
861 else
862 ''
862 ''
863 end
863 end
864 end
864 end
865
865
866 def favicon
866 def favicon
867 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
867 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
868 end
868 end
869
869
870 # Returns true if arg is expected in the API response
870 # Returns true if arg is expected in the API response
871 def include_in_api_response?(arg)
871 def include_in_api_response?(arg)
872 unless @included_in_api_response
872 unless @included_in_api_response
873 param = params[:include]
873 param = params[:include]
874 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
874 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
875 @included_in_api_response.collect!(&:strip)
875 @included_in_api_response.collect!(&:strip)
876 end
876 end
877 @included_in_api_response.include?(arg.to_s)
877 @included_in_api_response.include?(arg.to_s)
878 end
878 end
879
879
880 # Returns options or nil if nometa param or X-Redmine-Nometa header
881 # was set in the request
882 def api_meta(options)
883 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
884 # compatibility mode for activeresource clients that raise
885 # an error when unserializing an array with attributes
886 nil
887 else
888 options
889 end
890 end
891
880 private
892 private
881
893
882 def wiki_helper
894 def wiki_helper
883 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
895 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
884 extend helper
896 extend helper
885 return self
897 return self
886 end
898 end
887
899
888 def link_to_remote_content_update(text, url_params)
900 def link_to_remote_content_update(text, url_params)
889 link_to_remote(text,
901 link_to_remote(text,
890 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
902 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
891 {:href => url_for(:params => url_params)}
903 {:href => url_for(:params => url_params)}
892 )
904 )
893 end
905 end
894
906
895 end
907 end
@@ -1,28 +1,28
1 api.array :issues do
1 api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :limit => @limit) do
2 @issues.each do |issue|
2 @issues.each do |issue|
3 api.issue do
3 api.issue do
4 api.id issue.id
4 api.id issue.id
5 api.project(:id => issue.project_id, :name => issue.project.name) unless issue.project.nil?
5 api.project(:id => issue.project_id, :name => issue.project.name) unless issue.project.nil?
6 api.tracker(:id => issue.tracker_id, :name => issue.tracker.name) unless issue.tracker.nil?
6 api.tracker(:id => issue.tracker_id, :name => issue.tracker.name) unless issue.tracker.nil?
7 api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil?
7 api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil?
8 api.priority(:id => issue.priority_id, :name => issue.priority.name) unless issue.priority.nil?
8 api.priority(:id => issue.priority_id, :name => issue.priority.name) unless issue.priority.nil?
9 api.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil?
9 api.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil?
10 api.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil?
10 api.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil?
11 api.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil?
11 api.category(:id => issue.category_id, :name => issue.category.name) unless issue.category.nil?
12 api.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil?
12 api.fixed_version(:id => issue.fixed_version_id, :name => issue.fixed_version.name) unless issue.fixed_version.nil?
13 api.parent(:id => issue.parent_id) unless issue.parent.nil?
13 api.parent(:id => issue.parent_id) unless issue.parent.nil?
14
14
15 api.subject issue.subject
15 api.subject issue.subject
16 api.description issue.description
16 api.description issue.description
17 api.start_date issue.start_date
17 api.start_date issue.start_date
18 api.due_date issue.due_date
18 api.due_date issue.due_date
19 api.done_ratio issue.done_ratio
19 api.done_ratio issue.done_ratio
20 api.estimated_hours issue.estimated_hours
20 api.estimated_hours issue.estimated_hours
21
21
22 render_api_custom_values issue.custom_field_values, api
22 render_api_custom_values issue.custom_field_values, api
23
23
24 api.created_on issue.created_on
24 api.created_on issue.created_on
25 api.updated_on issue.updated_on
25 api.updated_on issue.updated_on
26 end
26 end
27 end
27 end
28 end
28 end
@@ -1,15 +1,15
1 api.array :users do
1 api.array :users, api_meta(:total_count => @user_count, :offset => @offset, :limit => @limit) do
2 @users.each do |user|
2 @users.each do |user|
3 api.user do
3 api.user do
4 api.id user.id
4 api.id user.id
5 api.login user.login
5 api.login user.login
6 api.firstname user.firstname
6 api.firstname user.firstname
7 api.lastname user.lastname
7 api.lastname user.lastname
8 api.mail user.mail
8 api.mail user.mail
9 api.created_on user.created_on
9 api.created_on user.created_on
10 api.last_login_on user.last_login_on
10 api.last_login_on user.last_login_on
11
11
12 render_api_custom_values user.visible_custom_field_values, api
12 render_api_custom_values user.visible_custom_field_values, api
13 end
13 end
14 end
14 end
15 end
15 end
@@ -1,74 +1,75
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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 'blankslate'
18 require 'blankslate'
19
19
20 module Redmine
20 module Redmine
21 module Views
21 module Views
22 module Builders
22 module Builders
23 class Structure < BlankSlate
23 class Structure < BlankSlate
24 def initialize
24 def initialize
25 @struct = [{}]
25 @struct = [{}]
26 end
26 end
27
27
28 def array(tag, &block)
28 def array(tag, options={}, &block)
29 @struct << []
29 @struct << []
30 block.call(self)
30 block.call(self)
31 ret = @struct.pop
31 ret = @struct.pop
32 @struct.last[tag] = ret
32 @struct.last[tag] = ret
33 @struct.last.merge!(options) if options
33 end
34 end
34
35
35 def method_missing(sym, *args, &block)
36 def method_missing(sym, *args, &block)
36 if args.any?
37 if args.any?
37 if args.first.is_a?(Hash)
38 if args.first.is_a?(Hash)
38 if @struct.last.is_a?(Array)
39 if @struct.last.is_a?(Array)
39 @struct.last << args.first unless block
40 @struct.last << args.first unless block
40 else
41 else
41 @struct.last[sym] = args.first
42 @struct.last[sym] = args.first
42 end
43 end
43 else
44 else
44 if @struct.last.is_a?(Array)
45 if @struct.last.is_a?(Array)
45 @struct.last << (args.last || {}).merge(:value => args.first)
46 @struct.last << (args.last || {}).merge(:value => args.first)
46 else
47 else
47 @struct.last[sym] = args.first
48 @struct.last[sym] = args.first
48 end
49 end
49 end
50 end
50 end
51 end
51
52
52 if block
53 if block
53 @struct << (args.first.is_a?(Hash) ? args.first : {})
54 @struct << (args.first.is_a?(Hash) ? args.first : {})
54 block.call(self)
55 block.call(self)
55 ret = @struct.pop
56 ret = @struct.pop
56 if @struct.last.is_a?(Array)
57 if @struct.last.is_a?(Array)
57 @struct.last << ret
58 @struct.last << ret
58 else
59 else
59 if @struct.last.has_key?(sym) && @struct.last[sym].is_a?(Hash)
60 if @struct.last.has_key?(sym) && @struct.last[sym].is_a?(Hash)
60 @struct.last[sym].merge! ret
61 @struct.last[sym].merge! ret
61 else
62 else
62 @struct.last[sym] = ret
63 @struct.last[sym] = ret
63 end
64 end
64 end
65 end
65 end
66 end
66 end
67 end
67
68
68 def output
69 def output
69 raise "Need to implement #{self.class.name}#output"
70 raise "Need to implement #{self.class.name}#output"
70 end
71 end
71 end
72 end
72 end
73 end
73 end
74 end
74 end
75 end
@@ -1,45 +1,45
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Views
19 module Views
20 module Builders
20 module Builders
21 class Xml < ::Builder::XmlMarkup
21 class Xml < ::Builder::XmlMarkup
22 def initialize
22 def initialize
23 super
23 super
24 instruct!
24 instruct!
25 end
25 end
26
26
27 def output
27 def output
28 target!
28 target!
29 end
29 end
30
30
31 def method_missing(sym, *args, &block)
31 def method_missing(sym, *args, &block)
32 if args.size == 1 && args.first.is_a?(Time)
32 if args.size == 1 && args.first.is_a?(Time)
33 __send__ sym, args.first.xmlschema, &block
33 __send__ sym, args.first.xmlschema, &block
34 else
34 else
35 super
35 super
36 end
36 end
37 end
37 end
38
38
39 def array(name, options={}, &block)
39 def array(name, options={}, &block)
40 __send__ name, options.merge(:type => 'array'), &block
40 __send__ name, (options || {}).merge(:type => 'array'), &block
41 end
41 end
42 end
42 end
43 end
43 end
44 end
44 end
45 end
45 end
@@ -1,471 +1,521
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 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
19
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
20 class ApiTest::IssuesTest < ActionController::IntegrationTest
21 fixtures :projects,
21 fixtures :projects,
22 :users,
22 :users,
23 :roles,
23 :roles,
24 :members,
24 :members,
25 :member_roles,
25 :member_roles,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :versions,
28 :versions,
29 :trackers,
29 :trackers,
30 :projects_trackers,
30 :projects_trackers,
31 :issue_categories,
31 :issue_categories,
32 :enabled_modules,
32 :enabled_modules,
33 :enumerations,
33 :enumerations,
34 :attachments,
34 :attachments,
35 :workflows,
35 :workflows,
36 :custom_fields,
36 :custom_fields,
37 :custom_values,
37 :custom_values,
38 :custom_fields_projects,
38 :custom_fields_projects,
39 :custom_fields_trackers,
39 :custom_fields_trackers,
40 :time_entries,
40 :time_entries,
41 :journals,
41 :journals,
42 :journal_details,
42 :journal_details,
43 :queries
43 :queries
44
44
45 def setup
45 def setup
46 Setting.rest_api_enabled = '1'
46 Setting.rest_api_enabled = '1'
47 end
47 end
48
48
49 context "/index.xml" do
49 # Use a private project to make sure auth is really working and not just
50 # Use a private project to make sure auth is really working and not just
50 # only showing public issues.
51 # only showing public issues.
51 context "/index.xml" do
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
52 should_allow_api_authentication(:get, "/projects/private-child/issues.xml")
53
54 should "contain metadata" do
55 get '/issues.xml'
56
57 assert_tag :tag => 'issues',
58 :attributes => {
59 :type => 'array',
60 :total_count => assigns(:issue_count),
61 :limit => 25,
62 :offset => 0
63 }
64 end
65
66 context "with offset and limit" do
67 should "use the params" do
68 get '/issues.xml?offset=2&limit=3'
69
70 assert_equal 3, assigns(:limit)
71 assert_equal 2, assigns(:offset)
72 assert_tag :tag => 'issues', :children => {:count => 3, :only => {:tag => 'issue'}}
73 end
74 end
75
76 context "with nometa param" do
77 should "not contain metadata" do
78 get '/issues.xml?nometa=1'
79
80 assert_tag :tag => 'issues',
81 :attributes => {
82 :type => 'array',
83 :total_count => nil,
84 :limit => nil,
85 :offset => nil
86 }
87 end
88 end
89
90 context "with nometa header" do
91 should "not contain metadata" do
92 get '/issues.xml', {}, {'X-Redmine-Nometa' => '1'}
93
94 assert_tag :tag => 'issues',
95 :attributes => {
96 :type => 'array',
97 :total_count => nil,
98 :limit => nil,
99 :offset => nil
100 }
101 end
102 end
53 end
103 end
54
104
55 context "/index.json" do
105 context "/index.json" do
56 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
106 should_allow_api_authentication(:get, "/projects/private-child/issues.json")
57 end
107 end
58
108
59 context "/index.xml with filter" do
109 context "/index.xml with filter" do
60 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
110 should_allow_api_authentication(:get, "/projects/private-child/issues.xml?status_id=5")
61
111
62 should "show only issues with the status_id" do
112 should "show only issues with the status_id" do
63 get '/issues.xml?status_id=5'
113 get '/issues.xml?status_id=5'
64 assert_tag :tag => 'issues',
114 assert_tag :tag => 'issues',
65 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
115 :children => { :count => Issue.visible.count(:conditions => {:status_id => 5}),
66 :only => { :tag => 'issue' } }
116 :only => { :tag => 'issue' } }
67 end
117 end
68 end
118 end
69
119
70 context "/index.json with filter" do
120 context "/index.json with filter" do
71 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
121 should_allow_api_authentication(:get, "/projects/private-child/issues.json?status_id=5")
72
122
73 should "show only issues with the status_id" do
123 should "show only issues with the status_id" do
74 get '/issues.json?status_id=5'
124 get '/issues.json?status_id=5'
75
125
76 json = ActiveSupport::JSON.decode(response.body)
126 json = ActiveSupport::JSON.decode(response.body)
77 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
127 status_ids_used = json['issues'].collect {|j| j['status']['id'] }
78 assert_equal 3, status_ids_used.length
128 assert_equal 3, status_ids_used.length
79 assert status_ids_used.all? {|id| id == 5 }
129 assert status_ids_used.all? {|id| id == 5 }
80 end
130 end
81
131
82 end
132 end
83
133
84 # Issue 6 is on a private project
134 # Issue 6 is on a private project
85 context "/issues/6.xml" do
135 context "/issues/6.xml" do
86 should_allow_api_authentication(:get, "/issues/6.xml")
136 should_allow_api_authentication(:get, "/issues/6.xml")
87 end
137 end
88
138
89 context "/issues/6.json" do
139 context "/issues/6.json" do
90 should_allow_api_authentication(:get, "/issues/6.json")
140 should_allow_api_authentication(:get, "/issues/6.json")
91 end
141 end
92
142
93 context "GET /issues/:id" do
143 context "GET /issues/:id" do
94 context "with journals" do
144 context "with journals" do
95 context ".xml" do
145 context ".xml" do
96 should "display journals" do
146 should "display journals" do
97 get '/issues/1.xml?include=journals'
147 get '/issues/1.xml?include=journals'
98
148
99 assert_tag :tag => 'issue',
149 assert_tag :tag => 'issue',
100 :child => {
150 :child => {
101 :tag => 'journals',
151 :tag => 'journals',
102 :attributes => { :type => 'array' },
152 :attributes => { :type => 'array' },
103 :child => {
153 :child => {
104 :tag => 'journal',
154 :tag => 'journal',
105 :attributes => { :id => '1'},
155 :attributes => { :id => '1'},
106 :child => {
156 :child => {
107 :tag => 'details',
157 :tag => 'details',
108 :attributes => { :type => 'array' },
158 :attributes => { :type => 'array' },
109 :child => {
159 :child => {
110 :tag => 'detail',
160 :tag => 'detail',
111 :attributes => { :name => 'status_id' },
161 :attributes => { :name => 'status_id' },
112 :child => {
162 :child => {
113 :tag => 'old_value',
163 :tag => 'old_value',
114 :content => '1',
164 :content => '1',
115 :sibling => {
165 :sibling => {
116 :tag => 'new_value',
166 :tag => 'new_value',
117 :content => '2'
167 :content => '2'
118 }
168 }
119 }
169 }
120 }
170 }
121 }
171 }
122 }
172 }
123 }
173 }
124 end
174 end
125 end
175 end
126 end
176 end
127
177
128 context "with custom fields" do
178 context "with custom fields" do
129 context ".xml" do
179 context ".xml" do
130 should "display custom fields" do
180 should "display custom fields" do
131 get '/issues/3.xml'
181 get '/issues/3.xml'
132
182
133 assert_tag :tag => 'issue',
183 assert_tag :tag => 'issue',
134 :child => {
184 :child => {
135 :tag => 'custom_fields',
185 :tag => 'custom_fields',
136 :attributes => { :type => 'array' },
186 :attributes => { :type => 'array' },
137 :child => {
187 :child => {
138 :tag => 'custom_field',
188 :tag => 'custom_field',
139 :attributes => { :id => '1'},
189 :attributes => { :id => '1'},
140 :child => {
190 :child => {
141 :tag => 'value',
191 :tag => 'value',
142 :content => 'MySQL'
192 :content => 'MySQL'
143 }
193 }
144 }
194 }
145 }
195 }
146
196
147 assert_nothing_raised do
197 assert_nothing_raised do
148 Hash.from_xml(response.body).to_xml
198 Hash.from_xml(response.body).to_xml
149 end
199 end
150 end
200 end
151 end
201 end
152 end
202 end
153
203
154 context "with subtasks" do
204 context "with subtasks" do
155 setup do
205 setup do
156 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
206 @c1 = Issue.generate!(:status_id => 1, :subject => "child c1", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
157 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
207 @c2 = Issue.generate!(:status_id => 1, :subject => "child c2", :tracker_id => 1, :project_id => 1, :parent_issue_id => 1)
158 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
208 @c3 = Issue.generate!(:status_id => 1, :subject => "child c3", :tracker_id => 1, :project_id => 1, :parent_issue_id => @c1.id)
159 end
209 end
160
210
161 context ".xml" do
211 context ".xml" do
162 should "display children" do
212 should "display children" do
163 get '/issues/1.xml?include=children'
213 get '/issues/1.xml?include=children'
164
214
165 assert_tag :tag => 'issue',
215 assert_tag :tag => 'issue',
166 :child => {
216 :child => {
167 :tag => 'children',
217 :tag => 'children',
168 :children => {:count => 2},
218 :children => {:count => 2},
169 :child => {
219 :child => {
170 :tag => 'issue',
220 :tag => 'issue',
171 :attributes => {:id => @c1.id.to_s},
221 :attributes => {:id => @c1.id.to_s},
172 :child => {
222 :child => {
173 :tag => 'subject',
223 :tag => 'subject',
174 :content => 'child c1',
224 :content => 'child c1',
175 :sibling => {
225 :sibling => {
176 :tag => 'children',
226 :tag => 'children',
177 :children => {:count => 1},
227 :children => {:count => 1},
178 :child => {
228 :child => {
179 :tag => 'issue',
229 :tag => 'issue',
180 :attributes => {:id => @c3.id.to_s}
230 :attributes => {:id => @c3.id.to_s}
181 }
231 }
182 }
232 }
183 }
233 }
184 }
234 }
185 }
235 }
186 end
236 end
187
237
188 context ".json" do
238 context ".json" do
189 should "display children" do
239 should "display children" do
190 get '/issues/1.json?include=children'
240 get '/issues/1.json?include=children'
191
241
192 json = ActiveSupport::JSON.decode(response.body)
242 json = ActiveSupport::JSON.decode(response.body)
193 assert_equal([
243 assert_equal([
194 {
244 {
195 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
245 'id' => @c1.id, 'subject' => 'child c1', 'tracker' => {'id' => 1, 'name' => 'Bug'},
196 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
246 'children' => [{ 'id' => @c3.id, 'subject' => 'child c3', 'tracker' => {'id' => 1, 'name' => 'Bug'} }]
197 },
247 },
198 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
248 { 'id' => @c2.id, 'subject' => 'child c2', 'tracker' => {'id' => 1, 'name' => 'Bug'} }
199 ],
249 ],
200 json['issue']['children'])
250 json['issue']['children'])
201 end
251 end
202 end
252 end
203 end
253 end
204 end
254 end
205 end
255 end
206
256
207 context "POST /issues.xml" do
257 context "POST /issues.xml" do
208 should_allow_api_authentication(:post,
258 should_allow_api_authentication(:post,
209 '/issues.xml',
259 '/issues.xml',
210 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
260 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
211 {:success_code => :created})
261 {:success_code => :created})
212
262
213 should "create an issue with the attributes" do
263 should "create an issue with the attributes" do
214 assert_difference('Issue.count') do
264 assert_difference('Issue.count') do
215 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
265 post '/issues.xml', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
216 end
266 end
217
267
218 issue = Issue.first(:order => 'id DESC')
268 issue = Issue.first(:order => 'id DESC')
219 assert_equal 1, issue.project_id
269 assert_equal 1, issue.project_id
220 assert_equal 2, issue.tracker_id
270 assert_equal 2, issue.tracker_id
221 assert_equal 3, issue.status_id
271 assert_equal 3, issue.status_id
222 assert_equal 'API test', issue.subject
272 assert_equal 'API test', issue.subject
223
273
224 assert_response :created
274 assert_response :created
225 assert_equal 'application/xml', @response.content_type
275 assert_equal 'application/xml', @response.content_type
226 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
276 assert_tag 'issue', :child => {:tag => 'id', :content => issue.id.to_s}
227 end
277 end
228 end
278 end
229
279
230 context "POST /issues.xml with failure" do
280 context "POST /issues.xml with failure" do
231 should_allow_api_authentication(:post,
281 should_allow_api_authentication(:post,
232 '/issues.xml',
282 '/issues.xml',
233 {:issue => {:project_id => 1}},
283 {:issue => {:project_id => 1}},
234 {:success_code => :unprocessable_entity})
284 {:success_code => :unprocessable_entity})
235
285
236 should "have an errors tag" do
286 should "have an errors tag" do
237 assert_no_difference('Issue.count') do
287 assert_no_difference('Issue.count') do
238 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
288 post '/issues.xml', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
239 end
289 end
240
290
241 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
291 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
242 end
292 end
243 end
293 end
244
294
245 context "POST /issues.json" do
295 context "POST /issues.json" do
246 should_allow_api_authentication(:post,
296 should_allow_api_authentication(:post,
247 '/issues.json',
297 '/issues.json',
248 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
298 {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}},
249 {:success_code => :created})
299 {:success_code => :created})
250
300
251 should "create an issue with the attributes" do
301 should "create an issue with the attributes" do
252 assert_difference('Issue.count') do
302 assert_difference('Issue.count') do
253 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
303 post '/issues.json', {:issue => {:project_id => 1, :subject => 'API test', :tracker_id => 2, :status_id => 3}}, :authorization => credentials('jsmith')
254 end
304 end
255
305
256 issue = Issue.first(:order => 'id DESC')
306 issue = Issue.first(:order => 'id DESC')
257 assert_equal 1, issue.project_id
307 assert_equal 1, issue.project_id
258 assert_equal 2, issue.tracker_id
308 assert_equal 2, issue.tracker_id
259 assert_equal 3, issue.status_id
309 assert_equal 3, issue.status_id
260 assert_equal 'API test', issue.subject
310 assert_equal 'API test', issue.subject
261 end
311 end
262
312
263 end
313 end
264
314
265 context "POST /issues.json with failure" do
315 context "POST /issues.json with failure" do
266 should_allow_api_authentication(:post,
316 should_allow_api_authentication(:post,
267 '/issues.json',
317 '/issues.json',
268 {:issue => {:project_id => 1}},
318 {:issue => {:project_id => 1}},
269 {:success_code => :unprocessable_entity})
319 {:success_code => :unprocessable_entity})
270
320
271 should "have an errors element" do
321 should "have an errors element" do
272 assert_no_difference('Issue.count') do
322 assert_no_difference('Issue.count') do
273 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
323 post '/issues.json', {:issue => {:project_id => 1}}, :authorization => credentials('jsmith')
274 end
324 end
275
325
276 json = ActiveSupport::JSON.decode(response.body)
326 json = ActiveSupport::JSON.decode(response.body)
277 assert json['errors'].include?(['subject', "can't be blank"])
327 assert json['errors'].include?(['subject', "can't be blank"])
278 end
328 end
279 end
329 end
280
330
281 # Issue 6 is on a private project
331 # Issue 6 is on a private project
282 context "PUT /issues/6.xml" do
332 context "PUT /issues/6.xml" do
283 setup do
333 setup do
284 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
334 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
285 @headers = { :authorization => credentials('jsmith') }
335 @headers = { :authorization => credentials('jsmith') }
286 end
336 end
287
337
288 should_allow_api_authentication(:put,
338 should_allow_api_authentication(:put,
289 '/issues/6.xml',
339 '/issues/6.xml',
290 {:issue => {:subject => 'API update', :notes => 'A new note'}},
340 {:issue => {:subject => 'API update', :notes => 'A new note'}},
291 {:success_code => :ok})
341 {:success_code => :ok})
292
342
293 should "not create a new issue" do
343 should "not create a new issue" do
294 assert_no_difference('Issue.count') do
344 assert_no_difference('Issue.count') do
295 put '/issues/6.xml', @parameters, @headers
345 put '/issues/6.xml', @parameters, @headers
296 end
346 end
297 end
347 end
298
348
299 should "create a new journal" do
349 should "create a new journal" do
300 assert_difference('Journal.count') do
350 assert_difference('Journal.count') do
301 put '/issues/6.xml', @parameters, @headers
351 put '/issues/6.xml', @parameters, @headers
302 end
352 end
303 end
353 end
304
354
305 should "add the note to the journal" do
355 should "add the note to the journal" do
306 put '/issues/6.xml', @parameters, @headers
356 put '/issues/6.xml', @parameters, @headers
307
357
308 journal = Journal.last
358 journal = Journal.last
309 assert_equal "A new note", journal.notes
359 assert_equal "A new note", journal.notes
310 end
360 end
311
361
312 should "update the issue" do
362 should "update the issue" do
313 put '/issues/6.xml', @parameters, @headers
363 put '/issues/6.xml', @parameters, @headers
314
364
315 issue = Issue.find(6)
365 issue = Issue.find(6)
316 assert_equal "API update", issue.subject
366 assert_equal "API update", issue.subject
317 end
367 end
318
368
319 end
369 end
320
370
321 context "PUT /issues/3.xml with custom fields" do
371 context "PUT /issues/3.xml with custom fields" do
322 setup do
372 setup do
323 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
373 @parameters = {:issue => {:custom_fields => [{'id' => '1', 'value' => 'PostgreSQL' }, {'id' => '2', 'value' => '150'}]}}
324 @headers = { :authorization => credentials('jsmith') }
374 @headers = { :authorization => credentials('jsmith') }
325 end
375 end
326
376
327 should "update custom fields" do
377 should "update custom fields" do
328 assert_no_difference('Issue.count') do
378 assert_no_difference('Issue.count') do
329 put '/issues/3.xml', @parameters, @headers
379 put '/issues/3.xml', @parameters, @headers
330 end
380 end
331
381
332 issue = Issue.find(3)
382 issue = Issue.find(3)
333 assert_equal '150', issue.custom_value_for(2).value
383 assert_equal '150', issue.custom_value_for(2).value
334 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
384 assert_equal 'PostgreSQL', issue.custom_value_for(1).value
335 end
385 end
336 end
386 end
337
387
338 context "PUT /issues/6.xml with failed update" do
388 context "PUT /issues/6.xml with failed update" do
339 setup do
389 setup do
340 @parameters = {:issue => {:subject => ''}}
390 @parameters = {:issue => {:subject => ''}}
341 @headers = { :authorization => credentials('jsmith') }
391 @headers = { :authorization => credentials('jsmith') }
342 end
392 end
343
393
344 should_allow_api_authentication(:put,
394 should_allow_api_authentication(:put,
345 '/issues/6.xml',
395 '/issues/6.xml',
346 {:issue => {:subject => ''}}, # Missing subject should fail
396 {:issue => {:subject => ''}}, # Missing subject should fail
347 {:success_code => :unprocessable_entity})
397 {:success_code => :unprocessable_entity})
348
398
349 should "not create a new issue" do
399 should "not create a new issue" do
350 assert_no_difference('Issue.count') do
400 assert_no_difference('Issue.count') do
351 put '/issues/6.xml', @parameters, @headers
401 put '/issues/6.xml', @parameters, @headers
352 end
402 end
353 end
403 end
354
404
355 should "not create a new journal" do
405 should "not create a new journal" do
356 assert_no_difference('Journal.count') do
406 assert_no_difference('Journal.count') do
357 put '/issues/6.xml', @parameters, @headers
407 put '/issues/6.xml', @parameters, @headers
358 end
408 end
359 end
409 end
360
410
361 should "have an errors tag" do
411 should "have an errors tag" do
362 put '/issues/6.xml', @parameters, @headers
412 put '/issues/6.xml', @parameters, @headers
363
413
364 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
414 assert_tag :errors, :child => {:tag => 'error', :content => "Subject can't be blank"}
365 end
415 end
366 end
416 end
367
417
368 context "PUT /issues/6.json" do
418 context "PUT /issues/6.json" do
369 setup do
419 setup do
370 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
420 @parameters = {:issue => {:subject => 'API update', :notes => 'A new note'}}
371 @headers = { :authorization => credentials('jsmith') }
421 @headers = { :authorization => credentials('jsmith') }
372 end
422 end
373
423
374 should_allow_api_authentication(:put,
424 should_allow_api_authentication(:put,
375 '/issues/6.json',
425 '/issues/6.json',
376 {:issue => {:subject => 'API update', :notes => 'A new note'}},
426 {:issue => {:subject => 'API update', :notes => 'A new note'}},
377 {:success_code => :ok})
427 {:success_code => :ok})
378
428
379 should "not create a new issue" do
429 should "not create a new issue" do
380 assert_no_difference('Issue.count') do
430 assert_no_difference('Issue.count') do
381 put '/issues/6.json', @parameters, @headers
431 put '/issues/6.json', @parameters, @headers
382 end
432 end
383 end
433 end
384
434
385 should "create a new journal" do
435 should "create a new journal" do
386 assert_difference('Journal.count') do
436 assert_difference('Journal.count') do
387 put '/issues/6.json', @parameters, @headers
437 put '/issues/6.json', @parameters, @headers
388 end
438 end
389 end
439 end
390
440
391 should "add the note to the journal" do
441 should "add the note to the journal" do
392 put '/issues/6.json', @parameters, @headers
442 put '/issues/6.json', @parameters, @headers
393
443
394 journal = Journal.last
444 journal = Journal.last
395 assert_equal "A new note", journal.notes
445 assert_equal "A new note", journal.notes
396 end
446 end
397
447
398 should "update the issue" do
448 should "update the issue" do
399 put '/issues/6.json', @parameters, @headers
449 put '/issues/6.json', @parameters, @headers
400
450
401 issue = Issue.find(6)
451 issue = Issue.find(6)
402 assert_equal "API update", issue.subject
452 assert_equal "API update", issue.subject
403 end
453 end
404
454
405 end
455 end
406
456
407 context "PUT /issues/6.json with failed update" do
457 context "PUT /issues/6.json with failed update" do
408 setup do
458 setup do
409 @parameters = {:issue => {:subject => ''}}
459 @parameters = {:issue => {:subject => ''}}
410 @headers = { :authorization => credentials('jsmith') }
460 @headers = { :authorization => credentials('jsmith') }
411 end
461 end
412
462
413 should_allow_api_authentication(:put,
463 should_allow_api_authentication(:put,
414 '/issues/6.json',
464 '/issues/6.json',
415 {:issue => {:subject => ''}}, # Missing subject should fail
465 {:issue => {:subject => ''}}, # Missing subject should fail
416 {:success_code => :unprocessable_entity})
466 {:success_code => :unprocessable_entity})
417
467
418 should "not create a new issue" do
468 should "not create a new issue" do
419 assert_no_difference('Issue.count') do
469 assert_no_difference('Issue.count') do
420 put '/issues/6.json', @parameters, @headers
470 put '/issues/6.json', @parameters, @headers
421 end
471 end
422 end
472 end
423
473
424 should "not create a new journal" do
474 should "not create a new journal" do
425 assert_no_difference('Journal.count') do
475 assert_no_difference('Journal.count') do
426 put '/issues/6.json', @parameters, @headers
476 put '/issues/6.json', @parameters, @headers
427 end
477 end
428 end
478 end
429
479
430 should "have an errors attribute" do
480 should "have an errors attribute" do
431 put '/issues/6.json', @parameters, @headers
481 put '/issues/6.json', @parameters, @headers
432
482
433 json = ActiveSupport::JSON.decode(response.body)
483 json = ActiveSupport::JSON.decode(response.body)
434 assert json['errors'].include?(['subject', "can't be blank"])
484 assert json['errors'].include?(['subject', "can't be blank"])
435 end
485 end
436 end
486 end
437
487
438 context "DELETE /issues/1.xml" do
488 context "DELETE /issues/1.xml" do
439 should_allow_api_authentication(:delete,
489 should_allow_api_authentication(:delete,
440 '/issues/6.xml',
490 '/issues/6.xml',
441 {},
491 {},
442 {:success_code => :ok})
492 {:success_code => :ok})
443
493
444 should "delete the issue" do
494 should "delete the issue" do
445 assert_difference('Issue.count',-1) do
495 assert_difference('Issue.count',-1) do
446 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
496 delete '/issues/6.xml', {}, :authorization => credentials('jsmith')
447 end
497 end
448
498
449 assert_nil Issue.find_by_id(6)
499 assert_nil Issue.find_by_id(6)
450 end
500 end
451 end
501 end
452
502
453 context "DELETE /issues/1.json" do
503 context "DELETE /issues/1.json" do
454 should_allow_api_authentication(:delete,
504 should_allow_api_authentication(:delete,
455 '/issues/6.json',
505 '/issues/6.json',
456 {},
506 {},
457 {:success_code => :ok})
507 {:success_code => :ok})
458
508
459 should "delete the issue" do
509 should "delete the issue" do
460 assert_difference('Issue.count',-1) do
510 assert_difference('Issue.count',-1) do
461 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
511 delete '/issues/6.json', {}, :authorization => credentials('jsmith')
462 end
512 end
463
513
464 assert_nil Issue.find_by_id(6)
514 assert_nil Issue.find_by_id(6)
465 end
515 end
466 end
516 end
467
517
468 def credentials(user, password=nil)
518 def credentials(user, password=nil)
469 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
519 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
470 end
520 end
471 end
521 end
General Comments 0
You need to be logged in to leave comments. Login now