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