##// END OF EJS Templates
Separation of RSS/API auth actions....
Jean-Philippe Lang -
r6077:93c2b92a4b5b
parent child
Show More
@@ -1,58 +1,75
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
1 18 class ActivitiesController < ApplicationController
2 19 menu_item :activity
3 20 before_filter :find_optional_project
4 accept_key_auth :index
21 accept_rss_auth :index
5 22
6 23 def index
7 24 @days = Setting.activity_days_default.to_i
8 25
9 26 if params[:from]
10 27 begin; @date_to = params[:from].to_date + 1; rescue; end
11 28 end
12 29
13 30 @date_to ||= Date.today + 1
14 31 @date_from = @date_to - @days
15 32 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
16 33 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
17 34
18 35 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
19 36 :with_subprojects => @with_subprojects,
20 37 :author => @author)
21 38 @activity.scope_select {|t| !params["show_#{t}"].nil?}
22 39 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
23 40
24 41 events = @activity.events(@date_from, @date_to)
25 42
26 43 if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
27 44 respond_to do |format|
28 45 format.html {
29 46 @events_by_day = events.group_by(&:event_date)
30 47 render :layout => false if request.xhr?
31 48 }
32 49 format.atom {
33 50 title = l(:label_activity)
34 51 if @author
35 52 title = @author.name
36 53 elsif @activity.scope.size == 1
37 54 title = l("label_#{@activity.scope.first.singularize}_plural")
38 55 end
39 56 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
40 57 }
41 58 end
42 59 end
43 60
44 61 rescue ActiveRecord::RecordNotFound
45 62 render_404
46 63 end
47 64
48 65 private
49 66
50 67 # TODO: refactor, duplicated in projects_controller
51 68 def find_optional_project
52 69 return true unless params[:id]
53 70 @project = Project.find(params[:id])
54 71 authorize
55 72 rescue ActiveRecord::RecordNotFound
56 73 render_404
57 74 end
58 75 end
@@ -1,490 +1,517
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25
26 26 layout 'base'
27 27 exempt_from_layout 'builder', 'rsb'
28 28
29 29 # Remove broken cookie after upgrade from 0.8.x (#4292)
30 30 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
31 31 # TODO: remove it when Rails is fixed
32 32 before_filter :delete_broken_cookies
33 33 def delete_broken_cookies
34 34 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
35 35 cookies.delete '_redmine_session'
36 36 redirect_to home_path
37 37 return false
38 38 end
39 39 end
40 40
41 41 before_filter :user_setup, :check_if_login_required, :set_localization
42 42 filter_parameter_logging :password
43 43 protect_from_forgery
44 44
45 45 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
46 46 rescue_from ::Unauthorized, :with => :deny_access
47 47
48 48 include Redmine::Search::Controller
49 49 include Redmine::MenuManager::MenuController
50 50 helper Redmine::MenuManager::MenuHelper
51 51
52 52 Redmine::Scm::Base.all.each do |scm|
53 53 require_dependency "repository/#{scm.underscore}"
54 54 end
55 55
56 56 def user_setup
57 57 # Check the settings cache for each request
58 58 Setting.check_cache
59 59 # Find the current user
60 60 User.current = find_current_user
61 61 end
62 62
63 63 # Returns the current user or nil if no user is logged in
64 64 # and starts a session if needed
65 65 def find_current_user
66 66 if session[:user_id]
67 67 # existing session
68 68 (User.active.find(session[:user_id]) rescue nil)
69 69 elsif cookies[:autologin] && Setting.autologin?
70 70 # auto-login feature starts a new session
71 71 user = User.try_to_autologin(cookies[:autologin])
72 72 session[:user_id] = user.id if user
73 73 user
74 elsif params[:format] == 'atom' && request.get? && params[:key] && accept_key_auth_actions.include?(params[:action])
74 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
75 75 # RSS key authentication does not start a session
76 76 User.find_by_rss_key(params[:key])
77 elsif Setting.rest_api_enabled? && api_request?
78 if (key = api_key_from_request) && accept_key_auth_actions.include?(params[:action])
77 elsif Setting.rest_api_enabled? && accept_api_auth?
78 if (key = api_key_from_request)
79 79 # Use API key
80 80 User.find_by_api_key(key)
81 81 else
82 82 # HTTP Basic, either username/password or API key/random
83 83 authenticate_with_http_basic do |username, password|
84 84 User.try_to_login(username, password) || User.find_by_api_key(username)
85 85 end
86 86 end
87 87 end
88 88 end
89 89
90 90 # Sets the logged in user
91 91 def logged_user=(user)
92 92 reset_session
93 93 if user && user.is_a?(User)
94 94 User.current = user
95 95 session[:user_id] = user.id
96 96 else
97 97 User.current = User.anonymous
98 98 end
99 99 end
100 100
101 101 # check if login is globally required to access the application
102 102 def check_if_login_required
103 103 # no check needed if user is already logged in
104 104 return true if User.current.logged?
105 105 require_login if Setting.login_required?
106 106 end
107 107
108 108 def set_localization
109 109 lang = nil
110 110 if User.current.logged?
111 111 lang = find_language(User.current.language)
112 112 end
113 113 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
114 114 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
115 115 if !accept_lang.blank?
116 116 accept_lang = accept_lang.downcase
117 117 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
118 118 end
119 119 end
120 120 lang ||= Setting.default_language
121 121 set_language_if_valid(lang)
122 122 end
123 123
124 124 def require_login
125 125 if !User.current.logged?
126 126 # Extract only the basic url parameters on non-GET requests
127 127 if request.get?
128 128 url = url_for(params)
129 129 else
130 130 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
131 131 end
132 132 respond_to do |format|
133 133 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
134 134 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
135 135 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
136 136 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
137 137 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
138 138 end
139 139 return false
140 140 end
141 141 true
142 142 end
143 143
144 144 def require_admin
145 145 return unless require_login
146 146 if !User.current.admin?
147 147 render_403
148 148 return false
149 149 end
150 150 true
151 151 end
152 152
153 153 def deny_access
154 154 User.current.logged? ? render_403 : require_login
155 155 end
156 156
157 157 # Authorize the user for the requested action
158 158 def authorize(ctrl = params[:controller], action = params[:action], global = false)
159 159 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
160 160 if allowed
161 161 true
162 162 else
163 163 if @project && @project.archived?
164 164 render_403 :message => :notice_not_authorized_archived_project
165 165 else
166 166 deny_access
167 167 end
168 168 end
169 169 end
170 170
171 171 # Authorize the user for the requested action outside a project
172 172 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
173 173 authorize(ctrl, action, global)
174 174 end
175 175
176 176 # Find project of id params[:id]
177 177 def find_project
178 178 @project = Project.find(params[:id])
179 179 rescue ActiveRecord::RecordNotFound
180 180 render_404
181 181 end
182 182
183 183 # Find project of id params[:project_id]
184 184 def find_project_by_project_id
185 185 @project = Project.find(params[:project_id])
186 186 rescue ActiveRecord::RecordNotFound
187 187 render_404
188 188 end
189 189
190 190 # Find a project based on params[:project_id]
191 191 # TODO: some subclasses override this, see about merging their logic
192 192 def find_optional_project
193 193 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
194 194 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
195 195 allowed ? true : deny_access
196 196 rescue ActiveRecord::RecordNotFound
197 197 render_404
198 198 end
199 199
200 200 # Finds and sets @project based on @object.project
201 201 def find_project_from_association
202 202 render_404 unless @object.present?
203 203
204 204 @project = @object.project
205 205 rescue ActiveRecord::RecordNotFound
206 206 render_404
207 207 end
208 208
209 209 def find_model_object
210 210 model = self.class.read_inheritable_attribute('model_object')
211 211 if model
212 212 @object = model.find(params[:id])
213 213 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
214 214 end
215 215 rescue ActiveRecord::RecordNotFound
216 216 render_404
217 217 end
218 218
219 219 def self.model_object(model)
220 220 write_inheritable_attribute('model_object', model)
221 221 end
222 222
223 223 # Filter for bulk issue operations
224 224 def find_issues
225 225 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
226 226 raise ActiveRecord::RecordNotFound if @issues.empty?
227 227 if @issues.detect {|issue| !issue.visible?}
228 228 deny_access
229 229 return
230 230 end
231 231 @projects = @issues.collect(&:project).compact.uniq
232 232 @project = @projects.first if @projects.size == 1
233 233 rescue ActiveRecord::RecordNotFound
234 234 render_404
235 235 end
236 236
237 237 # Check if project is unique before bulk operations
238 238 def check_project_uniqueness
239 239 unless @project
240 240 # TODO: let users bulk edit/move/destroy issues from different projects
241 241 render_error 'Can not bulk edit/move/destroy issues from different projects'
242 242 return false
243 243 end
244 244 end
245 245
246 246 # make sure that the user is a member of the project (or admin) if project is private
247 247 # used as a before_filter for actions that do not require any particular permission on the project
248 248 def check_project_privacy
249 249 if @project && @project.active?
250 250 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
251 251 true
252 252 else
253 253 User.current.logged? ? render_403 : require_login
254 254 end
255 255 else
256 256 @project = nil
257 257 render_404
258 258 false
259 259 end
260 260 end
261 261
262 262 def back_url
263 263 params[:back_url] || request.env['HTTP_REFERER']
264 264 end
265 265
266 266 def redirect_back_or_default(default)
267 267 back_url = CGI.unescape(params[:back_url].to_s)
268 268 if !back_url.blank?
269 269 begin
270 270 uri = URI.parse(back_url)
271 271 # do not redirect user to another host or to the login or register page
272 272 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
273 273 redirect_to(back_url)
274 274 return
275 275 end
276 276 rescue URI::InvalidURIError
277 277 # redirect to default
278 278 end
279 279 end
280 280 redirect_to default
281 281 false
282 282 end
283 283
284 284 def render_403(options={})
285 285 @project = nil
286 286 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
287 287 return false
288 288 end
289 289
290 290 def render_404(options={})
291 291 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
292 292 return false
293 293 end
294 294
295 295 # Renders an error response
296 296 def render_error(arg)
297 297 arg = {:message => arg} unless arg.is_a?(Hash)
298 298
299 299 @message = arg[:message]
300 300 @message = l(@message) if @message.is_a?(Symbol)
301 301 @status = arg[:status] || 500
302 302
303 303 respond_to do |format|
304 304 format.html {
305 305 render :template => 'common/error', :layout => use_layout, :status => @status
306 306 }
307 307 format.atom { head @status }
308 308 format.xml { head @status }
309 309 format.js { head @status }
310 310 format.json { head @status }
311 311 end
312 312 end
313 313
314 314 # Picks which layout to use based on the request
315 315 #
316 316 # @return [boolean, string] name of the layout to use or false for no layout
317 317 def use_layout
318 318 request.xhr? ? false : 'base'
319 319 end
320 320
321 321 def invalid_authenticity_token
322 322 if api_request?
323 323 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
324 324 end
325 325 render_error "Invalid form authenticity token."
326 326 end
327 327
328 328 def render_feed(items, options={})
329 329 @items = items || []
330 330 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
331 331 @items = @items.slice(0, Setting.feeds_limit.to_i)
332 332 @title = options[:title] || Setting.app_title
333 333 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
334 334 end
335
335
336 # TODO: remove in Redmine 1.4
336 337 def self.accept_key_auth(*actions)
337 actions = actions.flatten.map(&:to_s)
338 write_inheritable_attribute('accept_key_auth_actions', actions)
338 ActiveSupport::Deprecaction.warn "ApplicationController.accept_key_auth is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead."
339 accept_rss_auth(*actions)
339 340 end
340 341
342 # TODO: remove in Redmine 1.4
341 343 def accept_key_auth_actions
342 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
344 ActiveSupport::Deprecaction.warn "ApplicationController.accept_key_auth_actions is deprecated and will be removed in Redmine 1.4. Use accept_rss_auth (or accept_api_auth) instead."
345 self.class.accept_rss_auth
346 end
347
348 def self.accept_rss_auth(*actions)
349 if actions.any?
350 write_inheritable_attribute('accept_rss_auth_actions', actions)
351 else
352 read_inheritable_attribute('accept_rss_auth_actions') || []
353 end
354 end
355
356 def accept_rss_auth?(action=action_name)
357 self.class.accept_rss_auth.include?(action.to_sym)
358 end
359
360 def self.accept_api_auth(*actions)
361 if actions.any?
362 write_inheritable_attribute('accept_api_auth_actions', actions)
363 else
364 read_inheritable_attribute('accept_api_auth_actions') || []
365 end
366 end
367
368 def accept_api_auth?(action=action_name)
369 self.class.accept_api_auth.include?(action.to_sym)
343 370 end
344 371
345 372 # Returns the number of objects that should be displayed
346 373 # on the paginated list
347 374 def per_page_option
348 375 per_page = nil
349 376 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
350 377 per_page = params[:per_page].to_s.to_i
351 378 session[:per_page] = per_page
352 379 elsif session[:per_page]
353 380 per_page = session[:per_page]
354 381 else
355 382 per_page = Setting.per_page_options_array.first || 25
356 383 end
357 384 per_page
358 385 end
359 386
360 387 # Returns offset and limit used to retrieve objects
361 388 # for an API response based on offset, limit and page parameters
362 389 def api_offset_and_limit(options=params)
363 390 if options[:offset].present?
364 391 offset = options[:offset].to_i
365 392 if offset < 0
366 393 offset = 0
367 394 end
368 395 end
369 396 limit = options[:limit].to_i
370 397 if limit < 1
371 398 limit = 25
372 399 elsif limit > 100
373 400 limit = 100
374 401 end
375 402 if offset.nil? && options[:page].present?
376 403 offset = (options[:page].to_i - 1) * limit
377 404 offset = 0 if offset < 0
378 405 end
379 406 offset ||= 0
380 407
381 408 [offset, limit]
382 409 end
383 410
384 411 # qvalues http header parser
385 412 # code taken from webrick
386 413 def parse_qvalues(value)
387 414 tmp = []
388 415 if value
389 416 parts = value.split(/,\s*/)
390 417 parts.each {|part|
391 418 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
392 419 val = m[1]
393 420 q = (m[2] or 1).to_f
394 421 tmp.push([val, q])
395 422 end
396 423 }
397 424 tmp = tmp.sort_by{|val, q| -q}
398 425 tmp.collect!{|val, q| val}
399 426 end
400 427 return tmp
401 428 rescue
402 429 nil
403 430 end
404 431
405 432 # Returns a string that can be used as filename value in Content-Disposition header
406 433 def filename_for_content_disposition(name)
407 434 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
408 435 end
409 436
410 437 def api_request?
411 438 %w(xml json).include? params[:format]
412 439 end
413 440
414 441 # Returns the API key present in the request
415 442 def api_key_from_request
416 443 if params[:key].present?
417 444 params[:key]
418 445 elsif request.headers["X-Redmine-API-Key"].present?
419 446 request.headers["X-Redmine-API-Key"]
420 447 end
421 448 end
422 449
423 450 # Renders a warning flash if obj has unsaved attachments
424 451 def render_attachment_warning_if_needed(obj)
425 452 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
426 453 end
427 454
428 455 # Sets the `flash` notice or error based the number of issues that did not save
429 456 #
430 457 # @param [Array, Issue] issues all of the saved and unsaved Issues
431 458 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
432 459 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
433 460 if unsaved_issue_ids.empty?
434 461 flash[:notice] = l(:notice_successful_update) unless issues.empty?
435 462 else
436 463 flash[:error] = l(:notice_failed_to_save_issues,
437 464 :count => unsaved_issue_ids.size,
438 465 :total => issues.size,
439 466 :ids => '#' + unsaved_issue_ids.join(', #'))
440 467 end
441 468 end
442 469
443 470 # Rescues an invalid query statement. Just in case...
444 471 def query_statement_invalid(exception)
445 472 logger.error "Query::StatementInvalid: #{exception.message}" if logger
446 473 session.delete(:query)
447 474 sort_clear if respond_to?(:sort_clear)
448 475 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
449 476 end
450 477
451 478 # Converts the errors on an ActiveRecord object into a common JSON format
452 479 def object_errors_to_json(object)
453 480 object.errors.collect do |attribute, error|
454 481 { attribute => error }
455 482 end.to_json
456 483 end
457 484
458 485 # Renders API response on validation failure
459 486 def render_validation_errors(object)
460 487 options = { :status => :unprocessable_entity, :layout => false }
461 488 options.merge!(case params[:format]
462 489 when 'xml'; { :xml => object.errors }
463 490 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
464 491 else
465 492 raise "Unknown format #{params[:format]} in #render_validation_errors"
466 493 end
467 494 )
468 495 render options
469 496 end
470 497
471 498 # Overrides #default_template so that the api template
472 499 # is used automatically if it exists
473 500 def default_template(action_name = self.action_name)
474 501 if api_request?
475 502 begin
476 503 return self.view_paths.find_template(default_template_name(action_name), 'api')
477 504 rescue ::ActionView::MissingTemplate
478 505 # the api template was not found
479 506 # fallback to the default behaviour
480 507 end
481 508 end
482 509 super
483 510 end
484 511
485 512 # Overrides #pick_layout so that #render with no arguments
486 513 # doesn't use the layout for api requests
487 514 def pick_layout(*args)
488 515 api_request? ? nil : super
489 516 end
490 517 end
@@ -1,103 +1,103
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class BoardsController < ApplicationController
19 19 default_search_scope :messages
20 20 before_filter :find_project, :find_board_if_available, :authorize
21 accept_key_auth :index, :show
21 accept_rss_auth :index, :show
22 22
23 23 helper :messages
24 24 include MessagesHelper
25 25 helper :sort
26 26 include SortHelper
27 27 helper :watchers
28 28 include WatchersHelper
29 29
30 30 def index
31 31 @boards = @project.boards
32 32 # show the board if there is only one
33 33 if @boards.size == 1
34 34 @board = @boards.first
35 35 show
36 36 end
37 37 end
38 38
39 39 def show
40 40 respond_to do |format|
41 41 format.html {
42 42 sort_init 'updated_on', 'desc'
43 43 sort_update 'created_on' => "#{Message.table_name}.created_on",
44 44 'replies' => "#{Message.table_name}.replies_count",
45 45 'updated_on' => "#{Message.table_name}.updated_on"
46 46
47 47 @topic_count = @board.topics.count
48 48 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
49 49 @topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
50 50 :include => [:author, {:last_reply => :author}],
51 51 :limit => @topic_pages.items_per_page,
52 52 :offset => @topic_pages.current.offset
53 53 @message = Message.new
54 54 render :action => 'show', :layout => !request.xhr?
55 55 }
56 56 format.atom {
57 57 @messages = @board.messages.find :all, :order => 'created_on DESC',
58 58 :include => [:author, :board],
59 59 :limit => Setting.feeds_limit.to_i
60 60 render_feed(@messages, :title => "#{@project}: #{@board}")
61 61 }
62 62 end
63 63 end
64 64
65 65 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
66 66
67 67 def new
68 68 @board = Board.new(params[:board])
69 69 @board.project = @project
70 70 if request.post? && @board.save
71 71 flash[:notice] = l(:notice_successful_create)
72 72 redirect_to_settings_in_projects
73 73 end
74 74 end
75 75
76 76 def edit
77 77 if request.post? && @board.update_attributes(params[:board])
78 78 redirect_to_settings_in_projects
79 79 end
80 80 end
81 81
82 82 def destroy
83 83 @board.destroy
84 84 redirect_to_settings_in_projects
85 85 end
86 86
87 87 private
88 88 def redirect_to_settings_in_projects
89 89 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
90 90 end
91 91
92 92 def find_project
93 93 @project = Project.find(params[:project_id])
94 94 rescue ActiveRecord::RecordNotFound
95 95 render_404
96 96 end
97 97
98 98 def find_board_if_available
99 99 @board = @project.boards.find(params[:id]) if params[:id]
100 100 rescue ActiveRecord::RecordNotFound
101 101 render_404
102 102 end
103 103 end
@@ -1,101 +1,101
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueRelationsController < ApplicationController
19 19 before_filter :find_issue, :find_project_from_association, :authorize, :only => [:index, :create]
20 20 before_filter :find_relation, :except => [:index, :create]
21 21
22 accept_key_auth :index, :show, :create, :destroy
22 accept_api_auth :index, :show, :create, :destroy
23 23
24 24 def index
25 25 @relations = @issue.relations
26 26
27 27 respond_to do |format|
28 28 format.html { render :nothing => true }
29 29 format.api
30 30 end
31 31 end
32 32
33 33 def show
34 34 raise Unauthorized unless @relation.visible?
35 35
36 36 respond_to do |format|
37 37 format.html { render :nothing => true }
38 38 format.api
39 39 end
40 40 rescue ActiveRecord::RecordNotFound
41 41 render_404
42 42 end
43 43
44 44 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
45 45 def create
46 46 @relation = IssueRelation.new(params[:relation])
47 47 @relation.issue_from = @issue
48 48 if params[:relation] && m = params[:relation][:issue_to_id].to_s.match(/^#?(\d+)$/)
49 49 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
50 50 end
51 51 saved = @relation.save
52 52
53 53 respond_to do |format|
54 54 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
55 55 format.js do
56 56 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
57 57 render :update do |page|
58 58 page.replace_html "relations", :partial => 'issues/relations'
59 59 if @relation.errors.empty?
60 60 page << "$('relation_delay').value = ''"
61 61 page << "$('relation_issue_to_id').value = ''"
62 62 end
63 63 end
64 64 end
65 65 format.api {
66 66 if saved
67 67 render :action => 'show', :status => :created, :location => relation_url(@relation)
68 68 else
69 69 render_validation_errors(@relation)
70 70 end
71 71 }
72 72 end
73 73 end
74 74
75 75 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
76 76 def destroy
77 77 raise Unauthorized unless @relation.deletable?
78 78 @relation.destroy
79 79
80 80 respond_to do |format|
81 81 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
82 82 format.js { render(:update) {|page| page.remove "relation-#{@relation.id}"} }
83 83 format.api { head :ok }
84 84 end
85 85 rescue ActiveRecord::RecordNotFound
86 86 render_404
87 87 end
88 88
89 89 private
90 90 def find_issue
91 91 @issue = @object = Issue.find(params[:issue_id])
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def find_relation
97 97 @relation = IssueRelation.find(params[:id])
98 98 rescue ActiveRecord::RecordNotFound
99 99 render_404
100 100 end
101 101 end
@@ -1,335 +1,336
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
25 25 before_filter :find_project, :only => [:new, :create]
26 26 before_filter :authorize, :except => [:index]
27 27 before_filter :find_optional_project, :only => [:index]
28 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 accept_key_auth :index, :show, :create, :update, :destroy
30 accept_rss_auth :index, :show
31 accept_api_auth :index, :show, :create, :update, :destroy
31 32
32 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 34
34 35 helper :journals
35 36 helper :projects
36 37 include ProjectsHelper
37 38 helper :custom_fields
38 39 include CustomFieldsHelper
39 40 helper :issue_relations
40 41 include IssueRelationsHelper
41 42 helper :watchers
42 43 include WatchersHelper
43 44 helper :attachments
44 45 include AttachmentsHelper
45 46 helper :queries
46 47 include QueriesHelper
47 48 helper :repositories
48 49 include RepositoriesHelper
49 50 helper :sort
50 51 include SortHelper
51 52 include IssuesHelper
52 53 helper :timelog
53 54 helper :gantt
54 55 include Redmine::Export::PDF
55 56
56 57 verify :method => [:post, :delete],
57 58 :only => :destroy,
58 59 :render => { :nothing => true, :status => :method_not_allowed }
59 60
60 61 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
61 62 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
62 63 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
63 64
64 65 def index
65 66 retrieve_query
66 67 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
67 68 sort_update(@query.sortable_columns)
68 69
69 70 if @query.valid?
70 71 case params[:format]
71 72 when 'csv', 'pdf'
72 73 @limit = Setting.issues_export_limit.to_i
73 74 when 'atom'
74 75 @limit = Setting.feeds_limit.to_i
75 76 when 'xml', 'json'
76 77 @offset, @limit = api_offset_and_limit
77 78 else
78 79 @limit = per_page_option
79 80 end
80 81
81 82 @issue_count = @query.issue_count
82 83 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
83 84 @offset ||= @issue_pages.current.offset
84 85 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
85 86 :order => sort_clause,
86 87 :offset => @offset,
87 88 :limit => @limit)
88 89 @issue_count_by_group = @query.issue_count_by_group
89 90
90 91 respond_to do |format|
91 92 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
92 93 format.api
93 94 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
94 95 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
95 96 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
96 97 end
97 98 else
98 99 # Send html if the query is not valid
99 100 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
100 101 end
101 102 rescue ActiveRecord::RecordNotFound
102 103 render_404
103 104 end
104 105
105 106 def show
106 107 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
107 108 @journals.each_with_index {|j,i| j.indice = i+1}
108 109 @journals.reverse! if User.current.wants_comments_in_reverse_order?
109 110
110 111 if User.current.allowed_to?(:view_changesets, @project)
111 112 @changesets = @issue.changesets.visible.all
112 113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
113 114 end
114 115
115 116 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
116 117 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
117 118 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
118 119 @priorities = IssuePriority.active
119 120 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
120 121 respond_to do |format|
121 122 format.html { render :template => 'issues/show.rhtml' }
122 123 format.api
123 124 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
124 125 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
125 126 end
126 127 end
127 128
128 129 # Add a new issue
129 130 # The new issue will be created from an existing one if copy_from parameter is given
130 131 def new
131 132 respond_to do |format|
132 133 format.html { render :action => 'new', :layout => !request.xhr? }
133 134 format.js { render :partial => 'attributes' }
134 135 end
135 136 end
136 137
137 138 def create
138 139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
139 140 if @issue.save
140 141 attachments = Attachment.attach_files(@issue, params[:attachments])
141 142 render_attachment_warning_if_needed(@issue)
142 143 flash[:notice] = l(:notice_successful_create)
143 144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
144 145 respond_to do |format|
145 146 format.html {
146 147 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?} } :
147 148 { :action => 'show', :id => @issue })
148 149 }
149 150 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
150 151 end
151 152 return
152 153 else
153 154 respond_to do |format|
154 155 format.html { render :action => 'new' }
155 156 format.api { render_validation_errors(@issue) }
156 157 end
157 158 end
158 159 end
159 160
160 161 def edit
161 162 update_issue_from_params
162 163
163 164 @journal = @issue.current_journal
164 165
165 166 respond_to do |format|
166 167 format.html { }
167 168 format.xml { }
168 169 end
169 170 end
170 171
171 172 def update
172 173 update_issue_from_params
173 174
174 175 if @issue.save_issue_with_child_records(params, @time_entry)
175 176 render_attachment_warning_if_needed(@issue)
176 177 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
177 178
178 179 respond_to do |format|
179 180 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
180 181 format.api { head :ok }
181 182 end
182 183 else
183 184 render_attachment_warning_if_needed(@issue)
184 185 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
185 186 @journal = @issue.current_journal
186 187
187 188 respond_to do |format|
188 189 format.html { render :action => 'edit' }
189 190 format.api { render_validation_errors(@issue) }
190 191 end
191 192 end
192 193 end
193 194
194 195 # Bulk edit a set of issues
195 196 def bulk_edit
196 197 @issues.sort!
197 198 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
198 199 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
199 200 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
200 201 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
201 202 end
202 203
203 204 def bulk_update
204 205 @issues.sort!
205 206 attributes = parse_params_for_bulk_issue_attributes(params)
206 207
207 208 unsaved_issue_ids = []
208 209 @issues.each do |issue|
209 210 issue.reload
210 211 journal = issue.init_journal(User.current, params[:notes])
211 212 issue.safe_attributes = attributes
212 213 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
213 214 unless issue.save
214 215 # Keep unsaved issue ids to display them in flash error
215 216 unsaved_issue_ids << issue.id
216 217 end
217 218 end
218 219 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
219 220 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
220 221 end
221 222
222 223 def destroy
223 224 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
224 225 if @hours > 0
225 226 case params[:todo]
226 227 when 'destroy'
227 228 # nothing to do
228 229 when 'nullify'
229 230 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
230 231 when 'reassign'
231 232 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
232 233 if reassign_to.nil?
233 234 flash.now[:error] = l(:error_issue_not_found_in_project)
234 235 return
235 236 else
236 237 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
237 238 end
238 239 else
239 240 # display the destroy form if it's a user request
240 241 return unless api_request?
241 242 end
242 243 end
243 244 @issues.each do |issue|
244 245 begin
245 246 issue.reload.destroy
246 247 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
247 248 # nothing to do, issue was already deleted (eg. by a parent)
248 249 end
249 250 end
250 251 respond_to do |format|
251 252 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
252 253 format.api { head :ok }
253 254 end
254 255 end
255 256
256 257 private
257 258 def find_issue
258 259 # Issue.visible.find(...) can not be used to redirect user to the login form
259 260 # if the issue actually exists but requires authentication
260 261 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
261 262 unless @issue.visible?
262 263 deny_access
263 264 return
264 265 end
265 266 @project = @issue.project
266 267 rescue ActiveRecord::RecordNotFound
267 268 render_404
268 269 end
269 270
270 271 def find_project
271 272 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
272 273 @project = Project.find(project_id)
273 274 rescue ActiveRecord::RecordNotFound
274 275 render_404
275 276 end
276 277
277 278 # Used by #edit and #update to set some common instance variables
278 279 # from the params
279 280 # TODO: Refactor, not everything in here is needed by #edit
280 281 def update_issue_from_params
281 282 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
282 283 @priorities = IssuePriority.active
283 284 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
284 285 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
285 286 @time_entry.attributes = params[:time_entry]
286 287
287 288 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
288 289 @issue.init_journal(User.current, @notes)
289 290 @issue.safe_attributes = params[:issue]
290 291 end
291 292
292 293 # TODO: Refactor, lots of extra code in here
293 294 # TODO: Changing tracker on an existing issue should not trigger this
294 295 def build_new_issue_from_params
295 296 if params[:id].blank?
296 297 @issue = Issue.new
297 298 @issue.copy_from(params[:copy_from]) if params[:copy_from]
298 299 @issue.project = @project
299 300 else
300 301 @issue = @project.issues.visible.find(params[:id])
301 302 end
302 303
303 304 @issue.project = @project
304 305 @issue.author = User.current
305 306 # Tracker must be set before custom field values
306 307 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
307 308 if @issue.tracker.nil?
308 309 render_error l(:error_no_tracker_in_project)
309 310 return false
310 311 end
311 312 @issue.start_date ||= Date.today
312 313 if params[:issue].is_a?(Hash)
313 314 @issue.safe_attributes = params[:issue]
314 315 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
315 316 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
316 317 end
317 318 end
318 319 @priorities = IssuePriority.active
319 320 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
320 321 end
321 322
322 323 def check_for_default_issue_status
323 324 if IssueStatus.default.nil?
324 325 render_error l(:error_no_default_issue_status)
325 326 return false
326 327 end
327 328 end
328 329
329 330 def parse_params_for_bulk_issue_attributes(params)
330 331 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
331 332 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
332 333 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
333 334 attributes
334 335 end
335 336 end
@@ -1,119 +1,119
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class JournalsController < ApplicationController
19 19 before_filter :find_journal, :only => [:edit, :diff]
20 20 before_filter :find_issue, :only => [:new]
21 21 before_filter :find_optional_project, :only => [:index]
22 22 before_filter :authorize, :only => [:new, :edit, :diff]
23 accept_key_auth :index
23 accept_rss_auth :index
24 24 menu_item :issues
25 25
26 26 helper :issues
27 27 helper :custom_fields
28 28 helper :queries
29 29 include QueriesHelper
30 30 helper :sort
31 31 include SortHelper
32 32
33 33 def index
34 34 retrieve_query
35 35 sort_init 'id', 'desc'
36 36 sort_update(@query.sortable_columns)
37 37
38 38 if @query.valid?
39 39 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
40 40 :limit => 25)
41 41 end
42 42 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
43 43 render :layout => false, :content_type => 'application/atom+xml'
44 44 rescue ActiveRecord::RecordNotFound
45 45 render_404
46 46 end
47 47
48 48 def diff
49 49 @issue = @journal.issue
50 50 if params[:detail_id].present?
51 51 @detail = @journal.details.find_by_id(params[:detail_id])
52 52 else
53 53 @detail = @journal.details.detect {|d| d.prop_key == 'description'}
54 54 end
55 55 (render_404; return false) unless @issue && @detail
56 56 @diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
57 57 end
58 58
59 59 def new
60 60 journal = Journal.find(params[:journal_id]) if params[:journal_id]
61 61 if journal
62 62 user = journal.user
63 63 text = journal.notes
64 64 else
65 65 user = @issue.author
66 66 text = @issue.description
67 67 end
68 68 # Replaces pre blocks with [...]
69 69 text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
70 70 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
71 71 content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
72 72
73 73 render(:update) { |page|
74 74 page.<< "$('notes').value = \"#{escape_javascript content}\";"
75 75 page.show 'update'
76 76 page << "Form.Element.focus('notes');"
77 77 page << "Element.scrollTo('update');"
78 78 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
79 79 }
80 80 end
81 81
82 82 def edit
83 83 (render_403; return false) unless @journal.editable_by?(User.current)
84 84 if request.post?
85 85 @journal.update_attributes(:notes => params[:notes]) if params[:notes]
86 86 @journal.destroy if @journal.details.empty? && @journal.notes.blank?
87 87 call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
88 88 respond_to do |format|
89 89 format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
90 90 format.js { render :action => 'update' }
91 91 end
92 92 else
93 93 respond_to do |format|
94 94 format.html {
95 95 # TODO: implement non-JS journal update
96 96 render :nothing => true
97 97 }
98 98 format.js
99 99 end
100 100 end
101 101 end
102 102
103 103 private
104 104
105 105 def find_journal
106 106 @journal = Journal.find(params[:id])
107 107 @project = @journal.journalized.project
108 108 rescue ActiveRecord::RecordNotFound
109 109 render_404
110 110 end
111 111
112 112 # TODO: duplicated in IssuesController
113 113 def find_issue
114 114 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
115 115 @project = @issue.project
116 116 rescue ActiveRecord::RecordNotFound
117 117 render_404
118 118 end
119 119 end
@@ -1,108 +1,109
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class NewsController < ApplicationController
19 19 default_search_scope :news
20 20 model_object News
21 21 before_filter :find_model_object, :except => [:new, :create, :index]
22 22 before_filter :find_project_from_association, :except => [:new, :create, :index]
23 23 before_filter :find_project, :only => [:new, :create]
24 24 before_filter :authorize, :except => [:index]
25 25 before_filter :find_optional_project, :only => :index
26 accept_key_auth :index
26 accept_rss_auth :index
27 accept_api_auth :index
27 28
28 29 helper :watchers
29 30
30 31 def index
31 32 case params[:format]
32 33 when 'xml', 'json'
33 34 @offset, @limit = api_offset_and_limit
34 35 else
35 36 @limit = 10
36 37 end
37 38
38 39 scope = @project ? @project.news.visible : News.visible
39 40
40 41 @news_count = scope.count
41 42 @news_pages = Paginator.new self, @news_count, @limit, params['page']
42 43 @offset ||= @news_pages.current.offset
43 44 @newss = scope.all(:include => [:author, :project],
44 45 :order => "#{News.table_name}.created_on DESC",
45 46 :offset => @offset,
46 47 :limit => @limit)
47 48
48 49 respond_to do |format|
49 50 format.html { render :layout => false if request.xhr? }
50 51 format.api
51 52 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
52 53 end
53 54 end
54 55
55 56 def show
56 57 @comments = @news.comments
57 58 @comments.reverse! if User.current.wants_comments_in_reverse_order?
58 59 end
59 60
60 61 def new
61 62 @news = News.new(:project => @project, :author => User.current)
62 63 end
63 64
64 65 def create
65 66 @news = News.new(:project => @project, :author => User.current)
66 67 if request.post?
67 68 @news.attributes = params[:news]
68 69 if @news.save
69 70 flash[:notice] = l(:notice_successful_create)
70 71 redirect_to :controller => 'news', :action => 'index', :project_id => @project
71 72 else
72 73 render :action => 'new'
73 74 end
74 75 end
75 76 end
76 77
77 78 def edit
78 79 end
79 80
80 81 def update
81 82 if request.put? and @news.update_attributes(params[:news])
82 83 flash[:notice] = l(:notice_successful_update)
83 84 redirect_to :action => 'show', :id => @news
84 85 else
85 86 render :action => 'edit'
86 87 end
87 88 end
88 89
89 90 def destroy
90 91 @news.destroy
91 92 redirect_to :action => 'index', :project_id => @project
92 93 end
93 94
94 95 private
95 96 def find_project
96 97 @project = Project.find(params[:project_id])
97 98 rescue ActiveRecord::RecordNotFound
98 99 render_404
99 100 end
100 101
101 102 def find_optional_project
102 103 return true unless params[:project_id]
103 104 @project = Project.find(params[:project_id])
104 105 authorize
105 106 rescue ActiveRecord::RecordNotFound
106 107 render_404
107 108 end
108 109 end
@@ -1,268 +1,269
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ProjectsController < ApplicationController
19 19 menu_item :overview
20 20 menu_item :roadmap, :only => :roadmap
21 21 menu_item :settings, :only => :settings
22 22
23 23 before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
24 24 before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
25 25 before_filter :authorize_global, :only => [:new, :create]
26 26 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
27 accept_key_auth :index, :show, :create, :update, :destroy
27 accept_rss_auth :index
28 accept_api_auth :index, :show, :create, :update, :destroy
28 29
29 30 after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
30 31 if controller.request.post?
31 32 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
32 33 end
33 34 end
34 35
35 36 helper :sort
36 37 include SortHelper
37 38 helper :custom_fields
38 39 include CustomFieldsHelper
39 40 helper :issues
40 41 helper :queries
41 42 include QueriesHelper
42 43 helper :repositories
43 44 include RepositoriesHelper
44 45 include ProjectsHelper
45 46
46 47 # Lists visible projects
47 48 def index
48 49 respond_to do |format|
49 50 format.html {
50 51 @projects = Project.visible.find(:all, :order => 'lft')
51 52 }
52 53 format.api {
53 54 @offset, @limit = api_offset_and_limit
54 55 @project_count = Project.visible.count
55 56 @projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
56 57 }
57 58 format.atom {
58 59 projects = Project.visible.find(:all, :order => 'created_on DESC',
59 60 :limit => Setting.feeds_limit.to_i)
60 61 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 62 }
62 63 end
63 64 end
64 65
65 66 def new
66 67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 68 @trackers = Tracker.all
68 69 @project = Project.new(params[:project])
69 70 end
70 71
71 72 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
72 73 def create
73 74 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
74 75 @trackers = Tracker.all
75 76 @project = Project.new
76 77 @project.safe_attributes = params[:project]
77 78
78 79 if validate_parent_id && @project.save
79 80 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
80 81 # Add current user as a project member if he is not admin
81 82 unless User.current.admin?
82 83 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
83 84 m = Member.new(:user => User.current, :roles => [r])
84 85 @project.members << m
85 86 end
86 87 respond_to do |format|
87 88 format.html {
88 89 flash[:notice] = l(:notice_successful_create)
89 90 redirect_to :controller => 'projects', :action => 'settings', :id => @project
90 91 }
91 92 format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 93 end
93 94 else
94 95 respond_to do |format|
95 96 format.html { render :action => 'new' }
96 97 format.api { render_validation_errors(@project) }
97 98 end
98 99 end
99 100
100 101 end
101 102
102 103 def copy
103 104 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
104 105 @trackers = Tracker.all
105 106 @root_projects = Project.find(:all,
106 107 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
107 108 :order => 'name')
108 109 @source_project = Project.find(params[:id])
109 110 if request.get?
110 111 @project = Project.copy_from(@source_project)
111 112 if @project
112 113 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
113 114 else
114 115 redirect_to :controller => 'admin', :action => 'projects'
115 116 end
116 117 else
117 118 Mailer.with_deliveries(params[:notifications] == '1') do
118 119 @project = Project.new
119 120 @project.safe_attributes = params[:project]
120 121 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
121 122 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 123 flash[:notice] = l(:notice_successful_create)
123 124 redirect_to :controller => 'projects', :action => 'settings', :id => @project
124 125 elsif !@project.new_record?
125 126 # Project was created
126 127 # But some objects were not copied due to validation failures
127 128 # (eg. issues from disabled trackers)
128 129 # TODO: inform about that
129 130 redirect_to :controller => 'projects', :action => 'settings', :id => @project
130 131 end
131 132 end
132 133 end
133 134 rescue ActiveRecord::RecordNotFound
134 135 redirect_to :controller => 'admin', :action => 'projects'
135 136 end
136 137
137 138 # Show @project
138 139 def show
139 140 if params[:jump]
140 141 # try to redirect to the requested menu item
141 142 redirect_to_project_menu_item(@project, params[:jump]) && return
142 143 end
143 144
144 145 @users_by_role = @project.users_by_role
145 146 @subprojects = @project.children.visible.all
146 147 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
147 148 @trackers = @project.rolled_up_trackers
148 149
149 150 cond = @project.project_condition(Setting.display_subprojects_issues?)
150 151
151 152 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
152 153 :include => [:project, :status, :tracker],
153 154 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
154 155 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
155 156 :include => [:project, :status, :tracker],
156 157 :conditions => cond)
157 158
158 159 if User.current.allowed_to?(:view_time_entries, @project)
159 160 @total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
160 161 end
161 162
162 163 @key = User.current.rss_key
163 164
164 165 respond_to do |format|
165 166 format.html
166 167 format.api
167 168 end
168 169 end
169 170
170 171 def settings
171 172 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
172 173 @issue_category ||= IssueCategory.new
173 174 @member ||= @project.members.new
174 175 @trackers = Tracker.all
175 176 @repository ||= @project.repository
176 177 @wiki ||= @project.wiki
177 178 end
178 179
179 180 def edit
180 181 end
181 182
182 183 # TODO: convert to PUT only
183 184 verify :method => [:post, :put], :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
184 185 def update
185 186 @project.safe_attributes = params[:project]
186 187 if validate_parent_id && @project.save
187 188 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
188 189 respond_to do |format|
189 190 format.html {
190 191 flash[:notice] = l(:notice_successful_update)
191 192 redirect_to :action => 'settings', :id => @project
192 193 }
193 194 format.api { head :ok }
194 195 end
195 196 else
196 197 respond_to do |format|
197 198 format.html {
198 199 settings
199 200 render :action => 'settings'
200 201 }
201 202 format.api { render_validation_errors(@project) }
202 203 end
203 204 end
204 205 end
205 206
206 207 verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed }
207 208 def modules
208 209 @project.enabled_module_names = params[:enabled_module_names]
209 210 flash[:notice] = l(:notice_successful_update)
210 211 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
211 212 end
212 213
213 214 def archive
214 215 if request.post?
215 216 unless @project.archive
216 217 flash[:error] = l(:error_can_not_archive_project)
217 218 end
218 219 end
219 220 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
220 221 end
221 222
222 223 def unarchive
223 224 @project.unarchive if request.post? && !@project.active?
224 225 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
225 226 end
226 227
227 228 # Delete @project
228 229 def destroy
229 230 @project_to_destroy = @project
230 231 if request.get?
231 232 # display confirmation view
232 233 else
233 234 if api_request? || params[:confirm]
234 235 @project_to_destroy.destroy
235 236 respond_to do |format|
236 237 format.html { redirect_to :controller => 'admin', :action => 'projects' }
237 238 format.api { head :ok }
238 239 end
239 240 end
240 241 end
241 242 # hide project in layout
242 243 @project = nil
243 244 end
244 245
245 246 private
246 247 def find_optional_project
247 248 return true unless params[:id]
248 249 @project = Project.find(params[:id])
249 250 authorize
250 251 rescue ActiveRecord::RecordNotFound
251 252 render_404
252 253 end
253 254
254 255 # Validates parent_id param according to user's permissions
255 256 # TODO: move it to Project model in a validation that depends on User.current
256 257 def validate_parent_id
257 258 return true if User.current.admin?
258 259 parent_id = params[:project] && params[:project][:parent_id]
259 260 if parent_id || @project.new_record?
260 261 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
261 262 unless @project.allowed_parents.include?(parent)
262 263 @project.errors.add :parent_id, :invalid
263 264 return false
264 265 end
265 266 end
266 267 true
267 268 end
268 269 end
@@ -1,100 +1,100
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_query, :except => [:new, :index]
21 21 before_filter :find_optional_project, :only => :new
22 22
23 accept_key_auth :index
23 accept_api_auth :index
24 24
25 25 def index
26 26 case params[:format]
27 27 when 'xml', 'json'
28 28 @offset, @limit = api_offset_and_limit
29 29 else
30 30 @limit = per_page_option
31 31 end
32 32
33 33 @query_count = Query.visible.count
34 34 @query_pages = Paginator.new self, @query_count, @limit, params['page']
35 35 @queries = Query.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
36 36
37 37 respond_to do |format|
38 38 format.html { render :nothing => true }
39 39 format.api
40 40 end
41 41 end
42 42
43 43 def new
44 44 @query = Query.new(params[:query])
45 45 @query.project = params[:query_is_for_all] ? nil : @project
46 46 @query.user = User.current
47 47 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
48 48
49 49 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f]
50 50 @query.group_by ||= params[:group_by]
51 51 @query.column_names = params[:c] if params[:c]
52 52 @query.column_names = nil if params[:default_columns]
53 53
54 54 if request.post? && params[:confirm] && @query.save
55 55 flash[:notice] = l(:notice_successful_create)
56 56 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
57 57 return
58 58 end
59 59 render :layout => false if request.xhr?
60 60 end
61 61
62 62 def edit
63 63 if request.post?
64 64 @query.filters = {}
65 65 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f]
66 66 @query.attributes = params[:query]
67 67 @query.project = nil if params[:query_is_for_all]
68 68 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
69 69 @query.group_by ||= params[:group_by]
70 70 @query.column_names = params[:c] if params[:c]
71 71 @query.column_names = nil if params[:default_columns]
72 72
73 73 if @query.save
74 74 flash[:notice] = l(:notice_successful_update)
75 75 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
76 76 end
77 77 end
78 78 end
79 79
80 80 def destroy
81 81 @query.destroy if request.post?
82 82 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
83 83 end
84 84
85 85 private
86 86 def find_query
87 87 @query = Query.find(params[:id])
88 88 @project = @query.project
89 89 render_403 unless @query.editable_by?(User.current)
90 90 rescue ActiveRecord::RecordNotFound
91 91 render_404
92 92 end
93 93
94 94 def find_optional_project
95 95 @project = Project.find(params[:project_id]) if params[:project_id]
96 96 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
97 97 rescue ActiveRecord::RecordNotFound
98 98 render_404
99 99 end
100 100 end
@@ -1,374 +1,374
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception; end
23 23 class InvalidRevisionParam < Exception; end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 menu_item :repository
27 27 menu_item :settings, :only => :edit
28 28 default_search_scope :changesets
29 29
30 30 before_filter :find_repository, :except => :edit
31 31 before_filter :find_project, :only => :edit
32 32 before_filter :authorize
33 accept_key_auth :revisions
33 accept_rss_auth :revisions
34 34
35 35 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
36 36
37 37 def edit
38 38 @repository = @project.repository
39 39 if !@repository && !params[:repository_scm].blank?
40 40 @repository = Repository.factory(params[:repository_scm])
41 41 @repository.project = @project if @repository
42 42 end
43 43 if request.post? && @repository
44 44 p1 = params[:repository]
45 45 p = {}
46 46 p_extra = {}
47 47 p1.each do |k, v|
48 48 if k =~ /^extra_/
49 49 p_extra[k] = v
50 50 else
51 51 p[k] = v
52 52 end
53 53 end
54 54 @repository.attributes = p
55 55 @repository.merge_extra_info(p_extra)
56 56 @repository.save
57 57 end
58 58 render(:update) do |page|
59 59 page.replace_html "tab-content-repository",
60 60 :partial => 'projects/settings/repository'
61 61 if @repository && !@project.repository
62 62 @project.reload # needed to reload association
63 63 page.replace_html "main-menu", render_main_menu(@project)
64 64 end
65 65 end
66 66 end
67 67
68 68 def committers
69 69 @committers = @repository.committers
70 70 @users = @project.users
71 71 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
72 72 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
73 73 @users.compact!
74 74 @users.sort!
75 75 if request.post? && params[:committers].is_a?(Hash)
76 76 # Build a hash with repository usernames as keys and corresponding user ids as values
77 77 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
78 78 flash[:notice] = l(:notice_successful_update)
79 79 redirect_to :action => 'committers', :id => @project
80 80 end
81 81 end
82 82
83 83 def destroy
84 84 @repository.destroy
85 85 redirect_to :controller => 'projects',
86 86 :action => 'settings',
87 87 :id => @project,
88 88 :tab => 'repository'
89 89 end
90 90
91 91 def show
92 92 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
93 93
94 94 @entries = @repository.entries(@path, @rev)
95 95 @changeset = @repository.find_changeset_by_name(@rev)
96 96 if request.xhr?
97 97 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
98 98 else
99 99 (show_error_not_found; return) unless @entries
100 100 @changesets = @repository.latest_changesets(@path, @rev)
101 101 @properties = @repository.properties(@path, @rev)
102 102 render :action => 'show'
103 103 end
104 104 end
105 105
106 106 alias_method :browse, :show
107 107
108 108 def changes
109 109 @entry = @repository.entry(@path, @rev)
110 110 (show_error_not_found; return) unless @entry
111 111 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
112 112 @properties = @repository.properties(@path, @rev)
113 113 @changeset = @repository.find_changeset_by_name(@rev)
114 114 end
115 115
116 116 def revisions
117 117 @changeset_count = @repository.changesets.count
118 118 @changeset_pages = Paginator.new self, @changeset_count,
119 119 per_page_option,
120 120 params['page']
121 121 @changesets = @repository.changesets.find(:all,
122 122 :limit => @changeset_pages.items_per_page,
123 123 :offset => @changeset_pages.current.offset,
124 124 :include => [:user, :repository])
125 125
126 126 respond_to do |format|
127 127 format.html { render :layout => false if request.xhr? }
128 128 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
129 129 end
130 130 end
131 131
132 132 def entry
133 133 @entry = @repository.entry(@path, @rev)
134 134 (show_error_not_found; return) unless @entry
135 135
136 136 # If the entry is a dir, show the browser
137 137 (show; return) if @entry.is_dir?
138 138
139 139 @content = @repository.cat(@path, @rev)
140 140 (show_error_not_found; return) unless @content
141 141 if 'raw' == params[:format] ||
142 142 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
143 143 ! is_entry_text_data?(@content, @path)
144 144 # Force the download
145 145 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
146 146 send_type = Redmine::MimeType.of(@path)
147 147 send_opt[:type] = send_type.to_s if send_type
148 148 send_data @content, send_opt
149 149 else
150 150 # Prevent empty lines when displaying a file with Windows style eol
151 151 # TODO: UTF-16
152 152 # Is this needs? AttachmentsController reads file simply.
153 153 @content.gsub!("\r\n", "\n")
154 154 @changeset = @repository.find_changeset_by_name(@rev)
155 155 end
156 156 end
157 157
158 158 def is_entry_text_data?(ent, path)
159 159 # UTF-16 contains "\x00".
160 160 # It is very strict that file contains less than 30% of ascii symbols
161 161 # in non Western Europe.
162 162 return true if Redmine::MimeType.is_type?('text', path)
163 163 # Ruby 1.8.6 has a bug of integer divisions.
164 164 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
165 165 return false if ent.is_binary_data?
166 166 true
167 167 end
168 168 private :is_entry_text_data?
169 169
170 170 def annotate
171 171 @entry = @repository.entry(@path, @rev)
172 172 (show_error_not_found; return) unless @entry
173 173
174 174 @annotate = @repository.scm.annotate(@path, @rev)
175 175 (render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
176 176 @changeset = @repository.find_changeset_by_name(@rev)
177 177 end
178 178
179 179 def revision
180 180 raise ChangesetNotFound if @rev.blank?
181 181 @changeset = @repository.find_changeset_by_name(@rev)
182 182 raise ChangesetNotFound unless @changeset
183 183
184 184 respond_to do |format|
185 185 format.html
186 186 format.js {render :layout => false}
187 187 end
188 188 rescue ChangesetNotFound
189 189 show_error_not_found
190 190 end
191 191
192 192 def diff
193 193 if params[:format] == 'diff'
194 194 @diff = @repository.diff(@path, @rev, @rev_to)
195 195 (show_error_not_found; return) unless @diff
196 196 filename = "changeset_r#{@rev}"
197 197 filename << "_r#{@rev_to}" if @rev_to
198 198 send_data @diff.join, :filename => "#{filename}.diff",
199 199 :type => 'text/x-patch',
200 200 :disposition => 'attachment'
201 201 else
202 202 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
203 203 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
204 204
205 205 # Save diff type as user preference
206 206 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
207 207 User.current.pref[:diff_type] = @diff_type
208 208 User.current.preference.save
209 209 end
210 210 @cache_key = "repositories/diff/#{@repository.id}/" +
211 211 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
212 212 unless read_fragment(@cache_key)
213 213 @diff = @repository.diff(@path, @rev, @rev_to)
214 214 show_error_not_found unless @diff
215 215 end
216 216
217 217 @changeset = @repository.find_changeset_by_name(@rev)
218 218 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
219 219 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
220 220 end
221 221 end
222 222
223 223 def stats
224 224 end
225 225
226 226 def graph
227 227 data = nil
228 228 case params[:graph]
229 229 when "commits_per_month"
230 230 data = graph_commits_per_month(@repository)
231 231 when "commits_per_author"
232 232 data = graph_commits_per_author(@repository)
233 233 end
234 234 if data
235 235 headers["Content-Type"] = "image/svg+xml"
236 236 send_data(data, :type => "image/svg+xml", :disposition => "inline")
237 237 else
238 238 render_404
239 239 end
240 240 end
241 241
242 242 private
243 243
244 244 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
245 245
246 246 def find_repository
247 247 @project = Project.find(params[:id])
248 248 @repository = @project.repository
249 249 (render_404; return false) unless @repository
250 250 @path = params[:path].join('/') unless params[:path].nil?
251 251 @path ||= ''
252 252 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
253 253 @rev_to = params[:rev_to]
254 254
255 255 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
256 256 if @repository.branches.blank?
257 257 raise InvalidRevisionParam
258 258 end
259 259 end
260 260 rescue ActiveRecord::RecordNotFound
261 261 render_404
262 262 rescue InvalidRevisionParam
263 263 show_error_not_found
264 264 end
265 265
266 266 def show_error_not_found
267 267 render_error :message => l(:error_scm_not_found), :status => 404
268 268 end
269 269
270 270 # Handler for Redmine::Scm::Adapters::CommandFailed exception
271 271 def show_error_command_failed(exception)
272 272 render_error l(:error_scm_command_failed, exception.message)
273 273 end
274 274
275 275 def graph_commits_per_month(repository)
276 276 @date_to = Date.today
277 277 @date_from = @date_to << 11
278 278 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
279 279 commits_by_day = repository.changesets.count(
280 280 :all, :group => :commit_date,
281 281 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
282 282 commits_by_month = [0] * 12
283 283 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
284 284
285 285 changes_by_day = repository.changes.count(
286 286 :all, :group => :commit_date,
287 287 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
288 288 changes_by_month = [0] * 12
289 289 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
290 290
291 291 fields = []
292 292 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
293 293
294 294 graph = SVG::Graph::Bar.new(
295 295 :height => 300,
296 296 :width => 800,
297 297 :fields => fields.reverse,
298 298 :stack => :side,
299 299 :scale_integers => true,
300 300 :step_x_labels => 2,
301 301 :show_data_values => false,
302 302 :graph_title => l(:label_commits_per_month),
303 303 :show_graph_title => true
304 304 )
305 305
306 306 graph.add_data(
307 307 :data => commits_by_month[0..11].reverse,
308 308 :title => l(:label_revision_plural)
309 309 )
310 310
311 311 graph.add_data(
312 312 :data => changes_by_month[0..11].reverse,
313 313 :title => l(:label_change_plural)
314 314 )
315 315
316 316 graph.burn
317 317 end
318 318
319 319 def graph_commits_per_author(repository)
320 320 commits_by_author = repository.changesets.count(:all, :group => :committer)
321 321 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
322 322
323 323 changes_by_author = repository.changes.count(:all, :group => :committer)
324 324 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
325 325
326 326 fields = commits_by_author.collect {|r| r.first}
327 327 commits_data = commits_by_author.collect {|r| r.last}
328 328 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
329 329
330 330 fields = fields + [""]*(10 - fields.length) if fields.length<10
331 331 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
332 332 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
333 333
334 334 # Remove email adress in usernames
335 335 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
336 336
337 337 graph = SVG::Graph::BarHorizontal.new(
338 338 :height => 400,
339 339 :width => 800,
340 340 :fields => fields,
341 341 :stack => :side,
342 342 :scale_integers => true,
343 343 :show_data_values => false,
344 344 :rotate_y_labels => false,
345 345 :graph_title => l(:label_commits_per_author),
346 346 :show_graph_title => true
347 347 )
348 348 graph.add_data(
349 349 :data => commits_data,
350 350 :title => l(:label_revision_plural)
351 351 )
352 352 graph.add_data(
353 353 :data => changes_data,
354 354 :title => l(:label_change_plural)
355 355 )
356 356 graph.burn
357 357 end
358 358 end
359 359
360 360 class Date
361 361 def months_ago(date = Date.today)
362 362 (date.year - self.year)*12 + (date.month - self.month)
363 363 end
364 364
365 365 def weeks_ago(date = Date.today)
366 366 (date.year - self.year)*52 + (date.cweek - self.cweek)
367 367 end
368 368 end
369 369
370 370 class String
371 371 def with_leading_slash
372 372 starts_with?('/') ? self : "/#{self}"
373 373 end
374 374 end
@@ -1,323 +1,324
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimelogController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_project, :only => [:new, :create]
21 21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 23 before_filter :authorize, :except => [:index]
24 24 before_filter :find_optional_project, :only => [:index]
25 accept_key_auth :index, :show, :create, :update, :destroy
25 accept_rss_auth :index
26 accept_api_auth :index, :show, :create, :update, :destroy
26 27
27 28 helper :sort
28 29 include SortHelper
29 30 helper :issues
30 31 include TimelogHelper
31 32 helper :custom_fields
32 33 include CustomFieldsHelper
33 34
34 35 def index
35 36 sort_init 'spent_on', 'desc'
36 37 sort_update 'spent_on' => 'spent_on',
37 38 'user' => 'user_id',
38 39 'activity' => 'activity_id',
39 40 'project' => "#{Project.table_name}.name",
40 41 'issue' => 'issue_id',
41 42 'hours' => 'hours'
42 43
43 44 cond = ARCondition.new
44 45 if @issue
45 46 cond << "#{Issue.table_name}.root_id = #{@issue.root_id} AND #{Issue.table_name}.lft >= #{@issue.lft} AND #{Issue.table_name}.rgt <= #{@issue.rgt}"
46 47 elsif @project
47 48 cond << @project.project_condition(Setting.display_subprojects_issues?)
48 49 end
49 50
50 51 retrieve_date_range
51 52 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
52 53
53 54 respond_to do |format|
54 55 format.html {
55 56 # Paginate results
56 57 @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
57 58 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
58 59 @entries = TimeEntry.visible.find(:all,
59 60 :include => [:project, :activity, :user, {:issue => :tracker}],
60 61 :conditions => cond.conditions,
61 62 :order => sort_clause,
62 63 :limit => @entry_pages.items_per_page,
63 64 :offset => @entry_pages.current.offset)
64 65 @total_hours = TimeEntry.visible.sum(:hours, :include => [:project, :issue], :conditions => cond.conditions).to_f
65 66
66 67 render :layout => !request.xhr?
67 68 }
68 69 format.api {
69 70 @entry_count = TimeEntry.visible.count(:include => [:project, :issue], :conditions => cond.conditions)
70 71 @offset, @limit = api_offset_and_limit
71 72 @entries = TimeEntry.visible.find(:all,
72 73 :include => [:project, :activity, :user, {:issue => :tracker}],
73 74 :conditions => cond.conditions,
74 75 :order => sort_clause,
75 76 :limit => @limit,
76 77 :offset => @offset)
77 78 }
78 79 format.atom {
79 80 entries = TimeEntry.visible.find(:all,
80 81 :include => [:project, :activity, :user, {:issue => :tracker}],
81 82 :conditions => cond.conditions,
82 83 :order => "#{TimeEntry.table_name}.created_on DESC",
83 84 :limit => Setting.feeds_limit.to_i)
84 85 render_feed(entries, :title => l(:label_spent_time))
85 86 }
86 87 format.csv {
87 88 # Export all entries
88 89 @entries = TimeEntry.visible.find(:all,
89 90 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
90 91 :conditions => cond.conditions,
91 92 :order => sort_clause)
92 93 send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
93 94 }
94 95 end
95 96 end
96 97
97 98 def show
98 99 respond_to do |format|
99 100 # TODO: Implement html response
100 101 format.html { render :nothing => true, :status => 406 }
101 102 format.api
102 103 end
103 104 end
104 105
105 106 def new
106 107 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
107 108 @time_entry.attributes = params[:time_entry]
108 109
109 110 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
110 111 render :action => 'edit'
111 112 end
112 113
113 114 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
114 115 def create
115 116 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
116 117 @time_entry.attributes = params[:time_entry]
117 118
118 119 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
119 120
120 121 if @time_entry.save
121 122 respond_to do |format|
122 123 format.html {
123 124 flash[:notice] = l(:notice_successful_update)
124 125 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
125 126 }
126 127 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
127 128 end
128 129 else
129 130 respond_to do |format|
130 131 format.html { render :action => 'edit' }
131 132 format.api { render_validation_errors(@time_entry) }
132 133 end
133 134 end
134 135 end
135 136
136 137 def edit
137 138 @time_entry.attributes = params[:time_entry]
138 139
139 140 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
140 141 end
141 142
142 143 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
143 144 def update
144 145 @time_entry.attributes = params[:time_entry]
145 146
146 147 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
147 148
148 149 if @time_entry.save
149 150 respond_to do |format|
150 151 format.html {
151 152 flash[:notice] = l(:notice_successful_update)
152 153 redirect_back_or_default :action => 'index', :project_id => @time_entry.project
153 154 }
154 155 format.api { head :ok }
155 156 end
156 157 else
157 158 respond_to do |format|
158 159 format.html { render :action => 'edit' }
159 160 format.api { render_validation_errors(@time_entry) }
160 161 end
161 162 end
162 163 end
163 164
164 165 def bulk_edit
165 166 @available_activities = TimeEntryActivity.shared.active
166 167 @custom_fields = TimeEntry.first.available_custom_fields
167 168 end
168 169
169 170 def bulk_update
170 171 attributes = parse_params_for_bulk_time_entry_attributes(params)
171 172
172 173 unsaved_time_entry_ids = []
173 174 @time_entries.each do |time_entry|
174 175 time_entry.reload
175 176 time_entry.attributes = attributes
176 177 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
177 178 unless time_entry.save
178 179 # Keep unsaved time_entry ids to display them in flash error
179 180 unsaved_time_entry_ids << time_entry.id
180 181 end
181 182 end
182 183 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
183 184 redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
184 185 end
185 186
186 187 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
187 188 def destroy
188 189 @time_entries.each do |t|
189 190 begin
190 191 unless t.destroy && t.destroyed?
191 192 respond_to do |format|
192 193 format.html {
193 194 flash[:error] = l(:notice_unable_delete_time_entry)
194 195 redirect_to :back
195 196 }
196 197 format.api { render_validation_errors(t) }
197 198 end
198 199 return
199 200 end
200 201 rescue ::ActionController::RedirectBackError
201 202 redirect_to :action => 'index', :project_id => @projects.first
202 203 return
203 204 end
204 205 end
205 206
206 207 respond_to do |format|
207 208 format.html {
208 209 flash[:notice] = l(:notice_successful_delete)
209 210 redirect_back_or_default(:action => 'index', :project_id => @projects.first)
210 211 }
211 212 format.api { head :ok }
212 213 end
213 214 end
214 215
215 216 private
216 217 def find_time_entry
217 218 @time_entry = TimeEntry.find(params[:id])
218 219 unless @time_entry.editable_by?(User.current)
219 220 render_403
220 221 return false
221 222 end
222 223 @project = @time_entry.project
223 224 rescue ActiveRecord::RecordNotFound
224 225 render_404
225 226 end
226 227
227 228 def find_time_entries
228 229 @time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
229 230 raise ActiveRecord::RecordNotFound if @time_entries.empty?
230 231 @projects = @time_entries.collect(&:project).compact.uniq
231 232 @project = @projects.first if @projects.size == 1
232 233 rescue ActiveRecord::RecordNotFound
233 234 render_404
234 235 end
235 236
236 237 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
237 238 if unsaved_time_entry_ids.empty?
238 239 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
239 240 else
240 241 flash[:error] = l(:notice_failed_to_save_time_entries,
241 242 :count => unsaved_time_entry_ids.size,
242 243 :total => time_entries.size,
243 244 :ids => '#' + unsaved_time_entry_ids.join(', #'))
244 245 end
245 246 end
246 247
247 248 def find_project
248 249 if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
249 250 @issue = Issue.find(issue_id)
250 251 @project = @issue.project
251 252 elsif (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
252 253 @project = Project.find(project_id)
253 254 else
254 255 render_404
255 256 return false
256 257 end
257 258 rescue ActiveRecord::RecordNotFound
258 259 render_404
259 260 end
260 261
261 262 def find_optional_project
262 263 if !params[:issue_id].blank?
263 264 @issue = Issue.find(params[:issue_id])
264 265 @project = @issue.project
265 266 elsif !params[:project_id].blank?
266 267 @project = Project.find(params[:project_id])
267 268 end
268 269 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
269 270 end
270 271
271 272 # Retrieves the date range based on predefined ranges or specific from/to param dates
272 273 def retrieve_date_range
273 274 @free_period = false
274 275 @from, @to = nil, nil
275 276
276 277 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
277 278 case params[:period].to_s
278 279 when 'today'
279 280 @from = @to = Date.today
280 281 when 'yesterday'
281 282 @from = @to = Date.today - 1
282 283 when 'current_week'
283 284 @from = Date.today - (Date.today.cwday - 1)%7
284 285 @to = @from + 6
285 286 when 'last_week'
286 287 @from = Date.today - 7 - (Date.today.cwday - 1)%7
287 288 @to = @from + 6
288 289 when '7_days'
289 290 @from = Date.today - 7
290 291 @to = Date.today
291 292 when 'current_month'
292 293 @from = Date.civil(Date.today.year, Date.today.month, 1)
293 294 @to = (@from >> 1) - 1
294 295 when 'last_month'
295 296 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
296 297 @to = (@from >> 1) - 1
297 298 when '30_days'
298 299 @from = Date.today - 30
299 300 @to = Date.today
300 301 when 'current_year'
301 302 @from = Date.civil(Date.today.year, 1, 1)
302 303 @to = Date.civil(Date.today.year, 12, 31)
303 304 end
304 305 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
305 306 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
306 307 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
307 308 @free_period = true
308 309 else
309 310 # default
310 311 end
311 312
312 313 @from, @to = @to, @from if @from && @to && @from > @to
313 314 @from ||= (TimeEntry.earilest_date_for_project(@project) || Date.today)
314 315 @to ||= (TimeEntry.latest_date_for_project(@project) || Date.today)
315 316 end
316 317
317 318 def parse_params_for_bulk_time_entry_attributes(params)
318 319 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
319 320 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
320 321 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
321 322 attributes
322 323 end
323 324 end
@@ -1,240 +1,240
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class UsersController < ApplicationController
19 19 layout 'admin'
20 20
21 21 before_filter :require_admin, :except => :show
22 22 before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
23 accept_key_auth :index, :show, :create, :update, :destroy
23 accept_api_auth :index, :show, :create, :update, :destroy
24 24
25 25 helper :sort
26 26 include SortHelper
27 27 helper :custom_fields
28 28 include CustomFieldsHelper
29 29
30 30 def index
31 31 sort_init 'login', 'asc'
32 32 sort_update %w(login firstname lastname mail admin created_on last_login_on)
33 33
34 34 case params[:format]
35 35 when 'xml', 'json'
36 36 @offset, @limit = api_offset_and_limit
37 37 else
38 38 @limit = per_page_option
39 39 end
40 40
41 41 scope = User
42 42 scope = scope.in_group(params[:group_id].to_i) if params[:group_id].present?
43 43
44 44 @status = params[:status] ? params[:status].to_i : 1
45 45 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
46 46
47 47 unless params[:name].blank?
48 48 name = "%#{params[:name].strip.downcase}%"
49 49 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ? OR LOWER(mail) LIKE ?", name, name, name, name]
50 50 end
51 51
52 52 @user_count = scope.count(:conditions => c.conditions)
53 53 @user_pages = Paginator.new self, @user_count, @limit, params['page']
54 54 @offset ||= @user_pages.current.offset
55 55 @users = scope.find :all,
56 56 :order => sort_clause,
57 57 :conditions => c.conditions,
58 58 :limit => @limit,
59 59 :offset => @offset
60 60
61 61 respond_to do |format|
62 62 format.html {
63 63 @groups = Group.all.sort
64 64 render :layout => !request.xhr?
65 65 }
66 66 format.api
67 67 end
68 68 end
69 69
70 70 def show
71 71 # show projects based on current user visibility
72 72 @memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
73 73
74 74 events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
75 75 @events_by_day = events.group_by(&:event_date)
76 76
77 77 unless User.current.admin?
78 78 if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
79 79 render_404
80 80 return
81 81 end
82 82 end
83 83
84 84 respond_to do |format|
85 85 format.html { render :layout => 'base' }
86 86 format.api
87 87 end
88 88 end
89 89
90 90 def new
91 91 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
92 92 @auth_sources = AuthSource.find(:all)
93 93 end
94 94
95 95 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
96 96 def create
97 97 @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
98 98 @user.safe_attributes = params[:user]
99 99 @user.admin = params[:user][:admin] || false
100 100 @user.login = params[:user][:login]
101 101 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
102 102
103 103 # TODO: Similar to My#account
104 104 @user.pref.attributes = params[:pref]
105 105 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
106 106
107 107 if @user.save
108 108 @user.pref.save
109 109 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
110 110
111 111 Mailer.deliver_account_information(@user, params[:user][:password]) if params[:send_information]
112 112
113 113 respond_to do |format|
114 114 format.html {
115 115 flash[:notice] = l(:notice_successful_create)
116 116 redirect_to(params[:continue] ?
117 117 {:controller => 'users', :action => 'new'} :
118 118 {:controller => 'users', :action => 'edit', :id => @user}
119 119 )
120 120 }
121 121 format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
122 122 end
123 123 else
124 124 @auth_sources = AuthSource.find(:all)
125 125 # Clear password input
126 126 @user.password = @user.password_confirmation = nil
127 127
128 128 respond_to do |format|
129 129 format.html { render :action => 'new' }
130 130 format.api { render_validation_errors(@user) }
131 131 end
132 132 end
133 133 end
134 134
135 135 def edit
136 136 @auth_sources = AuthSource.find(:all)
137 137 @membership ||= Member.new
138 138 end
139 139
140 140 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
141 141 def update
142 142 @user.admin = params[:user][:admin] if params[:user][:admin]
143 143 @user.login = params[:user][:login] if params[:user][:login]
144 144 if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
145 145 @user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
146 146 end
147 147 @user.safe_attributes = params[:user]
148 148 # Was the account actived ? (do it before User#save clears the change)
149 149 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
150 150 # TODO: Similar to My#account
151 151 @user.pref.attributes = params[:pref]
152 152 @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
153 153
154 154 if @user.save
155 155 @user.pref.save
156 156 @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
157 157
158 158 if was_activated
159 159 Mailer.deliver_account_activated(@user)
160 160 elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
161 161 Mailer.deliver_account_information(@user, params[:user][:password])
162 162 end
163 163
164 164 respond_to do |format|
165 165 format.html {
166 166 flash[:notice] = l(:notice_successful_update)
167 167 redirect_to :back
168 168 }
169 169 format.api { head :ok }
170 170 end
171 171 else
172 172 @auth_sources = AuthSource.find(:all)
173 173 @membership ||= Member.new
174 174 # Clear password input
175 175 @user.password = @user.password_confirmation = nil
176 176
177 177 respond_to do |format|
178 178 format.html { render :action => :edit }
179 179 format.api { render_validation_errors(@user) }
180 180 end
181 181 end
182 182 rescue ::ActionController::RedirectBackError
183 183 redirect_to :controller => 'users', :action => 'edit', :id => @user
184 184 end
185 185
186 186 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
187 187 def destroy
188 188 @user.destroy
189 189 respond_to do |format|
190 190 format.html { redirect_to(users_url) }
191 191 format.api { head :ok }
192 192 end
193 193 end
194 194
195 195 def edit_membership
196 196 @membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
197 197 @membership.save if request.post?
198 198 respond_to do |format|
199 199 if @membership.valid?
200 200 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
201 201 format.js {
202 202 render(:update) {|page|
203 203 page.replace_html "tab-content-memberships", :partial => 'users/memberships'
204 204 page.visual_effect(:highlight, "member-#{@membership.id}")
205 205 }
206 206 }
207 207 else
208 208 format.js {
209 209 render(:update) {|page|
210 210 page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
211 211 }
212 212 }
213 213 end
214 214 end
215 215 end
216 216
217 217 def destroy_membership
218 218 @membership = Member.find(params[:membership_id])
219 219 if request.post? && @membership.deletable?
220 220 @membership.destroy
221 221 end
222 222 respond_to do |format|
223 223 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
224 224 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
225 225 end
226 226 end
227 227
228 228 private
229 229
230 230 def find_user
231 231 if params[:id] == 'current'
232 232 require_login || return
233 233 @user = User.current
234 234 else
235 235 @user = User.find(params[:id])
236 236 end
237 237 rescue ActiveRecord::RecordNotFound
238 238 render_404
239 239 end
240 240 end
@@ -1,192 +1,192
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class VersionsController < ApplicationController
19 19 menu_item :roadmap
20 20 model_object Version
21 21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23 23 before_filter :find_project, :only => [:index, :new, :create, :close_completed]
24 24 before_filter :authorize
25 25
26 accept_key_auth :index, :create, :update, :destroy
26 accept_api_auth :index, :create, :update, :destroy
27 27
28 28 helper :custom_fields
29 29 helper :projects
30 30
31 31 def index
32 32 respond_to do |format|
33 33 format.html {
34 34 @trackers = @project.trackers.find(:all, :order => 'position')
35 35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
36 36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
37 37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
38 38
39 39 @versions = @project.shared_versions || []
40 40 @versions += @project.rolled_up_versions.visible if @with_subprojects
41 41 @versions = @versions.uniq.sort
42 42 @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
43 43
44 44 @issues_by_version = {}
45 45 unless @selected_tracker_ids.empty?
46 46 @versions.each do |version|
47 47 issues = version.fixed_issues.visible.find(:all,
48 48 :include => [:project, :status, :tracker, :priority],
49 49 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
50 50 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
51 51 @issues_by_version[version] = issues
52 52 end
53 53 end
54 54 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
55 55 }
56 56 format.api {
57 57 @versions = @project.shared_versions.all
58 58 }
59 59 end
60 60 end
61 61
62 62 def show
63 63 respond_to do |format|
64 64 format.html {
65 65 @issues = @version.fixed_issues.visible.find(:all,
66 66 :include => [:status, :tracker, :priority],
67 67 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
68 68 }
69 69 format.api
70 70 end
71 71 end
72 72
73 73 def new
74 74 @version = @project.versions.build
75 75 if params[:version]
76 76 attributes = params[:version].dup
77 77 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
78 78 @version.attributes = attributes
79 79 end
80 80 end
81 81
82 82 def create
83 83 # TODO: refactor with code above in #new
84 84 @version = @project.versions.build
85 85 if params[:version]
86 86 attributes = params[:version].dup
87 87 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
88 88 @version.attributes = attributes
89 89 end
90 90
91 91 if request.post?
92 92 if @version.save
93 93 respond_to do |format|
94 94 format.html do
95 95 flash[:notice] = l(:notice_successful_create)
96 96 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
97 97 end
98 98 format.js do
99 99 # IE doesn't support the replace_html rjs method for select box options
100 100 render(:update) {|page| page.replace "issue_fixed_version_id",
101 101 content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
102 102 }
103 103 end
104 104 format.api do
105 105 render :action => 'show', :status => :created, :location => version_url(@version)
106 106 end
107 107 end
108 108 else
109 109 respond_to do |format|
110 110 format.html { render :action => 'new' }
111 111 format.js do
112 112 render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
113 113 end
114 114 format.api { render_validation_errors(@version) }
115 115 end
116 116 end
117 117 end
118 118 end
119 119
120 120 def edit
121 121 end
122 122
123 123 def update
124 124 if request.put? && params[:version]
125 125 attributes = params[:version].dup
126 126 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
127 127 if @version.update_attributes(attributes)
128 128 respond_to do |format|
129 129 format.html {
130 130 flash[:notice] = l(:notice_successful_update)
131 131 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
132 132 }
133 133 format.api { head :ok }
134 134 end
135 135 else
136 136 respond_to do |format|
137 137 format.html { render :action => 'edit' }
138 138 format.api { render_validation_errors(@version) }
139 139 end
140 140 end
141 141 end
142 142 end
143 143
144 144 def close_completed
145 145 if request.put?
146 146 @project.close_completed_versions
147 147 end
148 148 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
149 149 end
150 150
151 151 verify :method => :delete, :only => :destroy, :render => {:nothing => true, :status => :method_not_allowed }
152 152 def destroy
153 153 if @version.fixed_issues.empty?
154 154 @version.destroy
155 155 respond_to do |format|
156 156 format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
157 157 format.api { head :ok }
158 158 end
159 159 else
160 160 respond_to do |format|
161 161 format.html {
162 162 flash[:error] = l(:notice_unable_delete_version)
163 163 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
164 164 }
165 165 format.api { head :unprocessable_entity }
166 166 end
167 167 end
168 168 end
169 169
170 170 def status_by
171 171 respond_to do |format|
172 172 format.html { render :action => 'show' }
173 173 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
174 174 end
175 175 end
176 176
177 177 private
178 178 def find_project
179 179 @project = Project.find(params[:project_id])
180 180 rescue ActiveRecord::RecordNotFound
181 181 render_404
182 182 end
183 183
184 184 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
185 185 if ids = params[:tracker_ids]
186 186 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
187 187 else
188 188 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
189 189 end
190 190 end
191 191
192 192 end
General Comments 0
You need to be logged in to leave comments. Login now