##// END OF EJS Templates
Add view_issues permission (#3187)....
Jean-Philippe Lang -
r2925:dfd02040521b
parent child
Show More
@@ -0,0 +1,13
1 class AddViewIssuesPermission < ActiveRecord::Migration
2 def self.up
3 Role.find(:all).each do |r|
4 r.add_permission!(:view_issues)
5 end
6 end
7
8 def self.down
9 Role.find(:all).each do |r|
10 r.remove_permission!(:view_issues)
11 end
12 end
13 end
@@ -1,116 +1,116
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 SearchController < ApplicationController
19 19 before_filter :find_optional_project
20 20
21 21 helper :messages
22 22 include MessagesHelper
23 23
24 24 def index
25 25 @question = params[:q] || ""
26 26 @question.strip!
27 27 @all_words = params[:all_words] || (params[:submit] ? false : true)
28 28 @titles_only = !params[:titles_only].nil?
29 29
30 30 projects_to_search =
31 31 case params[:scope]
32 32 when 'all'
33 33 nil
34 34 when 'my_projects'
35 35 User.current.memberships.collect(&:project)
36 36 when 'subprojects'
37 37 @project ? (@project.self_and_descendants.active) : nil
38 38 else
39 39 @project
40 40 end
41 41
42 42 offset = nil
43 43 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
44 44
45 45 # quick jump to an issue
46 if @question.match(/^#?(\d+)$/) && Issue.find_by_id($1, :include => :project, :conditions => Project.visible_by(User.current))
46 if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1)
47 47 redirect_to :controller => "issues", :action => "show", :id => $1
48 48 return
49 49 end
50 50
51 51 @object_types = %w(issues news documents changesets wiki_pages messages projects)
52 52 if projects_to_search.is_a? Project
53 53 # don't search projects
54 54 @object_types.delete('projects')
55 55 # only show what the user is allowed to view
56 56 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
57 57 end
58 58
59 59 @scope = @object_types.select {|t| params[t]}
60 60 @scope = @object_types if @scope.empty?
61 61
62 62 # extract tokens from the question
63 63 # eg. hello "bye bye" => ["hello", "bye bye"]
64 64 @tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
65 65 # tokens must be at least 3 character long
66 66 @tokens = @tokens.uniq.select {|w| w.length > 2 }
67 67
68 68 if !@tokens.empty?
69 69 # no more than 5 tokens to search for
70 70 @tokens.slice! 5..-1 if @tokens.size > 5
71 71 # strings used in sql like statement
72 72 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
73 73
74 74 @results = []
75 75 @results_by_type = Hash.new {|h,k| h[k] = 0}
76 76
77 77 limit = 10
78 78 @scope.each do |s|
79 79 r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
80 80 :all_words => @all_words,
81 81 :titles_only => @titles_only,
82 82 :limit => (limit+1),
83 83 :offset => offset,
84 84 :before => params[:previous].nil?)
85 85 @results += r
86 86 @results_by_type[s] += c
87 87 end
88 88 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
89 89 if params[:previous].nil?
90 90 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
91 91 if @results.size > limit
92 92 @pagination_next_date = @results[limit-1].event_datetime
93 93 @results = @results[0, limit]
94 94 end
95 95 else
96 96 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
97 97 if @results.size > limit
98 98 @pagination_previous_date = @results[-(limit)].event_datetime
99 99 @results = @results[-(limit), limit]
100 100 end
101 101 end
102 102 else
103 103 @question = ""
104 104 end
105 105 render :layout => false if request.xhr?
106 106 end
107 107
108 108 private
109 109 def find_optional_project
110 110 return true unless params[:id]
111 111 @project = Project.find(params[:id])
112 112 check_project_privacy
113 113 rescue ActiveRecord::RecordNotFound
114 114 render_404
115 115 end
116 116 end
@@ -1,680 +1,680
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 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
38 38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
39 39 end
40 40
41 41 # Display a link to remote if user is authorized
42 42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
43 43 url = options[:url] || {}
44 44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
45 45 end
46 46
47 47 # Displays a link to user's account page if active
48 48 def link_to_user(user, options={})
49 49 if user.is_a?(User)
50 50 name = h(user.name(options[:format]))
51 51 if user.active?
52 52 link_to name, :controller => 'users', :action => 'show', :id => user
53 53 else
54 54 name
55 55 end
56 56 else
57 57 h(user.to_s)
58 58 end
59 59 end
60 60
61 61 def link_to_issue(issue, options={})
62 62 options[:class] ||= issue.css_classes
63 63 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
64 64 end
65 65
66 66 # Generates a link to an attachment.
67 67 # Options:
68 68 # * :text - Link text (default to attachment filename)
69 69 # * :download - Force download (default: false)
70 70 def link_to_attachment(attachment, options={})
71 71 text = options.delete(:text) || attachment.filename
72 72 action = options.delete(:download) ? 'download' : 'show'
73 73
74 74 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
75 75 end
76 76
77 77 def toggle_link(name, id, options={})
78 78 onclick = "Element.toggle('#{id}'); "
79 79 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
80 80 onclick << "return false;"
81 81 link_to(name, "#", :onclick => onclick)
82 82 end
83 83
84 84 def image_to_function(name, function, html_options = {})
85 85 html_options.symbolize_keys!
86 86 tag(:input, html_options.merge({
87 87 :type => "image", :src => image_path(name),
88 88 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
89 89 }))
90 90 end
91 91
92 92 def prompt_to_remote(name, text, param, url, html_options = {})
93 93 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
94 94 link_to name, {}, html_options
95 95 end
96 96
97 97 def format_activity_title(text)
98 98 h(truncate_single_line(text, :length => 100))
99 99 end
100 100
101 101 def format_activity_day(date)
102 102 date == Date.today ? l(:label_today).titleize : format_date(date)
103 103 end
104 104
105 105 def format_activity_description(text)
106 106 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
107 107 end
108 108
109 109 def due_date_distance_in_words(date)
110 110 if date
111 111 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
112 112 end
113 113 end
114 114
115 115 def render_page_hierarchy(pages, node=nil)
116 116 content = ''
117 117 if pages[node]
118 118 content << "<ul class=\"pages-hierarchy\">\n"
119 119 pages[node].each do |page|
120 120 content << "<li>"
121 121 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
122 122 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
123 123 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
124 124 content << "</li>\n"
125 125 end
126 126 content << "</ul>\n"
127 127 end
128 128 content
129 129 end
130 130
131 131 # Renders flash messages
132 132 def render_flash_messages
133 133 s = ''
134 134 flash.each do |k,v|
135 135 s << content_tag('div', v, :class => "flash #{k}")
136 136 end
137 137 s
138 138 end
139 139
140 140 # Renders tabs and their content
141 141 def render_tabs(tabs)
142 142 if tabs.any?
143 143 render :partial => 'common/tabs', :locals => {:tabs => tabs}
144 144 else
145 145 content_tag 'p', l(:label_no_data), :class => "nodata"
146 146 end
147 147 end
148 148
149 149 # Renders the project quick-jump box
150 150 def render_project_jump_box
151 151 # Retrieve them now to avoid a COUNT query
152 152 projects = User.current.projects.all
153 153 if projects.any?
154 154 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
155 155 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
156 156 '<option value="" disabled="disabled">---</option>'
157 157 s << project_tree_options_for_select(projects, :selected => @project) do |p|
158 158 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
159 159 end
160 160 s << '</select>'
161 161 s
162 162 end
163 163 end
164 164
165 165 def project_tree_options_for_select(projects, options = {})
166 166 s = ''
167 167 project_tree(projects) do |project, level|
168 168 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
169 169 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
170 170 tag_options.merge!(yield(project)) if block_given?
171 171 s << content_tag('option', name_prefix + h(project), tag_options)
172 172 end
173 173 s
174 174 end
175 175
176 176 # Yields the given block for each project with its level in the tree
177 177 def project_tree(projects, &block)
178 178 ancestors = []
179 179 projects.sort_by(&:lft).each do |project|
180 180 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
181 181 ancestors.pop
182 182 end
183 183 yield project, ancestors.size
184 184 ancestors << project
185 185 end
186 186 end
187 187
188 188 def project_nested_ul(projects, &block)
189 189 s = ''
190 190 if projects.any?
191 191 ancestors = []
192 192 projects.sort_by(&:lft).each do |project|
193 193 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
194 194 s << "<ul>\n"
195 195 else
196 196 ancestors.pop
197 197 s << "</li>"
198 198 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
199 199 ancestors.pop
200 200 s << "</ul></li>\n"
201 201 end
202 202 end
203 203 s << "<li>"
204 204 s << yield(project).to_s
205 205 ancestors << project
206 206 end
207 207 s << ("</li></ul>\n" * ancestors.size)
208 208 end
209 209 s
210 210 end
211 211
212 212 def principals_check_box_tags(name, principals)
213 213 s = ''
214 214 principals.sort.each do |principal|
215 215 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
216 216 end
217 217 s
218 218 end
219 219
220 220 # Truncates and returns the string as a single line
221 221 def truncate_single_line(string, *args)
222 222 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
223 223 end
224 224
225 225 def html_hours(text)
226 226 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
227 227 end
228 228
229 229 def authoring(created, author, options={})
230 230 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
231 231 end
232 232
233 233 def time_tag(time)
234 234 text = distance_of_time_in_words(Time.now, time)
235 235 if @project
236 236 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
237 237 else
238 238 content_tag('acronym', text, :title => format_time(time))
239 239 end
240 240 end
241 241
242 242 def syntax_highlight(name, content)
243 243 type = CodeRay::FileType[name]
244 244 type ? CodeRay.scan(content, type).html : h(content)
245 245 end
246 246
247 247 def to_path_param(path)
248 248 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
249 249 end
250 250
251 251 def pagination_links_full(paginator, count=nil, options={})
252 252 page_param = options.delete(:page_param) || :page
253 253 url_param = params.dup
254 254 # don't reuse query params if filters are present
255 255 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
256 256
257 257 html = ''
258 258 if paginator.current.previous
259 259 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
260 260 end
261 261
262 262 html << (pagination_links_each(paginator, options) do |n|
263 263 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
264 264 end || '')
265 265
266 266 if paginator.current.next
267 267 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
268 268 end
269 269
270 270 unless count.nil?
271 271 html << [
272 272 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
273 273 per_page_links(paginator.items_per_page)
274 274 ].compact.join(' | ')
275 275 end
276 276
277 277 html
278 278 end
279 279
280 280 def per_page_links(selected=nil)
281 281 url_param = params.dup
282 282 url_param.clear if url_param.has_key?(:set_filter)
283 283
284 284 links = Setting.per_page_options_array.collect do |n|
285 285 n == selected ? n : link_to_remote(n, {:update => "content",
286 286 :url => params.dup.merge(:per_page => n),
287 287 :method => :get},
288 288 {:href => url_for(url_param.merge(:per_page => n))})
289 289 end
290 290 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
291 291 end
292 292
293 293 def reorder_links(name, url)
294 294 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
295 295 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
296 296 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
297 297 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
298 298 end
299 299
300 300 def breadcrumb(*args)
301 301 elements = args.flatten
302 302 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
303 303 end
304 304
305 305 def other_formats_links(&block)
306 306 concat('<p class="other-formats">' + l(:label_export_to))
307 307 yield Redmine::Views::OtherFormatsBuilder.new(self)
308 308 concat('</p>')
309 309 end
310 310
311 311 def page_header_title
312 312 if @project.nil? || @project.new_record?
313 313 h(Setting.app_title)
314 314 else
315 315 b = []
316 316 ancestors = (@project.root? ? [] : @project.ancestors.visible)
317 317 if ancestors.any?
318 318 root = ancestors.shift
319 319 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
320 320 if ancestors.size > 2
321 321 b << '&#8230;'
322 322 ancestors = ancestors[-2, 2]
323 323 end
324 324 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
325 325 end
326 326 b << h(@project)
327 327 b.join(' &#187; ')
328 328 end
329 329 end
330 330
331 331 def html_title(*args)
332 332 if args.empty?
333 333 title = []
334 334 title << @project.name if @project
335 335 title += @html_title if @html_title
336 336 title << Setting.app_title
337 337 title.select {|t| !t.blank? }.join(' - ')
338 338 else
339 339 @html_title ||= []
340 340 @html_title += args
341 341 end
342 342 end
343 343
344 344 def accesskey(s)
345 345 Redmine::AccessKeys.key_for s
346 346 end
347 347
348 348 # Formats text according to system settings.
349 349 # 2 ways to call this method:
350 350 # * with a String: textilizable(text, options)
351 351 # * with an object and one of its attribute: textilizable(issue, :description, options)
352 352 def textilizable(*args)
353 353 options = args.last.is_a?(Hash) ? args.pop : {}
354 354 case args.size
355 355 when 1
356 356 obj = options[:object]
357 357 text = args.shift
358 358 when 2
359 359 obj = args.shift
360 360 text = obj.send(args.shift).to_s
361 361 else
362 362 raise ArgumentError, 'invalid arguments to textilizable'
363 363 end
364 364 return '' if text.blank?
365 365
366 366 only_path = options.delete(:only_path) == false ? false : true
367 367
368 368 # when using an image link, try to use an attachment, if possible
369 369 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
370 370
371 371 if attachments
372 372 attachments = attachments.sort_by(&:created_on).reverse
373 373 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
374 374 style = $1
375 375 filename = $6.downcase
376 376 # search for the picture in attachments
377 377 if found = attachments.detect { |att| att.filename.downcase == filename }
378 378 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
379 379 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
380 380 alt = desc.blank? ? nil : "(#{desc})"
381 381 "!#{style}#{image_url}#{alt}!"
382 382 else
383 383 m
384 384 end
385 385 end
386 386 end
387 387
388 388 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
389 389
390 390 # different methods for formatting wiki links
391 391 case options[:wiki_links]
392 392 when :local
393 393 # used for local links to html files
394 394 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
395 395 when :anchor
396 396 # used for single-file wiki export
397 397 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
398 398 else
399 399 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
400 400 end
401 401
402 402 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
403 403
404 404 # Wiki links
405 405 #
406 406 # Examples:
407 407 # [[mypage]]
408 408 # [[mypage|mytext]]
409 409 # wiki links can refer other project wikis, using project name or identifier:
410 410 # [[project:]] -> wiki starting page
411 411 # [[project:|mytext]]
412 412 # [[project:mypage]]
413 413 # [[project:mypage|mytext]]
414 414 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
415 415 link_project = project
416 416 esc, all, page, title = $1, $2, $3, $5
417 417 if esc.nil?
418 418 if page =~ /^([^\:]+)\:(.*)$/
419 419 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
420 420 page = $2
421 421 title ||= $1 if page.blank?
422 422 end
423 423
424 424 if link_project && link_project.wiki
425 425 # extract anchor
426 426 anchor = nil
427 427 if page =~ /^(.+?)\#(.+)$/
428 428 page, anchor = $1, $2
429 429 end
430 430 # check if page exists
431 431 wiki_page = link_project.wiki.find_page(page)
432 432 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
433 433 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
434 434 else
435 435 # project or wiki doesn't exist
436 436 all
437 437 end
438 438 else
439 439 all
440 440 end
441 441 end
442 442
443 443 # Redmine links
444 444 #
445 445 # Examples:
446 446 # Issues:
447 447 # #52 -> Link to issue #52
448 448 # Changesets:
449 449 # r52 -> Link to revision 52
450 450 # commit:a85130f -> Link to scmid starting with a85130f
451 451 # Documents:
452 452 # document#17 -> Link to document with id 17
453 453 # document:Greetings -> Link to the document with title "Greetings"
454 454 # document:"Some document" -> Link to the document with title "Some document"
455 455 # Versions:
456 456 # version#3 -> Link to version with id 3
457 457 # version:1.0.0 -> Link to version named "1.0.0"
458 458 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
459 459 # Attachments:
460 460 # attachment:file.zip -> Link to the attachment of the current object named file.zip
461 461 # Source files:
462 462 # source:some/file -> Link to the file located at /some/file in the project's repository
463 463 # source:some/file@52 -> Link to the file's revision 52
464 464 # source:some/file#L120 -> Link to line 120 of the file
465 465 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
466 466 # export:some/file -> Force the download of the file
467 467 # Forum messages:
468 468 # message#1218 -> Link to message with id 1218
469 469 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
470 470 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
471 471 link = nil
472 472 if esc.nil?
473 473 if prefix.nil? && sep == 'r'
474 474 if project && (changeset = project.changesets.find_by_revision(oid))
475 475 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
476 476 :class => 'changeset',
477 477 :title => truncate_single_line(changeset.comments, :length => 100))
478 478 end
479 479 elsif sep == '#'
480 480 oid = oid.to_i
481 481 case prefix
482 482 when nil
483 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
483 if issue = Issue.visible.find_by_id(oid, :include => :status)
484 484 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
485 485 :class => (issue.closed? ? 'issue closed' : 'issue'),
486 486 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
487 487 link = content_tag('del', link) if issue.closed?
488 488 end
489 489 when 'document'
490 490 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
491 491 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
492 492 :class => 'document'
493 493 end
494 494 when 'version'
495 495 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
496 496 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
497 497 :class => 'version'
498 498 end
499 499 when 'message'
500 500 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
501 501 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
502 502 :controller => 'messages',
503 503 :action => 'show',
504 504 :board_id => message.board,
505 505 :id => message.root,
506 506 :anchor => (message.parent ? "message-#{message.id}" : nil)},
507 507 :class => 'message'
508 508 end
509 509 end
510 510 elsif sep == ':'
511 511 # removes the double quotes if any
512 512 name = oid.gsub(%r{^"(.*)"$}, "\\1")
513 513 case prefix
514 514 when 'document'
515 515 if project && document = project.documents.find_by_title(name)
516 516 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
517 517 :class => 'document'
518 518 end
519 519 when 'version'
520 520 if project && version = project.versions.find_by_name(name)
521 521 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
522 522 :class => 'version'
523 523 end
524 524 when 'commit'
525 525 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
526 526 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
527 527 :class => 'changeset',
528 528 :title => truncate_single_line(changeset.comments, :length => 100)
529 529 end
530 530 when 'source', 'export'
531 531 if project && project.repository
532 532 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
533 533 path, rev, anchor = $1, $3, $5
534 534 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
535 535 :path => to_path_param(path),
536 536 :rev => rev,
537 537 :anchor => anchor,
538 538 :format => (prefix == 'export' ? 'raw' : nil)},
539 539 :class => (prefix == 'export' ? 'source download' : 'source')
540 540 end
541 541 when 'attachment'
542 542 if attachments && attachment = attachments.detect {|a| a.filename == name }
543 543 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
544 544 :class => 'attachment'
545 545 end
546 546 end
547 547 end
548 548 end
549 549 leading + (link || "#{prefix}#{sep}#{oid}")
550 550 end
551 551
552 552 text
553 553 end
554 554
555 555 # Same as Rails' simple_format helper without using paragraphs
556 556 def simple_format_without_paragraph(text)
557 557 text.to_s.
558 558 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
559 559 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
560 560 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
561 561 end
562 562
563 563 def lang_options_for_select(blank=true)
564 564 (blank ? [["(auto)", ""]] : []) +
565 565 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
566 566 end
567 567
568 568 def label_tag_for(name, option_tags = nil, options = {})
569 569 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
570 570 content_tag("label", label_text)
571 571 end
572 572
573 573 def labelled_tabular_form_for(name, object, options, &proc)
574 574 options[:html] ||= {}
575 575 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
576 576 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
577 577 end
578 578
579 579 def back_url_hidden_field_tag
580 580 back_url = params[:back_url] || request.env['HTTP_REFERER']
581 581 back_url = CGI.unescape(back_url.to_s)
582 582 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
583 583 end
584 584
585 585 def check_all_links(form_name)
586 586 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
587 587 " | " +
588 588 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
589 589 end
590 590
591 591 def progress_bar(pcts, options={})
592 592 pcts = [pcts, pcts] unless pcts.is_a?(Array)
593 593 pcts[1] = pcts[1] - pcts[0]
594 594 pcts << (100 - pcts[1] - pcts[0])
595 595 width = options[:width] || '100px;'
596 596 legend = options[:legend] || ''
597 597 content_tag('table',
598 598 content_tag('tr',
599 599 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
600 600 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
601 601 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
602 602 ), :class => 'progress', :style => "width: #{width};") +
603 603 content_tag('p', legend, :class => 'pourcent')
604 604 end
605 605
606 606 def context_menu_link(name, url, options={})
607 607 options[:class] ||= ''
608 608 if options.delete(:selected)
609 609 options[:class] << ' icon-checked disabled'
610 610 options[:disabled] = true
611 611 end
612 612 if options.delete(:disabled)
613 613 options.delete(:method)
614 614 options.delete(:confirm)
615 615 options.delete(:onclick)
616 616 options[:class] << ' disabled'
617 617 url = '#'
618 618 end
619 619 link_to name, url, options
620 620 end
621 621
622 622 def calendar_for(field_id)
623 623 include_calendar_headers_tags
624 624 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
625 625 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
626 626 end
627 627
628 628 def include_calendar_headers_tags
629 629 unless @calendar_headers_tags_included
630 630 @calendar_headers_tags_included = true
631 631 content_for :header_tags do
632 632 javascript_include_tag('calendar/calendar') +
633 633 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
634 634 javascript_include_tag('calendar/calendar-setup') +
635 635 stylesheet_link_tag('calendar')
636 636 end
637 637 end
638 638 end
639 639
640 640 def content_for(name, content = nil, &block)
641 641 @has_content ||= {}
642 642 @has_content[name] = true
643 643 super(name, content, &block)
644 644 end
645 645
646 646 def has_content?(name)
647 647 (@has_content && @has_content[name]) || false
648 648 end
649 649
650 650 # Returns the avatar image tag for the given +user+ if avatars are enabled
651 651 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
652 652 def avatar(user, options = { })
653 653 if Setting.gravatar_enabled?
654 654 options.merge!({:ssl => Setting.protocol == 'https'})
655 655 email = nil
656 656 if user.respond_to?(:mail)
657 657 email = user.mail
658 658 elsif user.to_s =~ %r{<(.+?)>}
659 659 email = $1
660 660 end
661 661 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
662 662 end
663 663 end
664 664
665 665 private
666 666
667 667 def wiki_helper
668 668 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
669 669 extend helper
670 670 return self
671 671 end
672 672
673 673 def link_to_remote_content_update(text, url_params)
674 674 link_to_remote(text,
675 675 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
676 676 {:href => url_for(:params => url_params)}
677 677 )
678 678 end
679 679
680 680 end
@@ -1,21 +1,21
1 1 <div class="contextual">
2 2 <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
3 <%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> |
3 <%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
4 4 <%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%>
5 5 </div>
6 6
7 7 <h2><%=l(:label_project_plural)%></h2>
8 8
9 9 <%= render_project_hierarchy(@projects)%>
10 10
11 11 <% if User.current.logged? %>
12 12 <p style="text-align:right;">
13 13 <span class="my-project"><%= l(:label_my_projects) %></span>
14 14 </p>
15 15 <% end %>
16 16
17 17 <% other_formats_links do |f| %>
18 18 <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
19 19 <% end %>
20 20
21 21 <% html_title(l(:label_project_plural)) -%>
@@ -1,59 +1,59
1 1 <div class="contextual">
2 2 &#171;
3 3 <% unless @changeset.previous.nil? -%>
4 4 <%= link_to l(:label_previous), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.previous.revision %>
5 5 <% else -%>
6 6 <%= l(:label_previous) %>
7 7 <% end -%>
8 8 |
9 9 <% unless @changeset.next.nil? -%>
10 10 <%= link_to l(:label_next), :controller => 'repositories', :action => 'revision', :id => @project, :rev => @changeset.next.revision %>
11 11 <% else -%>
12 12 <%= l(:label_next) %>
13 13 <% end -%>
14 14 &#187;&nbsp;
15 15
16 16 <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %>
17 17 <%= text_field_tag 'rev', @rev[0,8], :size => 8 %>
18 18 <%= submit_tag 'OK', :name => nil %>
19 19 <% end %>
20 20 </div>
21 21
22 22 <h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
23 23
24 24 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
25 25 <span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p>
26 26
27 27 <%= textilizable @changeset.comments %>
28 28
29 <% if @changeset.issues.any? %>
29 <% if @changeset.issues.visible.any? %>
30 30 <h3><%= l(:label_related_issues) %></h3>
31 31 <ul>
32 <% @changeset.issues.each do |issue| %>
32 <% @changeset.issues.visible.each do |issue| %>
33 33 <li><%= link_to_issue issue %>: <%=h issue.subject %></li>
34 34 <% end %>
35 35 </ul>
36 36 <% end %>
37 37
38 38 <% if User.current.allowed_to?(:browse_repository, @project) %>
39 39 <h3><%= l(:label_attachment_plural) %></h3>
40 40 <ul id="changes-legend">
41 41 <li class="change change-A"><%= l(:label_added) %></li>
42 42 <li class="change change-M"><%= l(:label_modified) %></li>
43 43 <li class="change change-C"><%= l(:label_copied) %></li>
44 44 <li class="change change-R"><%= l(:label_renamed) %></li>
45 45 <li class="change change-D"><%= l(:label_deleted) %></li>
46 46 </ul>
47 47
48 48 <p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
49 49
50 50 <div class="changeset-changes">
51 51 <%= render_changeset_changes %>
52 52 </div>
53 53 <% end %>
54 54
55 55 <% content_for :header_tags do %>
56 56 <%= stylesheet_link_tag "scm" %>
57 57 <% end %>
58 58
59 59 <% html_title("#{l(:label_revision)} #{@changeset.revision}") -%>
@@ -1,174 +1,174
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 if RUBY_VERSION < '1.9'
18 18 require 'faster_csv'
19 19 else
20 20 require 'csv'
21 21 FCSV = CSV
22 22 end
23 23
24 24 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
25 25
26 26 # Permissions
27 27 Redmine::AccessControl.map do |map|
28 28 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
29 29 map.permission :search_project, {:search => :index}, :public => true
30 30 map.permission :add_project, {:projects => :add}, :require => :loggedin
31 31 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
32 32 map.permission :select_project_modules, {:projects => :modules}, :require => :member
33 33 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
34 34 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :close_completed, :destroy]}, :require => :member
35 35
36 36 map.project_module :issue_tracking do |map|
37 37 # Issue categories
38 38 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
39 39 # Issues
40 40 map.permission :view_issues, {:projects => [:changelog, :roadmap],
41 41 :issues => [:index, :changes, :show, :context_menu],
42 42 :versions => [:show, :status_by],
43 43 :queries => :index,
44 :reports => :issue_report}, :public => true
44 :reports => :issue_report}
45 45 map.permission :add_issues, {:issues => :new}
46 46 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit]}
47 47 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
48 48 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
49 49 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
50 50 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
51 51 map.permission :move_issues, {:issues => :move}, :require => :loggedin
52 52 map.permission :delete_issues, {:issues => :destroy}, :require => :member
53 53 # Queries
54 54 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
55 55 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
56 56 # Gantt & calendar
57 57 map.permission :view_gantt, :issues => :gantt
58 58 map.permission :view_calendar, :issues => :calendar
59 59 # Watchers
60 60 map.permission :view_issue_watchers, {}
61 61 map.permission :add_issue_watchers, {:watchers => :new}
62 62 map.permission :delete_issue_watchers, {:watchers => :destroy}
63 63 end
64 64
65 65 map.project_module :time_tracking do |map|
66 66 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
67 67 map.permission :view_time_entries, :timelog => [:details, :report]
68 68 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
69 69 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
70 70 map.permission :manage_project_activities, {:projects => [:save_activities, :reset_activities]}, :require => :member
71 71 end
72 72
73 73 map.project_module :news do |map|
74 74 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
75 75 map.permission :view_news, {:news => [:index, :show]}, :public => true
76 76 map.permission :comment_news, {:news => :add_comment}
77 77 end
78 78
79 79 map.project_module :documents do |map|
80 80 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
81 81 map.permission :view_documents, :documents => [:index, :show, :download]
82 82 end
83 83
84 84 map.project_module :files do |map|
85 85 map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
86 86 map.permission :view_files, :projects => :list_files, :versions => :download
87 87 end
88 88
89 89 map.project_module :wiki do |map|
90 90 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
91 91 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
92 92 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
93 93 map.permission :view_wiki_pages, :wiki => [:index, :special]
94 94 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
95 95 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
96 96 map.permission :delete_wiki_pages_attachments, {}
97 97 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
98 98 end
99 99
100 100 map.project_module :repository do |map|
101 101 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
102 102 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
103 103 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
104 104 map.permission :commit_access, {}
105 105 end
106 106
107 107 map.project_module :boards do |map|
108 108 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
109 109 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
110 110 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
111 111 map.permission :edit_messages, {:messages => :edit}, :require => :member
112 112 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
113 113 map.permission :delete_messages, {:messages => :destroy}, :require => :member
114 114 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
115 115 end
116 116 end
117 117
118 118 Redmine::MenuManager.map :top_menu do |menu|
119 119 menu.push :home, :home_path
120 120 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
121 121 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
122 122 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
123 123 menu.push :help, Redmine::Info.help_url, :last => true
124 124 end
125 125
126 126 Redmine::MenuManager.map :account_menu do |menu|
127 127 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
128 128 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
129 129 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
130 130 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
131 131 end
132 132
133 133 Redmine::MenuManager.map :application_menu do |menu|
134 134 # Empty
135 135 end
136 136
137 137 Redmine::MenuManager.map :admin_menu do |menu|
138 138 # Empty
139 139 end
140 140
141 141 Redmine::MenuManager.map :project_menu do |menu|
142 142 menu.push :overview, { :controller => 'projects', :action => 'show' }
143 143 menu.push :activity, { :controller => 'projects', :action => 'activity' }
144 144 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
145 145 :if => Proc.new { |p| p.versions.any? }
146 146 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
147 147 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
148 148 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
149 149 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
150 150 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
151 151 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
152 152 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
153 153 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
154 154 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
155 155 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
156 156 menu.push :repository, { :controller => 'repositories', :action => 'show' },
157 157 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
158 158 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
159 159 end
160 160
161 161 Redmine::Activity.map do |activity|
162 162 activity.register :issues, :class_name => %w(Issue Journal)
163 163 activity.register :changesets
164 164 activity.register :news
165 165 activity.register :documents, :class_name => %w(Document Attachment)
166 166 activity.register :files, :class_name => 'Attachment'
167 167 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
168 168 activity.register :messages, :default => false
169 169 activity.register :time_entries, :default => false
170 170 end
171 171
172 172 Redmine::WikiFormatting.map do |format|
173 173 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
174 174 end
@@ -1,176 +1,180
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 Redmine
19 19 module DefaultData
20 20 class DataAlreadyLoaded < Exception; end
21 21
22 22 module Loader
23 23 include Redmine::I18n
24 24
25 25 class << self
26 26 # Returns true if no data is already loaded in the database
27 27 # otherwise false
28 28 def no_data?
29 29 !Role.find(:first, :conditions => {:builtin => 0}) &&
30 30 !Tracker.find(:first) &&
31 31 !IssueStatus.find(:first) &&
32 32 !Enumeration.find(:first)
33 33 end
34 34
35 35 # Loads the default data
36 36 # Raises a RecordNotSaved exception if something goes wrong
37 37 def load(lang=nil)
38 38 raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
39 39 set_language_if_valid(lang)
40 40
41 41 Role.transaction do
42 42 # Roles
43 43 manager = Role.create! :name => l(:default_role_manager),
44 44 :position => 1
45 45 manager.permissions = manager.setable_permissions.collect {|p| p.name}
46 46 manager.save!
47 47
48 48 developper = Role.create! :name => l(:default_role_developper),
49 49 :position => 2,
50 50 :permissions => [:manage_versions,
51 51 :manage_categories,
52 :view_issues,
52 53 :add_issues,
53 54 :edit_issues,
54 55 :manage_issue_relations,
55 56 :add_issue_notes,
56 57 :save_queries,
57 58 :view_gantt,
58 59 :view_calendar,
59 60 :log_time,
60 61 :view_time_entries,
61 62 :comment_news,
62 63 :view_documents,
63 64 :view_wiki_pages,
64 65 :view_wiki_edits,
65 66 :edit_wiki_pages,
66 67 :delete_wiki_pages,
67 68 :add_messages,
68 69 :edit_own_messages,
69 70 :view_files,
70 71 :manage_files,
71 72 :browse_repository,
72 73 :view_changesets,
73 74 :commit_access]
74 75
75 76 reporter = Role.create! :name => l(:default_role_reporter),
76 77 :position => 3,
77 :permissions => [:add_issues,
78 :permissions => [:view_issues,
79 :add_issues,
78 80 :add_issue_notes,
79 81 :save_queries,
80 82 :view_gantt,
81 83 :view_calendar,
82 84 :log_time,
83 85 :view_time_entries,
84 86 :comment_news,
85 87 :view_documents,
86 88 :view_wiki_pages,
87 89 :view_wiki_edits,
88 90 :add_messages,
89 91 :edit_own_messages,
90 92 :view_files,
91 93 :browse_repository,
92 94 :view_changesets]
93 95
94 Role.non_member.update_attribute :permissions, [:add_issues,
96 Role.non_member.update_attribute :permissions, [:view_issues,
97 :add_issues,
95 98 :add_issue_notes,
96 99 :save_queries,
97 100 :view_gantt,
98 101 :view_calendar,
99 102 :view_time_entries,
100 103 :comment_news,
101 104 :view_documents,
102 105 :view_wiki_pages,
103 106 :view_wiki_edits,
104 107 :add_messages,
105 108 :view_files,
106 109 :browse_repository,
107 110 :view_changesets]
108 111
109 Role.anonymous.update_attribute :permissions, [:view_gantt,
112 Role.anonymous.update_attribute :permissions, [:view_issues,
113 :view_gantt,
110 114 :view_calendar,
111 115 :view_time_entries,
112 116 :view_documents,
113 117 :view_wiki_pages,
114 118 :view_wiki_edits,
115 119 :view_files,
116 120 :browse_repository,
117 121 :view_changesets]
118 122
119 123 # Trackers
120 124 Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1)
121 125 Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2)
122 126 Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
123 127
124 128 # Issue statuses
125 129 new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
126 130 in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2)
127 131 resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
128 132 feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
129 133 closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
130 134 rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
131 135
132 136 # Workflow
133 137 Tracker.find(:all).each { |t|
134 138 IssueStatus.find(:all).each { |os|
135 139 IssueStatus.find(:all).each { |ns|
136 140 Workflow.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
137 141 }
138 142 }
139 143 }
140 144
141 145 Tracker.find(:all).each { |t|
142 146 [new, in_progress, resolved, feedback].each { |os|
143 147 [in_progress, resolved, feedback, closed].each { |ns|
144 148 Workflow.create!(:tracker_id => t.id, :role_id => developper.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
145 149 }
146 150 }
147 151 }
148 152
149 153 Tracker.find(:all).each { |t|
150 154 [new, in_progress, resolved, feedback].each { |os|
151 155 [closed].each { |ns|
152 156 Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
153 157 }
154 158 }
155 159 Workflow.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
156 160 }
157 161
158 162 # Enumerations
159 163 DocumentCategory.create!(:opt => "DCAT", :name => l(:default_doc_category_user), :position => 1)
160 164 DocumentCategory.create!(:opt => "DCAT", :name => l(:default_doc_category_tech), :position => 2)
161 165
162 166 IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_low), :position => 1)
163 167 IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_normal), :position => 2, :is_default => true)
164 168 IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_high), :position => 3)
165 169 IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_urgent), :position => 4)
166 170 IssuePriority.create!(:opt => "IPRI", :name => l(:default_priority_immediate), :position => 5)
167 171
168 172 TimeEntryActivity.create!(:opt => "ACTI", :name => l(:default_activity_design), :position => 1)
169 173 TimeEntryActivity.create!(:opt => "ACTI", :name => l(:default_activity_development), :position => 2)
170 174 end
171 175 true
172 176 end
173 177 end
174 178 end
175 179 end
176 180 end
@@ -1,179 +1,184
1 1 ---
2 2 roles_001:
3 3 name: Manager
4 4 id: 1
5 5 builtin: 0
6 6 permissions: |
7 7 ---
8 8 - :add_project
9 9 - :edit_project
10 10 - :manage_members
11 11 - :manage_versions
12 12 - :manage_categories
13 - :view_issues
13 14 - :add_issues
14 15 - :edit_issues
15 16 - :manage_issue_relations
16 17 - :add_issue_notes
17 18 - :move_issues
18 19 - :delete_issues
19 20 - :view_issue_watchers
20 21 - :add_issue_watchers
21 22 - :delete_issue_watchers
22 23 - :manage_public_queries
23 24 - :save_queries
24 25 - :view_gantt
25 26 - :view_calendar
26 27 - :log_time
27 28 - :view_time_entries
28 29 - :edit_time_entries
29 30 - :delete_time_entries
30 31 - :manage_news
31 32 - :comment_news
32 33 - :view_documents
33 34 - :manage_documents
34 35 - :view_wiki_pages
35 36 - :view_wiki_edits
36 37 - :edit_wiki_pages
37 38 - :delete_wiki_pages_attachments
38 39 - :protect_wiki_pages
39 40 - :delete_wiki_pages
40 41 - :rename_wiki_pages
41 42 - :add_messages
42 43 - :edit_messages
43 44 - :delete_messages
44 45 - :manage_boards
45 46 - :view_files
46 47 - :manage_files
47 48 - :browse_repository
48 49 - :manage_repository
49 50 - :view_changesets
50 51 - :manage_project_activities
51 52
52 53 position: 1
53 54 roles_002:
54 55 name: Developer
55 56 id: 2
56 57 builtin: 0
57 58 permissions: |
58 59 ---
59 60 - :edit_project
60 61 - :manage_members
61 62 - :manage_versions
62 63 - :manage_categories
64 - :view_issues
63 65 - :add_issues
64 66 - :edit_issues
65 67 - :manage_issue_relations
66 68 - :add_issue_notes
67 69 - :move_issues
68 70 - :delete_issues
69 71 - :view_issue_watchers
70 72 - :save_queries
71 73 - :view_gantt
72 74 - :view_calendar
73 75 - :log_time
74 76 - :view_time_entries
75 77 - :edit_own_time_entries
76 78 - :manage_news
77 79 - :comment_news
78 80 - :view_documents
79 81 - :manage_documents
80 82 - :view_wiki_pages
81 83 - :view_wiki_edits
82 84 - :edit_wiki_pages
83 85 - :protect_wiki_pages
84 86 - :delete_wiki_pages
85 87 - :add_messages
86 88 - :edit_own_messages
87 89 - :delete_own_messages
88 90 - :manage_boards
89 91 - :view_files
90 92 - :manage_files
91 93 - :browse_repository
92 94 - :view_changesets
93 95
94 96 position: 2
95 97 roles_003:
96 98 name: Reporter
97 99 id: 3
98 100 builtin: 0
99 101 permissions: |
100 102 ---
101 103 - :edit_project
102 104 - :manage_members
103 105 - :manage_versions
104 106 - :manage_categories
107 - :view_issues
105 108 - :add_issues
106 109 - :edit_issues
107 110 - :manage_issue_relations
108 111 - :add_issue_notes
109 112 - :move_issues
110 113 - :view_issue_watchers
111 114 - :save_queries
112 115 - :view_gantt
113 116 - :view_calendar
114 117 - :log_time
115 118 - :view_time_entries
116 119 - :manage_news
117 120 - :comment_news
118 121 - :view_documents
119 122 - :manage_documents
120 123 - :view_wiki_pages
121 124 - :view_wiki_edits
122 125 - :edit_wiki_pages
123 126 - :delete_wiki_pages
124 127 - :add_messages
125 128 - :manage_boards
126 129 - :view_files
127 130 - :manage_files
128 131 - :browse_repository
129 132 - :view_changesets
130 133
131 134 position: 3
132 135 roles_004:
133 136 name: Non member
134 137 id: 4
135 138 builtin: 1
136 139 permissions: |
137 140 ---
141 - :view_issues
138 142 - :add_issues
139 143 - :edit_issues
140 144 - :manage_issue_relations
141 145 - :add_issue_notes
142 146 - :move_issues
143 147 - :save_queries
144 148 - :view_gantt
145 149 - :view_calendar
146 150 - :log_time
147 151 - :view_time_entries
148 152 - :comment_news
149 153 - :view_documents
150 154 - :manage_documents
151 155 - :view_wiki_pages
152 156 - :view_wiki_edits
153 157 - :edit_wiki_pages
154 158 - :add_messages
155 159 - :view_files
156 160 - :manage_files
157 161 - :browse_repository
158 162 - :view_changesets
159 163
160 164 position: 4
161 165 roles_005:
162 166 name: Anonymous
163 167 id: 5
164 168 builtin: 2
165 169 permissions: |
166 170 ---
171 - :view_issues
167 172 - :add_issue_notes
168 173 - :view_gantt
169 174 - :view_calendar
170 175 - :view_time_entries
171 176 - :view_documents
172 177 - :view_wiki_pages
173 178 - :view_wiki_edits
174 179 - :view_files
175 180 - :browse_repository
176 181 - :view_changesets
177 182
178 183 position: 5
179 184
@@ -1,1106 +1,1126
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 < ActionController::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_trackers,
43 43 :time_entries,
44 44 :journals,
45 45 :journal_details,
46 46 :queries
47 47
48 48 def setup
49 49 @controller = IssuesController.new
50 50 @request = ActionController::TestRequest.new
51 51 @response = ActionController::TestResponse.new
52 52 User.current = nil
53 53 end
54 54
55 55 def test_index_routing
56 56 assert_routing(
57 57 {:method => :get, :path => '/issues'},
58 58 :controller => 'issues', :action => 'index'
59 59 )
60 60 end
61 61
62 62 def test_index
63 63 Setting.default_language = 'en'
64 64
65 65 get :index
66 66 assert_response :success
67 67 assert_template 'index.rhtml'
68 68 assert_not_nil assigns(:issues)
69 69 assert_nil assigns(:project)
70 70 assert_tag :tag => 'a', :content => /Can't print recipes/
71 71 assert_tag :tag => 'a', :content => /Subproject issue/
72 72 # private projects hidden
73 73 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
74 74 assert_no_tag :tag => 'a', :content => /Issue on project 2/
75 75 # project column
76 76 assert_tag :tag => 'th', :content => /Project/
77 77 end
78 78
79 79 def test_index_should_not_list_issues_when_module_disabled
80 80 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
81 81 get :index
82 82 assert_response :success
83 83 assert_template 'index.rhtml'
84 84 assert_not_nil assigns(:issues)
85 85 assert_nil assigns(:project)
86 86 assert_no_tag :tag => 'a', :content => /Can't print recipes/
87 87 assert_tag :tag => 'a', :content => /Subproject issue/
88 88 end
89 89
90 90 def test_index_with_project_routing
91 91 assert_routing(
92 92 {:method => :get, :path => '/projects/23/issues'},
93 93 :controller => 'issues', :action => 'index', :project_id => '23'
94 94 )
95 95 end
96 96
97 97 def test_index_should_not_list_issues_when_module_disabled
98 98 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
99 99 get :index
100 100 assert_response :success
101 101 assert_template 'index.rhtml'
102 102 assert_not_nil assigns(:issues)
103 103 assert_nil assigns(:project)
104 104 assert_no_tag :tag => 'a', :content => /Can't print recipes/
105 105 assert_tag :tag => 'a', :content => /Subproject issue/
106 106 end
107 107
108 108 def test_index_with_project_routing
109 109 assert_routing(
110 110 {:method => :get, :path => 'projects/23/issues'},
111 111 :controller => 'issues', :action => 'index', :project_id => '23'
112 112 )
113 113 end
114 114
115 115 def test_index_with_project
116 116 Setting.display_subprojects_issues = 0
117 117 get :index, :project_id => 1
118 118 assert_response :success
119 119 assert_template 'index.rhtml'
120 120 assert_not_nil assigns(:issues)
121 121 assert_tag :tag => 'a', :content => /Can't print recipes/
122 122 assert_no_tag :tag => 'a', :content => /Subproject issue/
123 123 end
124 124
125 125 def test_index_with_project_and_subprojects
126 126 Setting.display_subprojects_issues = 1
127 127 get :index, :project_id => 1
128 128 assert_response :success
129 129 assert_template 'index.rhtml'
130 130 assert_not_nil assigns(:issues)
131 131 assert_tag :tag => 'a', :content => /Can't print recipes/
132 132 assert_tag :tag => 'a', :content => /Subproject issue/
133 133 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
134 134 end
135 135
136 136 def test_index_with_project_and_subprojects_should_show_private_subprojects
137 137 @request.session[:user_id] = 2
138 138 Setting.display_subprojects_issues = 1
139 139 get :index, :project_id => 1
140 140 assert_response :success
141 141 assert_template 'index.rhtml'
142 142 assert_not_nil assigns(:issues)
143 143 assert_tag :tag => 'a', :content => /Can't print recipes/
144 144 assert_tag :tag => 'a', :content => /Subproject issue/
145 145 assert_tag :tag => 'a', :content => /Issue of a private subproject/
146 146 end
147 147
148 148 def test_index_with_project_routing_formatted
149 149 assert_routing(
150 150 {:method => :get, :path => 'projects/23/issues.pdf'},
151 151 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
152 152 )
153 153 assert_routing(
154 154 {:method => :get, :path => 'projects/23/issues.atom'},
155 155 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
156 156 )
157 157 end
158 158
159 159 def test_index_with_project_and_filter
160 160 get :index, :project_id => 1, :set_filter => 1
161 161 assert_response :success
162 162 assert_template 'index.rhtml'
163 163 assert_not_nil assigns(:issues)
164 164 end
165 165
166 166 def test_index_with_query
167 167 get :index, :project_id => 1, :query_id => 5
168 168 assert_response :success
169 169 assert_template 'index.rhtml'
170 170 assert_not_nil assigns(:issues)
171 171 assert_nil assigns(:issue_count_by_group)
172 172 end
173 173
174 174 def test_index_with_grouped_query
175 175 get :index, :project_id => 1, :query_id => 6
176 176 assert_response :success
177 177 assert_template 'index.rhtml'
178 178 assert_not_nil assigns(:issues)
179 179 assert_not_nil assigns(:issue_count_by_group)
180 180 end
181 181
182 182 def test_index_csv_with_project
183 183 Setting.default_language = 'en'
184 184
185 185 get :index, :format => 'csv'
186 186 assert_response :success
187 187 assert_not_nil assigns(:issues)
188 188 assert_equal 'text/csv', @response.content_type
189 189 assert @response.body.starts_with?("#,")
190 190
191 191 get :index, :project_id => 1, :format => 'csv'
192 192 assert_response :success
193 193 assert_not_nil assigns(:issues)
194 194 assert_equal 'text/csv', @response.content_type
195 195 end
196 196
197 197 def test_index_formatted
198 198 assert_routing(
199 199 {:method => :get, :path => 'issues.pdf'},
200 200 :controller => 'issues', :action => 'index', :format => 'pdf'
201 201 )
202 202 assert_routing(
203 203 {:method => :get, :path => 'issues.atom'},
204 204 :controller => 'issues', :action => 'index', :format => 'atom'
205 205 )
206 206 end
207 207
208 208 def test_index_pdf
209 209 get :index, :format => 'pdf'
210 210 assert_response :success
211 211 assert_not_nil assigns(:issues)
212 212 assert_equal 'application/pdf', @response.content_type
213 213
214 214 get :index, :project_id => 1, :format => 'pdf'
215 215 assert_response :success
216 216 assert_not_nil assigns(:issues)
217 217 assert_equal 'application/pdf', @response.content_type
218 218
219 219 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
220 220 assert_response :success
221 221 assert_not_nil assigns(:issues)
222 222 assert_equal 'application/pdf', @response.content_type
223 223 end
224 224
225 225 def test_index_sort
226 226 get :index, :sort => 'tracker,id:desc'
227 227 assert_response :success
228 228
229 229 sort_params = @request.session['issues_index_sort']
230 230 assert sort_params.is_a?(String)
231 231 assert_equal 'tracker,id:desc', sort_params
232 232
233 233 issues = assigns(:issues)
234 234 assert_not_nil issues
235 235 assert !issues.empty?
236 236 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
237 237 end
238 238
239 239 def test_gantt
240 240 get :gantt, :project_id => 1
241 241 assert_response :success
242 242 assert_template 'gantt.rhtml'
243 243 assert_not_nil assigns(:gantt)
244 244 events = assigns(:gantt).events
245 245 assert_not_nil events
246 246 # Issue with start and due dates
247 247 i = Issue.find(1)
248 248 assert_not_nil i.due_date
249 249 assert events.include?(Issue.find(1))
250 250 # Issue with without due date but targeted to a version with date
251 251 i = Issue.find(2)
252 252 assert_nil i.due_date
253 253 assert events.include?(i)
254 254 end
255 255
256 256 def test_cross_project_gantt
257 257 get :gantt
258 258 assert_response :success
259 259 assert_template 'gantt.rhtml'
260 260 assert_not_nil assigns(:gantt)
261 261 events = assigns(:gantt).events
262 262 assert_not_nil events
263 263 end
264 264
265 265 def test_gantt_export_to_pdf
266 266 get :gantt, :project_id => 1, :format => 'pdf'
267 267 assert_response :success
268 268 assert_equal 'application/pdf', @response.content_type
269 269 assert @response.body.starts_with?('%PDF')
270 270 assert_not_nil assigns(:gantt)
271 271 end
272 272
273 273 def test_cross_project_gantt_export_to_pdf
274 274 get :gantt, :format => 'pdf'
275 275 assert_response :success
276 276 assert_equal 'application/pdf', @response.content_type
277 277 assert @response.body.starts_with?('%PDF')
278 278 assert_not_nil assigns(:gantt)
279 279 end
280 280
281 281 if Object.const_defined?(:Magick)
282 282 def test_gantt_image
283 283 get :gantt, :project_id => 1, :format => 'png'
284 284 assert_response :success
285 285 assert_equal 'image/png', @response.content_type
286 286 end
287 287 else
288 288 puts "RMagick not installed. Skipping tests !!!"
289 289 end
290 290
291 291 def test_calendar
292 292 get :calendar, :project_id => 1
293 293 assert_response :success
294 294 assert_template 'calendar'
295 295 assert_not_nil assigns(:calendar)
296 296 end
297 297
298 298 def test_cross_project_calendar
299 299 get :calendar
300 300 assert_response :success
301 301 assert_template 'calendar'
302 302 assert_not_nil assigns(:calendar)
303 303 end
304 304
305 305 def test_changes
306 306 get :changes, :project_id => 1
307 307 assert_response :success
308 308 assert_not_nil assigns(:journals)
309 309 assert_equal 'application/atom+xml', @response.content_type
310 310 end
311 311
312 312 def test_show_routing
313 313 assert_routing(
314 314 {:method => :get, :path => '/issues/64'},
315 315 :controller => 'issues', :action => 'show', :id => '64'
316 316 )
317 317 end
318 318
319 319 def test_show_routing_formatted
320 320 assert_routing(
321 321 {:method => :get, :path => '/issues/2332.pdf'},
322 322 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
323 323 )
324 324 assert_routing(
325 325 {:method => :get, :path => '/issues/23123.atom'},
326 326 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
327 327 )
328 328 end
329 329
330 330 def test_show_by_anonymous
331 331 get :show, :id => 1
332 332 assert_response :success
333 333 assert_template 'show.rhtml'
334 334 assert_not_nil assigns(:issue)
335 335 assert_equal Issue.find(1), assigns(:issue)
336 336
337 337 # anonymous role is allowed to add a note
338 338 assert_tag :tag => 'form',
339 339 :descendant => { :tag => 'fieldset',
340 340 :child => { :tag => 'legend',
341 341 :content => /Notes/ } }
342 342 end
343 343
344 344 def test_show_by_manager
345 345 @request.session[:user_id] = 2
346 346 get :show, :id => 1
347 347 assert_response :success
348 348
349 349 assert_tag :tag => 'form',
350 350 :descendant => { :tag => 'fieldset',
351 351 :child => { :tag => 'legend',
352 352 :content => /Change properties/ } },
353 353 :descendant => { :tag => 'fieldset',
354 354 :child => { :tag => 'legend',
355 355 :content => /Log time/ } },
356 356 :descendant => { :tag => 'fieldset',
357 357 :child => { :tag => 'legend',
358 358 :content => /Notes/ } }
359 359 end
360 360
361 def test_show_should_deny_anonymous_access_without_permission
362 Role.anonymous.remove_permission!(:view_issues)
363 get :show, :id => 1
364 assert_response :redirect
365 end
366
367 def test_show_should_deny_non_member_access_without_permission
368 Role.non_member.remove_permission!(:view_issues)
369 @request.session[:user_id] = 9
370 get :show, :id => 1
371 assert_response 403
372 end
373
374 def test_show_should_deny_member_access_without_permission
375 Role.find(1).remove_permission!(:view_issues)
376 @request.session[:user_id] = 2
377 get :show, :id => 1
378 assert_response 403
379 end
380
361 381 def test_show_should_not_disclose_relations_to_invisible_issues
362 382 Setting.cross_project_issue_relations = '1'
363 383 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
364 384 # Relation to a private project issue
365 385 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
366 386
367 387 get :show, :id => 1
368 388 assert_response :success
369 389
370 390 assert_tag :div, :attributes => { :id => 'relations' },
371 391 :descendant => { :tag => 'a', :content => /#2$/ }
372 392 assert_no_tag :div, :attributes => { :id => 'relations' },
373 393 :descendant => { :tag => 'a', :content => /#4$/ }
374 394 end
375 395
376 396 def test_show_atom
377 397 get :show, :id => 2, :format => 'atom'
378 398 assert_response :success
379 399 assert_template 'changes.rxml'
380 400 # Inline image
381 401 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
382 402 end
383 403
384 404 def test_new_routing
385 405 assert_routing(
386 406 {:method => :get, :path => '/projects/1/issues/new'},
387 407 :controller => 'issues', :action => 'new', :project_id => '1'
388 408 )
389 409 assert_recognizes(
390 410 {:controller => 'issues', :action => 'new', :project_id => '1'},
391 411 {:method => :post, :path => '/projects/1/issues'}
392 412 )
393 413 end
394 414
395 415 def test_show_export_to_pdf
396 416 get :show, :id => 3, :format => 'pdf'
397 417 assert_response :success
398 418 assert_equal 'application/pdf', @response.content_type
399 419 assert @response.body.starts_with?('%PDF')
400 420 assert_not_nil assigns(:issue)
401 421 end
402 422
403 423 def test_get_new
404 424 @request.session[:user_id] = 2
405 425 get :new, :project_id => 1, :tracker_id => 1
406 426 assert_response :success
407 427 assert_template 'new'
408 428
409 429 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
410 430 :value => 'Default string' }
411 431 end
412 432
413 433 def test_get_new_without_tracker_id
414 434 @request.session[:user_id] = 2
415 435 get :new, :project_id => 1
416 436 assert_response :success
417 437 assert_template 'new'
418 438
419 439 issue = assigns(:issue)
420 440 assert_not_nil issue
421 441 assert_equal Project.find(1).trackers.first, issue.tracker
422 442 end
423 443
424 444 def test_get_new_with_no_default_status_should_display_an_error
425 445 @request.session[:user_id] = 2
426 446 IssueStatus.delete_all
427 447
428 448 get :new, :project_id => 1
429 449 assert_response 500
430 450 assert_not_nil flash[:error]
431 451 assert_tag :tag => 'div', :attributes => { :class => /error/ },
432 452 :content => /No default issue/
433 453 end
434 454
435 455 def test_get_new_with_no_tracker_should_display_an_error
436 456 @request.session[:user_id] = 2
437 457 Tracker.delete_all
438 458
439 459 get :new, :project_id => 1
440 460 assert_response 500
441 461 assert_not_nil flash[:error]
442 462 assert_tag :tag => 'div', :attributes => { :class => /error/ },
443 463 :content => /No tracker/
444 464 end
445 465
446 466 def test_update_new_form
447 467 @request.session[:user_id] = 2
448 468 xhr :post, :new, :project_id => 1,
449 469 :issue => {:tracker_id => 2,
450 470 :subject => 'This is the test_new issue',
451 471 :description => 'This is the description',
452 472 :priority_id => 5}
453 473 assert_response :success
454 474 assert_template 'new'
455 475 end
456 476
457 477 def test_post_new
458 478 @request.session[:user_id] = 2
459 479 assert_difference 'Issue.count' do
460 480 post :new, :project_id => 1,
461 481 :issue => {:tracker_id => 3,
462 482 :subject => 'This is the test_new issue',
463 483 :description => 'This is the description',
464 484 :priority_id => 5,
465 485 :estimated_hours => '',
466 486 :custom_field_values => {'2' => 'Value for field 2'}}
467 487 end
468 488 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
469 489
470 490 issue = Issue.find_by_subject('This is the test_new issue')
471 491 assert_not_nil issue
472 492 assert_equal 2, issue.author_id
473 493 assert_equal 3, issue.tracker_id
474 494 assert_nil issue.estimated_hours
475 495 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
476 496 assert_not_nil v
477 497 assert_equal 'Value for field 2', v.value
478 498 end
479 499
480 500 def test_post_new_and_continue
481 501 @request.session[:user_id] = 2
482 502 post :new, :project_id => 1,
483 503 :issue => {:tracker_id => 3,
484 504 :subject => 'This is first issue',
485 505 :priority_id => 5},
486 506 :continue => ''
487 507 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
488 508 end
489 509
490 510 def test_post_new_without_custom_fields_param
491 511 @request.session[:user_id] = 2
492 512 assert_difference 'Issue.count' do
493 513 post :new, :project_id => 1,
494 514 :issue => {:tracker_id => 1,
495 515 :subject => 'This is the test_new issue',
496 516 :description => 'This is the description',
497 517 :priority_id => 5}
498 518 end
499 519 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
500 520 end
501 521
502 522 def test_post_new_with_required_custom_field_and_without_custom_fields_param
503 523 field = IssueCustomField.find_by_name('Database')
504 524 field.update_attribute(:is_required, true)
505 525
506 526 @request.session[:user_id] = 2
507 527 post :new, :project_id => 1,
508 528 :issue => {:tracker_id => 1,
509 529 :subject => 'This is the test_new issue',
510 530 :description => 'This is the description',
511 531 :priority_id => 5}
512 532 assert_response :success
513 533 assert_template 'new'
514 534 issue = assigns(:issue)
515 535 assert_not_nil issue
516 536 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
517 537 end
518 538
519 539 def test_post_new_with_watchers
520 540 @request.session[:user_id] = 2
521 541 ActionMailer::Base.deliveries.clear
522 542
523 543 assert_difference 'Watcher.count', 2 do
524 544 post :new, :project_id => 1,
525 545 :issue => {:tracker_id => 1,
526 546 :subject => 'This is a new issue with watchers',
527 547 :description => 'This is the description',
528 548 :priority_id => 5,
529 549 :watcher_user_ids => ['2', '3']}
530 550 end
531 551 issue = Issue.find_by_subject('This is a new issue with watchers')
532 552 assert_not_nil issue
533 553 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
534 554
535 555 # Watchers added
536 556 assert_equal [2, 3], issue.watcher_user_ids.sort
537 557 assert issue.watched_by?(User.find(3))
538 558 # Watchers notified
539 559 mail = ActionMailer::Base.deliveries.last
540 560 assert_kind_of TMail::Mail, mail
541 561 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
542 562 end
543 563
544 564 def test_post_new_should_send_a_notification
545 565 ActionMailer::Base.deliveries.clear
546 566 @request.session[:user_id] = 2
547 567 assert_difference 'Issue.count' do
548 568 post :new, :project_id => 1,
549 569 :issue => {:tracker_id => 3,
550 570 :subject => 'This is the test_new issue',
551 571 :description => 'This is the description',
552 572 :priority_id => 5,
553 573 :estimated_hours => '',
554 574 :custom_field_values => {'2' => 'Value for field 2'}}
555 575 end
556 576 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
557 577
558 578 assert_equal 1, ActionMailer::Base.deliveries.size
559 579 end
560 580
561 581 def test_post_should_preserve_fields_values_on_validation_failure
562 582 @request.session[:user_id] = 2
563 583 post :new, :project_id => 1,
564 584 :issue => {:tracker_id => 1,
565 585 # empty subject
566 586 :subject => '',
567 587 :description => 'This is a description',
568 588 :priority_id => 6,
569 589 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
570 590 assert_response :success
571 591 assert_template 'new'
572 592
573 593 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
574 594 :content => 'This is a description'
575 595 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
576 596 :child => { :tag => 'option', :attributes => { :selected => 'selected',
577 597 :value => '6' },
578 598 :content => 'High' }
579 599 # Custom fields
580 600 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
581 601 :child => { :tag => 'option', :attributes => { :selected => 'selected',
582 602 :value => 'Oracle' },
583 603 :content => 'Oracle' }
584 604 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
585 605 :value => 'Value for field 2'}
586 606 end
587 607
588 608 def test_copy_routing
589 609 assert_routing(
590 610 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
591 611 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
592 612 )
593 613 end
594 614
595 615 def test_copy_issue
596 616 @request.session[:user_id] = 2
597 617 get :new, :project_id => 1, :copy_from => 1
598 618 assert_template 'new'
599 619 assert_not_nil assigns(:issue)
600 620 orig = Issue.find(1)
601 621 assert_equal orig.subject, assigns(:issue).subject
602 622 end
603 623
604 624 def test_edit_routing
605 625 assert_routing(
606 626 {:method => :get, :path => '/issues/1/edit'},
607 627 :controller => 'issues', :action => 'edit', :id => '1'
608 628 )
609 629 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
610 630 {:controller => 'issues', :action => 'edit', :id => '1'},
611 631 {:method => :post, :path => '/issues/1/edit'}
612 632 )
613 633 end
614 634
615 635 def test_get_edit
616 636 @request.session[:user_id] = 2
617 637 get :edit, :id => 1
618 638 assert_response :success
619 639 assert_template 'edit'
620 640 assert_not_nil assigns(:issue)
621 641 assert_equal Issue.find(1), assigns(:issue)
622 642 end
623 643
624 644 def test_get_edit_with_params
625 645 @request.session[:user_id] = 2
626 646 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
627 647 assert_response :success
628 648 assert_template 'edit'
629 649
630 650 issue = assigns(:issue)
631 651 assert_not_nil issue
632 652
633 653 assert_equal 5, issue.status_id
634 654 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
635 655 :child => { :tag => 'option',
636 656 :content => 'Closed',
637 657 :attributes => { :selected => 'selected' } }
638 658
639 659 assert_equal 7, issue.priority_id
640 660 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
641 661 :child => { :tag => 'option',
642 662 :content => 'Urgent',
643 663 :attributes => { :selected => 'selected' } }
644 664 end
645 665
646 666 def test_reply_routing
647 667 assert_routing(
648 668 {:method => :post, :path => '/issues/1/quoted'},
649 669 :controller => 'issues', :action => 'reply', :id => '1'
650 670 )
651 671 end
652 672
653 673 def test_reply_to_issue
654 674 @request.session[:user_id] = 2
655 675 get :reply, :id => 1
656 676 assert_response :success
657 677 assert_select_rjs :show, "update"
658 678 end
659 679
660 680 def test_reply_to_note
661 681 @request.session[:user_id] = 2
662 682 get :reply, :id => 1, :journal_id => 2
663 683 assert_response :success
664 684 assert_select_rjs :show, "update"
665 685 end
666 686
667 687 def test_post_edit_without_custom_fields_param
668 688 @request.session[:user_id] = 2
669 689 ActionMailer::Base.deliveries.clear
670 690
671 691 issue = Issue.find(1)
672 692 assert_equal '125', issue.custom_value_for(2).value
673 693 old_subject = issue.subject
674 694 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
675 695
676 696 assert_difference('Journal.count') do
677 697 assert_difference('JournalDetail.count', 2) do
678 698 post :edit, :id => 1, :issue => {:subject => new_subject,
679 699 :priority_id => '6',
680 700 :category_id => '1' # no change
681 701 }
682 702 end
683 703 end
684 704 assert_redirected_to :action => 'show', :id => '1'
685 705 issue.reload
686 706 assert_equal new_subject, issue.subject
687 707 # Make sure custom fields were not cleared
688 708 assert_equal '125', issue.custom_value_for(2).value
689 709
690 710 mail = ActionMailer::Base.deliveries.last
691 711 assert_kind_of TMail::Mail, mail
692 712 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
693 713 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
694 714 end
695 715
696 716 def test_post_edit_with_custom_field_change
697 717 @request.session[:user_id] = 2
698 718 issue = Issue.find(1)
699 719 assert_equal '125', issue.custom_value_for(2).value
700 720
701 721 assert_difference('Journal.count') do
702 722 assert_difference('JournalDetail.count', 3) do
703 723 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
704 724 :priority_id => '6',
705 725 :category_id => '1', # no change
706 726 :custom_field_values => { '2' => 'New custom value' }
707 727 }
708 728 end
709 729 end
710 730 assert_redirected_to :action => 'show', :id => '1'
711 731 issue.reload
712 732 assert_equal 'New custom value', issue.custom_value_for(2).value
713 733
714 734 mail = ActionMailer::Base.deliveries.last
715 735 assert_kind_of TMail::Mail, mail
716 736 assert mail.body.include?("Searchable field changed from 125 to New custom value")
717 737 end
718 738
719 739 def test_post_edit_with_status_and_assignee_change
720 740 issue = Issue.find(1)
721 741 assert_equal 1, issue.status_id
722 742 @request.session[:user_id] = 2
723 743 assert_difference('TimeEntry.count', 0) do
724 744 post :edit,
725 745 :id => 1,
726 746 :issue => { :status_id => 2, :assigned_to_id => 3 },
727 747 :notes => 'Assigned to dlopper',
728 748 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
729 749 end
730 750 assert_redirected_to :action => 'show', :id => '1'
731 751 issue.reload
732 752 assert_equal 2, issue.status_id
733 753 j = issue.journals.find(:first, :order => 'id DESC')
734 754 assert_equal 'Assigned to dlopper', j.notes
735 755 assert_equal 2, j.details.size
736 756
737 757 mail = ActionMailer::Base.deliveries.last
738 758 assert mail.body.include?("Status changed from New to Assigned")
739 759 # subject should contain the new status
740 760 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
741 761 end
742 762
743 763 def test_post_edit_with_note_only
744 764 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
745 765 # anonymous user
746 766 post :edit,
747 767 :id => 1,
748 768 :notes => notes
749 769 assert_redirected_to :action => 'show', :id => '1'
750 770 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
751 771 assert_equal notes, j.notes
752 772 assert_equal 0, j.details.size
753 773 assert_equal User.anonymous, j.user
754 774
755 775 mail = ActionMailer::Base.deliveries.last
756 776 assert mail.body.include?(notes)
757 777 end
758 778
759 779 def test_post_edit_with_note_and_spent_time
760 780 @request.session[:user_id] = 2
761 781 spent_hours_before = Issue.find(1).spent_hours
762 782 assert_difference('TimeEntry.count') do
763 783 post :edit,
764 784 :id => 1,
765 785 :notes => '2.5 hours added',
766 786 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
767 787 end
768 788 assert_redirected_to :action => 'show', :id => '1'
769 789
770 790 issue = Issue.find(1)
771 791
772 792 j = issue.journals.find(:first, :order => 'id DESC')
773 793 assert_equal '2.5 hours added', j.notes
774 794 assert_equal 0, j.details.size
775 795
776 796 t = issue.time_entries.find(:first, :order => 'id DESC')
777 797 assert_not_nil t
778 798 assert_equal 2.5, t.hours
779 799 assert_equal spent_hours_before + 2.5, issue.spent_hours
780 800 end
781 801
782 802 def test_post_edit_with_attachment_only
783 803 set_tmp_attachments_directory
784 804
785 805 # Delete all fixtured journals, a race condition can occur causing the wrong
786 806 # journal to get fetched in the next find.
787 807 Journal.delete_all
788 808
789 809 # anonymous user
790 810 post :edit,
791 811 :id => 1,
792 812 :notes => '',
793 813 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
794 814 assert_redirected_to :action => 'show', :id => '1'
795 815 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
796 816 assert j.notes.blank?
797 817 assert_equal 1, j.details.size
798 818 assert_equal 'testfile.txt', j.details.first.value
799 819 assert_equal User.anonymous, j.user
800 820
801 821 mail = ActionMailer::Base.deliveries.last
802 822 assert mail.body.include?('testfile.txt')
803 823 end
804 824
805 825 def test_post_edit_with_no_change
806 826 issue = Issue.find(1)
807 827 issue.journals.clear
808 828 ActionMailer::Base.deliveries.clear
809 829
810 830 post :edit,
811 831 :id => 1,
812 832 :notes => ''
813 833 assert_redirected_to :action => 'show', :id => '1'
814 834
815 835 issue.reload
816 836 assert issue.journals.empty?
817 837 # No email should be sent
818 838 assert ActionMailer::Base.deliveries.empty?
819 839 end
820 840
821 841 def test_post_edit_should_send_a_notification
822 842 @request.session[:user_id] = 2
823 843 ActionMailer::Base.deliveries.clear
824 844 issue = Issue.find(1)
825 845 old_subject = issue.subject
826 846 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
827 847
828 848 post :edit, :id => 1, :issue => {:subject => new_subject,
829 849 :priority_id => '6',
830 850 :category_id => '1' # no change
831 851 }
832 852 assert_equal 1, ActionMailer::Base.deliveries.size
833 853 end
834 854
835 855 def test_post_edit_with_invalid_spent_time
836 856 @request.session[:user_id] = 2
837 857 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
838 858
839 859 assert_no_difference('Journal.count') do
840 860 post :edit,
841 861 :id => 1,
842 862 :notes => notes,
843 863 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
844 864 end
845 865 assert_response :success
846 866 assert_template 'edit'
847 867
848 868 assert_tag :textarea, :attributes => { :name => 'notes' },
849 869 :content => notes
850 870 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
851 871 end
852 872
853 873 def test_get_bulk_edit
854 874 @request.session[:user_id] = 2
855 875 get :bulk_edit, :ids => [1, 2]
856 876 assert_response :success
857 877 assert_template 'bulk_edit'
858 878 end
859 879
860 880 def test_bulk_edit
861 881 @request.session[:user_id] = 2
862 882 # update issues priority
863 883 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
864 884 :assigned_to_id => '',
865 885 :custom_field_values => {'2' => ''},
866 886 :notes => 'Bulk editing'
867 887 assert_response 302
868 888 # check that the issues were updated
869 889 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
870 890
871 891 issue = Issue.find(1)
872 892 journal = issue.journals.find(:first, :order => 'created_on DESC')
873 893 assert_equal '125', issue.custom_value_for(2).value
874 894 assert_equal 'Bulk editing', journal.notes
875 895 assert_equal 1, journal.details.size
876 896 end
877 897
878 898 def test_bullk_edit_should_send_a_notification
879 899 @request.session[:user_id] = 2
880 900 ActionMailer::Base.deliveries.clear
881 901 post(:bulk_edit,
882 902 {
883 903 :ids => [1, 2],
884 904 :priority_id => 7,
885 905 :assigned_to_id => '',
886 906 :custom_field_values => {'2' => ''},
887 907 :notes => 'Bulk editing'
888 908 })
889 909
890 910 assert_response 302
891 911 assert_equal 2, ActionMailer::Base.deliveries.size
892 912 end
893 913
894 914 def test_bulk_edit_status
895 915 @request.session[:user_id] = 2
896 916 # update issues priority
897 917 post :bulk_edit, :ids => [1, 2], :priority_id => '',
898 918 :assigned_to_id => '',
899 919 :status_id => '5',
900 920 :notes => 'Bulk editing status'
901 921 assert_response 302
902 922 issue = Issue.find(1)
903 923 assert issue.closed?
904 924 end
905 925
906 926 def test_bulk_edit_custom_field
907 927 @request.session[:user_id] = 2
908 928 # update issues priority
909 929 post :bulk_edit, :ids => [1, 2], :priority_id => '',
910 930 :assigned_to_id => '',
911 931 :custom_field_values => {'2' => '777'},
912 932 :notes => 'Bulk editing custom field'
913 933 assert_response 302
914 934
915 935 issue = Issue.find(1)
916 936 journal = issue.journals.find(:first, :order => 'created_on DESC')
917 937 assert_equal '777', issue.custom_value_for(2).value
918 938 assert_equal 1, journal.details.size
919 939 assert_equal '125', journal.details.first.old_value
920 940 assert_equal '777', journal.details.first.value
921 941 end
922 942
923 943 def test_bulk_unassign
924 944 assert_not_nil Issue.find(2).assigned_to
925 945 @request.session[:user_id] = 2
926 946 # unassign issues
927 947 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
928 948 assert_response 302
929 949 # check that the issues were updated
930 950 assert_nil Issue.find(2).assigned_to
931 951 end
932 952
933 953 def test_move_routing
934 954 assert_routing(
935 955 {:method => :get, :path => '/issues/1/move'},
936 956 :controller => 'issues', :action => 'move', :id => '1'
937 957 )
938 958 assert_recognizes(
939 959 {:controller => 'issues', :action => 'move', :id => '1'},
940 960 {:method => :post, :path => '/issues/1/move'}
941 961 )
942 962 end
943 963
944 964 def test_move_one_issue_to_another_project
945 965 @request.session[:user_id] = 2
946 966 post :move, :id => 1, :new_project_id => 2
947 967 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
948 968 assert_equal 2, Issue.find(1).project_id
949 969 end
950 970
951 971 def test_bulk_move_to_another_project
952 972 @request.session[:user_id] = 2
953 973 post :move, :ids => [1, 2], :new_project_id => 2
954 974 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
955 975 # Issues moved to project 2
956 976 assert_equal 2, Issue.find(1).project_id
957 977 assert_equal 2, Issue.find(2).project_id
958 978 # No tracker change
959 979 assert_equal 1, Issue.find(1).tracker_id
960 980 assert_equal 2, Issue.find(2).tracker_id
961 981 end
962 982
963 983 def test_bulk_move_to_another_tracker
964 984 @request.session[:user_id] = 2
965 985 post :move, :ids => [1, 2], :new_tracker_id => 2
966 986 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
967 987 assert_equal 2, Issue.find(1).tracker_id
968 988 assert_equal 2, Issue.find(2).tracker_id
969 989 end
970 990
971 991 def test_bulk_copy_to_another_project
972 992 @request.session[:user_id] = 2
973 993 assert_difference 'Issue.count', 2 do
974 994 assert_no_difference 'Project.find(1).issues.count' do
975 995 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
976 996 end
977 997 end
978 998 assert_redirected_to 'projects/ecookbook/issues'
979 999 end
980 1000
981 1001 def test_context_menu_one_issue
982 1002 @request.session[:user_id] = 2
983 1003 get :context_menu, :ids => [1]
984 1004 assert_response :success
985 1005 assert_template 'context_menu'
986 1006 assert_tag :tag => 'a', :content => 'Edit',
987 1007 :attributes => { :href => '/issues/1/edit',
988 1008 :class => 'icon-edit' }
989 1009 assert_tag :tag => 'a', :content => 'Closed',
990 1010 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
991 1011 :class => '' }
992 1012 assert_tag :tag => 'a', :content => 'Immediate',
993 1013 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
994 1014 :class => '' }
995 1015 assert_tag :tag => 'a', :content => 'Dave Lopper',
996 1016 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
997 1017 :class => '' }
998 1018 assert_tag :tag => 'a', :content => 'Copy',
999 1019 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1000 1020 :class => 'icon-copy' }
1001 1021 assert_tag :tag => 'a', :content => 'Move',
1002 1022 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1003 1023 :class => 'icon-move' }
1004 1024 assert_tag :tag => 'a', :content => 'Delete',
1005 1025 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1006 1026 :class => 'icon-del' }
1007 1027 end
1008 1028
1009 1029 def test_context_menu_one_issue_by_anonymous
1010 1030 get :context_menu, :ids => [1]
1011 1031 assert_response :success
1012 1032 assert_template 'context_menu'
1013 1033 assert_tag :tag => 'a', :content => 'Delete',
1014 1034 :attributes => { :href => '#',
1015 1035 :class => 'icon-del disabled' }
1016 1036 end
1017 1037
1018 1038 def test_context_menu_multiple_issues_of_same_project
1019 1039 @request.session[:user_id] = 2
1020 1040 get :context_menu, :ids => [1, 2]
1021 1041 assert_response :success
1022 1042 assert_template 'context_menu'
1023 1043 assert_tag :tag => 'a', :content => 'Edit',
1024 1044 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1025 1045 :class => 'icon-edit' }
1026 1046 assert_tag :tag => 'a', :content => 'Immediate',
1027 1047 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1028 1048 :class => '' }
1029 1049 assert_tag :tag => 'a', :content => 'Dave Lopper',
1030 1050 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1031 1051 :class => '' }
1032 1052 assert_tag :tag => 'a', :content => 'Move',
1033 1053 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1034 1054 :class => 'icon-move' }
1035 1055 assert_tag :tag => 'a', :content => 'Delete',
1036 1056 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1037 1057 :class => 'icon-del' }
1038 1058 end
1039 1059
1040 1060 def test_context_menu_multiple_issues_of_different_project
1041 1061 @request.session[:user_id] = 2
1042 1062 get :context_menu, :ids => [1, 2, 4]
1043 1063 assert_response :success
1044 1064 assert_template 'context_menu'
1045 1065 assert_tag :tag => 'a', :content => 'Delete',
1046 1066 :attributes => { :href => '#',
1047 1067 :class => 'icon-del disabled' }
1048 1068 end
1049 1069
1050 1070 def test_destroy_routing
1051 1071 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1052 1072 {:controller => 'issues', :action => 'destroy', :id => '1'},
1053 1073 {:method => :post, :path => '/issues/1/destroy'}
1054 1074 )
1055 1075 end
1056 1076
1057 1077 def test_destroy_issue_with_no_time_entries
1058 1078 assert_nil TimeEntry.find_by_issue_id(2)
1059 1079 @request.session[:user_id] = 2
1060 1080 post :destroy, :id => 2
1061 1081 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1062 1082 assert_nil Issue.find_by_id(2)
1063 1083 end
1064 1084
1065 1085 def test_destroy_issues_with_time_entries
1066 1086 @request.session[:user_id] = 2
1067 1087 post :destroy, :ids => [1, 3]
1068 1088 assert_response :success
1069 1089 assert_template 'destroy'
1070 1090 assert_not_nil assigns(:hours)
1071 1091 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1072 1092 end
1073 1093
1074 1094 def test_destroy_issues_and_destroy_time_entries
1075 1095 @request.session[:user_id] = 2
1076 1096 post :destroy, :ids => [1, 3], :todo => 'destroy'
1077 1097 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1078 1098 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1079 1099 assert_nil TimeEntry.find_by_id([1, 2])
1080 1100 end
1081 1101
1082 1102 def test_destroy_issues_and_assign_time_entries_to_project
1083 1103 @request.session[:user_id] = 2
1084 1104 post :destroy, :ids => [1, 3], :todo => 'nullify'
1085 1105 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1086 1106 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1087 1107 assert_nil TimeEntry.find(1).issue_id
1088 1108 assert_nil TimeEntry.find(2).issue_id
1089 1109 end
1090 1110
1091 1111 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1092 1112 @request.session[:user_id] = 2
1093 1113 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1094 1114 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1095 1115 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1096 1116 assert_equal 2, TimeEntry.find(1).issue_id
1097 1117 assert_equal 2, TimeEntry.find(2).issue_id
1098 1118 end
1099 1119
1100 1120 def test_default_search_scope
1101 1121 get :index
1102 1122 assert_tag :div, :attributes => {:id => 'quick-search'},
1103 1123 :child => {:tag => 'form',
1104 1124 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1105 1125 end
1106 1126 end
@@ -1,352 +1,393
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
20 20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :versions,
24 24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 25 :enumerations,
26 26 :issues,
27 27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 28 :time_entries
29 29
30 30 def test_create
31 31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 32 assert issue.save
33 33 issue.reload
34 34 assert_equal 1.5, issue.estimated_hours
35 35 end
36 36
37 37 def test_create_minimal
38 38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 39 assert issue.save
40 40 assert issue.description.nil?
41 41 end
42 42
43 43 def test_create_with_required_custom_field
44 44 field = IssueCustomField.find_by_name('Database')
45 45 field.update_attribute(:is_required, true)
46 46
47 47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 48 assert issue.available_custom_fields.include?(field)
49 49 # No value for the custom field
50 50 assert !issue.save
51 51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 52 # Blank value
53 53 issue.custom_field_values = { field.id => '' }
54 54 assert !issue.save
55 55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 56 # Invalid value
57 57 issue.custom_field_values = { field.id => 'SQLServer' }
58 58 assert !issue.save
59 59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 60 # Valid value
61 61 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 62 assert issue.save
63 63 issue.reload
64 64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 65 end
66 66
67 def test_visible_scope_for_anonymous
68 # Anonymous user should see issues of public projects only
69 issues = Issue.visible(User.anonymous).all
70 assert issues.any?
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 # Anonymous user should not see issues without permission
73 Role.anonymous.remove_permission!(:view_issues)
74 issues = Issue.visible(User.anonymous).all
75 assert issues.empty?
76 end
77
78 def test_visible_scope_for_user
79 user = User.find(9)
80 assert user.projects.empty?
81 # Non member user should see issues of public projects only
82 issues = Issue.visible(user).all
83 assert issues.any?
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 # Non member user should not see issues without permission
86 Role.non_member.remove_permission!(:view_issues)
87 user.reload
88 issues = Issue.visible(user).all
89 assert issues.empty?
90 # User should see issues of projects for which he has view_issues permissions only
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 user.reload
93 issues = Issue.visible(user).all
94 assert issues.any?
95 assert_nil issues.detect {|issue| issue.project_id != 2}
96 end
97
98 def test_visible_scope_for_admin
99 user = User.find(1)
100 user.members.each(&:destroy)
101 assert user.projects.empty?
102 issues = Issue.visible(user).all
103 assert issues.any?
104 # Admin should see issues on private projects that he does not belong to
105 assert issues.detect {|issue| !issue.project.is_public?}
106 end
107
67 108 def test_errors_full_messages_should_include_custom_fields_errors
68 109 field = IssueCustomField.find_by_name('Database')
69 110
70 111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
71 112 assert issue.available_custom_fields.include?(field)
72 113 # Invalid value
73 114 issue.custom_field_values = { field.id => 'SQLServer' }
74 115
75 116 assert !issue.valid?
76 117 assert_equal 1, issue.errors.full_messages.size
77 118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
78 119 end
79 120
80 121 def test_update_issue_with_required_custom_field
81 122 field = IssueCustomField.find_by_name('Database')
82 123 field.update_attribute(:is_required, true)
83 124
84 125 issue = Issue.find(1)
85 126 assert_nil issue.custom_value_for(field)
86 127 assert issue.available_custom_fields.include?(field)
87 128 # No change to custom values, issue can be saved
88 129 assert issue.save
89 130 # Blank value
90 131 issue.custom_field_values = { field.id => '' }
91 132 assert !issue.save
92 133 # Valid value
93 134 issue.custom_field_values = { field.id => 'PostgreSQL' }
94 135 assert issue.save
95 136 issue.reload
96 137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
97 138 end
98 139
99 140 def test_should_not_update_attributes_if_custom_fields_validation_fails
100 141 issue = Issue.find(1)
101 142 field = IssueCustomField.find_by_name('Database')
102 143 assert issue.available_custom_fields.include?(field)
103 144
104 145 issue.custom_field_values = { field.id => 'Invalid' }
105 146 issue.subject = 'Should be not be saved'
106 147 assert !issue.save
107 148
108 149 issue.reload
109 150 assert_equal "Can't print recipes", issue.subject
110 151 end
111 152
112 153 def test_should_not_recreate_custom_values_objects_on_update
113 154 field = IssueCustomField.find_by_name('Database')
114 155
115 156 issue = Issue.find(1)
116 157 issue.custom_field_values = { field.id => 'PostgreSQL' }
117 158 assert issue.save
118 159 custom_value = issue.custom_value_for(field)
119 160 issue.reload
120 161 issue.custom_field_values = { field.id => 'MySQL' }
121 162 assert issue.save
122 163 issue.reload
123 164 assert_equal custom_value.id, issue.custom_value_for(field).id
124 165 end
125 166
126 167 def test_category_based_assignment
127 168 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
128 169 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
129 170 end
130 171
131 172 def test_copy
132 173 issue = Issue.new.copy_from(1)
133 174 assert issue.save
134 175 issue.reload
135 176 orig = Issue.find(1)
136 177 assert_equal orig.subject, issue.subject
137 178 assert_equal orig.tracker, issue.tracker
138 179 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
139 180 end
140 181
141 182 def test_should_close_duplicates
142 183 # Create 3 issues
143 184 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
144 185 assert issue1.save
145 186 issue2 = issue1.clone
146 187 assert issue2.save
147 188 issue3 = issue1.clone
148 189 assert issue3.save
149 190
150 191 # 2 is a dupe of 1
151 192 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
152 193 # And 3 is a dupe of 2
153 194 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
154 195 # And 3 is a dupe of 1 (circular duplicates)
155 196 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
156 197
157 198 assert issue1.reload.duplicates.include?(issue2)
158 199
159 200 # Closing issue 1
160 201 issue1.init_journal(User.find(:first), "Closing issue1")
161 202 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
162 203 assert issue1.save
163 204 # 2 and 3 should be also closed
164 205 assert issue2.reload.closed?
165 206 assert issue3.reload.closed?
166 207 end
167 208
168 209 def test_should_not_close_duplicated_issue
169 210 # Create 3 issues
170 211 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
171 212 assert issue1.save
172 213 issue2 = issue1.clone
173 214 assert issue2.save
174 215
175 216 # 2 is a dupe of 1
176 217 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
177 218 # 2 is a dup of 1 but 1 is not a duplicate of 2
178 219 assert !issue2.reload.duplicates.include?(issue1)
179 220
180 221 # Closing issue 2
181 222 issue2.init_journal(User.find(:first), "Closing issue2")
182 223 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
183 224 assert issue2.save
184 225 # 1 should not be also closed
185 226 assert !issue1.reload.closed?
186 227 end
187 228
188 229 def test_assignable_versions
189 230 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
190 231 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
191 232 end
192 233
193 234 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
194 235 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
195 236 assert !issue.save
196 237 assert_not_nil issue.errors.on(:fixed_version_id)
197 238 end
198 239
199 240 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
200 241 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
201 242 assert !issue.save
202 243 assert_not_nil issue.errors.on(:fixed_version_id)
203 244 end
204 245
205 246 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
206 247 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
207 248 assert issue.save
208 249 end
209 250
210 251 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
211 252 issue = Issue.find(11)
212 253 assert_equal 'closed', issue.fixed_version.status
213 254 issue.subject = 'Subject changed'
214 255 assert issue.save
215 256 end
216 257
217 258 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
218 259 issue = Issue.find(11)
219 260 issue.status_id = 1
220 261 assert !issue.save
221 262 assert_not_nil issue.errors.on_base
222 263 end
223 264
224 265 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
225 266 issue = Issue.find(11)
226 267 issue.status_id = 1
227 268 issue.fixed_version_id = 3
228 269 assert issue.save
229 270 end
230 271
231 272 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
232 273 issue = Issue.find(12)
233 274 assert_equal 'locked', issue.fixed_version.status
234 275 issue.status_id = 1
235 276 assert issue.save
236 277 end
237 278
238 279 def test_move_to_another_project_with_same_category
239 280 issue = Issue.find(1)
240 281 assert issue.move_to(Project.find(2))
241 282 issue.reload
242 283 assert_equal 2, issue.project_id
243 284 # Category changes
244 285 assert_equal 4, issue.category_id
245 286 # Make sure time entries were move to the target project
246 287 assert_equal 2, issue.time_entries.first.project_id
247 288 end
248 289
249 290 def test_move_to_another_project_without_same_category
250 291 issue = Issue.find(2)
251 292 assert issue.move_to(Project.find(2))
252 293 issue.reload
253 294 assert_equal 2, issue.project_id
254 295 # Category cleared
255 296 assert_nil issue.category_id
256 297 end
257 298
258 299 def test_copy_to_the_same_project
259 300 issue = Issue.find(1)
260 301 copy = nil
261 302 assert_difference 'Issue.count' do
262 303 copy = issue.move_to(issue.project, nil, :copy => true)
263 304 end
264 305 assert_kind_of Issue, copy
265 306 assert_equal issue.project, copy.project
266 307 assert_equal "125", copy.custom_value_for(2).value
267 308 end
268 309
269 310 def test_copy_to_another_project_and_tracker
270 311 issue = Issue.find(1)
271 312 copy = nil
272 313 assert_difference 'Issue.count' do
273 314 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
274 315 end
275 316 assert_kind_of Issue, copy
276 317 assert_equal Project.find(3), copy.project
277 318 assert_equal Tracker.find(2), copy.tracker
278 319 # Custom field #2 is not associated with target tracker
279 320 assert_nil copy.custom_value_for(2)
280 321 end
281 322
282 323 def test_issue_destroy
283 324 Issue.find(1).destroy
284 325 assert_nil Issue.find_by_id(1)
285 326 assert_nil TimeEntry.find_by_issue_id(1)
286 327 end
287 328
288 329 def test_blocked
289 330 blocked_issue = Issue.find(9)
290 331 blocking_issue = Issue.find(10)
291 332
292 333 assert blocked_issue.blocked?
293 334 assert !blocking_issue.blocked?
294 335 end
295 336
296 337 def test_blocked_issues_dont_allow_closed_statuses
297 338 blocked_issue = Issue.find(9)
298 339
299 340 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
300 341 assert !allowed_statuses.empty?
301 342 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
302 343 assert closed_statuses.empty?
303 344 end
304 345
305 346 def test_unblocked_issues_allow_closed_statuses
306 347 blocking_issue = Issue.find(10)
307 348
308 349 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
309 350 assert !allowed_statuses.empty?
310 351 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
311 352 assert !closed_statuses.empty?
312 353 end
313 354
314 355 def test_overdue
315 356 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
316 357 assert !Issue.new(:due_date => Date.today).overdue?
317 358 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
318 359 assert !Issue.new(:due_date => nil).overdue?
319 360 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
320 361 end
321 362
322 363 def test_assignable_users
323 364 assert_kind_of User, Issue.find(1).assignable_users.first
324 365 end
325 366
326 367 def test_create_should_send_email_notification
327 368 ActionMailer::Base.deliveries.clear
328 369 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
329 370
330 371 assert issue.save
331 372 assert_equal 1, ActionMailer::Base.deliveries.size
332 373 end
333 374
334 375 def test_stale_issue_should_not_send_email_notification
335 376 ActionMailer::Base.deliveries.clear
336 377 issue = Issue.find(1)
337 378 stale = Issue.find(1)
338 379
339 380 issue.init_journal(User.find(1))
340 381 issue.subject = 'Subjet update'
341 382 assert issue.save
342 383 assert_equal 1, ActionMailer::Base.deliveries.size
343 384 ActionMailer::Base.deliveries.clear
344 385
345 386 stale.init_journal(User.find(1))
346 387 stale.subject = 'Another subjet update'
347 388 assert_raise ActiveRecord::StaleObjectError do
348 389 stale.save
349 390 end
350 391 assert ActionMailer::Base.deliveries.empty?
351 392 end
352 393 end
General Comments 0
You need to be logged in to leave comments. Login now