##// END OF EJS Templates
Refactor: Decouple failed attachments and the flash messages...
Eric Davis -
r3414:fe1e3ccd1842
parent child
Show More
@@ -1,304 +1,309
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class ApplicationController < ActionController::Base
21 class ApplicationController < ActionController::Base
22 include Redmine::I18n
22 include Redmine::I18n
23
23
24 layout 'base'
24 layout 'base'
25 exempt_from_layout 'builder'
25 exempt_from_layout 'builder'
26
26
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
27 # Remove broken cookie after upgrade from 0.8.x (#4292)
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
29 # TODO: remove it when Rails is fixed
29 # TODO: remove it when Rails is fixed
30 before_filter :delete_broken_cookies
30 before_filter :delete_broken_cookies
31 def delete_broken_cookies
31 def delete_broken_cookies
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
33 cookies.delete '_redmine_session'
33 cookies.delete '_redmine_session'
34 redirect_to home_path
34 redirect_to home_path
35 return false
35 return false
36 end
36 end
37 end
37 end
38
38
39 before_filter :user_setup, :check_if_login_required, :set_localization
39 before_filter :user_setup, :check_if_login_required, :set_localization
40 filter_parameter_logging :password
40 filter_parameter_logging :password
41 protect_from_forgery
41 protect_from_forgery
42
42
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
44
44
45 include Redmine::Search::Controller
45 include Redmine::Search::Controller
46 include Redmine::MenuManager::MenuController
46 include Redmine::MenuManager::MenuController
47 helper Redmine::MenuManager::MenuHelper
47 helper Redmine::MenuManager::MenuHelper
48
48
49 Redmine::Scm::Base.all.each do |scm|
49 Redmine::Scm::Base.all.each do |scm|
50 require_dependency "repository/#{scm.underscore}"
50 require_dependency "repository/#{scm.underscore}"
51 end
51 end
52
52
53 def user_setup
53 def user_setup
54 # Check the settings cache for each request
54 # Check the settings cache for each request
55 Setting.check_cache
55 Setting.check_cache
56 # Find the current user
56 # Find the current user
57 User.current = find_current_user
57 User.current = find_current_user
58 end
58 end
59
59
60 # Returns the current user or nil if no user is logged in
60 # Returns the current user or nil if no user is logged in
61 # and starts a session if needed
61 # and starts a session if needed
62 def find_current_user
62 def find_current_user
63 if session[:user_id]
63 if session[:user_id]
64 # existing session
64 # existing session
65 (User.active.find(session[:user_id]) rescue nil)
65 (User.active.find(session[:user_id]) rescue nil)
66 elsif cookies[:autologin] && Setting.autologin?
66 elsif cookies[:autologin] && Setting.autologin?
67 # auto-login feature starts a new session
67 # auto-login feature starts a new session
68 user = User.try_to_autologin(cookies[:autologin])
68 user = User.try_to_autologin(cookies[:autologin])
69 session[:user_id] = user.id if user
69 session[:user_id] = user.id if user
70 user
70 user
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
72 # RSS key authentication does not start a session
72 # RSS key authentication does not start a session
73 User.find_by_rss_key(params[:key])
73 User.find_by_rss_key(params[:key])
74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
76 # Use API key
76 # Use API key
77 User.find_by_api_key(params[:key])
77 User.find_by_api_key(params[:key])
78 else
78 else
79 # HTTP Basic, either username/password or API key/random
79 # HTTP Basic, either username/password or API key/random
80 authenticate_with_http_basic do |username, password|
80 authenticate_with_http_basic do |username, password|
81 User.try_to_login(username, password) || User.find_by_api_key(username)
81 User.try_to_login(username, password) || User.find_by_api_key(username)
82 end
82 end
83 end
83 end
84 end
84 end
85 end
85 end
86
86
87 # Sets the logged in user
87 # Sets the logged in user
88 def logged_user=(user)
88 def logged_user=(user)
89 reset_session
89 reset_session
90 if user && user.is_a?(User)
90 if user && user.is_a?(User)
91 User.current = user
91 User.current = user
92 session[:user_id] = user.id
92 session[:user_id] = user.id
93 else
93 else
94 User.current = User.anonymous
94 User.current = User.anonymous
95 end
95 end
96 end
96 end
97
97
98 # check if login is globally required to access the application
98 # check if login is globally required to access the application
99 def check_if_login_required
99 def check_if_login_required
100 # no check needed if user is already logged in
100 # no check needed if user is already logged in
101 return true if User.current.logged?
101 return true if User.current.logged?
102 require_login if Setting.login_required?
102 require_login if Setting.login_required?
103 end
103 end
104
104
105 def set_localization
105 def set_localization
106 lang = nil
106 lang = nil
107 if User.current.logged?
107 if User.current.logged?
108 lang = find_language(User.current.language)
108 lang = find_language(User.current.language)
109 end
109 end
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
112 if !accept_lang.blank?
112 if !accept_lang.blank?
113 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
113 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
114 end
114 end
115 end
115 end
116 lang ||= Setting.default_language
116 lang ||= Setting.default_language
117 set_language_if_valid(lang)
117 set_language_if_valid(lang)
118 end
118 end
119
119
120 def require_login
120 def require_login
121 if !User.current.logged?
121 if !User.current.logged?
122 # Extract only the basic url parameters on non-GET requests
122 # Extract only the basic url parameters on non-GET requests
123 if request.get?
123 if request.get?
124 url = url_for(params)
124 url = url_for(params)
125 else
125 else
126 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
126 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
127 end
127 end
128 respond_to do |format|
128 respond_to do |format|
129 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
129 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
130 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
130 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
131 format.xml { head :unauthorized }
131 format.xml { head :unauthorized }
132 format.json { head :unauthorized }
132 format.json { head :unauthorized }
133 end
133 end
134 return false
134 return false
135 end
135 end
136 true
136 true
137 end
137 end
138
138
139 def require_admin
139 def require_admin
140 return unless require_login
140 return unless require_login
141 if !User.current.admin?
141 if !User.current.admin?
142 render_403
142 render_403
143 return false
143 return false
144 end
144 end
145 true
145 true
146 end
146 end
147
147
148 def deny_access
148 def deny_access
149 User.current.logged? ? render_403 : require_login
149 User.current.logged? ? render_403 : require_login
150 end
150 end
151
151
152 # Authorize the user for the requested action
152 # Authorize the user for the requested action
153 def authorize(ctrl = params[:controller], action = params[:action], global = false)
153 def authorize(ctrl = params[:controller], action = params[:action], global = false)
154 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
154 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
155 allowed ? true : deny_access
155 allowed ? true : deny_access
156 end
156 end
157
157
158 # Authorize the user for the requested action outside a project
158 # Authorize the user for the requested action outside a project
159 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
159 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
160 authorize(ctrl, action, global)
160 authorize(ctrl, action, global)
161 end
161 end
162
162
163 # Find project of id params[:id]
163 # Find project of id params[:id]
164 def find_project
164 def find_project
165 @project = Project.find(params[:id])
165 @project = Project.find(params[:id])
166 rescue ActiveRecord::RecordNotFound
166 rescue ActiveRecord::RecordNotFound
167 render_404
167 render_404
168 end
168 end
169
169
170 # make sure that the user is a member of the project (or admin) if project is private
170 # make sure that the user is a member of the project (or admin) if project is private
171 # used as a before_filter for actions that do not require any particular permission on the project
171 # used as a before_filter for actions that do not require any particular permission on the project
172 def check_project_privacy
172 def check_project_privacy
173 if @project && @project.active?
173 if @project && @project.active?
174 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
174 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
175 true
175 true
176 else
176 else
177 User.current.logged? ? render_403 : require_login
177 User.current.logged? ? render_403 : require_login
178 end
178 end
179 else
179 else
180 @project = nil
180 @project = nil
181 render_404
181 render_404
182 false
182 false
183 end
183 end
184 end
184 end
185
185
186 def redirect_back_or_default(default)
186 def redirect_back_or_default(default)
187 back_url = CGI.unescape(params[:back_url].to_s)
187 back_url = CGI.unescape(params[:back_url].to_s)
188 if !back_url.blank?
188 if !back_url.blank?
189 begin
189 begin
190 uri = URI.parse(back_url)
190 uri = URI.parse(back_url)
191 # do not redirect user to another host or to the login or register page
191 # do not redirect user to another host or to the login or register page
192 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
192 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
193 redirect_to(back_url)
193 redirect_to(back_url)
194 return
194 return
195 end
195 end
196 rescue URI::InvalidURIError
196 rescue URI::InvalidURIError
197 # redirect to default
197 # redirect to default
198 end
198 end
199 end
199 end
200 redirect_to default
200 redirect_to default
201 end
201 end
202
202
203 def render_403
203 def render_403
204 @project = nil
204 @project = nil
205 respond_to do |format|
205 respond_to do |format|
206 format.html { render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 }
206 format.html { render :template => "common/403", :layout => (request.xhr? ? false : 'base'), :status => 403 }
207 format.atom { head 403 }
207 format.atom { head 403 }
208 format.xml { head 403 }
208 format.xml { head 403 }
209 format.json { head 403 }
209 format.json { head 403 }
210 end
210 end
211 return false
211 return false
212 end
212 end
213
213
214 def render_404
214 def render_404
215 respond_to do |format|
215 respond_to do |format|
216 format.html { render :template => "common/404", :layout => !request.xhr?, :status => 404 }
216 format.html { render :template => "common/404", :layout => !request.xhr?, :status => 404 }
217 format.atom { head 404 }
217 format.atom { head 404 }
218 format.xml { head 404 }
218 format.xml { head 404 }
219 format.json { head 404 }
219 format.json { head 404 }
220 end
220 end
221 return false
221 return false
222 end
222 end
223
223
224 def render_error(msg)
224 def render_error(msg)
225 respond_to do |format|
225 respond_to do |format|
226 format.html {
226 format.html {
227 flash.now[:error] = msg
227 flash.now[:error] = msg
228 render :text => '', :layout => !request.xhr?, :status => 500
228 render :text => '', :layout => !request.xhr?, :status => 500
229 }
229 }
230 format.atom { head 500 }
230 format.atom { head 500 }
231 format.xml { head 500 }
231 format.xml { head 500 }
232 format.json { head 500 }
232 format.json { head 500 }
233 end
233 end
234 end
234 end
235
235
236 def invalid_authenticity_token
236 def invalid_authenticity_token
237 if api_request?
237 if api_request?
238 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
238 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
239 end
239 end
240 render_error "Invalid form authenticity token."
240 render_error "Invalid form authenticity token."
241 end
241 end
242
242
243 def render_feed(items, options={})
243 def render_feed(items, options={})
244 @items = items || []
244 @items = items || []
245 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
245 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
246 @items = @items.slice(0, Setting.feeds_limit.to_i)
246 @items = @items.slice(0, Setting.feeds_limit.to_i)
247 @title = options[:title] || Setting.app_title
247 @title = options[:title] || Setting.app_title
248 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
248 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
249 end
249 end
250
250
251 def self.accept_key_auth(*actions)
251 def self.accept_key_auth(*actions)
252 actions = actions.flatten.map(&:to_s)
252 actions = actions.flatten.map(&:to_s)
253 write_inheritable_attribute('accept_key_auth_actions', actions)
253 write_inheritable_attribute('accept_key_auth_actions', actions)
254 end
254 end
255
255
256 def accept_key_auth_actions
256 def accept_key_auth_actions
257 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
257 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
258 end
258 end
259
259
260 # Returns the number of objects that should be displayed
260 # Returns the number of objects that should be displayed
261 # on the paginated list
261 # on the paginated list
262 def per_page_option
262 def per_page_option
263 per_page = nil
263 per_page = nil
264 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
264 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
265 per_page = params[:per_page].to_s.to_i
265 per_page = params[:per_page].to_s.to_i
266 session[:per_page] = per_page
266 session[:per_page] = per_page
267 elsif session[:per_page]
267 elsif session[:per_page]
268 per_page = session[:per_page]
268 per_page = session[:per_page]
269 else
269 else
270 per_page = Setting.per_page_options_array.first || 25
270 per_page = Setting.per_page_options_array.first || 25
271 end
271 end
272 per_page
272 per_page
273 end
273 end
274
274
275 # qvalues http header parser
275 # qvalues http header parser
276 # code taken from webrick
276 # code taken from webrick
277 def parse_qvalues(value)
277 def parse_qvalues(value)
278 tmp = []
278 tmp = []
279 if value
279 if value
280 parts = value.split(/,\s*/)
280 parts = value.split(/,\s*/)
281 parts.each {|part|
281 parts.each {|part|
282 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
282 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
283 val = m[1]
283 val = m[1]
284 q = (m[2] or 1).to_f
284 q = (m[2] or 1).to_f
285 tmp.push([val, q])
285 tmp.push([val, q])
286 end
286 end
287 }
287 }
288 tmp = tmp.sort_by{|val, q| -q}
288 tmp = tmp.sort_by{|val, q| -q}
289 tmp.collect!{|val, q| val}
289 tmp.collect!{|val, q| val}
290 end
290 end
291 return tmp
291 return tmp
292 rescue
292 rescue
293 nil
293 nil
294 end
294 end
295
295
296 # Returns a string that can be used as filename value in Content-Disposition header
296 # Returns a string that can be used as filename value in Content-Disposition header
297 def filename_for_content_disposition(name)
297 def filename_for_content_disposition(name)
298 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
298 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
299 end
299 end
300
300
301 def api_request?
301 def api_request?
302 %w(xml json).include? params[:format]
302 %w(xml json).include? params[:format]
303 end
303 end
304
305 # Renders a warning flash if obj has unsaved attachments
306 def render_attachment_warning_if_needed(obj)
307 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
308 end
304 end
309 end
@@ -1,91 +1,91
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class DocumentsController < ApplicationController
18 class DocumentsController < ApplicationController
19 default_search_scope :documents
19 default_search_scope :documents
20 before_filter :find_project, :only => [:index, :new]
20 before_filter :find_project, :only => [:index, :new]
21 before_filter :find_document, :except => [:index, :new]
21 before_filter :find_document, :except => [:index, :new]
22 before_filter :authorize
22 before_filter :authorize
23
23
24 helper :attachments
24 helper :attachments
25
25
26 def index
26 def index
27 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
28 documents = @project.documents.find :all, :include => [:attachments, :category]
28 documents = @project.documents.find :all, :include => [:attachments, :category]
29 case @sort_by
29 case @sort_by
30 when 'date'
30 when 'date'
31 @grouped = documents.group_by {|d| d.updated_on.to_date }
31 @grouped = documents.group_by {|d| d.updated_on.to_date }
32 when 'title'
32 when 'title'
33 @grouped = documents.group_by {|d| d.title.first.upcase}
33 @grouped = documents.group_by {|d| d.title.first.upcase}
34 when 'author'
34 when 'author'
35 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
36 else
36 else
37 @grouped = documents.group_by(&:category)
37 @grouped = documents.group_by(&:category)
38 end
38 end
39 @document = @project.documents.build
39 @document = @project.documents.build
40 render :layout => false if request.xhr?
40 render :layout => false if request.xhr?
41 end
41 end
42
42
43 def show
43 def show
44 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
44 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
45 end
45 end
46
46
47 def new
47 def new
48 @document = @project.documents.build(params[:document])
48 @document = @project.documents.build(params[:document])
49 if request.post? and @document.save
49 if request.post? and @document.save
50 attachments = Attachment.attach_files(@document, params[:attachments])
50 attachments = Attachment.attach_files(@document, params[:attachments])
51 flash[:warning] = attachments[:flash] if attachments[:flash]
51 render_attachment_warning_if_needed(@document)
52 flash[:notice] = l(:notice_successful_create)
52 flash[:notice] = l(:notice_successful_create)
53 redirect_to :action => 'index', :project_id => @project
53 redirect_to :action => 'index', :project_id => @project
54 end
54 end
55 end
55 end
56
56
57 def edit
57 def edit
58 @categories = DocumentCategory.all
58 @categories = DocumentCategory.all
59 if request.post? and @document.update_attributes(params[:document])
59 if request.post? and @document.update_attributes(params[:document])
60 flash[:notice] = l(:notice_successful_update)
60 flash[:notice] = l(:notice_successful_update)
61 redirect_to :action => 'show', :id => @document
61 redirect_to :action => 'show', :id => @document
62 end
62 end
63 end
63 end
64
64
65 def destroy
65 def destroy
66 @document.destroy
66 @document.destroy
67 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
67 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
68 end
68 end
69
69
70 def add_attachment
70 def add_attachment
71 attachments = Attachment.attach_files(@document, params[:attachments])
71 attachments = Attachment.attach_files(@document, params[:attachments])
72 flash[:warning] = attachments[:flash] if attachments[:flash]
72 render_attachment_warning_if_needed(@document)
73
73
74 Mailer.deliver_attachments_added(attachments[:files]) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
74 Mailer.deliver_attachments_added(attachments[:files]) if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
75 redirect_to :action => 'show', :id => @document
75 redirect_to :action => 'show', :id => @document
76 end
76 end
77
77
78 private
78 private
79 def find_project
79 def find_project
80 @project = Project.find(params[:project_id])
80 @project = Project.find(params[:project_id])
81 rescue ActiveRecord::RecordNotFound
81 rescue ActiveRecord::RecordNotFound
82 render_404
82 render_404
83 end
83 end
84
84
85 def find_document
85 def find_document
86 @document = Document.find(params[:id])
86 @document = Document.find(params[:id])
87 @project = @document.project
87 @project = @document.project
88 rescue ActiveRecord::RecordNotFound
88 rescue ActiveRecord::RecordNotFound
89 render_404
89 render_404
90 end
90 end
91 end
91 end
@@ -1,589 +1,590
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :update, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 include ProjectsHelper
33 include ProjectsHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 include QueriesHelper
43 include QueriesHelper
44 helper :sort
44 helper :sort
45 include SortHelper
45 include SortHelper
46 include IssuesHelper
46 include IssuesHelper
47 helper :timelog
47 helper :timelog
48 include Redmine::Export::PDF
48 include Redmine::Export::PDF
49
49
50 verify :method => [:post, :delete],
50 verify :method => [:post, :delete],
51 :only => :destroy,
51 :only => :destroy,
52 :render => { :nothing => true, :status => :method_not_allowed }
52 :render => { :nothing => true, :status => :method_not_allowed }
53
53
54 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
54 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
55
55
56 def index
56 def index
57 retrieve_query
57 retrieve_query
58 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
59 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
59 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
60
60
61 if @query.valid?
61 if @query.valid?
62 limit = case params[:format]
62 limit = case params[:format]
63 when 'csv', 'pdf'
63 when 'csv', 'pdf'
64 Setting.issues_export_limit.to_i
64 Setting.issues_export_limit.to_i
65 when 'atom'
65 when 'atom'
66 Setting.feeds_limit.to_i
66 Setting.feeds_limit.to_i
67 else
67 else
68 per_page_option
68 per_page_option
69 end
69 end
70
70
71 @issue_count = @query.issue_count
71 @issue_count = @query.issue_count
72 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
72 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
73 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
73 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
74 :order => sort_clause,
74 :order => sort_clause,
75 :offset => @issue_pages.current.offset,
75 :offset => @issue_pages.current.offset,
76 :limit => limit)
76 :limit => limit)
77 @issue_count_by_group = @query.issue_count_by_group
77 @issue_count_by_group = @query.issue_count_by_group
78
78
79 respond_to do |format|
79 respond_to do |format|
80 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
80 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
81 format.xml { render :layout => false }
81 format.xml { render :layout => false }
82 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
82 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
83 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
83 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
84 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
84 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
85 end
85 end
86 else
86 else
87 # Send html if the query is not valid
87 # Send html if the query is not valid
88 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
88 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
89 end
89 end
90 rescue ActiveRecord::RecordNotFound
90 rescue ActiveRecord::RecordNotFound
91 render_404
91 render_404
92 end
92 end
93
93
94 def changes
94 def changes
95 retrieve_query
95 retrieve_query
96 sort_init 'id', 'desc'
96 sort_init 'id', 'desc'
97 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
97 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
98
98
99 if @query.valid?
99 if @query.valid?
100 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
100 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
101 :limit => 25)
101 :limit => 25)
102 end
102 end
103 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
103 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
104 render :layout => false, :content_type => 'application/atom+xml'
104 render :layout => false, :content_type => 'application/atom+xml'
105 rescue ActiveRecord::RecordNotFound
105 rescue ActiveRecord::RecordNotFound
106 render_404
106 render_404
107 end
107 end
108
108
109 def show
109 def show
110 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
110 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
111 @journals.each_with_index {|j,i| j.indice = i+1}
111 @journals.each_with_index {|j,i| j.indice = i+1}
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
112 @journals.reverse! if User.current.wants_comments_in_reverse_order?
113 @changesets = @issue.changesets.visible.all
113 @changesets = @issue.changesets.visible.all
114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
114 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
115 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
115 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
116 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
116 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
117 @priorities = IssuePriority.all
117 @priorities = IssuePriority.all
118 @time_entry = TimeEntry.new
118 @time_entry = TimeEntry.new
119 respond_to do |format|
119 respond_to do |format|
120 format.html { render :template => 'issues/show.rhtml' }
120 format.html { render :template => 'issues/show.rhtml' }
121 format.xml { render :layout => false }
121 format.xml { render :layout => false }
122 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
122 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
123 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
123 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
124 end
124 end
125 end
125 end
126
126
127 # Add a new issue
127 # Add a new issue
128 # The new issue will be created from an existing one if copy_from parameter is given
128 # The new issue will be created from an existing one if copy_from parameter is given
129 def new
129 def new
130 @issue = Issue.new
130 @issue = Issue.new
131 @issue.copy_from(params[:copy_from]) if params[:copy_from]
131 @issue.copy_from(params[:copy_from]) if params[:copy_from]
132 @issue.project = @project
132 @issue.project = @project
133 # Tracker must be set before custom field values
133 # Tracker must be set before custom field values
134 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
134 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
135 if @issue.tracker.nil?
135 if @issue.tracker.nil?
136 render_error l(:error_no_tracker_in_project)
136 render_error l(:error_no_tracker_in_project)
137 return
137 return
138 end
138 end
139 if params[:issue].is_a?(Hash)
139 if params[:issue].is_a?(Hash)
140 @issue.safe_attributes = params[:issue]
140 @issue.safe_attributes = params[:issue]
141 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
141 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
142 end
142 end
143 @issue.author = User.current
143 @issue.author = User.current
144
144
145 default_status = IssueStatus.default
145 default_status = IssueStatus.default
146 unless default_status
146 unless default_status
147 render_error l(:error_no_default_issue_status)
147 render_error l(:error_no_default_issue_status)
148 return
148 return
149 end
149 end
150 @issue.status = default_status
150 @issue.status = default_status
151 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
151 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
152
152
153 if request.get? || request.xhr?
153 if request.get? || request.xhr?
154 @issue.start_date ||= Date.today
154 @issue.start_date ||= Date.today
155 else
155 else
156 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
156 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
157 # Check that the user is allowed to apply the requested status
157 # Check that the user is allowed to apply the requested status
158 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
158 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
159 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
159 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
160 if @issue.save
160 if @issue.save
161 attachments = Attachment.attach_files(@issue, params[:attachments])
161 attachments = Attachment.attach_files(@issue, params[:attachments])
162 flash[:warning] = attachments[:flash] if attachments[:flash]
162 render_attachment_warning_if_needed(@issue)
163 flash[:notice] = l(:notice_successful_create)
163 flash[:notice] = l(:notice_successful_create)
164 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
164 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
165 respond_to do |format|
165 respond_to do |format|
166 format.html {
166 format.html {
167 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
167 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
168 { :action => 'show', :id => @issue })
168 { :action => 'show', :id => @issue })
169 }
169 }
170 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
170 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
171 end
171 end
172 return
172 return
173 else
173 else
174 respond_to do |format|
174 respond_to do |format|
175 format.html { }
175 format.html { }
176 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
176 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
177 end
177 end
178 end
178 end
179 end
179 end
180 @priorities = IssuePriority.all
180 @priorities = IssuePriority.all
181 render :layout => !request.xhr?
181 render :layout => !request.xhr?
182 end
182 end
183
183
184 # Attributes that can be updated on workflow transition (without :edit permission)
184 # Attributes that can be updated on workflow transition (without :edit permission)
185 # TODO: make it configurable (at least per role)
185 # TODO: make it configurable (at least per role)
186 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
186 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
187
187
188 def edit
188 def edit
189 update_issue_from_params
189 update_issue_from_params
190
190
191 respond_to do |format|
191 respond_to do |format|
192 format.html { }
192 format.html { }
193 format.xml { }
193 format.xml { }
194 end
194 end
195 end
195 end
196
196
197 def update
197 def update
198 update_issue_from_params
198 update_issue_from_params
199
199
200 if issue_update
200 if issue_update
201 respond_to do |format|
201 respond_to do |format|
202 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
202 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
203 format.xml { head :ok }
203 format.xml { head :ok }
204 end
204 end
205 else
205 else
206 respond_to do |format|
206 respond_to do |format|
207 format.html { render :action => 'edit' }
207 format.html { render :action => 'edit' }
208 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
208 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
209 end
209 end
210 end
210 end
211
211
212 rescue ActiveRecord::StaleObjectError
212 rescue ActiveRecord::StaleObjectError
213 # Optimistic locking exception
213 # Optimistic locking exception
214 flash.now[:error] = l(:notice_locking_conflict)
214 flash.now[:error] = l(:notice_locking_conflict)
215 # Remove the previously added attachments if issue was not updated
215 # Remove the previously added attachments if issue was not updated
216 attachments[:files].each(&:destroy) if attachments[:files]
216 attachments[:files].each(&:destroy) if attachments[:files]
217 end
217 end
218
218
219 def reply
219 def reply
220 journal = Journal.find(params[:journal_id]) if params[:journal_id]
220 journal = Journal.find(params[:journal_id]) if params[:journal_id]
221 if journal
221 if journal
222 user = journal.user
222 user = journal.user
223 text = journal.notes
223 text = journal.notes
224 else
224 else
225 user = @issue.author
225 user = @issue.author
226 text = @issue.description
226 text = @issue.description
227 end
227 end
228 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
228 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
229 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
229 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
230 render(:update) { |page|
230 render(:update) { |page|
231 page.<< "$('notes').value = \"#{content}\";"
231 page.<< "$('notes').value = \"#{content}\";"
232 page.show 'update'
232 page.show 'update'
233 page << "Form.Element.focus('notes');"
233 page << "Form.Element.focus('notes');"
234 page << "Element.scrollTo('update');"
234 page << "Element.scrollTo('update');"
235 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
235 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
236 }
236 }
237 end
237 end
238
238
239 # Bulk edit a set of issues
239 # Bulk edit a set of issues
240 def bulk_edit
240 def bulk_edit
241 if request.post?
241 if request.post?
242 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
242 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
243 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
243 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
244 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
244 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
245
245
246 unsaved_issue_ids = []
246 unsaved_issue_ids = []
247 @issues.each do |issue|
247 @issues.each do |issue|
248 journal = issue.init_journal(User.current, params[:notes])
248 journal = issue.init_journal(User.current, params[:notes])
249 issue.safe_attributes = attributes
249 issue.safe_attributes = attributes
250 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
250 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
251 unless issue.save
251 unless issue.save
252 # Keep unsaved issue ids to display them in flash error
252 # Keep unsaved issue ids to display them in flash error
253 unsaved_issue_ids << issue.id
253 unsaved_issue_ids << issue.id
254 end
254 end
255 end
255 end
256 if unsaved_issue_ids.empty?
256 if unsaved_issue_ids.empty?
257 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
257 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
258 else
258 else
259 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
259 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
260 :total => @issues.size,
260 :total => @issues.size,
261 :ids => '#' + unsaved_issue_ids.join(', #'))
261 :ids => '#' + unsaved_issue_ids.join(', #'))
262 end
262 end
263 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
263 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
264 return
264 return
265 end
265 end
266 @available_statuses = Workflow.available_statuses(@project)
266 @available_statuses = Workflow.available_statuses(@project)
267 @custom_fields = @project.all_issue_custom_fields
267 @custom_fields = @project.all_issue_custom_fields
268 end
268 end
269
269
270 def move
270 def move
271 @copy = params[:copy_options] && params[:copy_options][:copy]
271 @copy = params[:copy_options] && params[:copy_options][:copy]
272 @allowed_projects = []
272 @allowed_projects = []
273 # find projects to which the user is allowed to move the issue
273 # find projects to which the user is allowed to move the issue
274 if User.current.admin?
274 if User.current.admin?
275 # admin is allowed to move issues to any active (visible) project
275 # admin is allowed to move issues to any active (visible) project
276 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
276 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
277 else
277 else
278 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
278 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
279 end
279 end
280 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
280 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
281 @target_project ||= @project
281 @target_project ||= @project
282 @trackers = @target_project.trackers
282 @trackers = @target_project.trackers
283 @available_statuses = Workflow.available_statuses(@project)
283 @available_statuses = Workflow.available_statuses(@project)
284 if request.post?
284 if request.post?
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
285 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
286 unsaved_issue_ids = []
286 unsaved_issue_ids = []
287 moved_issues = []
287 moved_issues = []
288 @issues.each do |issue|
288 @issues.each do |issue|
289 changed_attributes = {}
289 changed_attributes = {}
290 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
290 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
291 unless params[valid_attribute].blank?
291 unless params[valid_attribute].blank?
292 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
292 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
293 end
293 end
294 end
294 end
295 issue.init_journal(User.current)
295 issue.init_journal(User.current)
296 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
296 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
297 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
297 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
298 moved_issues << r
298 moved_issues << r
299 else
299 else
300 unsaved_issue_ids << issue.id
300 unsaved_issue_ids << issue.id
301 end
301 end
302 end
302 end
303 if unsaved_issue_ids.empty?
303 if unsaved_issue_ids.empty?
304 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
304 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
305 else
305 else
306 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
306 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
307 :total => @issues.size,
307 :total => @issues.size,
308 :ids => '#' + unsaved_issue_ids.join(', #'))
308 :ids => '#' + unsaved_issue_ids.join(', #'))
309 end
309 end
310 if params[:follow]
310 if params[:follow]
311 if @issues.size == 1 && moved_issues.size == 1
311 if @issues.size == 1 && moved_issues.size == 1
312 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
312 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
313 else
313 else
314 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
314 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
315 end
315 end
316 else
316 else
317 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
317 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
318 end
318 end
319 return
319 return
320 end
320 end
321 render :layout => false if request.xhr?
321 render :layout => false if request.xhr?
322 end
322 end
323
323
324 def destroy
324 def destroy
325 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
325 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
326 if @hours > 0
326 if @hours > 0
327 case params[:todo]
327 case params[:todo]
328 when 'destroy'
328 when 'destroy'
329 # nothing to do
329 # nothing to do
330 when 'nullify'
330 when 'nullify'
331 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
331 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
332 when 'reassign'
332 when 'reassign'
333 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
333 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
334 if reassign_to.nil?
334 if reassign_to.nil?
335 flash.now[:error] = l(:error_issue_not_found_in_project)
335 flash.now[:error] = l(:error_issue_not_found_in_project)
336 return
336 return
337 else
337 else
338 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
338 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
339 end
339 end
340 else
340 else
341 unless params[:format] == 'xml'
341 unless params[:format] == 'xml'
342 # display the destroy form if it's a user request
342 # display the destroy form if it's a user request
343 return
343 return
344 end
344 end
345 end
345 end
346 end
346 end
347 @issues.each(&:destroy)
347 @issues.each(&:destroy)
348 respond_to do |format|
348 respond_to do |format|
349 format.html { redirect_to :action => 'index', :project_id => @project }
349 format.html { redirect_to :action => 'index', :project_id => @project }
350 format.xml { head :ok }
350 format.xml { head :ok }
351 end
351 end
352 end
352 end
353
353
354 def gantt
354 def gantt
355 @gantt = Redmine::Helpers::Gantt.new(params)
355 @gantt = Redmine::Helpers::Gantt.new(params)
356 retrieve_query
356 retrieve_query
357 @query.group_by = nil
357 @query.group_by = nil
358 if @query.valid?
358 if @query.valid?
359 events = []
359 events = []
360 # Issues that have start and due dates
360 # Issues that have start and due dates
361 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
361 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
362 :order => "start_date, due_date",
362 :order => "start_date, due_date",
363 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
363 :conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
364 )
364 )
365 # Issues that don't have a due date but that are assigned to a version with a date
365 # Issues that don't have a due date but that are assigned to a version with a date
366 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
366 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
367 :order => "start_date, effective_date",
367 :order => "start_date, effective_date",
368 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
368 :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
369 )
369 )
370 # Versions
370 # Versions
371 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
371 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
372
372
373 @gantt.events = events
373 @gantt.events = events
374 end
374 end
375
375
376 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
376 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
377
377
378 respond_to do |format|
378 respond_to do |format|
379 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
379 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
380 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
380 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
381 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
381 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
382 end
382 end
383 end
383 end
384
384
385 def calendar
385 def calendar
386 if params[:year] and params[:year].to_i > 1900
386 if params[:year] and params[:year].to_i > 1900
387 @year = params[:year].to_i
387 @year = params[:year].to_i
388 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
388 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
389 @month = params[:month].to_i
389 @month = params[:month].to_i
390 end
390 end
391 end
391 end
392 @year ||= Date.today.year
392 @year ||= Date.today.year
393 @month ||= Date.today.month
393 @month ||= Date.today.month
394
394
395 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
395 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
396 retrieve_query
396 retrieve_query
397 @query.group_by = nil
397 @query.group_by = nil
398 if @query.valid?
398 if @query.valid?
399 events = []
399 events = []
400 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
400 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
401 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
401 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
402 )
402 )
403 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
403 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
404
404
405 @calendar.events = events
405 @calendar.events = events
406 end
406 end
407
407
408 render :layout => false if request.xhr?
408 render :layout => false if request.xhr?
409 end
409 end
410
410
411 def context_menu
411 def context_menu
412 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
412 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
413 if (@issues.size == 1)
413 if (@issues.size == 1)
414 @issue = @issues.first
414 @issue = @issues.first
415 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
415 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
416 end
416 end
417 projects = @issues.collect(&:project).compact.uniq
417 projects = @issues.collect(&:project).compact.uniq
418 @project = projects.first if projects.size == 1
418 @project = projects.first if projects.size == 1
419
419
420 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
420 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
421 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
421 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
422 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
422 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
423 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
423 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
424 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
424 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
425 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
425 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
426 }
426 }
427 if @project
427 if @project
428 @assignables = @project.assignable_users
428 @assignables = @project.assignable_users
429 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
429 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
430 @trackers = @project.trackers
430 @trackers = @project.trackers
431 end
431 end
432
432
433 @priorities = IssuePriority.all.reverse
433 @priorities = IssuePriority.all.reverse
434 @statuses = IssueStatus.find(:all, :order => 'position')
434 @statuses = IssueStatus.find(:all, :order => 'position')
435 @back = params[:back_url] || request.env['HTTP_REFERER']
435 @back = params[:back_url] || request.env['HTTP_REFERER']
436
436
437 render :layout => false
437 render :layout => false
438 end
438 end
439
439
440 def update_form
440 def update_form
441 if params[:id].blank?
441 if params[:id].blank?
442 @issue = Issue.new
442 @issue = Issue.new
443 @issue.project = @project
443 @issue.project = @project
444 else
444 else
445 @issue = @project.issues.visible.find(params[:id])
445 @issue = @project.issues.visible.find(params[:id])
446 end
446 end
447 @issue.attributes = params[:issue]
447 @issue.attributes = params[:issue]
448 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
448 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
449 @priorities = IssuePriority.all
449 @priorities = IssuePriority.all
450
450
451 render :partial => 'attributes'
451 render :partial => 'attributes'
452 end
452 end
453
453
454 def preview
454 def preview
455 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
455 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
456 @attachements = @issue.attachments if @issue
456 @attachements = @issue.attachments if @issue
457 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
457 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
458 render :partial => 'common/preview'
458 render :partial => 'common/preview'
459 end
459 end
460
460
461 private
461 private
462 def find_issue
462 def find_issue
463 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
463 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
464 @project = @issue.project
464 @project = @issue.project
465 rescue ActiveRecord::RecordNotFound
465 rescue ActiveRecord::RecordNotFound
466 render_404
466 render_404
467 end
467 end
468
468
469 # Filter for bulk operations
469 # Filter for bulk operations
470 def find_issues
470 def find_issues
471 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
471 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
472 raise ActiveRecord::RecordNotFound if @issues.empty?
472 raise ActiveRecord::RecordNotFound if @issues.empty?
473 projects = @issues.collect(&:project).compact.uniq
473 projects = @issues.collect(&:project).compact.uniq
474 if projects.size == 1
474 if projects.size == 1
475 @project = projects.first
475 @project = projects.first
476 else
476 else
477 # TODO: let users bulk edit/move/destroy issues from different projects
477 # TODO: let users bulk edit/move/destroy issues from different projects
478 render_error 'Can not bulk edit/move/destroy issues from different projects'
478 render_error 'Can not bulk edit/move/destroy issues from different projects'
479 return false
479 return false
480 end
480 end
481 rescue ActiveRecord::RecordNotFound
481 rescue ActiveRecord::RecordNotFound
482 render_404
482 render_404
483 end
483 end
484
484
485 def find_project
485 def find_project
486 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
486 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
487 @project = Project.find(project_id)
487 @project = Project.find(project_id)
488 rescue ActiveRecord::RecordNotFound
488 rescue ActiveRecord::RecordNotFound
489 render_404
489 render_404
490 end
490 end
491
491
492 def find_optional_project
492 def find_optional_project
493 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
493 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
494 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
494 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
495 allowed ? true : deny_access
495 allowed ? true : deny_access
496 rescue ActiveRecord::RecordNotFound
496 rescue ActiveRecord::RecordNotFound
497 render_404
497 render_404
498 end
498 end
499
499
500 # Retrieve query from session or build a new query
500 # Retrieve query from session or build a new query
501 def retrieve_query
501 def retrieve_query
502 if !params[:query_id].blank?
502 if !params[:query_id].blank?
503 cond = "project_id IS NULL"
503 cond = "project_id IS NULL"
504 cond << " OR project_id = #{@project.id}" if @project
504 cond << " OR project_id = #{@project.id}" if @project
505 @query = Query.find(params[:query_id], :conditions => cond)
505 @query = Query.find(params[:query_id], :conditions => cond)
506 @query.project = @project
506 @query.project = @project
507 session[:query] = {:id => @query.id, :project_id => @query.project_id}
507 session[:query] = {:id => @query.id, :project_id => @query.project_id}
508 sort_clear
508 sort_clear
509 else
509 else
510 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
510 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
511 # Give it a name, required to be valid
511 # Give it a name, required to be valid
512 @query = Query.new(:name => "_")
512 @query = Query.new(:name => "_")
513 @query.project = @project
513 @query.project = @project
514 if params[:fields] and params[:fields].is_a? Array
514 if params[:fields] and params[:fields].is_a? Array
515 params[:fields].each do |field|
515 params[:fields].each do |field|
516 @query.add_filter(field, params[:operators][field], params[:values][field])
516 @query.add_filter(field, params[:operators][field], params[:values][field])
517 end
517 end
518 else
518 else
519 @query.available_filters.keys.each do |field|
519 @query.available_filters.keys.each do |field|
520 @query.add_short_filter(field, params[field]) if params[field]
520 @query.add_short_filter(field, params[field]) if params[field]
521 end
521 end
522 end
522 end
523 @query.group_by = params[:group_by]
523 @query.group_by = params[:group_by]
524 @query.column_names = params[:query] && params[:query][:column_names]
524 @query.column_names = params[:query] && params[:query][:column_names]
525 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
525 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
526 else
526 else
527 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
527 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
528 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
528 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
529 @query.project = @project
529 @query.project = @project
530 end
530 end
531 end
531 end
532 end
532 end
533
533
534 # Rescues an invalid query statement. Just in case...
534 # Rescues an invalid query statement. Just in case...
535 def query_statement_invalid(exception)
535 def query_statement_invalid(exception)
536 logger.error "Query::StatementInvalid: #{exception.message}" if logger
536 logger.error "Query::StatementInvalid: #{exception.message}" if logger
537 session.delete(:query)
537 session.delete(:query)
538 sort_clear
538 sort_clear
539 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
539 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
540 end
540 end
541
541
542 # Used by #edit and #update to set some common instance variables
542 # Used by #edit and #update to set some common instance variables
543 # from the params
543 # from the params
544 # TODO: Refactor, not everything in here is needed by #edit
544 # TODO: Refactor, not everything in here is needed by #edit
545 def update_issue_from_params
545 def update_issue_from_params
546 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
546 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
547 @priorities = IssuePriority.all
547 @priorities = IssuePriority.all
548 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
548 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
549 @time_entry = TimeEntry.new
549 @time_entry = TimeEntry.new
550
550
551 @notes = params[:notes]
551 @notes = params[:notes]
552 @journal = @issue.init_journal(User.current, @notes)
552 @journal = @issue.init_journal(User.current, @notes)
553 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
553 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
554 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
554 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
555 attrs = params[:issue].dup
555 attrs = params[:issue].dup
556 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
556 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
557 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
557 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
558 @issue.safe_attributes = attrs
558 @issue.safe_attributes = attrs
559 end
559 end
560
560
561 end
561 end
562
562
563 # TODO: Temporary utility method for #update. Should be split off
563 # TODO: Temporary utility method for #update. Should be split off
564 # and moved to the Issue model (accepts_nested_attributes_for maybe?)
564 # and moved to the Issue model (accepts_nested_attributes_for maybe?)
565 def issue_update
565 def issue_update
566 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, @project)
566 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, @project)
567 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
567 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
568 @time_entry.attributes = params[:time_entry]
568 @time_entry.attributes = params[:time_entry]
569 @issue.time_entries << @time_entry
569 @issue.time_entries << @time_entry
570 end
570 end
571
571
572 if @issue.valid?
572 if @issue.valid?
573 attachments = Attachment.attach_files(@issue, params[:attachments])
573 attachments = Attachment.attach_files(@issue, params[:attachments])
574 flash[:warning] = attachments[:flash] if attachments[:flash]
574 render_attachment_warning_if_needed(@issue)
575
575 attachments[:files].each {|a| @journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
576 attachments[:files].each {|a| @journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
576 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => @journal})
577 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => @journal})
577 if @issue.save
578 if @issue.save
578 if !@journal.new_record?
579 if !@journal.new_record?
579 # Only send notification if something was actually changed
580 # Only send notification if something was actually changed
580 flash[:notice] = l(:notice_successful_update)
581 flash[:notice] = l(:notice_successful_update)
581 end
582 end
582 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => @journal})
583 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => @journal})
583 return true
584 return true
584 end
585 end
585 end
586 end
586 # failure, returns false
587 # failure, returns false
587
588
588 end
589 end
589 end
590 end
@@ -1,149 +1,149
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MessagesController < ApplicationController
18 class MessagesController < ApplicationController
19 menu_item :boards
19 menu_item :boards
20 default_search_scope :messages
20 default_search_scope :messages
21 before_filter :find_board, :only => [:new, :preview]
21 before_filter :find_board, :only => [:new, :preview]
22 before_filter :find_message, :except => [:new, :preview]
22 before_filter :find_message, :except => [:new, :preview]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
24
24
25 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
25 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
26 verify :xhr => true, :only => :quote
26 verify :xhr => true, :only => :quote
27
27
28 helper :watchers
28 helper :watchers
29 helper :attachments
29 helper :attachments
30 include AttachmentsHelper
30 include AttachmentsHelper
31
31
32 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
32 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
33
33
34 # Show a topic and its replies
34 # Show a topic and its replies
35 def show
35 def show
36 page = params[:page]
36 page = params[:page]
37 # Find the page of the requested reply
37 # Find the page of the requested reply
38 if params[:r] && page.nil?
38 if params[:r] && page.nil?
39 offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
39 offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
40 page = 1 + offset / REPLIES_PER_PAGE
40 page = 1 + offset / REPLIES_PER_PAGE
41 end
41 end
42
42
43 @reply_count = @topic.children.count
43 @reply_count = @topic.children.count
44 @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
44 @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
45 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
45 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
46 :order => "#{Message.table_name}.created_on ASC",
46 :order => "#{Message.table_name}.created_on ASC",
47 :limit => @reply_pages.items_per_page,
47 :limit => @reply_pages.items_per_page,
48 :offset => @reply_pages.current.offset)
48 :offset => @reply_pages.current.offset)
49
49
50 @reply = Message.new(:subject => "RE: #{@message.subject}")
50 @reply = Message.new(:subject => "RE: #{@message.subject}")
51 render :action => "show", :layout => false if request.xhr?
51 render :action => "show", :layout => false if request.xhr?
52 end
52 end
53
53
54 # Create a new topic
54 # Create a new topic
55 def new
55 def new
56 @message = Message.new(params[:message])
56 @message = Message.new(params[:message])
57 @message.author = User.current
57 @message.author = User.current
58 @message.board = @board
58 @message.board = @board
59 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
59 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
60 @message.locked = params[:message]['locked']
60 @message.locked = params[:message]['locked']
61 @message.sticky = params[:message]['sticky']
61 @message.sticky = params[:message]['sticky']
62 end
62 end
63 if request.post? && @message.save
63 if request.post? && @message.save
64 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
64 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
65 attachments = Attachment.attach_files(@message, params[:attachments])
65 attachments = Attachment.attach_files(@message, params[:attachments])
66 flash[:warning] = attachments[:flash] if attachments[:flash]
66 render_attachment_warning_if_needed(@message)
67 redirect_to :action => 'show', :id => @message
67 redirect_to :action => 'show', :id => @message
68 end
68 end
69 end
69 end
70
70
71 # Reply to a topic
71 # Reply to a topic
72 def reply
72 def reply
73 @reply = Message.new(params[:reply])
73 @reply = Message.new(params[:reply])
74 @reply.author = User.current
74 @reply.author = User.current
75 @reply.board = @board
75 @reply.board = @board
76 @topic.children << @reply
76 @topic.children << @reply
77 if !@reply.new_record?
77 if !@reply.new_record?
78 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
78 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
79 attachments = Attachment.attach_files(@reply, params[:attachments])
79 attachments = Attachment.attach_files(@reply, params[:attachments])
80 flash[:warning] = attachments[:flash] if attachments[:flash]
80 render_attachment_warning_if_needed(@reply)
81 end
81 end
82 redirect_to :action => 'show', :id => @topic, :r => @reply
82 redirect_to :action => 'show', :id => @topic, :r => @reply
83 end
83 end
84
84
85 # Edit a message
85 # Edit a message
86 def edit
86 def edit
87 (render_403; return false) unless @message.editable_by?(User.current)
87 (render_403; return false) unless @message.editable_by?(User.current)
88 if params[:message]
88 if params[:message]
89 @message.locked = params[:message]['locked']
89 @message.locked = params[:message]['locked']
90 @message.sticky = params[:message]['sticky']
90 @message.sticky = params[:message]['sticky']
91 end
91 end
92 if request.post? && @message.update_attributes(params[:message])
92 if request.post? && @message.update_attributes(params[:message])
93 attachments = Attachment.attach_files(@message, params[:attachments])
93 attachments = Attachment.attach_files(@message, params[:attachments])
94 flash[:warning] = attachments[:flash] if attachments[:flash]
94 render_attachment_warning_if_needed(@message)
95 flash[:notice] = l(:notice_successful_update)
95 flash[:notice] = l(:notice_successful_update)
96 @message.reload
96 @message.reload
97 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
97 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
98 end
98 end
99 end
99 end
100
100
101 # Delete a messages
101 # Delete a messages
102 def destroy
102 def destroy
103 (render_403; return false) unless @message.destroyable_by?(User.current)
103 (render_403; return false) unless @message.destroyable_by?(User.current)
104 @message.destroy
104 @message.destroy
105 redirect_to @message.parent.nil? ?
105 redirect_to @message.parent.nil? ?
106 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
106 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
107 { :action => 'show', :id => @message.parent, :r => @message }
107 { :action => 'show', :id => @message.parent, :r => @message }
108 end
108 end
109
109
110 def quote
110 def quote
111 user = @message.author
111 user = @message.author
112 text = @message.content
112 text = @message.content
113 subject = @message.subject.gsub('"', '\"')
113 subject = @message.subject.gsub('"', '\"')
114 subject = "RE: #{subject}" unless subject.starts_with?('RE:')
114 subject = "RE: #{subject}" unless subject.starts_with?('RE:')
115 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
115 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
116 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
116 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
117 render(:update) { |page|
117 render(:update) { |page|
118 page << "$('reply_subject').value = \"#{subject}\";"
118 page << "$('reply_subject').value = \"#{subject}\";"
119 page.<< "$('message_content').value = \"#{content}\";"
119 page.<< "$('message_content').value = \"#{content}\";"
120 page.show 'reply'
120 page.show 'reply'
121 page << "Form.Element.focus('message_content');"
121 page << "Form.Element.focus('message_content');"
122 page << "Element.scrollTo('reply');"
122 page << "Element.scrollTo('reply');"
123 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
123 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
124 }
124 }
125 end
125 end
126
126
127 def preview
127 def preview
128 message = @board.messages.find_by_id(params[:id])
128 message = @board.messages.find_by_id(params[:id])
129 @attachements = message.attachments if message
129 @attachements = message.attachments if message
130 @text = (params[:message] || params[:reply])[:content]
130 @text = (params[:message] || params[:reply])[:content]
131 render :partial => 'common/preview'
131 render :partial => 'common/preview'
132 end
132 end
133
133
134 private
134 private
135 def find_message
135 def find_message
136 find_board
136 find_board
137 @message = @board.messages.find(params[:id], :include => :parent)
137 @message = @board.messages.find(params[:id], :include => :parent)
138 @topic = @message.root
138 @topic = @message.root
139 rescue ActiveRecord::RecordNotFound
139 rescue ActiveRecord::RecordNotFound
140 render_404
140 render_404
141 end
141 end
142
142
143 def find_board
143 def find_board
144 @board = Board.find(params[:board_id], :include => :project)
144 @board = Board.find(params[:board_id], :include => :project)
145 @project = @board.project
145 @project = @board.project
146 rescue ActiveRecord::RecordNotFound
146 rescue ActiveRecord::RecordNotFound
147 render_404
147 render_404
148 end
148 end
149 end
149 end
@@ -1,444 +1,444
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24
24
25 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
25 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
26 before_filter :find_optional_project, :only => :activity
26 before_filter :find_optional_project, :only => :activity
27 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
27 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize_global, :only => :add
28 before_filter :authorize_global, :only => :add
29 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
32 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 if controller.request.post?
33 if controller.request.post?
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
34 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 end
35 end
36 end
36 end
37
37
38 helper :sort
38 helper :sort
39 include SortHelper
39 include SortHelper
40 helper :custom_fields
40 helper :custom_fields
41 include CustomFieldsHelper
41 include CustomFieldsHelper
42 helper :issues
42 helper :issues
43 helper IssuesHelper
43 helper IssuesHelper
44 helper :queries
44 helper :queries
45 include QueriesHelper
45 include QueriesHelper
46 helper :repositories
46 helper :repositories
47 include RepositoriesHelper
47 include RepositoriesHelper
48 include ProjectsHelper
48 include ProjectsHelper
49
49
50 # Lists visible projects
50 # Lists visible projects
51 def index
51 def index
52 respond_to do |format|
52 respond_to do |format|
53 format.html {
53 format.html {
54 @projects = Project.visible.find(:all, :order => 'lft')
54 @projects = Project.visible.find(:all, :order => 'lft')
55 }
55 }
56 format.xml {
56 format.xml {
57 @projects = Project.visible.find(:all, :order => 'lft')
57 @projects = Project.visible.find(:all, :order => 'lft')
58 }
58 }
59 format.atom {
59 format.atom {
60 projects = Project.visible.find(:all, :order => 'created_on DESC',
60 projects = Project.visible.find(:all, :order => 'created_on DESC',
61 :limit => Setting.feeds_limit.to_i)
61 :limit => Setting.feeds_limit.to_i)
62 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
62 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
63 }
63 }
64 end
64 end
65 end
65 end
66
66
67 # Add a new project
67 # Add a new project
68 def add
68 def add
69 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
69 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
70 @trackers = Tracker.all
70 @trackers = Tracker.all
71 @project = Project.new(params[:project])
71 @project = Project.new(params[:project])
72 if request.get?
72 if request.get?
73 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
74 @project.trackers = Tracker.all
74 @project.trackers = Tracker.all
75 @project.is_public = Setting.default_projects_public?
75 @project.is_public = Setting.default_projects_public?
76 @project.enabled_module_names = Setting.default_projects_modules
76 @project.enabled_module_names = Setting.default_projects_modules
77 else
77 else
78 @project.enabled_module_names = params[:enabled_modules]
78 @project.enabled_module_names = params[:enabled_modules]
79 if validate_parent_id && @project.save
79 if validate_parent_id && @project.save
80 @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')
81 # 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
82 unless User.current.admin?
82 unless User.current.admin?
83 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
84 m = Member.new(:user => User.current, :roles => [r])
84 m = Member.new(:user => User.current, :roles => [r])
85 @project.members << m
85 @project.members << m
86 end
86 end
87 respond_to do |format|
87 respond_to do |format|
88 format.html {
88 format.html {
89 flash[:notice] = l(:notice_successful_create)
89 flash[:notice] = l(:notice_successful_create)
90 redirect_to :controller => 'projects', :action => 'settings', :id => @project
90 redirect_to :controller => 'projects', :action => 'settings', :id => @project
91 }
91 }
92 format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
92 format.xml { head :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
93 end
93 end
94 else
94 else
95 respond_to do |format|
95 respond_to do |format|
96 format.html
96 format.html
97 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
97 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
98 end
98 end
99 end
99 end
100 end
100 end
101 end
101 end
102
102
103 def copy
103 def copy
104 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
104 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
105 @trackers = Tracker.all
105 @trackers = Tracker.all
106 @root_projects = Project.find(:all,
106 @root_projects = Project.find(:all,
107 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
107 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
108 :order => 'name')
108 :order => 'name')
109 @source_project = Project.find(params[:id])
109 @source_project = Project.find(params[:id])
110 if request.get?
110 if request.get?
111 @project = Project.copy_from(@source_project)
111 @project = Project.copy_from(@source_project)
112 if @project
112 if @project
113 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
113 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
114 else
114 else
115 redirect_to :controller => 'admin', :action => 'projects'
115 redirect_to :controller => 'admin', :action => 'projects'
116 end
116 end
117 else
117 else
118 @project = Project.new(params[:project])
118 @project = Project.new(params[:project])
119 @project.enabled_module_names = params[:enabled_modules]
119 @project.enabled_module_names = params[:enabled_modules]
120 if validate_parent_id && @project.copy(@source_project, :only => params[:only])
120 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')
121 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
122 flash[:notice] = l(:notice_successful_create)
122 flash[:notice] = l(:notice_successful_create)
123 redirect_to :controller => 'admin', :action => 'projects'
123 redirect_to :controller => 'admin', :action => 'projects'
124 elsif !@project.new_record?
124 elsif !@project.new_record?
125 # Project was created
125 # Project was created
126 # But some objects were not copied due to validation failures
126 # But some objects were not copied due to validation failures
127 # (eg. issues from disabled trackers)
127 # (eg. issues from disabled trackers)
128 # TODO: inform about that
128 # TODO: inform about that
129 redirect_to :controller => 'admin', :action => 'projects'
129 redirect_to :controller => 'admin', :action => 'projects'
130 end
130 end
131 end
131 end
132 rescue ActiveRecord::RecordNotFound
132 rescue ActiveRecord::RecordNotFound
133 redirect_to :controller => 'admin', :action => 'projects'
133 redirect_to :controller => 'admin', :action => 'projects'
134 end
134 end
135
135
136 # Show @project
136 # Show @project
137 def show
137 def show
138 if params[:jump]
138 if params[:jump]
139 # try to redirect to the requested menu item
139 # try to redirect to the requested menu item
140 redirect_to_project_menu_item(@project, params[:jump]) && return
140 redirect_to_project_menu_item(@project, params[:jump]) && return
141 end
141 end
142
142
143 @users_by_role = @project.users_by_role
143 @users_by_role = @project.users_by_role
144 @subprojects = @project.children.visible
144 @subprojects = @project.children.visible
145 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
145 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
146 @trackers = @project.rolled_up_trackers
146 @trackers = @project.rolled_up_trackers
147
147
148 cond = @project.project_condition(Setting.display_subprojects_issues?)
148 cond = @project.project_condition(Setting.display_subprojects_issues?)
149
149
150 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
150 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
151 :include => [:project, :status, :tracker],
151 :include => [:project, :status, :tracker],
152 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
152 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
153 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
153 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
154 :include => [:project, :status, :tracker],
154 :include => [:project, :status, :tracker],
155 :conditions => cond)
155 :conditions => cond)
156
156
157 TimeEntry.visible_by(User.current) do
157 TimeEntry.visible_by(User.current) do
158 @total_hours = TimeEntry.sum(:hours,
158 @total_hours = TimeEntry.sum(:hours,
159 :include => :project,
159 :include => :project,
160 :conditions => cond).to_f
160 :conditions => cond).to_f
161 end
161 end
162 @key = User.current.rss_key
162 @key = User.current.rss_key
163
163
164 respond_to do |format|
164 respond_to do |format|
165 format.html
165 format.html
166 format.xml
166 format.xml
167 end
167 end
168 end
168 end
169
169
170 def settings
170 def settings
171 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
171 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
172 @issue_category ||= IssueCategory.new
172 @issue_category ||= IssueCategory.new
173 @member ||= @project.members.new
173 @member ||= @project.members.new
174 @trackers = Tracker.all
174 @trackers = Tracker.all
175 @repository ||= @project.repository
175 @repository ||= @project.repository
176 @wiki ||= @project.wiki
176 @wiki ||= @project.wiki
177 end
177 end
178
178
179 # Edit @project
179 # Edit @project
180 def edit
180 def edit
181 if request.get?
181 if request.get?
182 else
182 else
183 @project.attributes = params[:project]
183 @project.attributes = params[:project]
184 if validate_parent_id && @project.save
184 if validate_parent_id && @project.save
185 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
185 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
186 respond_to do |format|
186 respond_to do |format|
187 format.html {
187 format.html {
188 flash[:notice] = l(:notice_successful_update)
188 flash[:notice] = l(:notice_successful_update)
189 redirect_to :action => 'settings', :id => @project
189 redirect_to :action => 'settings', :id => @project
190 }
190 }
191 format.xml { head :ok }
191 format.xml { head :ok }
192 end
192 end
193 else
193 else
194 respond_to do |format|
194 respond_to do |format|
195 format.html {
195 format.html {
196 settings
196 settings
197 render :action => 'settings'
197 render :action => 'settings'
198 }
198 }
199 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
199 format.xml { render :xml => @project.errors, :status => :unprocessable_entity }
200 end
200 end
201 end
201 end
202 end
202 end
203 end
203 end
204
204
205 def modules
205 def modules
206 @project.enabled_module_names = params[:enabled_modules]
206 @project.enabled_module_names = params[:enabled_modules]
207 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
207 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
208 end
208 end
209
209
210 def archive
210 def archive
211 if request.post?
211 if request.post?
212 unless @project.archive
212 unless @project.archive
213 flash[:error] = l(:error_can_not_archive_project)
213 flash[:error] = l(:error_can_not_archive_project)
214 end
214 end
215 end
215 end
216 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
216 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
217 end
217 end
218
218
219 def unarchive
219 def unarchive
220 @project.unarchive if request.post? && !@project.active?
220 @project.unarchive if request.post? && !@project.active?
221 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
221 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
222 end
222 end
223
223
224 # Delete @project
224 # Delete @project
225 def destroy
225 def destroy
226 @project_to_destroy = @project
226 @project_to_destroy = @project
227 if request.get?
227 if request.get?
228 # display confirmation view
228 # display confirmation view
229 else
229 else
230 if params[:format] == 'xml' || params[:confirm]
230 if params[:format] == 'xml' || params[:confirm]
231 @project_to_destroy.destroy
231 @project_to_destroy.destroy
232 respond_to do |format|
232 respond_to do |format|
233 format.html { redirect_to :controller => 'admin', :action => 'projects' }
233 format.html { redirect_to :controller => 'admin', :action => 'projects' }
234 format.xml { head :ok }
234 format.xml { head :ok }
235 end
235 end
236 end
236 end
237 end
237 end
238 # hide project in layout
238 # hide project in layout
239 @project = nil
239 @project = nil
240 end
240 end
241
241
242 # Add a new issue category to @project
242 # Add a new issue category to @project
243 def add_issue_category
243 def add_issue_category
244 @category = @project.issue_categories.build(params[:category])
244 @category = @project.issue_categories.build(params[:category])
245 if request.post?
245 if request.post?
246 if @category.save
246 if @category.save
247 respond_to do |format|
247 respond_to do |format|
248 format.html do
248 format.html do
249 flash[:notice] = l(:notice_successful_create)
249 flash[:notice] = l(:notice_successful_create)
250 redirect_to :action => 'settings', :tab => 'categories', :id => @project
250 redirect_to :action => 'settings', :tab => 'categories', :id => @project
251 end
251 end
252 format.js do
252 format.js do
253 # IE doesn't support the replace_html rjs method for select box options
253 # IE doesn't support the replace_html rjs method for select box options
254 render(:update) {|page| page.replace "issue_category_id",
254 render(:update) {|page| page.replace "issue_category_id",
255 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
255 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
256 }
256 }
257 end
257 end
258 end
258 end
259 else
259 else
260 respond_to do |format|
260 respond_to do |format|
261 format.html
261 format.html
262 format.js do
262 format.js do
263 render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
263 render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
264 end
264 end
265 end
265 end
266 end
266 end
267 end
267 end
268 end
268 end
269
269
270 # Add a new version to @project
270 # Add a new version to @project
271 def add_version
271 def add_version
272 @version = @project.versions.build
272 @version = @project.versions.build
273 if params[:version]
273 if params[:version]
274 attributes = params[:version].dup
274 attributes = params[:version].dup
275 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
275 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
276 @version.attributes = attributes
276 @version.attributes = attributes
277 end
277 end
278 if request.post?
278 if request.post?
279 if @version.save
279 if @version.save
280 respond_to do |format|
280 respond_to do |format|
281 format.html do
281 format.html do
282 flash[:notice] = l(:notice_successful_create)
282 flash[:notice] = l(:notice_successful_create)
283 redirect_to :action => 'settings', :tab => 'versions', :id => @project
283 redirect_to :action => 'settings', :tab => 'versions', :id => @project
284 end
284 end
285 format.js do
285 format.js do
286 # IE doesn't support the replace_html rjs method for select box options
286 # IE doesn't support the replace_html rjs method for select box options
287 render(:update) {|page| page.replace "issue_fixed_version_id",
287 render(:update) {|page| page.replace "issue_fixed_version_id",
288 content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
288 content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
289 }
289 }
290 end
290 end
291 end
291 end
292 else
292 else
293 respond_to do |format|
293 respond_to do |format|
294 format.html
294 format.html
295 format.js do
295 format.js do
296 render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
296 render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
297 end
297 end
298 end
298 end
299 end
299 end
300 end
300 end
301 end
301 end
302
302
303 def add_file
303 def add_file
304 if request.post?
304 if request.post?
305 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
305 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
306 attachments = Attachment.attach_files(container, params[:attachments])
306 attachments = Attachment.attach_files(container, params[:attachments])
307 flash[:warning] = attachments[:flash] if attachments[:flash]
307 render_attachment_warning_if_needed(container)
308
308
309 if !attachments.empty? && Setting.notified_events.include?('file_added')
309 if !attachments.empty? && Setting.notified_events.include?('file_added')
310 Mailer.deliver_attachments_added(attachments[:files])
310 Mailer.deliver_attachments_added(attachments[:files])
311 end
311 end
312 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
312 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
313 return
313 return
314 end
314 end
315 @versions = @project.versions.sort
315 @versions = @project.versions.sort
316 end
316 end
317
317
318 def save_activities
318 def save_activities
319 if request.post? && params[:enumerations]
319 if request.post? && params[:enumerations]
320 Project.transaction do
320 Project.transaction do
321 params[:enumerations].each do |id, activity|
321 params[:enumerations].each do |id, activity|
322 @project.update_or_create_time_entry_activity(id, activity)
322 @project.update_or_create_time_entry_activity(id, activity)
323 end
323 end
324 end
324 end
325 end
325 end
326
326
327 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
327 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
328 end
328 end
329
329
330 def reset_activities
330 def reset_activities
331 @project.time_entry_activities.each do |time_entry_activity|
331 @project.time_entry_activities.each do |time_entry_activity|
332 time_entry_activity.destroy(time_entry_activity.parent)
332 time_entry_activity.destroy(time_entry_activity.parent)
333 end
333 end
334 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
334 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
335 end
335 end
336
336
337 def list_files
337 def list_files
338 sort_init 'filename', 'asc'
338 sort_init 'filename', 'asc'
339 sort_update 'filename' => "#{Attachment.table_name}.filename",
339 sort_update 'filename' => "#{Attachment.table_name}.filename",
340 'created_on' => "#{Attachment.table_name}.created_on",
340 'created_on' => "#{Attachment.table_name}.created_on",
341 'size' => "#{Attachment.table_name}.filesize",
341 'size' => "#{Attachment.table_name}.filesize",
342 'downloads' => "#{Attachment.table_name}.downloads"
342 'downloads' => "#{Attachment.table_name}.downloads"
343
343
344 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
344 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
345 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
345 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
346 render :layout => !request.xhr?
346 render :layout => !request.xhr?
347 end
347 end
348
348
349 def roadmap
349 def roadmap
350 @trackers = @project.trackers.find(:all, :order => 'position')
350 @trackers = @project.trackers.find(:all, :order => 'position')
351 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
351 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
352 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
352 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
353 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
353 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
354
354
355 @versions = @project.shared_versions.sort
355 @versions = @project.shared_versions.sort
356 @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
356 @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
357
357
358 @issues_by_version = {}
358 @issues_by_version = {}
359 unless @selected_tracker_ids.empty?
359 unless @selected_tracker_ids.empty?
360 @versions.each do |version|
360 @versions.each do |version|
361 issues = version.fixed_issues.visible.find(:all,
361 issues = version.fixed_issues.visible.find(:all,
362 :include => [:project, :status, :tracker, :priority],
362 :include => [:project, :status, :tracker, :priority],
363 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
363 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids},
364 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
364 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
365 @issues_by_version[version] = issues
365 @issues_by_version[version] = issues
366 end
366 end
367 end
367 end
368 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
368 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
369 end
369 end
370
370
371 def activity
371 def activity
372 @days = Setting.activity_days_default.to_i
372 @days = Setting.activity_days_default.to_i
373
373
374 if params[:from]
374 if params[:from]
375 begin; @date_to = params[:from].to_date + 1; rescue; end
375 begin; @date_to = params[:from].to_date + 1; rescue; end
376 end
376 end
377
377
378 @date_to ||= Date.today + 1
378 @date_to ||= Date.today + 1
379 @date_from = @date_to - @days
379 @date_from = @date_to - @days
380 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
380 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
381 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
381 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
382
382
383 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
383 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
384 :with_subprojects => @with_subprojects,
384 :with_subprojects => @with_subprojects,
385 :author => @author)
385 :author => @author)
386 @activity.scope_select {|t| !params["show_#{t}"].nil?}
386 @activity.scope_select {|t| !params["show_#{t}"].nil?}
387 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
387 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
388
388
389 events = @activity.events(@date_from, @date_to)
389 events = @activity.events(@date_from, @date_to)
390
390
391 if events.empty? || stale?(:etag => [events.first, User.current])
391 if events.empty? || stale?(:etag => [events.first, User.current])
392 respond_to do |format|
392 respond_to do |format|
393 format.html {
393 format.html {
394 @events_by_day = events.group_by(&:event_date)
394 @events_by_day = events.group_by(&:event_date)
395 render :layout => false if request.xhr?
395 render :layout => false if request.xhr?
396 }
396 }
397 format.atom {
397 format.atom {
398 title = l(:label_activity)
398 title = l(:label_activity)
399 if @author
399 if @author
400 title = @author.name
400 title = @author.name
401 elsif @activity.scope.size == 1
401 elsif @activity.scope.size == 1
402 title = l("label_#{@activity.scope.first.singularize}_plural")
402 title = l("label_#{@activity.scope.first.singularize}_plural")
403 end
403 end
404 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
404 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
405 }
405 }
406 end
406 end
407 end
407 end
408
408
409 rescue ActiveRecord::RecordNotFound
409 rescue ActiveRecord::RecordNotFound
410 render_404
410 render_404
411 end
411 end
412
412
413 private
413 private
414 def find_optional_project
414 def find_optional_project
415 return true unless params[:id]
415 return true unless params[:id]
416 @project = Project.find(params[:id])
416 @project = Project.find(params[:id])
417 authorize
417 authorize
418 rescue ActiveRecord::RecordNotFound
418 rescue ActiveRecord::RecordNotFound
419 render_404
419 render_404
420 end
420 end
421
421
422 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
422 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
423 if ids = params[:tracker_ids]
423 if ids = params[:tracker_ids]
424 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
424 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
425 else
425 else
426 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
426 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
427 end
427 end
428 end
428 end
429
429
430 # Validates parent_id param according to user's permissions
430 # Validates parent_id param according to user's permissions
431 # TODO: move it to Project model in a validation that depends on User.current
431 # TODO: move it to Project model in a validation that depends on User.current
432 def validate_parent_id
432 def validate_parent_id
433 return true if User.current.admin?
433 return true if User.current.admin?
434 parent_id = params[:project] && params[:project][:parent_id]
434 parent_id = params[:project] && params[:project][:parent_id]
435 if parent_id || @project.new_record?
435 if parent_id || @project.new_record?
436 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
436 parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
437 unless @project.allowed_parents.include?(parent)
437 unless @project.allowed_parents.include?(parent)
438 @project.errors.add :parent_id, :invalid
438 @project.errors.add :parent_id, :invalid
439 return false
439 return false
440 end
440 end
441 end
441 end
442 true
442 true
443 end
443 end
444 end
444 end
@@ -1,248 +1,248
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'diff'
18 require 'diff'
19
19
20 class WikiController < ApplicationController
20 class WikiController < ApplicationController
21 default_search_scope :wiki_pages
21 default_search_scope :wiki_pages
22 before_filter :find_wiki, :authorize
22 before_filter :find_wiki, :authorize
23 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
23 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
24
24
25 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
25 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
26
26
27 helper :attachments
27 helper :attachments
28 include AttachmentsHelper
28 include AttachmentsHelper
29 helper :watchers
29 helper :watchers
30
30
31 # display a page (in editing mode if it doesn't exist)
31 # display a page (in editing mode if it doesn't exist)
32 def index
32 def index
33 page_title = params[:page]
33 page_title = params[:page]
34 @page = @wiki.find_or_new_page(page_title)
34 @page = @wiki.find_or_new_page(page_title)
35 if @page.new_record?
35 if @page.new_record?
36 if User.current.allowed_to?(:edit_wiki_pages, @project)
36 if User.current.allowed_to?(:edit_wiki_pages, @project)
37 edit
37 edit
38 render :action => 'edit'
38 render :action => 'edit'
39 else
39 else
40 render_404
40 render_404
41 end
41 end
42 return
42 return
43 end
43 end
44 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
44 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
45 # Redirects user to the current version if he's not allowed to view previous versions
45 # Redirects user to the current version if he's not allowed to view previous versions
46 redirect_to :version => nil
46 redirect_to :version => nil
47 return
47 return
48 end
48 end
49 @content = @page.content_for_version(params[:version])
49 @content = @page.content_for_version(params[:version])
50 if User.current.allowed_to?(:export_wiki_pages, @project)
50 if User.current.allowed_to?(:export_wiki_pages, @project)
51 if params[:format] == 'html'
51 if params[:format] == 'html'
52 export = render_to_string :action => 'export', :layout => false
52 export = render_to_string :action => 'export', :layout => false
53 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
53 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
54 return
54 return
55 elsif params[:format] == 'txt'
55 elsif params[:format] == 'txt'
56 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
56 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
57 return
57 return
58 end
58 end
59 end
59 end
60 @editable = editable?
60 @editable = editable?
61 render :action => 'show'
61 render :action => 'show'
62 end
62 end
63
63
64 # edit an existing page or a new one
64 # edit an existing page or a new one
65 def edit
65 def edit
66 @page = @wiki.find_or_new_page(params[:page])
66 @page = @wiki.find_or_new_page(params[:page])
67 return render_403 unless editable?
67 return render_403 unless editable?
68 @page.content = WikiContent.new(:page => @page) if @page.new_record?
68 @page.content = WikiContent.new(:page => @page) if @page.new_record?
69
69
70 @content = @page.content_for_version(params[:version])
70 @content = @page.content_for_version(params[:version])
71 @content.text = initial_page_content(@page) if @content.text.blank?
71 @content.text = initial_page_content(@page) if @content.text.blank?
72 # don't keep previous comment
72 # don't keep previous comment
73 @content.comments = nil
73 @content.comments = nil
74 if request.get?
74 if request.get?
75 # To prevent StaleObjectError exception when reverting to a previous version
75 # To prevent StaleObjectError exception when reverting to a previous version
76 @content.version = @page.content.version
76 @content.version = @page.content.version
77 else
77 else
78 if !@page.new_record? && @content.text == params[:content][:text]
78 if !@page.new_record? && @content.text == params[:content][:text]
79 attachments = Attachment.attach_files(@page, params[:attachments])
79 attachments = Attachment.attach_files(@page, params[:attachments])
80 flash[:warning] = attachments[:flash] if attachments[:flash]
80 render_attachment_warning_if_needed(@page)
81 # don't save if text wasn't changed
81 # don't save if text wasn't changed
82 redirect_to :action => 'index', :id => @project, :page => @page.title
82 redirect_to :action => 'index', :id => @project, :page => @page.title
83 return
83 return
84 end
84 end
85 #@content.text = params[:content][:text]
85 #@content.text = params[:content][:text]
86 #@content.comments = params[:content][:comments]
86 #@content.comments = params[:content][:comments]
87 @content.attributes = params[:content]
87 @content.attributes = params[:content]
88 @content.author = User.current
88 @content.author = User.current
89 # if page is new @page.save will also save content, but not if page isn't a new record
89 # if page is new @page.save will also save content, but not if page isn't a new record
90 if (@page.new_record? ? @page.save : @content.save)
90 if (@page.new_record? ? @page.save : @content.save)
91 attachments = Attachment.attach_files(@page, params[:attachments])
91 attachments = Attachment.attach_files(@page, params[:attachments])
92 flash[:warning] = attachments[:flash] if attachments[:flash]
92 render_attachment_warning_if_needed(@page)
93 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
93 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
94 redirect_to :action => 'index', :id => @project, :page => @page.title
94 redirect_to :action => 'index', :id => @project, :page => @page.title
95 end
95 end
96 end
96 end
97 rescue ActiveRecord::StaleObjectError
97 rescue ActiveRecord::StaleObjectError
98 # Optimistic locking exception
98 # Optimistic locking exception
99 flash[:error] = l(:notice_locking_conflict)
99 flash[:error] = l(:notice_locking_conflict)
100 end
100 end
101
101
102 # rename a page
102 # rename a page
103 def rename
103 def rename
104 return render_403 unless editable?
104 return render_403 unless editable?
105 @page.redirect_existing_links = true
105 @page.redirect_existing_links = true
106 # used to display the *original* title if some AR validation errors occur
106 # used to display the *original* title if some AR validation errors occur
107 @original_title = @page.pretty_title
107 @original_title = @page.pretty_title
108 if request.post? && @page.update_attributes(params[:wiki_page])
108 if request.post? && @page.update_attributes(params[:wiki_page])
109 flash[:notice] = l(:notice_successful_update)
109 flash[:notice] = l(:notice_successful_update)
110 redirect_to :action => 'index', :id => @project, :page => @page.title
110 redirect_to :action => 'index', :id => @project, :page => @page.title
111 end
111 end
112 end
112 end
113
113
114 def protect
114 def protect
115 @page.update_attribute :protected, params[:protected]
115 @page.update_attribute :protected, params[:protected]
116 redirect_to :action => 'index', :id => @project, :page => @page.title
116 redirect_to :action => 'index', :id => @project, :page => @page.title
117 end
117 end
118
118
119 # show page history
119 # show page history
120 def history
120 def history
121 @version_count = @page.content.versions.count
121 @version_count = @page.content.versions.count
122 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
122 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
123 # don't load text
123 # don't load text
124 @versions = @page.content.versions.find :all,
124 @versions = @page.content.versions.find :all,
125 :select => "id, author_id, comments, updated_on, version",
125 :select => "id, author_id, comments, updated_on, version",
126 :order => 'version DESC',
126 :order => 'version DESC',
127 :limit => @version_pages.items_per_page + 1,
127 :limit => @version_pages.items_per_page + 1,
128 :offset => @version_pages.current.offset
128 :offset => @version_pages.current.offset
129
129
130 render :layout => false if request.xhr?
130 render :layout => false if request.xhr?
131 end
131 end
132
132
133 def diff
133 def diff
134 @diff = @page.diff(params[:version], params[:version_from])
134 @diff = @page.diff(params[:version], params[:version_from])
135 render_404 unless @diff
135 render_404 unless @diff
136 end
136 end
137
137
138 def annotate
138 def annotate
139 @annotate = @page.annotate(params[:version])
139 @annotate = @page.annotate(params[:version])
140 render_404 unless @annotate
140 render_404 unless @annotate
141 end
141 end
142
142
143 # Removes a wiki page and its history
143 # Removes a wiki page and its history
144 # Children can be either set as root pages, removed or reassigned to another parent page
144 # Children can be either set as root pages, removed or reassigned to another parent page
145 def destroy
145 def destroy
146 return render_403 unless editable?
146 return render_403 unless editable?
147
147
148 @descendants_count = @page.descendants.size
148 @descendants_count = @page.descendants.size
149 if @descendants_count > 0
149 if @descendants_count > 0
150 case params[:todo]
150 case params[:todo]
151 when 'nullify'
151 when 'nullify'
152 # Nothing to do
152 # Nothing to do
153 when 'destroy'
153 when 'destroy'
154 # Removes all its descendants
154 # Removes all its descendants
155 @page.descendants.each(&:destroy)
155 @page.descendants.each(&:destroy)
156 when 'reassign'
156 when 'reassign'
157 # Reassign children to another parent page
157 # Reassign children to another parent page
158 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
158 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
159 return unless reassign_to
159 return unless reassign_to
160 @page.children.each do |child|
160 @page.children.each do |child|
161 child.update_attribute(:parent, reassign_to)
161 child.update_attribute(:parent, reassign_to)
162 end
162 end
163 else
163 else
164 @reassignable_to = @wiki.pages - @page.self_and_descendants
164 @reassignable_to = @wiki.pages - @page.self_and_descendants
165 return
165 return
166 end
166 end
167 end
167 end
168 @page.destroy
168 @page.destroy
169 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
169 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
170 end
170 end
171
171
172 # display special pages
172 # display special pages
173 def special
173 def special
174 page_title = params[:page].downcase
174 page_title = params[:page].downcase
175 case page_title
175 case page_title
176 # show pages index, sorted by title
176 # show pages index, sorted by title
177 when 'page_index', 'date_index'
177 when 'page_index', 'date_index'
178 # eager load information about last updates, without loading text
178 # eager load information about last updates, without loading text
179 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
179 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
180 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
180 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
181 :order => 'title'
181 :order => 'title'
182 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
182 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
183 @pages_by_parent_id = @pages.group_by(&:parent_id)
183 @pages_by_parent_id = @pages.group_by(&:parent_id)
184 # export wiki to a single html file
184 # export wiki to a single html file
185 when 'export'
185 when 'export'
186 if User.current.allowed_to?(:export_wiki_pages, @project)
186 if User.current.allowed_to?(:export_wiki_pages, @project)
187 @pages = @wiki.pages.find :all, :order => 'title'
187 @pages = @wiki.pages.find :all, :order => 'title'
188 export = render_to_string :action => 'export_multiple', :layout => false
188 export = render_to_string :action => 'export_multiple', :layout => false
189 send_data(export, :type => 'text/html', :filename => "wiki.html")
189 send_data(export, :type => 'text/html', :filename => "wiki.html")
190 else
190 else
191 redirect_to :action => 'index', :id => @project, :page => nil
191 redirect_to :action => 'index', :id => @project, :page => nil
192 end
192 end
193 return
193 return
194 else
194 else
195 # requested special page doesn't exist, redirect to default page
195 # requested special page doesn't exist, redirect to default page
196 redirect_to :action => 'index', :id => @project, :page => nil
196 redirect_to :action => 'index', :id => @project, :page => nil
197 return
197 return
198 end
198 end
199 render :action => "special_#{page_title}"
199 render :action => "special_#{page_title}"
200 end
200 end
201
201
202 def preview
202 def preview
203 page = @wiki.find_page(params[:page])
203 page = @wiki.find_page(params[:page])
204 # page is nil when previewing a new page
204 # page is nil when previewing a new page
205 return render_403 unless page.nil? || editable?(page)
205 return render_403 unless page.nil? || editable?(page)
206 if page
206 if page
207 @attachements = page.attachments
207 @attachements = page.attachments
208 @previewed = page.content
208 @previewed = page.content
209 end
209 end
210 @text = params[:content][:text]
210 @text = params[:content][:text]
211 render :partial => 'common/preview'
211 render :partial => 'common/preview'
212 end
212 end
213
213
214 def add_attachment
214 def add_attachment
215 return render_403 unless editable?
215 return render_403 unless editable?
216 attachments = Attachment.attach_files(@page, params[:attachments])
216 attachments = Attachment.attach_files(@page, params[:attachments])
217 flash[:warning] = attachments[:flash] if attachments[:flash]
217 render_attachment_warning_if_needed(@page)
218 redirect_to :action => 'index', :page => @page.title
218 redirect_to :action => 'index', :page => @page.title
219 end
219 end
220
220
221 private
221 private
222
222
223 def find_wiki
223 def find_wiki
224 @project = Project.find(params[:id])
224 @project = Project.find(params[:id])
225 @wiki = @project.wiki
225 @wiki = @project.wiki
226 render_404 unless @wiki
226 render_404 unless @wiki
227 rescue ActiveRecord::RecordNotFound
227 rescue ActiveRecord::RecordNotFound
228 render_404
228 render_404
229 end
229 end
230
230
231 # Finds the requested page and returns a 404 error if it doesn't exist
231 # Finds the requested page and returns a 404 error if it doesn't exist
232 def find_existing_page
232 def find_existing_page
233 @page = @wiki.find_page(params[:page])
233 @page = @wiki.find_page(params[:page])
234 render_404 if @page.nil?
234 render_404 if @page.nil?
235 end
235 end
236
236
237 # Returns true if the current user is allowed to edit the page, otherwise false
237 # Returns true if the current user is allowed to edit the page, otherwise false
238 def editable?(page = @page)
238 def editable?(page = @page)
239 page.editable_by?(User.current)
239 page.editable_by?(User.current)
240 end
240 end
241
241
242 # Returns the default content of a new wiki page
242 # Returns the default content of a new wiki page
243 def initial_page_content(page)
243 def initial_page_content(page)
244 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
244 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
245 extend helper unless self.instance_of?(helper)
245 extend helper unless self.instance_of?(helper)
246 helper.instance_method(:initial_page_content).bind(self).call(page)
246 helper.instance_method(:initial_page_content).bind(self).call(page)
247 end
247 end
248 end
248 end
@@ -1,191 +1,186
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/md5"
18 require "digest/md5"
19
19
20 class Attachment < ActiveRecord::Base
20 class Attachment < ActiveRecord::Base
21 belongs_to :container, :polymorphic => true
21 belongs_to :container, :polymorphic => true
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23
23
24 validates_presence_of :container, :filename, :author
24 validates_presence_of :container, :filename, :author
25 validates_length_of :filename, :maximum => 255
25 validates_length_of :filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
26 validates_length_of :disk_filename, :maximum => 255
27
27
28 acts_as_event :title => :filename,
28 acts_as_event :title => :filename,
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30
30
31 acts_as_activity_provider :type => 'files',
31 acts_as_activity_provider :type => 'files',
32 :permission => :view_files,
32 :permission => :view_files,
33 :author_key => :author_id,
33 :author_key => :author_id,
34 :find_options => {:select => "#{Attachment.table_name}.*",
34 :find_options => {:select => "#{Attachment.table_name}.*",
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
36 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37
37
38 acts_as_activity_provider :type => 'documents',
38 acts_as_activity_provider :type => 'documents',
39 :permission => :view_documents,
39 :permission => :view_documents,
40 :author_key => :author_id,
40 :author_key => :author_id,
41 :find_options => {:select => "#{Attachment.table_name}.*",
41 :find_options => {:select => "#{Attachment.table_name}.*",
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44
44
45 cattr_accessor :storage_path
45 cattr_accessor :storage_path
46 @@storage_path = "#{RAILS_ROOT}/files"
46 @@storage_path = "#{RAILS_ROOT}/files"
47
47
48 def validate
48 def validate
49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
49 if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
50 errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)
51 end
51 end
52 end
52 end
53
53
54 def file=(incoming_file)
54 def file=(incoming_file)
55 unless incoming_file.nil?
55 unless incoming_file.nil?
56 @temp_file = incoming_file
56 @temp_file = incoming_file
57 if @temp_file.size > 0
57 if @temp_file.size > 0
58 self.filename = sanitize_filename(@temp_file.original_filename)
58 self.filename = sanitize_filename(@temp_file.original_filename)
59 self.disk_filename = Attachment.disk_filename(filename)
59 self.disk_filename = Attachment.disk_filename(filename)
60 self.content_type = @temp_file.content_type.to_s.chomp
60 self.content_type = @temp_file.content_type.to_s.chomp
61 if content_type.blank?
61 if content_type.blank?
62 self.content_type = Redmine::MimeType.of(filename)
62 self.content_type = Redmine::MimeType.of(filename)
63 end
63 end
64 self.filesize = @temp_file.size
64 self.filesize = @temp_file.size
65 end
65 end
66 end
66 end
67 end
67 end
68
68
69 def file
69 def file
70 nil
70 nil
71 end
71 end
72
72
73 # Copies the temporary file to its final location
73 # Copies the temporary file to its final location
74 # and computes its MD5 hash
74 # and computes its MD5 hash
75 def before_save
75 def before_save
76 if @temp_file && (@temp_file.size > 0)
76 if @temp_file && (@temp_file.size > 0)
77 logger.debug("saving '#{self.diskfile}'")
77 logger.debug("saving '#{self.diskfile}'")
78 md5 = Digest::MD5.new
78 md5 = Digest::MD5.new
79 File.open(diskfile, "wb") do |f|
79 File.open(diskfile, "wb") do |f|
80 buffer = ""
80 buffer = ""
81 while (buffer = @temp_file.read(8192))
81 while (buffer = @temp_file.read(8192))
82 f.write(buffer)
82 f.write(buffer)
83 md5.update(buffer)
83 md5.update(buffer)
84 end
84 end
85 end
85 end
86 self.digest = md5.hexdigest
86 self.digest = md5.hexdigest
87 end
87 end
88 # Don't save the content type if it's longer than the authorized length
88 # Don't save the content type if it's longer than the authorized length
89 if self.content_type && self.content_type.length > 255
89 if self.content_type && self.content_type.length > 255
90 self.content_type = nil
90 self.content_type = nil
91 end
91 end
92 end
92 end
93
93
94 # Deletes file on the disk
94 # Deletes file on the disk
95 def after_destroy
95 def after_destroy
96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
96 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
97 end
97 end
98
98
99 # Returns file's location on disk
99 # Returns file's location on disk
100 def diskfile
100 def diskfile
101 "#{@@storage_path}/#{self.disk_filename}"
101 "#{@@storage_path}/#{self.disk_filename}"
102 end
102 end
103
103
104 def increment_download
104 def increment_download
105 increment!(:downloads)
105 increment!(:downloads)
106 end
106 end
107
107
108 def project
108 def project
109 container.project
109 container.project
110 end
110 end
111
111
112 def visible?(user=User.current)
112 def visible?(user=User.current)
113 container.attachments_visible?(user)
113 container.attachments_visible?(user)
114 end
114 end
115
115
116 def deletable?(user=User.current)
116 def deletable?(user=User.current)
117 container.attachments_deletable?(user)
117 container.attachments_deletable?(user)
118 end
118 end
119
119
120 def image?
120 def image?
121 self.filename =~ /\.(jpe?g|gif|png)$/i
121 self.filename =~ /\.(jpe?g|gif|png)$/i
122 end
122 end
123
123
124 def is_text?
124 def is_text?
125 Redmine::MimeType.is_type?('text', filename)
125 Redmine::MimeType.is_type?('text', filename)
126 end
126 end
127
127
128 def is_diff?
128 def is_diff?
129 self.filename =~ /\.(patch|diff)$/i
129 self.filename =~ /\.(patch|diff)$/i
130 end
130 end
131
131
132 # Returns true if the file is readable
132 # Returns true if the file is readable
133 def readable?
133 def readable?
134 File.readable?(diskfile)
134 File.readable?(diskfile)
135 end
135 end
136
136
137 # Bulk attaches a set of files to an object
137 # Bulk attaches a set of files to an object
138 #
138 #
139 # Returns a Hash of the results:
139 # Returns a Hash of the results:
140 # :files => array of the attached files
140 # :files => array of the attached files
141 # :unsaved => array of the files that could not be attached
141 # :unsaved => array of the files that could not be attached
142 # :flash => warning message
143 def self.attach_files(obj, attachments)
142 def self.attach_files(obj, attachments)
144 attached = []
143 attached = []
145 unsaved = []
144 unsaved = []
146 flash = nil
147 if attachments && attachments.is_a?(Hash)
145 if attachments && attachments.is_a?(Hash)
148 attachments.each_value do |attachment|
146 attachments.each_value do |attachment|
149 file = attachment['file']
147 file = attachment['file']
150 next unless file && file.size > 0
148 next unless file && file.size > 0
151 a = Attachment.create(:container => obj,
149 a = Attachment.create(:container => obj,
152 :file => file,
150 :file => file,
153 :description => attachment['description'].to_s.strip,
151 :description => attachment['description'].to_s.strip,
154 :author => User.current)
152 :author => User.current)
155 a.new_record? ? (unsaved << a) : (attached << a)
153 a.new_record? ? (obj.unsaved_attachments << a) : (attached << a)
156 end
157 if unsaved.any?
158 flash = l(:warning_attachments_not_saved, unsaved.size)
159 end
154 end
160 end
155 end
161 {:files => attached, :flash => flash, :unsaved => unsaved}
156 {:files => attached, :unsaved => obj.unsaved_attachments}
162 end
157 end
163
158
164 private
159 private
165 def sanitize_filename(value)
160 def sanitize_filename(value)
166 # get only the filename, not the whole path
161 # get only the filename, not the whole path
167 just_filename = value.gsub(/^.*(\\|\/)/, '')
162 just_filename = value.gsub(/^.*(\\|\/)/, '')
168 # NOTE: File.basename doesn't work right with Windows paths on Unix
163 # NOTE: File.basename doesn't work right with Windows paths on Unix
169 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
164 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
170
165
171 # Finally, replace all non alphanumeric, hyphens or periods with underscore
166 # Finally, replace all non alphanumeric, hyphens or periods with underscore
172 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
167 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
173 end
168 end
174
169
175 # Returns an ASCII or hashed filename
170 # Returns an ASCII or hashed filename
176 def self.disk_filename(filename)
171 def self.disk_filename(filename)
177 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
172 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
178 ascii = ''
173 ascii = ''
179 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
174 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
180 ascii = filename
175 ascii = filename
181 else
176 else
182 ascii = Digest::MD5.hexdigest(filename)
177 ascii = Digest::MD5.hexdigest(filename)
183 # keep the extension if any
178 # keep the extension if any
184 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
179 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
185 end
180 end
186 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
181 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
187 timestamp.succ!
182 timestamp.succ!
188 end
183 end
189 "#{timestamp}_#{ascii}"
184 "#{timestamp}_#{ascii}"
190 end
185 end
191 end
186 end
@@ -1,57 +1,63
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Attachable
20 module Attachable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_attachable(options = {})
26 def acts_as_attachable(options = {})
27 cattr_accessor :attachable_options
27 cattr_accessor :attachable_options
28 self.attachable_options = {}
28 self.attachable_options = {}
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31
31
32 has_many :attachments, options.merge(:as => :container,
32 has_many :attachments, options.merge(:as => :container,
33 :order => "#{Attachment.table_name}.created_on",
33 :order => "#{Attachment.table_name}.created_on",
34 :dependent => :destroy)
34 :dependent => :destroy)
35 attr_accessor :unsaved_attachments
36 after_initialize :initialize_unsaved_attachments
35 send :include, Redmine::Acts::Attachable::InstanceMethods
37 send :include, Redmine::Acts::Attachable::InstanceMethods
36 end
38 end
37 end
39 end
38
40
39 module InstanceMethods
41 module InstanceMethods
40 def self.included(base)
42 def self.included(base)
41 base.extend ClassMethods
43 base.extend ClassMethods
42 end
44 end
43
45
44 def attachments_visible?(user=User.current)
46 def attachments_visible?(user=User.current)
45 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
47 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
46 end
48 end
47
49
48 def attachments_deletable?(user=User.current)
50 def attachments_deletable?(user=User.current)
49 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
51 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
50 end
52 end
51
53
54 def initialize_unsaved_attachments
55 @unsaved_attachments ||= []
56 end
57
52 module ClassMethods
58 module ClassMethods
53 end
59 end
54 end
60 end
55 end
61 end
56 end
62 end
57 end
63 end
General Comments 0
You need to be logged in to leave comments. Login now