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