##// END OF EJS Templates
AttachmentsController now handles attachments deletion....
Jean-Philippe Lang -
r2114:5d2899ee1b3e
parent child
Show More
@@ -0,0 +1,2
1 require File.dirname(__FILE__) + '/lib/acts_as_attachable'
2 ActiveRecord::Base.send(:include, Redmine::Acts::Attachable)
@@ -0,0 +1,57
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Acts
20 module Attachable
21 def self.included(base)
22 base.extend ClassMethods
23 end
24
25 module ClassMethods
26 def acts_as_attachable(options = {})
27 cattr_accessor :attachable_options
28 self.attachable_options = {}
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
31
32 has_many :attachments, options.merge(:as => :container,
33 :order => "#{Attachment.table_name}.created_on",
34 :dependent => :destroy)
35 send :include, Redmine::Acts::Attachable::InstanceMethods
36 end
37 end
38
39 module InstanceMethods
40 def self.included(base)
41 base.extend ClassMethods
42 end
43
44 def attachments_visible?(user=User.current)
45 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
46 end
47
48 def attachments_deletable?(user=User.current)
49 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
50 end
51
52 module ClassMethods
53 end
54 end
55 end
56 end
57 end
@@ -1,55 +1,72
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
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 AttachmentsController < ApplicationController
19 19 before_filter :find_project
20
20 before_filter :read_authorize, :except => :destroy
21 before_filter :delete_authorize, :only => :destroy
22
23 verify :method => :post, :only => :destroy
24
21 25 def show
22 26 if @attachment.is_diff?
23 27 @diff = File.new(@attachment.diskfile, "rb").read
24 28 render :action => 'diff'
25 29 elsif @attachment.is_text?
26 30 @content = File.new(@attachment.diskfile, "rb").read
27 31 render :action => 'file'
28 32 elsif
29 33 download
30 34 end
31 35 end
32 36
33 37 def download
34 38 @attachment.increment_download if @attachment.container.is_a?(Version)
35 39
36 40 # images are sent inline
37 41 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
38 42 :type => @attachment.content_type,
39 43 :disposition => (@attachment.image? ? 'inline' : 'attachment')
44
40 45 end
41
46
47 def destroy
48 # Make sure association callbacks are called
49 @attachment.container.attachments.delete(@attachment)
50 redirect_to :back
51 rescue ::ActionController::RedirectBackError
52 redirect_to :controller => 'projects', :action => 'show', :id => @project
53 end
54
42 55 private
43 56 def find_project
44 57 @attachment = Attachment.find(params[:id])
45 58 # Show 404 if the filename in the url is wrong
46 59 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
47
48 60 @project = @attachment.project
49 permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
50 allowed = User.current.allowed_to?(permission, @project)
51 allowed ? true : (User.current.logged? ? render_403 : require_login)
52 61 rescue ActiveRecord::RecordNotFound
53 62 render_404
54 63 end
64
65 def read_authorize
66 @attachment.visible? ? true : deny_access
67 end
68
69 def delete_authorize
70 @attachment.deletable? ? true : deny_access
71 end
55 72 end
@@ -1,92 +1,87
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 before_filter :find_project, :only => [:index, :new]
20 20 before_filter :find_document, :except => [:index, :new]
21 21 before_filter :authorize
22 22
23 23 helper :attachments
24 24
25 25 def index
26 26 @sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
27 27 documents = @project.documents.find :all, :include => [:attachments, :category]
28 28 case @sort_by
29 29 when 'date'
30 30 @grouped = documents.group_by {|d| d.created_on.to_date }
31 31 when 'title'
32 32 @grouped = documents.group_by {|d| d.title.first.upcase}
33 33 when 'author'
34 34 @grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
35 35 else
36 36 @grouped = documents.group_by(&:category)
37 37 end
38 38 render :layout => false if request.xhr?
39 39 end
40 40
41 41 def show
42 42 @attachments = @document.attachments.find(:all, :order => "created_on DESC")
43 43 end
44 44
45 45 def new
46 46 @document = @project.documents.build(params[:document])
47 47 if request.post? and @document.save
48 48 attach_files(@document, params[:attachments])
49 49 flash[:notice] = l(:notice_successful_create)
50 50 Mailer.deliver_document_added(@document) if Setting.notified_events.include?('document_added')
51 51 redirect_to :action => 'index', :project_id => @project
52 52 end
53 53 end
54 54
55 55 def edit
56 56 @categories = Enumeration::get_values('DCAT')
57 57 if request.post? and @document.update_attributes(params[:document])
58 58 flash[:notice] = l(:notice_successful_update)
59 59 redirect_to :action => 'show', :id => @document
60 60 end
61 61 end
62 62
63 63 def destroy
64 64 @document.destroy
65 65 redirect_to :controller => 'documents', :action => 'index', :project_id => @project
66 66 end
67 67
68 68 def add_attachment
69 69 attachments = attach_files(@document, params[:attachments])
70 70 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('document_added')
71 71 redirect_to :action => 'show', :id => @document
72 72 end
73
74 def destroy_attachment
75 @document.attachments.find(params[:attachment_id]).destroy
76 redirect_to :action => 'show', :id => @document
77 end
78 73
79 74 private
80 75 def find_project
81 76 @project = Project.find(params[:project_id])
82 77 rescue ActiveRecord::RecordNotFound
83 78 render_404
84 79 end
85 80
86 81 def find_document
87 82 @document = Document.find(params[:id])
88 83 @project = @document.project
89 84 rescue ActiveRecord::RecordNotFound
90 85 render_404
91 86 end
92 87 end
@@ -1,493 +1,482
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
21 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
21 before_filter :find_issue, :only => [:show, :edit, :reply]
22 22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 26 accept_key_auth :index, :changes
27 27
28 28 helper :journals
29 29 helper :projects
30 30 include ProjectsHelper
31 31 helper :custom_fields
32 32 include CustomFieldsHelper
33 33 helper :ifpdf
34 34 include IfpdfHelper
35 35 helper :issue_relations
36 36 include IssueRelationsHelper
37 37 helper :watchers
38 38 include WatchersHelper
39 39 helper :attachments
40 40 include AttachmentsHelper
41 41 helper :queries
42 42 helper :sort
43 43 include SortHelper
44 44 include IssuesHelper
45 45 helper :timelog
46 46
47 47 def index
48 48 sort_init "#{Issue.table_name}.id", "desc"
49 49 sort_update
50 50 retrieve_query
51 51 if @query.valid?
52 52 limit = per_page_option
53 53 respond_to do |format|
54 54 format.html { }
55 55 format.atom { }
56 56 format.csv { limit = Setting.issues_export_limit.to_i }
57 57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 58 end
59 59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 61 @issues = Issue.find :all, :order => sort_clause,
62 62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 63 :conditions => @query.statement,
64 64 :limit => limit,
65 65 :offset => @issue_pages.current.offset
66 66 respond_to do |format|
67 67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 71 end
72 72 else
73 73 # Send html if the query is not valid
74 74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 75 end
76 76 rescue ActiveRecord::RecordNotFound
77 77 render_404
78 78 end
79 79
80 80 def changes
81 81 sort_init "#{Issue.table_name}.id", "desc"
82 82 sort_update
83 83 retrieve_query
84 84 if @query.valid?
85 85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 86 :conditions => @query.statement,
87 87 :limit => 25,
88 88 :order => "#{Journal.table_name}.created_on DESC"
89 89 end
90 90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 91 render :layout => false, :content_type => 'application/atom+xml'
92 92 rescue ActiveRecord::RecordNotFound
93 93 render_404
94 94 end
95 95
96 96 def show
97 97 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 98 @journals.each_with_index {|j,i| j.indice = i+1}
99 99 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 100 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 101 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 102 @priorities = Enumeration::get_values('IPRI')
103 103 @time_entry = TimeEntry.new
104 104 respond_to do |format|
105 105 format.html { render :template => 'issues/show.rhtml' }
106 106 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 107 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 108 end
109 109 end
110 110
111 111 # Add a new issue
112 112 # The new issue will be created from an existing one if copy_from parameter is given
113 113 def new
114 114 @issue = Issue.new
115 115 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 116 @issue.project = @project
117 117 # Tracker must be set before custom field values
118 118 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 119 if @issue.tracker.nil?
120 120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 121 render :nothing => true, :layout => true
122 122 return
123 123 end
124 124 @issue.attributes = params[:issue]
125 125 @issue.author = User.current
126 126
127 127 default_status = IssueStatus.default
128 128 unless default_status
129 129 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
130 130 render :nothing => true, :layout => true
131 131 return
132 132 end
133 133 @issue.status = default_status
134 134 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
135 135
136 136 if request.get? || request.xhr?
137 137 @issue.start_date ||= Date.today
138 138 else
139 139 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
140 140 # Check that the user is allowed to apply the requested status
141 141 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
142 142 if @issue.save
143 143 attach_files(@issue, params[:attachments])
144 144 flash[:notice] = l(:notice_successful_create)
145 145 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
146 146 redirect_to :controller => 'issues', :action => 'show', :id => @issue
147 147 return
148 148 end
149 149 end
150 150 @priorities = Enumeration::get_values('IPRI')
151 151 render :layout => !request.xhr?
152 152 end
153 153
154 154 # Attributes that can be updated on workflow transition (without :edit permission)
155 155 # TODO: make it configurable (at least per role)
156 156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157 157
158 158 def edit
159 159 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
160 160 @priorities = Enumeration::get_values('IPRI')
161 161 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
162 162 @time_entry = TimeEntry.new
163 163
164 164 @notes = params[:notes]
165 165 journal = @issue.init_journal(User.current, @notes)
166 166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
167 167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
168 168 attrs = params[:issue].dup
169 169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
170 170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
171 171 @issue.attributes = attrs
172 172 end
173 173
174 174 if request.post?
175 175 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
176 176 @time_entry.attributes = params[:time_entry]
177 177 attachments = attach_files(@issue, params[:attachments])
178 178 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
179 179
180 180 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
181 181
182 182 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
183 183 # Log spend time
184 184 if current_role.allowed_to?(:log_time)
185 185 @time_entry.save
186 186 end
187 187 if !journal.new_record?
188 188 # Only send notification if something was actually changed
189 189 flash[:notice] = l(:notice_successful_update)
190 190 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
191 191 end
192 192 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
193 193 end
194 194 end
195 195 rescue ActiveRecord::StaleObjectError
196 196 # Optimistic locking exception
197 197 flash.now[:error] = l(:notice_locking_conflict)
198 198 end
199 199
200 200 def reply
201 201 journal = Journal.find(params[:journal_id]) if params[:journal_id]
202 202 if journal
203 203 user = journal.user
204 204 text = journal.notes
205 205 else
206 206 user = @issue.author
207 207 text = @issue.description
208 208 end
209 209 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
210 210 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
211 211 render(:update) { |page|
212 212 page.<< "$('notes').value = \"#{content}\";"
213 213 page.show 'update'
214 214 page << "Form.Element.focus('notes');"
215 215 page << "Element.scrollTo('update');"
216 216 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
217 217 }
218 218 end
219 219
220 220 # Bulk edit a set of issues
221 221 def bulk_edit
222 222 if request.post?
223 223 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
224 224 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
225 225 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
226 226 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
227 227 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
228 228
229 229 unsaved_issue_ids = []
230 230 @issues.each do |issue|
231 231 journal = issue.init_journal(User.current, params[:notes])
232 232 issue.priority = priority if priority
233 233 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
234 234 issue.category = category if category || params[:category_id] == 'none'
235 235 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
236 236 issue.start_date = params[:start_date] unless params[:start_date].blank?
237 237 issue.due_date = params[:due_date] unless params[:due_date].blank?
238 238 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
239 239 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
240 240 # Don't save any change to the issue if the user is not authorized to apply the requested status
241 241 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
242 242 # Send notification for each issue (if changed)
243 243 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
244 244 else
245 245 # Keep unsaved issue ids to display them in flash error
246 246 unsaved_issue_ids << issue.id
247 247 end
248 248 end
249 249 if unsaved_issue_ids.empty?
250 250 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
251 251 else
252 252 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
253 253 end
254 254 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
255 255 return
256 256 end
257 257 # Find potential statuses the user could be allowed to switch issues to
258 258 @available_statuses = Workflow.find(:all, :include => :new_status,
259 259 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
260 260 end
261 261
262 262 def move
263 263 @allowed_projects = []
264 264 # find projects to which the user is allowed to move the issue
265 265 if User.current.admin?
266 266 # admin is allowed to move issues to any active (visible) project
267 267 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
268 268 else
269 269 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
270 270 end
271 271 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
272 272 @target_project ||= @project
273 273 @trackers = @target_project.trackers
274 274 if request.post?
275 275 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
276 276 unsaved_issue_ids = []
277 277 @issues.each do |issue|
278 278 issue.init_journal(User.current)
279 279 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
280 280 end
281 281 if unsaved_issue_ids.empty?
282 282 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
283 283 else
284 284 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
285 285 end
286 286 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
287 287 return
288 288 end
289 289 render :layout => false if request.xhr?
290 290 end
291 291
292 292 def destroy
293 293 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
294 294 if @hours > 0
295 295 case params[:todo]
296 296 when 'destroy'
297 297 # nothing to do
298 298 when 'nullify'
299 299 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
300 300 when 'reassign'
301 301 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
302 302 if reassign_to.nil?
303 303 flash.now[:error] = l(:error_issue_not_found_in_project)
304 304 return
305 305 else
306 306 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
307 307 end
308 308 else
309 309 # display the destroy form
310 310 return
311 311 end
312 312 end
313 313 @issues.each(&:destroy)
314 314 redirect_to :action => 'index', :project_id => @project
315 315 end
316
317 def destroy_attachment
318 a = @issue.attachments.find(params[:attachment_id])
319 a.destroy
320 journal = @issue.init_journal(User.current)
321 journal.details << JournalDetail.new(:property => 'attachment',
322 :prop_key => a.id,
323 :old_value => a.filename)
324 journal.save
325 redirect_to :action => 'show', :id => @issue
326 end
327 316
328 317 def gantt
329 318 @gantt = Redmine::Helpers::Gantt.new(params)
330 319 retrieve_query
331 320 if @query.valid?
332 321 events = []
333 322 # Issues that have start and due dates
334 323 events += Issue.find(:all,
335 324 :order => "start_date, due_date",
336 325 :include => [:tracker, :status, :assigned_to, :priority, :project],
337 326 :conditions => ["(#{@query.statement}) AND (((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]
338 327 )
339 328 # Issues that don't have a due date but that are assigned to a version with a date
340 329 events += Issue.find(:all,
341 330 :order => "start_date, effective_date",
342 331 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
343 332 :conditions => ["(#{@query.statement}) AND (((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]
344 333 )
345 334 # Versions
346 335 events += Version.find(:all, :include => :project,
347 336 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
348 337
349 338 @gantt.events = events
350 339 end
351 340
352 341 respond_to do |format|
353 342 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
354 343 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
355 344 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
356 345 end
357 346 end
358 347
359 348 def calendar
360 349 if params[:year] and params[:year].to_i > 1900
361 350 @year = params[:year].to_i
362 351 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
363 352 @month = params[:month].to_i
364 353 end
365 354 end
366 355 @year ||= Date.today.year
367 356 @month ||= Date.today.month
368 357
369 358 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
370 359 retrieve_query
371 360 if @query.valid?
372 361 events = []
373 362 events += Issue.find(:all,
374 363 :include => [:tracker, :status, :assigned_to, :priority, :project],
375 364 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
376 365 )
377 366 events += Version.find(:all, :include => :project,
378 367 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
379 368
380 369 @calendar.events = events
381 370 end
382 371
383 372 render :layout => false if request.xhr?
384 373 end
385 374
386 375 def context_menu
387 376 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
388 377 if (@issues.size == 1)
389 378 @issue = @issues.first
390 379 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
391 380 end
392 381 projects = @issues.collect(&:project).compact.uniq
393 382 @project = projects.first if projects.size == 1
394 383
395 384 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
396 385 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
397 386 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
398 387 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
399 388 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
400 389 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
401 390 }
402 391 if @project
403 392 @assignables = @project.assignable_users
404 393 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
405 394 end
406 395
407 396 @priorities = Enumeration.get_values('IPRI').reverse
408 397 @statuses = IssueStatus.find(:all, :order => 'position')
409 398 @back = request.env['HTTP_REFERER']
410 399
411 400 render :layout => false
412 401 end
413 402
414 403 def update_form
415 404 @issue = Issue.new(params[:issue])
416 405 render :action => :new, :layout => false
417 406 end
418 407
419 408 def preview
420 409 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
421 410 @attachements = @issue.attachments if @issue
422 411 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
423 412 render :partial => 'common/preview'
424 413 end
425 414
426 415 private
427 416 def find_issue
428 417 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
429 418 @project = @issue.project
430 419 rescue ActiveRecord::RecordNotFound
431 420 render_404
432 421 end
433 422
434 423 # Filter for bulk operations
435 424 def find_issues
436 425 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
437 426 raise ActiveRecord::RecordNotFound if @issues.empty?
438 427 projects = @issues.collect(&:project).compact.uniq
439 428 if projects.size == 1
440 429 @project = projects.first
441 430 else
442 431 # TODO: let users bulk edit/move/destroy issues from different projects
443 432 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
444 433 end
445 434 rescue ActiveRecord::RecordNotFound
446 435 render_404
447 436 end
448 437
449 438 def find_project
450 439 @project = Project.find(params[:project_id])
451 440 rescue ActiveRecord::RecordNotFound
452 441 render_404
453 442 end
454 443
455 444 def find_optional_project
456 445 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
457 446 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
458 447 allowed ? true : deny_access
459 448 rescue ActiveRecord::RecordNotFound
460 449 render_404
461 450 end
462 451
463 452 # Retrieve query from session or build a new query
464 453 def retrieve_query
465 454 if !params[:query_id].blank?
466 455 cond = "project_id IS NULL"
467 456 cond << " OR project_id = #{@project.id}" if @project
468 457 @query = Query.find(params[:query_id], :conditions => cond)
469 458 @query.project = @project
470 459 session[:query] = {:id => @query.id, :project_id => @query.project_id}
471 460 else
472 461 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
473 462 # Give it a name, required to be valid
474 463 @query = Query.new(:name => "_")
475 464 @query.project = @project
476 465 if params[:fields] and params[:fields].is_a? Array
477 466 params[:fields].each do |field|
478 467 @query.add_filter(field, params[:operators][field], params[:values][field])
479 468 end
480 469 else
481 470 @query.available_filters.keys.each do |field|
482 471 @query.add_short_filter(field, params[field]) if params[field]
483 472 end
484 473 end
485 474 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
486 475 else
487 476 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
488 477 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
489 478 @query.project = @project
490 479 end
491 480 end
492 481 end
493 482 end
@@ -1,60 +1,54
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class VersionsController < ApplicationController
19 19 menu_item :roadmap
20 20 before_filter :find_project, :authorize
21 21
22 22 def show
23 23 end
24 24
25 25 def edit
26 26 if request.post? and @version.update_attributes(params[:version])
27 27 flash[:notice] = l(:notice_successful_update)
28 28 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
29 29 end
30 30 end
31 31
32 32 def destroy
33 33 @version.destroy
34 34 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
35 35 rescue
36 36 flash[:error] = l(:notice_unable_delete_version)
37 37 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
38 38 end
39 39
40 def destroy_file
41 @version.attachments.find(params[:attachment_id]).destroy
42 flash[:notice] = l(:notice_successful_delete)
43 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
44 end
45
46 40 def status_by
47 41 respond_to do |format|
48 42 format.html { render :action => 'show' }
49 43 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
50 44 end
51 45 end
52 46
53 47 private
54 48 def find_project
55 49 @version = Version.find(params[:id])
56 50 @project = @version.project
57 51 rescue ActiveRecord::RecordNotFound
58 52 render_404
59 53 end
60 54 end
@@ -1,218 +1,211
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 before_filter :find_wiki, :authorize
22 22
23 verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
23 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
24 24
25 25 helper :attachments
26 26 include AttachmentsHelper
27 27
28 28 # display a page (in editing mode if it doesn't exist)
29 29 def index
30 30 page_title = params[:page]
31 31 @page = @wiki.find_or_new_page(page_title)
32 32 if @page.new_record?
33 33 if User.current.allowed_to?(:edit_wiki_pages, @project)
34 34 edit
35 35 render :action => 'edit'
36 36 else
37 37 render_404
38 38 end
39 39 return
40 40 end
41 41 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
42 42 # Redirects user to the current version if he's not allowed to view previous versions
43 43 redirect_to :version => nil
44 44 return
45 45 end
46 46 @content = @page.content_for_version(params[:version])
47 47 if params[:export] == 'html'
48 48 export = render_to_string :action => 'export', :layout => false
49 49 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
50 50 return
51 51 elsif params[:export] == 'txt'
52 52 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
53 53 return
54 54 end
55 55 @editable = editable?
56 56 render :action => 'show'
57 57 end
58 58
59 59 # edit an existing page or a new one
60 60 def edit
61 61 @page = @wiki.find_or_new_page(params[:page])
62 62 return render_403 unless editable?
63 63 @page.content = WikiContent.new(:page => @page) if @page.new_record?
64 64
65 65 @content = @page.content_for_version(params[:version])
66 66 @content.text = initial_page_content(@page) if @content.text.blank?
67 67 # don't keep previous comment
68 68 @content.comments = nil
69 69 if request.get?
70 70 # To prevent StaleObjectError exception when reverting to a previous version
71 71 @content.version = @page.content.version
72 72 else
73 73 if !@page.new_record? && @content.text == params[:content][:text]
74 74 # don't save if text wasn't changed
75 75 redirect_to :action => 'index', :id => @project, :page => @page.title
76 76 return
77 77 end
78 78 #@content.text = params[:content][:text]
79 79 #@content.comments = params[:content][:comments]
80 80 @content.attributes = params[:content]
81 81 @content.author = User.current
82 82 # if page is new @page.save will also save content, but not if page isn't a new record
83 83 if (@page.new_record? ? @page.save : @content.save)
84 84 redirect_to :action => 'index', :id => @project, :page => @page.title
85 85 end
86 86 end
87 87 rescue ActiveRecord::StaleObjectError
88 88 # Optimistic locking exception
89 89 flash[:error] = l(:notice_locking_conflict)
90 90 end
91 91
92 92 # rename a page
93 93 def rename
94 94 @page = @wiki.find_page(params[:page])
95 95 return render_403 unless editable?
96 96 @page.redirect_existing_links = true
97 97 # used to display the *original* title if some AR validation errors occur
98 98 @original_title = @page.pretty_title
99 99 if request.post? && @page.update_attributes(params[:wiki_page])
100 100 flash[:notice] = l(:notice_successful_update)
101 101 redirect_to :action => 'index', :id => @project, :page => @page.title
102 102 end
103 103 end
104 104
105 105 def protect
106 106 page = @wiki.find_page(params[:page])
107 107 page.update_attribute :protected, params[:protected]
108 108 redirect_to :action => 'index', :id => @project, :page => page.title
109 109 end
110 110
111 111 # show page history
112 112 def history
113 113 @page = @wiki.find_page(params[:page])
114 114
115 115 @version_count = @page.content.versions.count
116 116 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
117 117 # don't load text
118 118 @versions = @page.content.versions.find :all,
119 119 :select => "id, author_id, comments, updated_on, version",
120 120 :order => 'version DESC',
121 121 :limit => @version_pages.items_per_page + 1,
122 122 :offset => @version_pages.current.offset
123 123
124 124 render :layout => false if request.xhr?
125 125 end
126 126
127 127 def diff
128 128 @page = @wiki.find_page(params[:page])
129 129 @diff = @page.diff(params[:version], params[:version_from])
130 130 render_404 unless @diff
131 131 end
132 132
133 133 def annotate
134 134 @page = @wiki.find_page(params[:page])
135 135 @annotate = @page.annotate(params[:version])
136 136 end
137 137
138 138 # remove a wiki page and its history
139 139 def destroy
140 140 @page = @wiki.find_page(params[:page])
141 141 return render_403 unless editable?
142 142 @page.destroy if @page
143 143 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
144 144 end
145 145
146 146 # display special pages
147 147 def special
148 148 page_title = params[:page].downcase
149 149 case page_title
150 150 # show pages index, sorted by title
151 151 when 'page_index', 'date_index'
152 152 # eager load information about last updates, without loading text
153 153 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
154 154 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
155 155 :order => 'title'
156 156 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
157 157 @pages_by_parent_id = @pages.group_by(&:parent_id)
158 158 # export wiki to a single html file
159 159 when 'export'
160 160 @pages = @wiki.pages.find :all, :order => 'title'
161 161 export = render_to_string :action => 'export_multiple', :layout => false
162 162 send_data(export, :type => 'text/html', :filename => "wiki.html")
163 163 return
164 164 else
165 165 # requested special page doesn't exist, redirect to default page
166 166 redirect_to :action => 'index', :id => @project, :page => nil and return
167 167 end
168 168 render :action => "special_#{page_title}"
169 169 end
170 170
171 171 def preview
172 172 page = @wiki.find_page(params[:page])
173 173 # page is nil when previewing a new page
174 174 return render_403 unless page.nil? || editable?(page)
175 175 if page
176 176 @attachements = page.attachments
177 177 @previewed = page.content
178 178 end
179 179 @text = params[:content][:text]
180 180 render :partial => 'common/preview'
181 181 end
182 182
183 183 def add_attachment
184 184 @page = @wiki.find_page(params[:page])
185 185 return render_403 unless editable?
186 186 attach_files(@page, params[:attachments])
187 187 redirect_to :action => 'index', :page => @page.title
188 188 end
189 189
190 def destroy_attachment
191 @page = @wiki.find_page(params[:page])
192 return render_403 unless editable?
193 @page.attachments.find(params[:attachment_id]).destroy
194 redirect_to :action => 'index', :page => @page.title
195 end
196
197 190 private
198 191
199 192 def find_wiki
200 193 @project = Project.find(params[:id])
201 194 @wiki = @project.wiki
202 195 render_404 unless @wiki
203 196 rescue ActiveRecord::RecordNotFound
204 197 render_404
205 198 end
206 199
207 200 # Returns true if the current user is allowed to edit the page, otherwise false
208 201 def editable?(page = @page)
209 202 page.editable_by?(User.current)
210 203 end
211 204
212 205 # Returns the default content of a new wiki page
213 206 def initial_page_content(page)
214 207 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
215 208 extend helper unless self.instance_of?(helper)
216 209 helper.instance_method(:initial_page_content).bind(self).call(page)
217 210 end
218 211 end
@@ -1,29 +1,34
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 module AttachmentsHelper
19 # displays the links to a collection of attachments
20 def link_to_attachments(attachments, options = {})
21 if attachments.any?
22 render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
19 # Displays view/delete links to the attachments of the given object
20 # Options:
21 # :author -- author names are not displayed if set to false
22 def link_to_attachments(container, options = {})
23 options.assert_valid_keys(:author)
24
25 if container.attachments.any?
26 options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
27 render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
23 28 end
24 29 end
25 30
26 31 def to_utf8(str)
27 32 str
28 33 end
29 34 end
@@ -1,143 +1,151
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"}
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 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 50 end
51 51
52 52 def file=(incoming_file)
53 53 unless incoming_file.nil?
54 54 @temp_file = incoming_file
55 55 if @temp_file.size > 0
56 56 self.filename = sanitize_filename(@temp_file.original_filename)
57 57 self.disk_filename = Attachment.disk_filename(filename)
58 58 self.content_type = @temp_file.content_type.to_s.chomp
59 59 self.filesize = @temp_file.size
60 60 end
61 61 end
62 62 end
63 63
64 64 def file
65 65 nil
66 66 end
67 67
68 68 # Copy temp file to its final location
69 69 def before_save
70 70 if @temp_file && (@temp_file.size > 0)
71 71 logger.debug("saving '#{self.diskfile}'")
72 72 File.open(diskfile, "wb") do |f|
73 73 f.write(@temp_file.read)
74 74 end
75 75 self.digest = self.class.digest(diskfile)
76 76 end
77 77 # Don't save the content type if it's longer than the authorized length
78 78 if self.content_type && self.content_type.length > 255
79 79 self.content_type = nil
80 80 end
81 81 end
82 82
83 83 # Deletes file on the disk
84 84 def after_destroy
85 85 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
86 86 end
87 87
88 88 # Returns file's location on disk
89 89 def diskfile
90 90 "#{@@storage_path}/#{self.disk_filename}"
91 91 end
92 92
93 93 def increment_download
94 94 increment!(:downloads)
95 95 end
96 96
97 97 def project
98 98 container.project
99 99 end
100 100
101 def visible?(user=User.current)
102 container.attachments_visible?(user)
103 end
104
105 def deletable?(user=User.current)
106 container.attachments_deletable?(user)
107 end
108
101 109 def image?
102 110 self.filename =~ /\.(jpe?g|gif|png)$/i
103 111 end
104 112
105 113 def is_text?
106 114 Redmine::MimeType.is_type?('text', filename)
107 115 end
108 116
109 117 def is_diff?
110 118 self.filename =~ /\.(patch|diff)$/i
111 119 end
112 120
113 121 private
114 122 def sanitize_filename(value)
115 123 # get only the filename, not the whole path
116 124 just_filename = value.gsub(/^.*(\\|\/)/, '')
117 125 # NOTE: File.basename doesn't work right with Windows paths on Unix
118 126 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
119 127
120 128 # Finally, replace all non alphanumeric, hyphens or periods with underscore
121 129 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
122 130 end
123 131
124 132 # Returns an ASCII or hashed filename
125 133 def self.disk_filename(filename)
126 134 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
127 135 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
128 136 df << filename
129 137 else
130 138 df << Digest::MD5.hexdigest(filename)
131 139 # keep the extension if any
132 140 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
133 141 end
134 142 df
135 143 end
136 144
137 145 # Returns the MD5 digest of the file at given path
138 146 def self.digest(filename)
139 147 File.open(filename, 'rb') do |f|
140 148 Digest::MD5.hexdigest(f.read)
141 149 end
142 150 end
143 151 end
@@ -1,31 +1,31
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 Document < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
21 has_many :attachments, :as => :container, :dependent => :destroy
21 acts_as_attachable :delete_permission => :manage_documents
22 22
23 23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
24 24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
25 25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26 26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
27 27 acts_as_activity_provider :find_options => {:include => :project}
28 28
29 29 validates_presence_of :project, :title, :category
30 30 validates_length_of :title, :maximum => 60
31 31 end
@@ -1,264 +1,275
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 Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :attachments, :as => :container, :dependent => :destroy
30 29 has_many :time_entries, :dependent => :delete_all
31 30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 31
33 32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 34
35 acts_as_attachable :after_remove => :attachment_removed
36 36 acts_as_customizable
37 37 acts_as_watchable
38 38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 39 :include => [:project, :journals],
40 40 # sort by id so that limited eager loading doesn't break with postgresql
41 41 :order_column => "#{table_name}.id"
42 42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
43 43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
44 44
45 45 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 46 :author_key => :author_id
47 47
48 48 validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
49 49 validates_length_of :subject, :maximum => 255
50 50 validates_inclusion_of :done_ratio, :in => 0..100
51 51 validates_numericality_of :estimated_hours, :allow_nil => true
52 52
53 53 def after_initialize
54 54 if new_record?
55 55 # set default values for new records only
56 56 self.status ||= IssueStatus.default
57 57 self.priority ||= Enumeration.default('IPRI')
58 58 end
59 59 end
60 60
61 61 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
62 62 def available_custom_fields
63 63 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
64 64 end
65 65
66 66 def copy_from(arg)
67 67 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
68 68 self.attributes = issue.attributes.dup
69 69 self.custom_values = issue.custom_values.collect {|v| v.clone}
70 70 self
71 71 end
72 72
73 73 # Move an issue to a new project and tracker
74 74 def move_to(new_project, new_tracker = nil)
75 75 transaction do
76 76 if new_project && project_id != new_project.id
77 77 # delete issue relations
78 78 unless Setting.cross_project_issue_relations?
79 79 self.relations_from.clear
80 80 self.relations_to.clear
81 81 end
82 82 # issue is moved to another project
83 83 # reassign to the category with same name if any
84 84 new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
85 85 self.category = new_category
86 86 self.fixed_version = nil
87 87 self.project = new_project
88 88 end
89 89 if new_tracker
90 90 self.tracker = new_tracker
91 91 end
92 92 if save
93 93 # Manually update project_id on related time entries
94 94 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
95 95 else
96 96 rollback_db_transaction
97 97 return false
98 98 end
99 99 end
100 100 return true
101 101 end
102 102
103 103 def priority_id=(pid)
104 104 self.priority = nil
105 105 write_attribute(:priority_id, pid)
106 106 end
107 107
108 108 def estimated_hours=(h)
109 109 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
110 110 end
111 111
112 112 def validate
113 113 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
114 114 errors.add :due_date, :activerecord_error_not_a_date
115 115 end
116 116
117 117 if self.due_date and self.start_date and self.due_date < self.start_date
118 118 errors.add :due_date, :activerecord_error_greater_than_start_date
119 119 end
120 120
121 121 if start_date && soonest_start && start_date < soonest_start
122 122 errors.add :start_date, :activerecord_error_invalid
123 123 end
124 124 end
125 125
126 126 def validate_on_create
127 127 errors.add :tracker_id, :activerecord_error_invalid unless project.trackers.include?(tracker)
128 128 end
129 129
130 130 def before_create
131 131 # default assignment based on category
132 132 if assigned_to.nil? && category && category.assigned_to
133 133 self.assigned_to = category.assigned_to
134 134 end
135 135 end
136 136
137 137 def before_save
138 138 if @current_journal
139 139 # attributes changes
140 140 (Issue.column_names - %w(id description)).each {|c|
141 141 @current_journal.details << JournalDetail.new(:property => 'attr',
142 142 :prop_key => c,
143 143 :old_value => @issue_before_change.send(c),
144 144 :value => send(c)) unless send(c)==@issue_before_change.send(c)
145 145 }
146 146 # custom fields changes
147 147 custom_values.each {|c|
148 148 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
149 149 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
150 150 @current_journal.details << JournalDetail.new(:property => 'cf',
151 151 :prop_key => c.custom_field_id,
152 152 :old_value => @custom_values_before_change[c.custom_field_id],
153 153 :value => c.value)
154 154 }
155 155 @current_journal.save
156 156 end
157 157 # Save the issue even if the journal is not saved (because empty)
158 158 true
159 159 end
160 160
161 161 def after_save
162 162 # Reload is needed in order to get the right status
163 163 reload
164 164
165 165 # Update start/due dates of following issues
166 166 relations_from.each(&:set_issue_to_dates)
167 167
168 168 # Close duplicates if the issue was closed
169 169 if @issue_before_change && !@issue_before_change.closed? && self.closed?
170 170 duplicates.each do |duplicate|
171 171 # Reload is need in case the duplicate was updated by a previous duplicate
172 172 duplicate.reload
173 173 # Don't re-close it if it's already closed
174 174 next if duplicate.closed?
175 175 # Same user and notes
176 176 duplicate.init_journal(@current_journal.user, @current_journal.notes)
177 177 duplicate.update_attribute :status, self.status
178 178 end
179 179 end
180 180 end
181 181
182 182 def init_journal(user, notes = "")
183 183 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
184 184 @issue_before_change = self.clone
185 185 @issue_before_change.status = self.status
186 186 @custom_values_before_change = {}
187 187 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
188 188 # Make sure updated_on is updated when adding a note.
189 189 updated_on_will_change!
190 190 @current_journal
191 191 end
192 192
193 193 # Return true if the issue is closed, otherwise false
194 194 def closed?
195 195 self.status.is_closed?
196 196 end
197 197
198 198 # Users the issue can be assigned to
199 199 def assignable_users
200 200 project.assignable_users
201 201 end
202 202
203 203 # Returns an array of status that user is able to apply
204 204 def new_statuses_allowed_to(user)
205 205 statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
206 206 statuses << status unless statuses.empty?
207 207 statuses.uniq.sort
208 208 end
209 209
210 210 # Returns the mail adresses of users that should be notified for the issue
211 211 def recipients
212 212 recipients = project.recipients
213 213 # Author and assignee are always notified unless they have been locked
214 214 recipients << author.mail if author && author.active?
215 215 recipients << assigned_to.mail if assigned_to && assigned_to.active?
216 216 recipients.compact.uniq
217 217 end
218 218
219 219 def spent_hours
220 220 @spent_hours ||= time_entries.sum(:hours) || 0
221 221 end
222 222
223 223 def relations
224 224 (relations_from + relations_to).sort
225 225 end
226 226
227 227 def all_dependent_issues
228 228 dependencies = []
229 229 relations_from.each do |relation|
230 230 dependencies << relation.issue_to
231 231 dependencies += relation.issue_to.all_dependent_issues
232 232 end
233 233 dependencies
234 234 end
235 235
236 236 # Returns an array of issues that duplicate this one
237 237 def duplicates
238 238 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
239 239 end
240 240
241 241 # Returns the due date or the target due date if any
242 242 # Used on gantt chart
243 243 def due_before
244 244 due_date || (fixed_version ? fixed_version.effective_date : nil)
245 245 end
246 246
247 247 def duration
248 248 (start_date && due_date) ? due_date - start_date : 0
249 249 end
250 250
251 251 def soonest_start
252 252 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
253 253 end
254 254
255 255 def self.visible_by(usr)
256 256 with_scope(:find => { :conditions => Project.visible_by(usr) }) do
257 257 yield
258 258 end
259 259 end
260 260
261 261 def to_s
262 262 "#{tracker} ##{id}: #{subject}"
263 263 end
264
265 private
266
267 # Callback on attachment deletion
268 def attachment_removed(obj)
269 journal = init_journal(User.current)
270 journal.details << JournalDetail.new(:property => 'attachment',
271 :prop_key => obj.id,
272 :old_value => obj.filename)
273 journal.save
274 end
264 275 end
@@ -1,89 +1,89
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 Message < ActiveRecord::Base
19 19 belongs_to :board
20 20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 has_many :attachments, :as => :container, :dependent => :destroy
22 acts_as_attachable
23 23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24 24
25 25 acts_as_searchable :columns => ['subject', 'content'],
26 26 :include => {:board, :project},
27 27 :project_key => 'project_id',
28 28 :date_column => "#{table_name}.created_on"
29 29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 30 :description => :content,
31 31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
32 32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
33 33 {:id => o.parent_id, :anchor => "message-#{o.id}"})}
34 34
35 35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
36 36 :author_key => :author_id
37 37 acts_as_watchable
38 38
39 39 attr_protected :locked, :sticky
40 40 validates_presence_of :subject, :content
41 41 validates_length_of :subject, :maximum => 255
42 42
43 43 after_create :add_author_as_watcher
44 44
45 45 def validate_on_create
46 46 # Can not reply to a locked topic
47 47 errors.add_to_base 'Topic is locked' if root.locked? && self != root
48 48 end
49 49
50 50 def after_create
51 51 board.update_attribute(:last_message_id, self.id)
52 52 board.increment! :messages_count
53 53 if parent
54 54 parent.reload.update_attribute(:last_reply_id, self.id)
55 55 else
56 56 board.increment! :topics_count
57 57 end
58 58 end
59 59
60 60 def after_destroy
61 61 # The following line is required so that the previous counter
62 62 # updates (due to children removal) are not overwritten
63 63 board.reload
64 64 board.decrement! :messages_count
65 65 board.decrement! :topics_count unless parent
66 66 end
67 67
68 68 def sticky?
69 69 sticky == 1
70 70 end
71 71
72 72 def project
73 73 board.project
74 74 end
75 75
76 76 def editable_by?(usr)
77 77 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
78 78 end
79 79
80 80 def destroyable_by?(usr)
81 81 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
82 82 end
83 83
84 84 private
85 85
86 86 def add_author_as_watcher
87 87 Watcher.create(:watchable => self.root, :user => author)
88 88 end
89 89 end
@@ -1,106 +1,107
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 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 Version < ActiveRecord::Base
19 19 before_destroy :check_integrity
20 20 belongs_to :project
21 21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
22 has_many :attachments, :as => :container, :dependent => :destroy
22 acts_as_attachable :view_permission => :view_files,
23 :delete_permission => :manage_files
23 24
24 25 validates_presence_of :name
25 26 validates_uniqueness_of :name, :scope => [:project_id]
26 27 validates_length_of :name, :maximum => 60
27 28 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
28 29
29 30 def start_date
30 31 effective_date
31 32 end
32 33
33 34 def due_date
34 35 effective_date
35 36 end
36 37
37 38 # Returns the total estimated time for this version
38 39 def estimated_hours
39 40 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
40 41 end
41 42
42 43 # Returns the total reported time for this version
43 44 def spent_hours
44 45 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
45 46 end
46 47
47 48 # Returns true if the version is completed: due date reached and no open issues
48 49 def completed?
49 50 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
50 51 end
51 52
52 53 def completed_pourcent
53 54 if fixed_issues.count == 0
54 55 0
55 56 elsif open_issues_count == 0
56 57 100
57 58 else
58 59 (closed_issues_count * 100 + Issue.sum('done_ratio', :include => 'status', :conditions => ["fixed_version_id = ? AND is_closed = ?", id, false]).to_f) / fixed_issues.count
59 60 end
60 61 end
61 62
62 63 def closed_pourcent
63 64 if fixed_issues.count == 0
64 65 0
65 66 else
66 67 closed_issues_count * 100.0 / fixed_issues.count
67 68 end
68 69 end
69 70
70 71 # Returns true if the version is overdue: due date reached and some open issues
71 72 def overdue?
72 73 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
73 74 end
74 75
75 76 def open_issues_count
76 77 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
77 78 end
78 79
79 80 def closed_issues_count
80 81 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
81 82 end
82 83
83 84 def wiki_page
84 85 if project.wiki && !wiki_page_title.blank?
85 86 @wiki_page ||= project.wiki.find_page(wiki_page_title)
86 87 end
87 88 @wiki_page
88 89 end
89 90
90 91 def to_s; name end
91 92
92 93 # Versions are sorted by effective_date and name
93 94 # Those with no effective_date are at the end, sorted by name
94 95 def <=>(version)
95 96 if self.effective_date
96 97 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
97 98 else
98 99 version.effective_date ? 1 : (self.name <=> version.name)
99 100 end
100 101 end
101 102
102 103 private
103 104 def check_integrity
104 105 raise "Can't delete version" if self.fixed_issues.find(:first)
105 106 end
106 107 end
@@ -1,184 +1,188
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 require 'enumerator'
20 20
21 21 class WikiPage < ActiveRecord::Base
22 22 belongs_to :wiki
23 23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
24 has_many :attachments, :as => :container, :dependent => :destroy
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
25 25 acts_as_tree :order => 'title'
26 26
27 27 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
28 28 :description => :text,
29 29 :datetime => :created_on,
30 30 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
31 31
32 32 acts_as_searchable :columns => ['title', 'text'],
33 33 :include => [{:wiki => :project}, :content],
34 34 :project_key => "#{Wiki.table_name}.project_id"
35 35
36 36 attr_accessor :redirect_existing_links
37 37
38 38 validates_presence_of :title
39 39 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
40 40 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
41 41 validates_associated :content
42 42
43 43 def title=(value)
44 44 value = Wiki.titleize(value)
45 45 @previous_title = read_attribute(:title) if @previous_title.blank?
46 46 write_attribute(:title, value)
47 47 end
48 48
49 49 def before_save
50 50 self.title = Wiki.titleize(title)
51 51 # Manage redirects if the title has changed
52 52 if !@previous_title.blank? && (@previous_title != title) && !new_record?
53 53 # Update redirects that point to the old title
54 54 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
55 55 r.redirects_to = title
56 56 r.title == r.redirects_to ? r.destroy : r.save
57 57 end
58 58 # Remove redirects for the new title
59 59 wiki.redirects.find_all_by_title(title).each(&:destroy)
60 60 # Create a redirect to the new title
61 61 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
62 62 @previous_title = nil
63 63 end
64 64 end
65 65
66 66 def before_destroy
67 67 # Remove redirects to this page
68 68 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
69 69 end
70 70
71 71 def pretty_title
72 72 WikiPage.pretty_title(title)
73 73 end
74 74
75 75 def content_for_version(version=nil)
76 76 result = content.versions.find_by_version(version.to_i) if version
77 77 result ||= content
78 78 result
79 79 end
80 80
81 81 def diff(version_to=nil, version_from=nil)
82 82 version_to = version_to ? version_to.to_i : self.content.version
83 83 version_from = version_from ? version_from.to_i : version_to - 1
84 84 version_to, version_from = version_from, version_to unless version_from < version_to
85 85
86 86 content_to = content.versions.find_by_version(version_to)
87 87 content_from = content.versions.find_by_version(version_from)
88 88
89 89 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
90 90 end
91 91
92 92 def annotate(version=nil)
93 93 version = version ? version.to_i : self.content.version
94 94 c = content.versions.find_by_version(version)
95 95 c ? WikiAnnotate.new(c) : nil
96 96 end
97 97
98 98 def self.pretty_title(str)
99 99 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
100 100 end
101 101
102 102 def project
103 103 wiki.project
104 104 end
105 105
106 106 def text
107 107 content.text if content
108 108 end
109 109
110 110 # Returns true if usr is allowed to edit the page, otherwise false
111 111 def editable_by?(usr)
112 112 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
113 113 end
114
115 def attachments_deletable?(usr=User.current)
116 editable_by?(usr) && super(usr)
117 end
114 118
115 119 def parent_title
116 120 @parent_title || (self.parent && self.parent.pretty_title)
117 121 end
118 122
119 123 def parent_title=(t)
120 124 @parent_title = t
121 125 parent_page = t.blank? ? nil : self.wiki.find_page(t)
122 126 self.parent = parent_page
123 127 end
124 128
125 129 protected
126 130
127 131 def validate
128 132 errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil?
129 133 errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
130 134 errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id)
131 135 end
132 136 end
133 137
134 138 class WikiDiff
135 139 attr_reader :diff, :words, :content_to, :content_from
136 140
137 141 def initialize(content_to, content_from)
138 142 @content_to = content_to
139 143 @content_from = content_from
140 144 @words = content_to.text.split(/(\s+)/)
141 145 @words = @words.select {|word| word != ' '}
142 146 words_from = content_from.text.split(/(\s+)/)
143 147 words_from = words_from.select {|word| word != ' '}
144 148 @diff = words_from.diff @words
145 149 end
146 150 end
147 151
148 152 class WikiAnnotate
149 153 attr_reader :lines, :content
150 154
151 155 def initialize(content)
152 156 @content = content
153 157 current = content
154 158 current_lines = current.text.split(/\r?\n/)
155 159 @lines = current_lines.collect {|t| [nil, nil, t]}
156 160 positions = []
157 161 current_lines.size.times {|i| positions << i}
158 162 while (current.previous)
159 163 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
160 164 d.each_slice(3) do |s|
161 165 sign, line = s[0], s[1]
162 166 if sign == '+' && positions[line] && positions[line] != -1
163 167 if @lines[positions[line]][0].nil?
164 168 @lines[positions[line]][0] = current.version
165 169 @lines[positions[line]][1] = current.author
166 170 end
167 171 end
168 172 end
169 173 d.each_slice(3) do |s|
170 174 sign, line = s[0], s[1]
171 175 if sign == '-'
172 176 positions.insert(line, -1)
173 177 else
174 178 positions[line] = nil
175 179 end
176 180 end
177 181 positions.compact!
178 182 # Stop if every line is annotated
179 183 break unless @lines.detect { |line| line[0].nil? }
180 184 current = current.previous
181 185 end
182 186 @lines.each { |line| line[0] ||= current.version }
183 187 end
184 188 end
@@ -1,18 +1,18
1 1 <div class="attachments">
2 2 <% for attachment in attachments %>
3 3 <p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
4 4 <%= h(" - #{attachment.description}") unless attachment.description.blank? %>
5 5 <span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
6 <% if options[:delete_url] %>
7 <%= link_to image_tag('delete.png'), options[:delete_url].update({:attachment_id => attachment}),
6 <% if options[:deletable] %>
7 <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => attachment},
8 8 :confirm => l(:text_are_you_sure),
9 9 :method => :post,
10 10 :class => 'delete',
11 11 :title => l(:button_delete) %>
12 12 <% end %>
13 <% unless options[:no_author] %>
13 <% if options[:author] %>
14 14 <span class="author"><%= attachment.author %>, <%= format_time(attachment.created_on) %></span>
15 15 <% end %>
16 16 </p>
17 17 <% end %>
18 18 </div>
@@ -1,28 +1,28
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized l(:button_edit), {:controller => 'documents', :action => 'edit', :id => @document}, :class => 'icon icon-edit', :accesskey => accesskey(:edit) %>
3 3 <%= link_to_if_authorized l(:button_delete), {:controller => 'documents', :action => 'destroy', :id => @document}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
4 4 </div>
5 5
6 6 <h2><%=h @document.title %></h2>
7 7
8 8 <p><em><%=h @document.category.name %><br />
9 9 <%= format_date @document.created_on %></em></p>
10 10 <div class="wiki">
11 11 <%= textilizable @document.description, :attachments => @document.attachments %>
12 12 </div>
13 13
14 14 <h3><%= l(:label_attachment_plural) %></h3>
15 <%= link_to_attachments @attachments, :delete_url => (authorize_for('documents', 'destroy_attachment') ? {:controller => 'documents', :action => 'destroy_attachment', :id => @document} : nil) %>
15 <%= link_to_attachments @document %>
16 16
17 17 <% if authorize_for('documents', 'add_attachment') %>
18 18 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
19 19 :id => 'attach_files_link' %></p>
20 20 <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
21 21 <div class="box">
22 22 <p><%= render :partial => 'attachments/form' %></p>
23 23 </div>
24 24 <%= submit_tag l(:button_add) %>
25 25 <% end %>
26 26 <% end %>
27 27
28 28 <% html_title @document.title -%>
@@ -1,128 +1,126
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
3 3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
4 4 <%= watcher_tag(@issue, User.current) %>
5 5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
6 6 <%= link_to_if_authorized l(:button_move), {:controller => 'issues', :action => 'move', :id => @issue }, :class => 'icon icon-move' %>
7 7 <%= link_to_if_authorized l(:button_delete), {:controller => 'issues', :action => 'destroy', :id => @issue}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
8 8 </div>
9 9
10 10 <h2><%= @issue.tracker.name %> #<%= @issue.id %></h2>
11 11
12 12 <div class="issue <%= "status-#{@issue.status.position} priority-#{@issue.priority.position}" %>">
13 13 <%= avatar(@issue.author, :size => "64") %>
14 14 <h3><%=h @issue.subject %></h3>
15 15 <p class="author">
16 16 <%= authoring @issue.created_on, @issue.author %>.
17 17 <%= l(:label_updated_time, distance_of_time_in_words(Time.now, @issue.updated_on)) + '.' if @issue.created_on != @issue.updated_on %>
18 18 </p>
19 19
20 20 <table width="100%">
21 21 <tr>
22 22 <td style="width:15%" class="status"><b><%=l(:field_status)%>:</b></td><td style="width:35%" class="status status-<%= @issue.status.name %>"><%= @issue.status.name %></td>
23 23 <td style="width:15%" class="start-date"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
24 24 </tr>
25 25 <tr>
26 26 <td class="priority"><b><%=l(:field_priority)%>:</b></td><td class="priority priority-<%= @issue.priority.name %>"><%= @issue.priority.name %></td>
27 27 <td class="due-date"><b><%=l(:field_due_date)%>:</b></td><td class="due-date"><%= format_date(@issue.due_date) %></td>
28 28 </tr>
29 29 <tr>
30 30 <td class="assigned-to"><b><%=l(:field_assigned_to)%>:</b></td><td><%= avatar(@issue.assigned_to, :size => "14") %><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
31 31 <td class="progress"><b><%=l(:field_done_ratio)%>:</b></td><td class="progress"><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
32 32 </tr>
33 33 <tr>
34 34 <td class="category"><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
35 35 <% if User.current.allowed_to?(:view_time_entries, @project) %>
36 36 <td class="spent-time"><b><%=l(:label_spent_time)%>:</b></td>
37 37 <td class="spent-hours"><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
38 38 <% end %>
39 39 </tr>
40 40 <tr>
41 41 <td class="fixed-version"><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
42 42 <% if @issue.estimated_hours %>
43 43 <td class="estimated-hours"><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
44 44 <% end %>
45 45 </tr>
46 46 <tr>
47 47 <% n = 0 -%>
48 48 <% @issue.custom_values.each do |value| -%>
49 49 <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
50 50 <% n = n + 1
51 51 if (n > 1)
52 52 n = 0 %>
53 53 </tr><tr>
54 54 <%end
55 55 end %>
56 56 </tr>
57 57 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
58 58 </table>
59 59 <hr />
60 60
61 61 <div class="contextual">
62 62 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %>
63 63 </div>
64 64
65 65 <p><strong><%=l(:field_description)%></strong></p>
66 66 <div class="wiki">
67 67 <%= textilizable @issue, :description, :attachments => @issue.attachments %>
68 68 </div>
69 69
70 <% if @issue.attachments.any? %>
71 <%= link_to_attachments @issue.attachments, :delete_url => (authorize_for('issues', 'destroy_attachment') ? {:controller => 'issues', :action => 'destroy_attachment', :id => @issue} : nil) %>
72 <% end %>
70 <%= link_to_attachments @issue %>
73 71
74 72 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
75 73 <hr />
76 74 <div id="relations">
77 75 <%= render :partial => 'relations' %>
78 76 </div>
79 77 <% end %>
80 78
81 79 <% if User.current.allowed_to?(:add_issue_watchers, @project) ||
82 80 (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
83 81 <hr />
84 82 <div id="watchers">
85 83 <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
86 84 </div>
87 85 <% end %>
88 86
89 87 </div>
90 88
91 89 <% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
92 90 <div id="issue-changesets">
93 91 <h3><%=l(:label_associated_revisions)%></h3>
94 92 <%= render :partial => 'changesets', :locals => { :changesets => @issue.changesets} %>
95 93 </div>
96 94 <% end %>
97 95
98 96 <% if @journals.any? %>
99 97 <div id="history">
100 98 <h3><%=l(:label_history)%></h3>
101 99 <%= render :partial => 'history', :locals => { :journals => @journals } %>
102 100 </div>
103 101 <% end %>
104 102 <div style="clear: both;"></div>
105 103
106 104 <% if authorize_for('issues', 'edit') %>
107 105 <div id="update" style="display:none;">
108 106 <h3><%= l(:button_update) %></h3>
109 107 <%= render :partial => 'edit' %>
110 108 </div>
111 109 <% end %>
112 110
113 111 <p class="other-formats">
114 112 <%= l(:label_export_to) %>
115 113 <span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
116 114 <span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
117 115 </p>
118 116
119 117 <% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
120 118
121 119 <% content_for :sidebar do %>
122 120 <%= render :partial => 'issues/sidebar' %>
123 121 <% end %>
124 122
125 123 <% content_for :header_tags do %>
126 124 <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
127 125 <%= stylesheet_link_tag 'scm' %>
128 126 <% end %>
@@ -1,61 +1,61
1 1 <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}),
2 2 link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %>
3 3
4 4 <div class="contextual">
5 5 <%= watcher_tag(@topic, User.current) %>
6 6 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %>
7 7 <%= link_to(l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit') if @message.editable_by?(User.current) %>
8 8 <%= link_to(l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if @message.destroyable_by?(User.current) %>
9 9 </div>
10 10
11 11 <h2><%=h @topic.subject %></h2>
12 12
13 13 <div class="message">
14 14 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
15 15 <div class="wiki">
16 16 <%= textilizable(@topic.content, :attachments => @topic.attachments) %>
17 17 </div>
18 <%= link_to_attachments @topic.attachments, :no_author => true %>
18 <%= link_to_attachments @topic, :author => false %>
19 19 </div>
20 20 <br />
21 21
22 22 <% unless @replies.empty? %>
23 23 <h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3>
24 24 <% @replies.each do |message| %>
25 25 <a name="<%= "message-#{message.id}" %>"></a>
26 26 <div class="contextual">
27 27 <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %>
28 28 <%= link_to(image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit)) if message.editable_by?(User.current) %>
29 29 <%= link_to(image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete)) if message.destroyable_by?(User.current) %>
30 30 </div>
31 31 <div class="message reply">
32 32 <h4><%=h message.subject %> - <%= authoring message.created_on, message.author %></h4>
33 33 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
34 <%= link_to_attachments message.attachments, :no_author => true %>
34 <%= link_to_attachments message, :author => false %>
35 35 </div>
36 36 <% end %>
37 37 <% end %>
38 38
39 39 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
40 40 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
41 41 <div id="reply" style="display:none;">
42 42 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
43 43 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
44 44 <%= submit_tag l(:button_submit) %>
45 45 <%= link_to_remote l(:label_preview),
46 46 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
47 47 :method => 'post',
48 48 :update => 'preview',
49 49 :with => "Form.serialize('message-form')",
50 50 :complete => "Element.scrollTo('preview')"
51 51 }, :accesskey => accesskey(:preview) %>
52 52 <% end %>
53 53 <div id="preview" class="wiki"></div>
54 54 </div>
55 55 <% end %>
56 56
57 57 <% content_for :header_tags do %>
58 58 <%= stylesheet_link_tag 'scm' %>
59 59 <% end %>
60 60
61 61 <% html_title h(@topic.subject) %>
@@ -1,44 +1,45
1 1 <div class="contextual">
2 2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_attachment_plural)%></h2>
6 6
7 <% delete_allowed = authorize_for('versions', 'destroy_file') %>
7 <% delete_allowed = User.current.allowed_to?(:manage_files, @project) %>
8 8
9 9 <table class="list">
10 10 <thead><tr>
11 11 <th><%=l(:field_version)%></th>
12 12 <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
13 13 <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
14 14 <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
15 15 <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
16 16 <th>MD5</th>
17 17 <% if delete_allowed %><th></th><% end %>
18 18 </tr></thead>
19 19 <tbody>
20 20 <% for version in @versions %>
21 21 <% unless version.attachments.empty? %>
22 22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
23 23 <% for file in version.attachments %>
24 24 <tr class="<%= cycle("odd", "even") %>">
25 25 <td></td>
26 26 <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
27 27 <td align="center"><%= format_time(file.created_on) %></td>
28 28 <td align="center"><%= number_to_human_size(file.filesize) %></td>
29 29 <td align="center"><%= file.downloads %></td>
30 30 <td align="center"><small><%= file.digest %></small></td>
31 31 <% if delete_allowed %>
32 32 <td align="center">
33 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %>
33 <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => file},
34 :confirm => l(:text_are_you_sure), :method => :post %>
34 35 </td>
35 36 <% end %>
36 37 </tr>
37 38 <% end
38 39 reset_cycle %>
39 40 <% end %>
40 41 <% end %>
41 42 </tbody>
42 43 </table>
43 44
44 45 <% html_title(l(:label_attachment_plural)) -%>
@@ -1,59 +1,59
1 1 <div class="contextual">
2 2 <% if @editable %>
3 3 <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %>
4 4 <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
5 5 <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
6 6 <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %>
7 7 <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
8 8 <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %>
9 9 <% end %>
10 10 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
11 11 </div>
12 12
13 13 <%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %>
14 14
15 15 <% if @content.version != @page.content.version %>
16 16 <p>
17 17 <%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
18 18 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
19 19 <%= '(' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => @page.title, :version => @content.version) + ')' if @content.version > 1 %> -
20 20 <%= link_to((l(:label_next) + ' &#187;'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
21 21 <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
22 22 <br />
23 23 <em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br />
24 24 <%=h @content.comments %>
25 25 </p>
26 26 <hr />
27 27 <% end %>
28 28
29 29 <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
30 30
31 <%= link_to_attachments @page.attachments, :delete_url => ((@editable && authorize_for('wiki', 'destroy_attachment')) ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %>
31 <%= link_to_attachments @page %>
32 32
33 33 <% if @editable && authorize_for('wiki', 'add_attachment') %>
34 34 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
35 35 :id => 'attach_files_link' %></p>
36 36 <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
37 37 <div class="box">
38 38 <p><%= render :partial => 'attachments/form' %></p>
39 39 </div>
40 40 <%= submit_tag l(:button_add) %>
41 41 <%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
42 42 <% end %>
43 43 <% end %>
44 44
45 45 <p class="other-formats">
46 46 <%= l(:label_export_to) %>
47 47 <span><%= link_to 'HTML', {:page => @page.title, :export => 'html', :version => @content.version}, :class => 'html' %></span>
48 48 <span><%= link_to 'TXT', {:page => @page.title, :export => 'txt', :version => @content.version}, :class => 'text' %></span>
49 49 </p>
50 50
51 51 <% content_for :header_tags do %>
52 52 <%= stylesheet_link_tag 'scm' %>
53 53 <% end %>
54 54
55 55 <% content_for :sidebar do %>
56 56 <%= render :partial => 'sidebar' %>
57 57 <% end %>
58 58
59 59 <% html_title @page.pretty_title %>
@@ -1,163 +1,163
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/activity'
4 4 require 'redmine/mime_type'
5 5 require 'redmine/core_ext'
6 6 require 'redmine/themes'
7 7 require 'redmine/hook'
8 8 require 'redmine/plugin'
9 9 require 'redmine/wiki_formatting'
10 10
11 11 begin
12 12 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
13 13 rescue LoadError
14 14 # RMagick is not available
15 15 end
16 16
17 17 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
18 18
19 19 # Permissions
20 20 Redmine::AccessControl.map do |map|
21 21 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
22 22 map.permission :search_project, {:search => :index}, :public => true
23 23 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
24 24 map.permission :select_project_modules, {:projects => :modules}, :require => :member
25 25 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
26 26 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
27 27
28 28 map.project_module :issue_tracking do |map|
29 29 # Issue categories
30 30 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
31 31 # Issues
32 32 map.permission :view_issues, {:projects => [:changelog, :roadmap],
33 33 :issues => [:index, :changes, :show, :context_menu],
34 34 :versions => [:show, :status_by],
35 35 :queries => :index,
36 36 :reports => :issue_report}, :public => true
37 37 map.permission :add_issues, {:issues => :new}
38 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
38 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]}
39 39 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
40 40 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
41 41 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
42 42 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
43 43 map.permission :move_issues, {:issues => :move}, :require => :loggedin
44 44 map.permission :delete_issues, {:issues => :destroy}, :require => :member
45 45 # Queries
46 46 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
47 47 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
48 48 # Gantt & calendar
49 49 map.permission :view_gantt, :issues => :gantt
50 50 map.permission :view_calendar, :issues => :calendar
51 51 # Watchers
52 52 map.permission :view_issue_watchers, {}
53 53 map.permission :add_issue_watchers, {:watchers => :new}
54 54 end
55 55
56 56 map.project_module :time_tracking do |map|
57 57 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
58 58 map.permission :view_time_entries, :timelog => [:details, :report]
59 59 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
60 60 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
61 61 end
62 62
63 63 map.project_module :news do |map|
64 64 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
65 65 map.permission :view_news, {:news => [:index, :show]}, :public => true
66 66 map.permission :comment_news, {:news => :add_comment}
67 67 end
68 68
69 69 map.project_module :documents do |map|
70 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
70 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
71 71 map.permission :view_documents, :documents => [:index, :show, :download]
72 72 end
73 73
74 74 map.project_module :files do |map|
75 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
75 map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
76 76 map.permission :view_files, :projects => :list_files, :versions => :download
77 77 end
78 78
79 79 map.project_module :wiki do |map|
80 80 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
81 81 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
82 82 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
83 83 map.permission :view_wiki_pages, :wiki => [:index, :special]
84 84 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
85 85 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
86 map.permission :delete_wiki_pages_attachments, :wiki => :destroy_attachment
86 map.permission :delete_wiki_pages_attachments, {}
87 87 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
88 88 end
89 89
90 90 map.project_module :repository do |map|
91 91 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
92 92 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
93 93 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
94 94 map.permission :commit_access, {}
95 95 end
96 96
97 97 map.project_module :boards do |map|
98 98 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
99 99 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
100 100 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
101 101 map.permission :edit_messages, {:messages => :edit}, :require => :member
102 102 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
103 103 map.permission :delete_messages, {:messages => :destroy}, :require => :member
104 104 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
105 105 end
106 106 end
107 107
108 108 Redmine::MenuManager.map :top_menu do |menu|
109 109 menu.push :home, :home_path
110 110 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
111 111 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
112 112 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
113 113 menu.push :help, Redmine::Info.help_url, :last => true
114 114 end
115 115
116 116 Redmine::MenuManager.map :account_menu do |menu|
117 117 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
118 118 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
119 119 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
120 120 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
121 121 end
122 122
123 123 Redmine::MenuManager.map :application_menu do |menu|
124 124 # Empty
125 125 end
126 126
127 127 Redmine::MenuManager.map :admin_menu do |menu|
128 128 # Empty
129 129 end
130 130
131 131 Redmine::MenuManager.map :project_menu do |menu|
132 132 menu.push :overview, { :controller => 'projects', :action => 'show' }
133 133 menu.push :activity, { :controller => 'projects', :action => 'activity' }
134 134 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
135 135 :if => Proc.new { |p| p.versions.any? }
136 136 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
137 137 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
138 138 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
139 139 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
140 140 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
141 141 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
142 142 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
143 143 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
144 144 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
145 145 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
146 146 menu.push :repository, { :controller => 'repositories', :action => 'show' },
147 147 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
148 148 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
149 149 end
150 150
151 151 Redmine::Activity.map do |activity|
152 152 activity.register :issues, :class_name => %w(Issue Journal)
153 153 activity.register :changesets
154 154 activity.register :news
155 155 activity.register :documents, :class_name => %w(Document Attachment)
156 156 activity.register :files, :class_name => 'Attachment'
157 157 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
158 158 activity.register :messages, :default => false
159 159 end
160 160
161 161 Redmine::WikiFormatting.map do |format|
162 162 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
163 163 end
@@ -1,79 +1,108
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 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'attachments_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class AttachmentsController; def rescue_action(e) raise e end; end
23 23
24 24
25 25 class AttachmentsControllerTest < Test::Unit::TestCase
26 26 fixtures :users, :projects, :roles, :members, :enabled_modules, :issues, :attachments
27 27
28 28 def setup
29 29 @controller = AttachmentsController.new
30 30 @request = ActionController::TestRequest.new
31 31 @response = ActionController::TestResponse.new
32 32 Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
33 33 User.current = nil
34 34 end
35 35
36 36 def test_routing
37 37 assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
38 38 assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
39 39 assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
40 40 assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
41 41 end
42 42
43 43 def test_recognizes
44 44 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
45 45 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
46 46 assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
47 47 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
48 48 assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
49 49 end
50 50
51 51 def test_show_diff
52 52 get :show, :id => 5
53 53 assert_response :success
54 54 assert_template 'diff'
55 55 end
56 56
57 57 def test_show_text_file
58 58 get :show, :id => 4
59 59 assert_response :success
60 60 assert_template 'file'
61 61 end
62 62
63 63 def test_show_other
64 64 get :show, :id => 6
65 65 assert_response :success
66 66 assert_equal 'application/octet-stream', @response.content_type
67 67 end
68 68
69 69 def test_download_text_file
70 70 get :download, :id => 4
71 71 assert_response :success
72 72 assert_equal 'application/x-ruby', @response.content_type
73 73 end
74 74
75 75 def test_anonymous_on_private_private
76 76 get :download, :id => 7
77 77 assert_redirected_to 'account/login'
78 78 end
79
80 def test_destroy_issue_attachment
81 issue = Issue.find(3)
82 @request.session[:user_id] = 2
83
84 assert_difference 'issue.attachments.count', -1 do
85 post :destroy, :id => 1
86 end
87 # no referrer
88 assert_redirected_to 'projects/show/ecookbook'
89 assert_nil Attachment.find_by_id(1)
90 j = issue.journals.find(:first, :order => 'created_on DESC')
91 assert_equal 'attachment', j.details.first.property
92 assert_equal '1', j.details.first.prop_key
93 assert_equal 'error281.txt', j.details.first.old_value
94 end
95
96 def test_destroy_wiki_page_attachment
97 @request.session[:user_id] = 2
98 assert_difference 'Attachment.count', -1 do
99 post :destroy, :id => 3
100 end
101 end
102
103 def test_destroy_without_permission
104 post :destroy, :id => 3
105 assert_redirected_to '/login'
106 assert Attachment.find_by_id(3)
107 end
79 108 end
@@ -1,729 +1,716
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 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :issues,
30 30 :issue_statuses,
31 31 :versions,
32 32 :trackers,
33 33 :projects_trackers,
34 34 :issue_categories,
35 35 :enabled_modules,
36 36 :enumerations,
37 37 :attachments,
38 38 :workflows,
39 39 :custom_fields,
40 40 :custom_values,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index
54 54 get :index
55 55 assert_response :success
56 56 assert_template 'index.rhtml'
57 57 assert_not_nil assigns(:issues)
58 58 assert_nil assigns(:project)
59 59 assert_tag :tag => 'a', :content => /Can't print recipes/
60 60 assert_tag :tag => 'a', :content => /Subproject issue/
61 61 # private projects hidden
62 62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
63 63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
64 64 end
65 65
66 66 def test_index_should_not_list_issues_when_module_disabled
67 67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
68 68 get :index
69 69 assert_response :success
70 70 assert_template 'index.rhtml'
71 71 assert_not_nil assigns(:issues)
72 72 assert_nil assigns(:project)
73 73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
74 74 assert_tag :tag => 'a', :content => /Subproject issue/
75 75 end
76 76
77 77 def test_index_with_project
78 78 Setting.display_subprojects_issues = 0
79 79 get :index, :project_id => 1
80 80 assert_response :success
81 81 assert_template 'index.rhtml'
82 82 assert_not_nil assigns(:issues)
83 83 assert_tag :tag => 'a', :content => /Can't print recipes/
84 84 assert_no_tag :tag => 'a', :content => /Subproject issue/
85 85 end
86 86
87 87 def test_index_with_project_and_subprojects
88 88 Setting.display_subprojects_issues = 1
89 89 get :index, :project_id => 1
90 90 assert_response :success
91 91 assert_template 'index.rhtml'
92 92 assert_not_nil assigns(:issues)
93 93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 94 assert_tag :tag => 'a', :content => /Subproject issue/
95 95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
96 96 end
97 97
98 98 def test_index_with_project_and_subprojects_should_show_private_subprojects
99 99 @request.session[:user_id] = 2
100 100 Setting.display_subprojects_issues = 1
101 101 get :index, :project_id => 1
102 102 assert_response :success
103 103 assert_template 'index.rhtml'
104 104 assert_not_nil assigns(:issues)
105 105 assert_tag :tag => 'a', :content => /Can't print recipes/
106 106 assert_tag :tag => 'a', :content => /Subproject issue/
107 107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
108 108 end
109 109
110 110 def test_index_with_project_and_filter
111 111 get :index, :project_id => 1, :set_filter => 1
112 112 assert_response :success
113 113 assert_template 'index.rhtml'
114 114 assert_not_nil assigns(:issues)
115 115 end
116 116
117 117 def test_index_csv_with_project
118 118 get :index, :format => 'csv'
119 119 assert_response :success
120 120 assert_not_nil assigns(:issues)
121 121 assert_equal 'text/csv', @response.content_type
122 122
123 123 get :index, :project_id => 1, :format => 'csv'
124 124 assert_response :success
125 125 assert_not_nil assigns(:issues)
126 126 assert_equal 'text/csv', @response.content_type
127 127 end
128 128
129 129 def test_index_pdf
130 130 get :index, :format => 'pdf'
131 131 assert_response :success
132 132 assert_not_nil assigns(:issues)
133 133 assert_equal 'application/pdf', @response.content_type
134 134
135 135 get :index, :project_id => 1, :format => 'pdf'
136 136 assert_response :success
137 137 assert_not_nil assigns(:issues)
138 138 assert_equal 'application/pdf', @response.content_type
139 139 end
140 140
141 141 def test_gantt
142 142 get :gantt, :project_id => 1
143 143 assert_response :success
144 144 assert_template 'gantt.rhtml'
145 145 assert_not_nil assigns(:gantt)
146 146 events = assigns(:gantt).events
147 147 assert_not_nil events
148 148 # Issue with start and due dates
149 149 i = Issue.find(1)
150 150 assert_not_nil i.due_date
151 151 assert events.include?(Issue.find(1))
152 152 # Issue with without due date but targeted to a version with date
153 153 i = Issue.find(2)
154 154 assert_nil i.due_date
155 155 assert events.include?(i)
156 156 end
157 157
158 158 def test_cross_project_gantt
159 159 get :gantt
160 160 assert_response :success
161 161 assert_template 'gantt.rhtml'
162 162 assert_not_nil assigns(:gantt)
163 163 events = assigns(:gantt).events
164 164 assert_not_nil events
165 165 end
166 166
167 167 def test_gantt_export_to_pdf
168 168 get :gantt, :project_id => 1, :format => 'pdf'
169 169 assert_response :success
170 170 assert_template 'gantt.rfpdf'
171 171 assert_equal 'application/pdf', @response.content_type
172 172 assert_not_nil assigns(:gantt)
173 173 end
174 174
175 175 def test_cross_project_gantt_export_to_pdf
176 176 get :gantt, :format => 'pdf'
177 177 assert_response :success
178 178 assert_template 'gantt.rfpdf'
179 179 assert_equal 'application/pdf', @response.content_type
180 180 assert_not_nil assigns(:gantt)
181 181 end
182 182
183 183 if Object.const_defined?(:Magick)
184 184 def test_gantt_image
185 185 get :gantt, :project_id => 1, :format => 'png'
186 186 assert_response :success
187 187 assert_equal 'image/png', @response.content_type
188 188 end
189 189 else
190 190 puts "RMagick not installed. Skipping tests !!!"
191 191 end
192 192
193 193 def test_calendar
194 194 get :calendar, :project_id => 1
195 195 assert_response :success
196 196 assert_template 'calendar'
197 197 assert_not_nil assigns(:calendar)
198 198 end
199 199
200 200 def test_cross_project_calendar
201 201 get :calendar
202 202 assert_response :success
203 203 assert_template 'calendar'
204 204 assert_not_nil assigns(:calendar)
205 205 end
206 206
207 207 def test_changes
208 208 get :changes, :project_id => 1
209 209 assert_response :success
210 210 assert_not_nil assigns(:journals)
211 211 assert_equal 'application/atom+xml', @response.content_type
212 212 end
213 213
214 214 def test_show_by_anonymous
215 215 get :show, :id => 1
216 216 assert_response :success
217 217 assert_template 'show.rhtml'
218 218 assert_not_nil assigns(:issue)
219 219 assert_equal Issue.find(1), assigns(:issue)
220 220
221 221 # anonymous role is allowed to add a note
222 222 assert_tag :tag => 'form',
223 223 :descendant => { :tag => 'fieldset',
224 224 :child => { :tag => 'legend',
225 225 :content => /Notes/ } }
226 226 end
227 227
228 228 def test_show_by_manager
229 229 @request.session[:user_id] = 2
230 230 get :show, :id => 1
231 231 assert_response :success
232 232
233 233 assert_tag :tag => 'form',
234 234 :descendant => { :tag => 'fieldset',
235 235 :child => { :tag => 'legend',
236 236 :content => /Change properties/ } },
237 237 :descendant => { :tag => 'fieldset',
238 238 :child => { :tag => 'legend',
239 239 :content => /Log time/ } },
240 240 :descendant => { :tag => 'fieldset',
241 241 :child => { :tag => 'legend',
242 242 :content => /Notes/ } }
243 243 end
244 244
245 245 def test_get_new
246 246 @request.session[:user_id] = 2
247 247 get :new, :project_id => 1, :tracker_id => 1
248 248 assert_response :success
249 249 assert_template 'new'
250 250
251 251 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
252 252 :value => 'Default string' }
253 253 end
254 254
255 255 def test_get_new_without_tracker_id
256 256 @request.session[:user_id] = 2
257 257 get :new, :project_id => 1
258 258 assert_response :success
259 259 assert_template 'new'
260 260
261 261 issue = assigns(:issue)
262 262 assert_not_nil issue
263 263 assert_equal Project.find(1).trackers.first, issue.tracker
264 264 end
265 265
266 266 def test_update_new_form
267 267 @request.session[:user_id] = 2
268 268 xhr :post, :new, :project_id => 1,
269 269 :issue => {:tracker_id => 2,
270 270 :subject => 'This is the test_new issue',
271 271 :description => 'This is the description',
272 272 :priority_id => 5}
273 273 assert_response :success
274 274 assert_template 'new'
275 275 end
276 276
277 277 def test_post_new
278 278 @request.session[:user_id] = 2
279 279 post :new, :project_id => 1,
280 280 :issue => {:tracker_id => 3,
281 281 :subject => 'This is the test_new issue',
282 282 :description => 'This is the description',
283 283 :priority_id => 5,
284 284 :estimated_hours => '',
285 285 :custom_field_values => {'2' => 'Value for field 2'}}
286 286 assert_redirected_to 'issues/show'
287 287
288 288 issue = Issue.find_by_subject('This is the test_new issue')
289 289 assert_not_nil issue
290 290 assert_equal 2, issue.author_id
291 291 assert_equal 3, issue.tracker_id
292 292 assert_nil issue.estimated_hours
293 293 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
294 294 assert_not_nil v
295 295 assert_equal 'Value for field 2', v.value
296 296 end
297 297
298 298 def test_post_new_without_custom_fields_param
299 299 @request.session[:user_id] = 2
300 300 post :new, :project_id => 1,
301 301 :issue => {:tracker_id => 1,
302 302 :subject => 'This is the test_new issue',
303 303 :description => 'This is the description',
304 304 :priority_id => 5}
305 305 assert_redirected_to 'issues/show'
306 306 end
307 307
308 308 def test_post_new_with_required_custom_field_and_without_custom_fields_param
309 309 field = IssueCustomField.find_by_name('Database')
310 310 field.update_attribute(:is_required, true)
311 311
312 312 @request.session[:user_id] = 2
313 313 post :new, :project_id => 1,
314 314 :issue => {:tracker_id => 1,
315 315 :subject => 'This is the test_new issue',
316 316 :description => 'This is the description',
317 317 :priority_id => 5}
318 318 assert_response :success
319 319 assert_template 'new'
320 320 issue = assigns(:issue)
321 321 assert_not_nil issue
322 322 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
323 323 end
324 324
325 325 def test_post_should_preserve_fields_values_on_validation_failure
326 326 @request.session[:user_id] = 2
327 327 post :new, :project_id => 1,
328 328 :issue => {:tracker_id => 1,
329 329 :subject => 'This is the test_new issue',
330 330 # empty description
331 331 :description => '',
332 332 :priority_id => 6,
333 333 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
334 334 assert_response :success
335 335 assert_template 'new'
336 336
337 337 assert_tag :input, :attributes => { :name => 'issue[subject]',
338 338 :value => 'This is the test_new issue' }
339 339 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
340 340 :child => { :tag => 'option', :attributes => { :selected => 'selected',
341 341 :value => '6' },
342 342 :content => 'High' }
343 343 # Custom fields
344 344 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
345 345 :child => { :tag => 'option', :attributes => { :selected => 'selected',
346 346 :value => 'Oracle' },
347 347 :content => 'Oracle' }
348 348 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
349 349 :value => 'Value for field 2'}
350 350 end
351 351
352 352 def test_copy_issue
353 353 @request.session[:user_id] = 2
354 354 get :new, :project_id => 1, :copy_from => 1
355 355 assert_template 'new'
356 356 assert_not_nil assigns(:issue)
357 357 orig = Issue.find(1)
358 358 assert_equal orig.subject, assigns(:issue).subject
359 359 end
360 360
361 361 def test_get_edit
362 362 @request.session[:user_id] = 2
363 363 get :edit, :id => 1
364 364 assert_response :success
365 365 assert_template 'edit'
366 366 assert_not_nil assigns(:issue)
367 367 assert_equal Issue.find(1), assigns(:issue)
368 368 end
369 369
370 370 def test_get_edit_with_params
371 371 @request.session[:user_id] = 2
372 372 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
373 373 assert_response :success
374 374 assert_template 'edit'
375 375
376 376 issue = assigns(:issue)
377 377 assert_not_nil issue
378 378
379 379 assert_equal 5, issue.status_id
380 380 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
381 381 :child => { :tag => 'option',
382 382 :content => 'Closed',
383 383 :attributes => { :selected => 'selected' } }
384 384
385 385 assert_equal 7, issue.priority_id
386 386 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
387 387 :child => { :tag => 'option',
388 388 :content => 'Urgent',
389 389 :attributes => { :selected => 'selected' } }
390 390 end
391 391
392 392 def test_reply_to_issue
393 393 @request.session[:user_id] = 2
394 394 get :reply, :id => 1
395 395 assert_response :success
396 396 assert_select_rjs :show, "update"
397 397 end
398 398
399 399 def test_reply_to_note
400 400 @request.session[:user_id] = 2
401 401 get :reply, :id => 1, :journal_id => 2
402 402 assert_response :success
403 403 assert_select_rjs :show, "update"
404 404 end
405 405
406 406 def test_post_edit_without_custom_fields_param
407 407 @request.session[:user_id] = 2
408 408 ActionMailer::Base.deliveries.clear
409 409
410 410 issue = Issue.find(1)
411 411 assert_equal '125', issue.custom_value_for(2).value
412 412 old_subject = issue.subject
413 413 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
414 414
415 415 assert_difference('Journal.count') do
416 416 assert_difference('JournalDetail.count', 2) do
417 417 post :edit, :id => 1, :issue => {:subject => new_subject,
418 418 :priority_id => '6',
419 419 :category_id => '1' # no change
420 420 }
421 421 end
422 422 end
423 423 assert_redirected_to 'issues/show/1'
424 424 issue.reload
425 425 assert_equal new_subject, issue.subject
426 426 # Make sure custom fields were not cleared
427 427 assert_equal '125', issue.custom_value_for(2).value
428 428
429 429 mail = ActionMailer::Base.deliveries.last
430 430 assert_kind_of TMail::Mail, mail
431 431 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
432 432 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
433 433 end
434 434
435 435 def test_post_edit_with_custom_field_change
436 436 @request.session[:user_id] = 2
437 437 issue = Issue.find(1)
438 438 assert_equal '125', issue.custom_value_for(2).value
439 439
440 440 assert_difference('Journal.count') do
441 441 assert_difference('JournalDetail.count', 3) do
442 442 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
443 443 :priority_id => '6',
444 444 :category_id => '1', # no change
445 445 :custom_field_values => { '2' => 'New custom value' }
446 446 }
447 447 end
448 448 end
449 449 assert_redirected_to 'issues/show/1'
450 450 issue.reload
451 451 assert_equal 'New custom value', issue.custom_value_for(2).value
452 452
453 453 mail = ActionMailer::Base.deliveries.last
454 454 assert_kind_of TMail::Mail, mail
455 455 assert mail.body.include?("Searchable field changed from 125 to New custom value")
456 456 end
457 457
458 458 def test_post_edit_with_status_and_assignee_change
459 459 issue = Issue.find(1)
460 460 assert_equal 1, issue.status_id
461 461 @request.session[:user_id] = 2
462 462 assert_difference('TimeEntry.count', 0) do
463 463 post :edit,
464 464 :id => 1,
465 465 :issue => { :status_id => 2, :assigned_to_id => 3 },
466 466 :notes => 'Assigned to dlopper',
467 467 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
468 468 end
469 469 assert_redirected_to 'issues/show/1'
470 470 issue.reload
471 471 assert_equal 2, issue.status_id
472 472 j = issue.journals.find(:first, :order => 'id DESC')
473 473 assert_equal 'Assigned to dlopper', j.notes
474 474 assert_equal 2, j.details.size
475 475
476 476 mail = ActionMailer::Base.deliveries.last
477 477 assert mail.body.include?("Status changed from New to Assigned")
478 478 end
479 479
480 480 def test_post_edit_with_note_only
481 481 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
482 482 # anonymous user
483 483 post :edit,
484 484 :id => 1,
485 485 :notes => notes
486 486 assert_redirected_to 'issues/show/1'
487 487 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
488 488 assert_equal notes, j.notes
489 489 assert_equal 0, j.details.size
490 490 assert_equal User.anonymous, j.user
491 491
492 492 mail = ActionMailer::Base.deliveries.last
493 493 assert mail.body.include?(notes)
494 494 end
495 495
496 496 def test_post_edit_with_note_and_spent_time
497 497 @request.session[:user_id] = 2
498 498 spent_hours_before = Issue.find(1).spent_hours
499 499 assert_difference('TimeEntry.count') do
500 500 post :edit,
501 501 :id => 1,
502 502 :notes => '2.5 hours added',
503 503 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
504 504 end
505 505 assert_redirected_to 'issues/show/1'
506 506
507 507 issue = Issue.find(1)
508 508
509 509 j = issue.journals.find(:first, :order => 'id DESC')
510 510 assert_equal '2.5 hours added', j.notes
511 511 assert_equal 0, j.details.size
512 512
513 513 t = issue.time_entries.find(:first, :order => 'id DESC')
514 514 assert_not_nil t
515 515 assert_equal 2.5, t.hours
516 516 assert_equal spent_hours_before + 2.5, issue.spent_hours
517 517 end
518 518
519 519 def test_post_edit_with_attachment_only
520 520 set_tmp_attachments_directory
521 521
522 522 # Delete all fixtured journals, a race condition can occur causing the wrong
523 523 # journal to get fetched in the next find.
524 524 Journal.delete_all
525 525
526 526 # anonymous user
527 527 post :edit,
528 528 :id => 1,
529 529 :notes => '',
530 530 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
531 531 assert_redirected_to 'issues/show/1'
532 532 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
533 533 assert j.notes.blank?
534 534 assert_equal 1, j.details.size
535 535 assert_equal 'testfile.txt', j.details.first.value
536 536 assert_equal User.anonymous, j.user
537 537
538 538 mail = ActionMailer::Base.deliveries.last
539 539 assert mail.body.include?('testfile.txt')
540 540 end
541 541
542 542 def test_post_edit_with_no_change
543 543 issue = Issue.find(1)
544 544 issue.journals.clear
545 545 ActionMailer::Base.deliveries.clear
546 546
547 547 post :edit,
548 548 :id => 1,
549 549 :notes => ''
550 550 assert_redirected_to 'issues/show/1'
551 551
552 552 issue.reload
553 553 assert issue.journals.empty?
554 554 # No email should be sent
555 555 assert ActionMailer::Base.deliveries.empty?
556 556 end
557 557
558 558 def test_bulk_edit
559 559 @request.session[:user_id] = 2
560 560 # update issues priority
561 561 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
562 562 assert_response 302
563 563 # check that the issues were updated
564 564 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
565 565 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
566 566 end
567 567
568 568 def test_bulk_unassign
569 569 assert_not_nil Issue.find(2).assigned_to
570 570 @request.session[:user_id] = 2
571 571 # unassign issues
572 572 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
573 573 assert_response 302
574 574 # check that the issues were updated
575 575 assert_nil Issue.find(2).assigned_to
576 576 end
577 577
578 578 def test_move_one_issue_to_another_project
579 579 @request.session[:user_id] = 1
580 580 post :move, :id => 1, :new_project_id => 2
581 581 assert_redirected_to 'projects/ecookbook/issues'
582 582 assert_equal 2, Issue.find(1).project_id
583 583 end
584 584
585 585 def test_bulk_move_to_another_project
586 586 @request.session[:user_id] = 1
587 587 post :move, :ids => [1, 2], :new_project_id => 2
588 588 assert_redirected_to 'projects/ecookbook/issues'
589 589 # Issues moved to project 2
590 590 assert_equal 2, Issue.find(1).project_id
591 591 assert_equal 2, Issue.find(2).project_id
592 592 # No tracker change
593 593 assert_equal 1, Issue.find(1).tracker_id
594 594 assert_equal 2, Issue.find(2).tracker_id
595 595 end
596 596
597 597 def test_bulk_move_to_another_tracker
598 598 @request.session[:user_id] = 1
599 599 post :move, :ids => [1, 2], :new_tracker_id => 2
600 600 assert_redirected_to 'projects/ecookbook/issues'
601 601 assert_equal 2, Issue.find(1).tracker_id
602 602 assert_equal 2, Issue.find(2).tracker_id
603 603 end
604 604
605 605 def test_context_menu_one_issue
606 606 @request.session[:user_id] = 2
607 607 get :context_menu, :ids => [1]
608 608 assert_response :success
609 609 assert_template 'context_menu'
610 610 assert_tag :tag => 'a', :content => 'Edit',
611 611 :attributes => { :href => '/issues/edit/1',
612 612 :class => 'icon-edit' }
613 613 assert_tag :tag => 'a', :content => 'Closed',
614 614 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
615 615 :class => '' }
616 616 assert_tag :tag => 'a', :content => 'Immediate',
617 617 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
618 618 :class => '' }
619 619 assert_tag :tag => 'a', :content => 'Dave Lopper',
620 620 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
621 621 :class => '' }
622 622 assert_tag :tag => 'a', :content => 'Copy',
623 623 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
624 624 :class => 'icon-copy' }
625 625 assert_tag :tag => 'a', :content => 'Move',
626 626 :attributes => { :href => '/issues/move?ids%5B%5D=1',
627 627 :class => 'icon-move' }
628 628 assert_tag :tag => 'a', :content => 'Delete',
629 629 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
630 630 :class => 'icon-del' }
631 631 end
632 632
633 633 def test_context_menu_one_issue_by_anonymous
634 634 get :context_menu, :ids => [1]
635 635 assert_response :success
636 636 assert_template 'context_menu'
637 637 assert_tag :tag => 'a', :content => 'Delete',
638 638 :attributes => { :href => '#',
639 639 :class => 'icon-del disabled' }
640 640 end
641 641
642 642 def test_context_menu_multiple_issues_of_same_project
643 643 @request.session[:user_id] = 2
644 644 get :context_menu, :ids => [1, 2]
645 645 assert_response :success
646 646 assert_template 'context_menu'
647 647 assert_tag :tag => 'a', :content => 'Edit',
648 648 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
649 649 :class => 'icon-edit' }
650 650 assert_tag :tag => 'a', :content => 'Immediate',
651 651 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
652 652 :class => '' }
653 653 assert_tag :tag => 'a', :content => 'Dave Lopper',
654 654 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
655 655 :class => '' }
656 656 assert_tag :tag => 'a', :content => 'Move',
657 657 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
658 658 :class => 'icon-move' }
659 659 assert_tag :tag => 'a', :content => 'Delete',
660 660 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
661 661 :class => 'icon-del' }
662 662 end
663 663
664 664 def test_context_menu_multiple_issues_of_different_project
665 665 @request.session[:user_id] = 2
666 666 get :context_menu, :ids => [1, 2, 4]
667 667 assert_response :success
668 668 assert_template 'context_menu'
669 669 assert_tag :tag => 'a', :content => 'Delete',
670 670 :attributes => { :href => '#',
671 671 :class => 'icon-del disabled' }
672 672 end
673 673
674 674 def test_destroy_issue_with_no_time_entries
675 675 assert_nil TimeEntry.find_by_issue_id(2)
676 676 @request.session[:user_id] = 2
677 677 post :destroy, :id => 2
678 678 assert_redirected_to 'projects/ecookbook/issues'
679 679 assert_nil Issue.find_by_id(2)
680 680 end
681 681
682 682 def test_destroy_issues_with_time_entries
683 683 @request.session[:user_id] = 2
684 684 post :destroy, :ids => [1, 3]
685 685 assert_response :success
686 686 assert_template 'destroy'
687 687 assert_not_nil assigns(:hours)
688 688 assert Issue.find_by_id(1) && Issue.find_by_id(3)
689 689 end
690 690
691 691 def test_destroy_issues_and_destroy_time_entries
692 692 @request.session[:user_id] = 2
693 693 post :destroy, :ids => [1, 3], :todo => 'destroy'
694 694 assert_redirected_to 'projects/ecookbook/issues'
695 695 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
696 696 assert_nil TimeEntry.find_by_id([1, 2])
697 697 end
698 698
699 699 def test_destroy_issues_and_assign_time_entries_to_project
700 700 @request.session[:user_id] = 2
701 701 post :destroy, :ids => [1, 3], :todo => 'nullify'
702 702 assert_redirected_to 'projects/ecookbook/issues'
703 703 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
704 704 assert_nil TimeEntry.find(1).issue_id
705 705 assert_nil TimeEntry.find(2).issue_id
706 706 end
707 707
708 708 def test_destroy_issues_and_reassign_time_entries_to_another_issue
709 709 @request.session[:user_id] = 2
710 710 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
711 711 assert_redirected_to 'projects/ecookbook/issues'
712 712 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
713 713 assert_equal 2, TimeEntry.find(1).issue_id
714 714 assert_equal 2, TimeEntry.find(2).issue_id
715 715 end
716
717 def test_destroy_attachment
718 issue = Issue.find(3)
719 a = issue.attachments.size
720 @request.session[:user_id] = 2
721 post :destroy_attachment, :id => 3, :attachment_id => 1
722 assert_redirected_to 'issues/show/3'
723 assert_nil Attachment.find_by_id(1)
724 issue.reload
725 assert_equal((a-1), issue.attachments.size)
726 j = issue.journals.find(:first, :order => 'created_on DESC')
727 assert_equal 'attachment', j.details.first.property
728 end
729 716 end
@@ -1,261 +1,254
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 File.dirname(__FILE__) + '/../test_helper'
19 19 require 'wiki_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class WikiController; def rescue_action(e) raise e end; end
23 23
24 24 class WikiControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
26 26
27 27 def setup
28 28 @controller = WikiController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_show_start_page
35 35 get :index, :id => 'ecookbook'
36 36 assert_response :success
37 37 assert_template 'show'
38 38 assert_tag :tag => 'h1', :content => /CookBook documentation/
39 39
40 40 # child_pages macro
41 41 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
42 42 :child => { :tag => 'li',
43 43 :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
44 44 :content => 'Page with an inline image' } }
45 45 end
46 46
47 47 def test_show_page_with_name
48 48 get :index, :id => 1, :page => 'Another_page'
49 49 assert_response :success
50 50 assert_template 'show'
51 51 assert_tag :tag => 'h1', :content => /Another page/
52 52 # Included page with an inline image
53 53 assert_tag :tag => 'p', :content => /This is an inline image/
54 54 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
55 55 :alt => 'This is a logo' }
56 56 end
57 57
58 58 def test_show_unexistent_page_without_edit_right
59 59 get :index, :id => 1, :page => 'Unexistent page'
60 60 assert_response 404
61 61 end
62 62
63 63 def test_show_unexistent_page_with_edit_right
64 64 @request.session[:user_id] = 2
65 65 get :index, :id => 1, :page => 'Unexistent page'
66 66 assert_response :success
67 67 assert_template 'edit'
68 68 end
69 69
70 70 def test_create_page
71 71 @request.session[:user_id] = 2
72 72 post :edit, :id => 1,
73 73 :page => 'New page',
74 74 :content => {:comments => 'Created the page',
75 75 :text => "h1. New page\n\nThis is a new page",
76 76 :version => 0}
77 77 assert_redirected_to 'wiki/ecookbook/New_page'
78 78 page = Project.find(1).wiki.find_page('New page')
79 79 assert !page.new_record?
80 80 assert_not_nil page.content
81 81 assert_equal 'Created the page', page.content.comments
82 82 end
83 83
84 84 def test_preview
85 85 @request.session[:user_id] = 2
86 86 xhr :post, :preview, :id => 1, :page => 'CookBook_documentation',
87 87 :content => { :comments => '',
88 88 :text => 'this is a *previewed text*',
89 89 :version => 3 }
90 90 assert_response :success
91 91 assert_template 'common/_preview'
92 92 assert_tag :tag => 'strong', :content => /previewed text/
93 93 end
94 94
95 95 def test_preview_new_page
96 96 @request.session[:user_id] = 2
97 97 xhr :post, :preview, :id => 1, :page => 'New page',
98 98 :content => { :text => 'h1. New page',
99 99 :comments => '',
100 100 :version => 0 }
101 101 assert_response :success
102 102 assert_template 'common/_preview'
103 103 assert_tag :tag => 'h1', :content => /New page/
104 104 end
105 105
106 106 def test_history
107 107 get :history, :id => 1, :page => 'CookBook_documentation'
108 108 assert_response :success
109 109 assert_template 'history'
110 110 assert_not_nil assigns(:versions)
111 111 assert_equal 3, assigns(:versions).size
112 112 assert_select "input[type=submit][name=commit]"
113 113 end
114 114
115 115 def test_history_with_one_version
116 116 get :history, :id => 1, :page => 'Another_page'
117 117 assert_response :success
118 118 assert_template 'history'
119 119 assert_not_nil assigns(:versions)
120 120 assert_equal 1, assigns(:versions).size
121 121 assert_select "input[type=submit][name=commit]", false
122 122 end
123 123
124 124 def test_diff
125 125 get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1
126 126 assert_response :success
127 127 assert_template 'diff'
128 128 assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
129 129 :content => /updated/
130 130 end
131 131
132 132 def test_annotate
133 133 get :annotate, :id => 1, :page => 'CookBook_documentation', :version => 2
134 134 assert_response :success
135 135 assert_template 'annotate'
136 136 # Line 1
137 137 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1' },
138 138 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/ },
139 139 :child => { :tag => 'td', :content => /h1\. CookBook documentation/ }
140 140 # Line 2
141 141 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '2' },
142 142 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
143 143 :child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
144 144 end
145 145
146 146 def test_rename_with_redirect
147 147 @request.session[:user_id] = 2
148 148 post :rename, :id => 1, :page => 'Another_page',
149 149 :wiki_page => { :title => 'Another renamed page',
150 150 :redirect_existing_links => 1 }
151 151 assert_redirected_to 'wiki/ecookbook/Another_renamed_page'
152 152 wiki = Project.find(1).wiki
153 153 # Check redirects
154 154 assert_not_nil wiki.find_page('Another page')
155 155 assert_nil wiki.find_page('Another page', :with_redirect => false)
156 156 end
157 157
158 158 def test_rename_without_redirect
159 159 @request.session[:user_id] = 2
160 160 post :rename, :id => 1, :page => 'Another_page',
161 161 :wiki_page => { :title => 'Another renamed page',
162 162 :redirect_existing_links => "0" }
163 163 assert_redirected_to 'wiki/ecookbook/Another_renamed_page'
164 164 wiki = Project.find(1).wiki
165 165 # Check that there's no redirects
166 166 assert_nil wiki.find_page('Another page')
167 167 end
168 168
169 169 def test_destroy
170 170 @request.session[:user_id] = 2
171 171 post :destroy, :id => 1, :page => 'CookBook_documentation'
172 172 assert_redirected_to 'wiki/ecookbook/Page_index/special'
173 173 end
174 174
175 175 def test_page_index
176 176 get :special, :id => 'ecookbook', :page => 'Page_index'
177 177 assert_response :success
178 178 assert_template 'special_page_index'
179 179 pages = assigns(:pages)
180 180 assert_not_nil pages
181 181 assert_equal Project.find(1).wiki.pages.size, pages.size
182 182
183 183 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
184 184 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' },
185 185 :content => 'CookBook documentation' },
186 186 :child => { :tag => 'ul',
187 187 :child => { :tag => 'li',
188 188 :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
189 189 :content => 'Page with an inline image' } } } },
190 190 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Another_page' },
191 191 :content => 'Another page' } }
192 192 end
193 193
194 194 def test_not_found
195 195 get :index, :id => 999
196 196 assert_response 404
197 197 end
198 198
199 199 def test_protect_page
200 200 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
201 201 assert !page.protected?
202 202 @request.session[:user_id] = 2
203 203 post :protect, :id => 1, :page => page.title, :protected => '1'
204 204 assert_redirected_to 'wiki/ecookbook/Another_page'
205 205 assert page.reload.protected?
206 206 end
207 207
208 208 def test_unprotect_page
209 209 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
210 210 assert page.protected?
211 211 @request.session[:user_id] = 2
212 212 post :protect, :id => 1, :page => page.title, :protected => '0'
213 213 assert_redirected_to 'wiki/ecookbook'
214 214 assert !page.reload.protected?
215 215 end
216 216
217 217 def test_show_page_with_edit_link
218 218 @request.session[:user_id] = 2
219 219 get :index, :id => 1
220 220 assert_response :success
221 221 assert_template 'show'
222 222 assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
223 223 end
224 224
225 225 def test_show_page_without_edit_link
226 226 @request.session[:user_id] = 4
227 227 get :index, :id => 1
228 228 assert_response :success
229 229 assert_template 'show'
230 230 assert_no_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
231 231 end
232 232
233 233 def test_edit_unprotected_page
234 234 # Non members can edit unprotected wiki pages
235 235 @request.session[:user_id] = 4
236 236 get :edit, :id => 1, :page => 'Another_page'
237 237 assert_response :success
238 238 assert_template 'edit'
239 239 end
240 240
241 241 def test_edit_protected_page_by_nonmember
242 242 # Non members can't edit protected wiki pages
243 243 @request.session[:user_id] = 4
244 244 get :edit, :id => 1, :page => 'CookBook_documentation'
245 245 assert_response 403
246 246 end
247 247
248 248 def test_edit_protected_page_by_member
249 249 @request.session[:user_id] = 2
250 250 get :edit, :id => 1, :page => 'CookBook_documentation'
251 251 assert_response :success
252 252 assert_template 'edit'
253 253 end
254
255 def test_destroy_attachment
256 @request.session[:user_id] = 2
257 assert_difference 'Attachment.count', -1 do
258 post :destroy_attachment, :id => 1, :page => 'Page_with_an_inline_image', :attachment_id => 3
259 end
260 end
261 254 end
General Comments 0
You need to be logged in to leave comments. Login now