##// END OF EJS Templates
Allow non-unique names for projects (#630)....
Jean-Philippe Lang -
r4277:fa3d71bed952
parent child
Show More
@@ -1,885 +1,885
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 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 'forwardable'
19 19 require 'cgi'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23 include Redmine::I18n
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 # Return true if user is authorized for controller/action, otherwise false
30 30 def authorize_for(controller, action)
31 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 32 end
33 33
34 34 # Display a link if user is authorized
35 35 #
36 36 # @param [String] name Anchor text (passed to link_to)
37 37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 38 # @param [optional, Hash] html_options Options passed to link_to
39 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 42 end
43 43
44 44 # Display a link to remote if user is authorized
45 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 46 url = options[:url] || {}
47 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active?
55 55 link_to name, :controller => 'users', :action => 'show', :id => user
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 #
72 72 def link_to_issue(issue, options={})
73 73 title = nil
74 74 subject = nil
75 75 if options[:subject] == false
76 76 title = truncate(issue.subject, :length => 60)
77 77 else
78 78 subject = issue.subject
79 79 if options[:truncate]
80 80 subject = truncate(subject, :length => options[:truncate])
81 81 end
82 82 end
83 83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 84 :class => issue.css_classes,
85 85 :title => title
86 86 s << ": #{h subject}" if subject
87 87 s = "#{h issue.project} - " + s if options[:project]
88 88 s
89 89 end
90 90
91 91 # Generates a link to an attachment.
92 92 # Options:
93 93 # * :text - Link text (default to attachment filename)
94 94 # * :download - Force download (default: false)
95 95 def link_to_attachment(attachment, options={})
96 96 text = options.delete(:text) || attachment.filename
97 97 action = options.delete(:download) ? 'download' : 'show'
98 98
99 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, project, options={})
106 106 text = options.delete(:text) || format_revision(revision)
107 107
108 108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 109 end
110 110
111 111 # Generates a link to a project if active
112 112 # Examples:
113 113 #
114 114 # link_to_project(project) # => link to the specified project overview
115 115 # link_to_project(project, :action=>'settings') # => link to project settings
116 116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 118 #
119 119 def link_to_project(project, options={}, html_options = nil)
120 120 if project.active?
121 121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 122 link_to(h(project), url, html_options)
123 123 else
124 124 h(project)
125 125 end
126 126 end
127 127
128 128 def toggle_link(name, id, options={})
129 129 onclick = "Element.toggle('#{id}'); "
130 130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 131 onclick << "return false;"
132 132 link_to(name, "#", :onclick => onclick)
133 133 end
134 134
135 135 def image_to_function(name, function, html_options = {})
136 136 html_options.symbolize_keys!
137 137 tag(:input, html_options.merge({
138 138 :type => "image", :src => image_path(name),
139 139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 140 }))
141 141 end
142 142
143 143 def prompt_to_remote(name, text, param, url, html_options = {})
144 144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 145 link_to name, {}, html_options
146 146 end
147 147
148 148 def format_activity_title(text)
149 149 h(truncate_single_line(text, :length => 100))
150 150 end
151 151
152 152 def format_activity_day(date)
153 153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 154 end
155 155
156 156 def format_activity_description(text)
157 157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 158 end
159 159
160 160 def format_version_name(version)
161 161 if version.project == @project
162 162 h(version)
163 163 else
164 164 h("#{version.project} - #{version}")
165 165 end
166 166 end
167 167
168 168 def due_date_distance_in_words(date)
169 169 if date
170 170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 171 end
172 172 end
173 173
174 174 def render_page_hierarchy(pages, node=nil)
175 175 content = ''
176 176 if pages[node]
177 177 content << "<ul class=\"pages-hierarchy\">\n"
178 178 pages[node].each do |page|
179 179 content << "<li>"
180 180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 183 content << "</li>\n"
184 184 end
185 185 content << "</ul>\n"
186 186 end
187 187 content
188 188 end
189 189
190 190 # Renders flash messages
191 191 def render_flash_messages
192 192 s = ''
193 193 flash.each do |k,v|
194 194 s << content_tag('div', v, :class => "flash #{k}")
195 195 end
196 196 s
197 197 end
198 198
199 199 # Renders tabs and their content
200 200 def render_tabs(tabs)
201 201 if tabs.any?
202 202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 203 else
204 204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 205 end
206 206 end
207 207
208 208 # Renders the project quick-jump box
209 209 def render_project_jump_box
210 210 # Retrieve them now to avoid a COUNT query
211 211 projects = User.current.projects.all
212 212 if projects.any?
213 213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 215 '<option value="" disabled="disabled">---</option>'
216 216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 218 end
219 219 s << '</select>'
220 220 s
221 221 end
222 222 end
223 223
224 224 def project_tree_options_for_select(projects, options = {})
225 225 s = ''
226 226 project_tree(projects) do |project, level|
227 227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 228 tag_options = {:value => project.id}
229 229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 230 tag_options[:selected] = 'selected'
231 231 else
232 232 tag_options[:selected] = nil
233 233 end
234 234 tag_options.merge!(yield(project)) if block_given?
235 235 s << content_tag('option', name_prefix + h(project), tag_options)
236 236 end
237 237 s
238 238 end
239 239
240 240 # Yields the given block for each project with its level in the tree
241 241 #
242 242 # Wrapper for Project#project_tree
243 243 def project_tree(projects, &block)
244 244 Project.project_tree(projects, &block)
245 245 end
246 246
247 247 def project_nested_ul(projects, &block)
248 248 s = ''
249 249 if projects.any?
250 250 ancestors = []
251 251 projects.sort_by(&:lft).each do |project|
252 252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 253 s << "<ul>\n"
254 254 else
255 255 ancestors.pop
256 256 s << "</li>"
257 257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 258 ancestors.pop
259 259 s << "</ul></li>\n"
260 260 end
261 261 end
262 262 s << "<li>"
263 263 s << yield(project).to_s
264 264 ancestors << project
265 265 end
266 266 s << ("</li></ul>\n" * ancestors.size)
267 267 end
268 268 s
269 269 end
270 270
271 271 def principals_check_box_tags(name, principals)
272 272 s = ''
273 273 principals.sort.each do |principal|
274 274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 275 end
276 276 s
277 277 end
278 278
279 279 # Truncates and returns the string as a single line
280 280 def truncate_single_line(string, *args)
281 281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 282 end
283 283
284 284 # Truncates at line break after 250 characters or options[:length]
285 285 def truncate_lines(string, options={})
286 286 length = options[:length] || 250
287 287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 288 "#{$1}..."
289 289 else
290 290 string
291 291 end
292 292 end
293 293
294 294 def html_hours(text)
295 295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 296 end
297 297
298 298 def authoring(created, author, options={})
299 299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 300 end
301 301
302 302 def time_tag(time)
303 303 text = distance_of_time_in_words(Time.now, time)
304 304 if @project
305 305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 306 else
307 307 content_tag('acronym', text, :title => format_time(time))
308 308 end
309 309 end
310 310
311 311 def syntax_highlight(name, content)
312 312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 313 end
314 314
315 315 def to_path_param(path)
316 316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 317 end
318 318
319 319 def pagination_links_full(paginator, count=nil, options={})
320 320 page_param = options.delete(:page_param) || :page
321 321 per_page_links = options.delete(:per_page_links)
322 322 url_param = params.dup
323 323 # don't reuse query params if filters are present
324 324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325 325
326 326 html = ''
327 327 if paginator.current.previous
328 328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 329 end
330 330
331 331 html << (pagination_links_each(paginator, options) do |n|
332 332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 333 end || '')
334 334
335 335 if paginator.current.next
336 336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 337 end
338 338
339 339 unless count.nil?
340 340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 342 html << " | #{links}"
343 343 end
344 344 end
345 345
346 346 html
347 347 end
348 348
349 349 def per_page_links(selected=nil)
350 350 url_param = params.dup
351 351 url_param.clear if url_param.has_key?(:set_filter)
352 352
353 353 links = Setting.per_page_options_array.collect do |n|
354 354 n == selected ? n : link_to_remote(n, {:update => "content",
355 355 :url => params.dup.merge(:per_page => n),
356 356 :method => :get},
357 357 {:href => url_for(url_param.merge(:per_page => n))})
358 358 end
359 359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 360 end
361 361
362 362 def reorder_links(name, url)
363 363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
364 364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
365 365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
366 366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
367 367 end
368 368
369 369 def breadcrumb(*args)
370 370 elements = args.flatten
371 371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 372 end
373 373
374 374 def other_formats_links(&block)
375 375 concat('<p class="other-formats">' + l(:label_export_to))
376 376 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 377 concat('</p>')
378 378 end
379 379
380 380 def page_header_title
381 381 if @project.nil? || @project.new_record?
382 382 h(Setting.app_title)
383 383 else
384 384 b = []
385 385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 386 if ancestors.any?
387 387 root = ancestors.shift
388 388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 389 if ancestors.size > 2
390 390 b << '&#8230;'
391 391 ancestors = ancestors[-2, 2]
392 392 end
393 393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 394 end
395 395 b << h(@project)
396 396 b.join(' &#187; ')
397 397 end
398 398 end
399 399
400 400 def html_title(*args)
401 401 if args.empty?
402 402 title = []
403 403 title << @project.name if @project
404 404 title += @html_title if @html_title
405 405 title << Setting.app_title
406 406 title.select {|t| !t.blank? }.join(' - ')
407 407 else
408 408 @html_title ||= []
409 409 @html_title += args
410 410 end
411 411 end
412 412
413 413 # Returns the theme, controller name, and action as css classes for the
414 414 # HTML body.
415 415 def body_css_classes
416 416 css = []
417 417 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 418 css << 'theme-' + theme.name
419 419 end
420 420
421 421 css << 'controller-' + params[:controller]
422 422 css << 'action-' + params[:action]
423 423 css.join(' ')
424 424 end
425 425
426 426 def accesskey(s)
427 427 Redmine::AccessKeys.key_for s
428 428 end
429 429
430 430 # Formats text according to system settings.
431 431 # 2 ways to call this method:
432 432 # * with a String: textilizable(text, options)
433 433 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 434 def textilizable(*args)
435 435 options = args.last.is_a?(Hash) ? args.pop : {}
436 436 case args.size
437 437 when 1
438 438 obj = options[:object]
439 439 text = args.shift
440 440 when 2
441 441 obj = args.shift
442 442 attr = args.shift
443 443 text = obj.send(attr).to_s
444 444 else
445 445 raise ArgumentError, 'invalid arguments to textilizable'
446 446 end
447 447 return '' if text.blank?
448 448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 449 only_path = options.delete(:only_path) == false ? false : true
450 450
451 451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452 452
453 453 parse_non_pre_blocks(text) do |text|
454 454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
455 455 send method_name, text, project, obj, attr, only_path, options
456 456 end
457 457 end
458 458 end
459 459
460 460 def parse_non_pre_blocks(text)
461 461 s = StringScanner.new(text)
462 462 tags = []
463 463 parsed = ''
464 464 while !s.eos?
465 465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
466 466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
467 467 if tags.empty?
468 468 yield text
469 469 end
470 470 parsed << text
471 471 if tag
472 472 if closing
473 473 if tags.last == tag.downcase
474 474 tags.pop
475 475 end
476 476 else
477 477 tags << tag.downcase
478 478 end
479 479 parsed << full_tag
480 480 end
481 481 end
482 482 # Close any non closing tags
483 483 while tag = tags.pop
484 484 parsed << "</#{tag}>"
485 485 end
486 486 parsed
487 487 end
488 488
489 489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
490 490 # when using an image link, try to use an attachment, if possible
491 491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
492 492 attachments = nil
493 493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
494 494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
495 495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
496 496 # search for the picture in attachments
497 497 if found = attachments.detect { |att| att.filename.downcase == filename }
498 498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
499 499 desc = found.description.to_s.gsub('"', '')
500 500 if !desc.blank? && alttext.blank?
501 501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
502 502 end
503 503 "src=\"#{image_url}\"#{alt}"
504 504 else
505 505 m
506 506 end
507 507 end
508 508 end
509 509 end
510 510
511 511 # Wiki links
512 512 #
513 513 # Examples:
514 514 # [[mypage]]
515 515 # [[mypage|mytext]]
516 516 # wiki links can refer other project wikis, using project name or identifier:
517 517 # [[project:]] -> wiki starting page
518 518 # [[project:|mytext]]
519 519 # [[project:mypage]]
520 520 # [[project:mypage|mytext]]
521 521 def parse_wiki_links(text, project, obj, attr, only_path, options)
522 522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
523 523 link_project = project
524 524 esc, all, page, title = $1, $2, $3, $5
525 525 if esc.nil?
526 526 if page =~ /^([^\:]+)\:(.*)$/
527 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
527 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
528 528 page = $2
529 529 title ||= $1 if page.blank?
530 530 end
531 531
532 532 if link_project && link_project.wiki
533 533 # extract anchor
534 534 anchor = nil
535 535 if page =~ /^(.+?)\#(.+)$/
536 536 page, anchor = $1, $2
537 537 end
538 538 # check if page exists
539 539 wiki_page = link_project.wiki.find_page(page)
540 540 url = case options[:wiki_links]
541 541 when :local; "#{title}.html"
542 542 when :anchor; "##{title}" # used for single-file wiki export
543 543 else
544 544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
545 545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
546 546 end
547 547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
548 548 else
549 549 # project or wiki doesn't exist
550 550 all
551 551 end
552 552 else
553 553 all
554 554 end
555 555 end
556 556 end
557 557
558 558 # Redmine links
559 559 #
560 560 # Examples:
561 561 # Issues:
562 562 # #52 -> Link to issue #52
563 563 # Changesets:
564 564 # r52 -> Link to revision 52
565 565 # commit:a85130f -> Link to scmid starting with a85130f
566 566 # Documents:
567 567 # document#17 -> Link to document with id 17
568 568 # document:Greetings -> Link to the document with title "Greetings"
569 569 # document:"Some document" -> Link to the document with title "Some document"
570 570 # Versions:
571 571 # version#3 -> Link to version with id 3
572 572 # version:1.0.0 -> Link to version named "1.0.0"
573 573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
574 574 # Attachments:
575 575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
576 576 # Source files:
577 577 # source:some/file -> Link to the file located at /some/file in the project's repository
578 578 # source:some/file@52 -> Link to the file's revision 52
579 579 # source:some/file#L120 -> Link to line 120 of the file
580 580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
581 581 # export:some/file -> Force the download of the file
582 582 # Forum messages:
583 583 # message#1218 -> Link to message with id 1218
584 584 def parse_redmine_links(text, project, obj, attr, only_path, options)
585 585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
586 586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
587 587 link = nil
588 588 if esc.nil?
589 589 if prefix.nil? && sep == 'r'
590 590 if project && (changeset = project.changesets.find_by_revision(identifier))
591 591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
592 592 :class => 'changeset',
593 593 :title => truncate_single_line(changeset.comments, :length => 100))
594 594 end
595 595 elsif sep == '#'
596 596 oid = identifier.to_i
597 597 case prefix
598 598 when nil
599 599 if issue = Issue.visible.find_by_id(oid, :include => :status)
600 600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
601 601 :class => issue.css_classes,
602 602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
603 603 end
604 604 when 'document'
605 605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
606 606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
607 607 :class => 'document'
608 608 end
609 609 when 'version'
610 610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
611 611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
612 612 :class => 'version'
613 613 end
614 614 when 'message'
615 615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
616 616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
617 617 :controller => 'messages',
618 618 :action => 'show',
619 619 :board_id => message.board,
620 620 :id => message.root,
621 621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
622 622 :class => 'message'
623 623 end
624 624 when 'project'
625 625 if p = Project.visible.find_by_id(oid)
626 626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
627 627 end
628 628 end
629 629 elsif sep == ':'
630 630 # removes the double quotes if any
631 631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
632 632 case prefix
633 633 when 'document'
634 634 if project && document = project.documents.find_by_title(name)
635 635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 636 :class => 'document'
637 637 end
638 638 when 'version'
639 639 if project && version = project.versions.find_by_name(name)
640 640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 641 :class => 'version'
642 642 end
643 643 when 'commit'
644 644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
645 645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
646 646 :class => 'changeset',
647 647 :title => truncate_single_line(changeset.comments, :length => 100)
648 648 end
649 649 when 'source', 'export'
650 650 if project && project.repository
651 651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
652 652 path, rev, anchor = $1, $3, $5
653 653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
654 654 :path => to_path_param(path),
655 655 :rev => rev,
656 656 :anchor => anchor,
657 657 :format => (prefix == 'export' ? 'raw' : nil)},
658 658 :class => (prefix == 'export' ? 'source download' : 'source')
659 659 end
660 660 when 'attachment'
661 661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
662 662 if attachments && attachment = attachments.detect {|a| a.filename == name }
663 663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
664 664 :class => 'attachment'
665 665 end
666 666 when 'project'
667 667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
668 668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
669 669 end
670 670 end
671 671 end
672 672 end
673 673 leading + (link || "#{prefix}#{sep}#{identifier}")
674 674 end
675 675 end
676 676
677 677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
678 678 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
679 679
680 680 # Headings and TOC
681 681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
682 682 def parse_headings(text, project, obj, attr, only_path, options)
683 683 headings = []
684 684 text.gsub!(HEADING_RE) do
685 685 level, attrs, content = $1.to_i, $2, $3
686 686 item = strip_tags(content).strip
687 687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
688 688 headings << [level, anchor, item]
689 689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
690 690 end unless options[:headings] == false
691 691
692 692 text.gsub!(TOC_RE) do
693 693 if headings.empty?
694 694 ''
695 695 else
696 696 div_class = 'toc'
697 697 div_class << ' right' if $1 == '>'
698 698 div_class << ' left' if $1 == '<'
699 699 out = "<ul class=\"#{div_class}\"><li>"
700 700 root = headings.map(&:first).min
701 701 current = root
702 702 started = false
703 703 headings.each do |level, anchor, item|
704 704 if level > current
705 705 out << '<ul><li>' * (level - current)
706 706 elsif level < current
707 707 out << "</li></ul>\n" * (current - level) + "</li><li>"
708 708 elsif started
709 709 out << '</li><li>'
710 710 end
711 711 out << "<a href=\"##{anchor}\">#{item}</a>"
712 712 current = level
713 713 started = true
714 714 end
715 715 out << '</li></ul>' * (current - root)
716 716 out << '</li></ul>'
717 717 end
718 718 end
719 719 end
720 720
721 721 # Same as Rails' simple_format helper without using paragraphs
722 722 def simple_format_without_paragraph(text)
723 723 text.to_s.
724 724 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
725 725 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
726 726 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
727 727 end
728 728
729 729 def lang_options_for_select(blank=true)
730 730 (blank ? [["(auto)", ""]] : []) +
731 731 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
732 732 end
733 733
734 734 def label_tag_for(name, option_tags = nil, options = {})
735 735 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
736 736 content_tag("label", label_text)
737 737 end
738 738
739 739 def labelled_tabular_form_for(name, object, options, &proc)
740 740 options[:html] ||= {}
741 741 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
742 742 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
743 743 end
744 744
745 745 def back_url_hidden_field_tag
746 746 back_url = params[:back_url] || request.env['HTTP_REFERER']
747 747 back_url = CGI.unescape(back_url.to_s)
748 748 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
749 749 end
750 750
751 751 def check_all_links(form_name)
752 752 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
753 753 " | " +
754 754 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
755 755 end
756 756
757 757 def progress_bar(pcts, options={})
758 758 pcts = [pcts, pcts] unless pcts.is_a?(Array)
759 759 pcts = pcts.collect(&:round)
760 760 pcts[1] = pcts[1] - pcts[0]
761 761 pcts << (100 - pcts[1] - pcts[0])
762 762 width = options[:width] || '100px;'
763 763 legend = options[:legend] || ''
764 764 content_tag('table',
765 765 content_tag('tr',
766 766 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
767 767 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
768 768 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
769 769 ), :class => 'progress', :style => "width: #{width};") +
770 770 content_tag('p', legend, :class => 'pourcent')
771 771 end
772 772
773 773 def checked_image(checked=true)
774 774 if checked
775 775 image_tag 'toggle_check.png'
776 776 end
777 777 end
778 778
779 779 def context_menu(url)
780 780 unless @context_menu_included
781 781 content_for :header_tags do
782 782 javascript_include_tag('context_menu') +
783 783 stylesheet_link_tag('context_menu')
784 784 end
785 785 if l(:direction) == 'rtl'
786 786 content_for :header_tags do
787 787 stylesheet_link_tag('context_menu_rtl')
788 788 end
789 789 end
790 790 @context_menu_included = true
791 791 end
792 792 javascript_tag "new ContextMenu('#{ url_for(url) }')"
793 793 end
794 794
795 795 def context_menu_link(name, url, options={})
796 796 options[:class] ||= ''
797 797 if options.delete(:selected)
798 798 options[:class] << ' icon-checked disabled'
799 799 options[:disabled] = true
800 800 end
801 801 if options.delete(:disabled)
802 802 options.delete(:method)
803 803 options.delete(:confirm)
804 804 options.delete(:onclick)
805 805 options[:class] << ' disabled'
806 806 url = '#'
807 807 end
808 808 link_to name, url, options
809 809 end
810 810
811 811 def calendar_for(field_id)
812 812 include_calendar_headers_tags
813 813 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
814 814 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
815 815 end
816 816
817 817 def include_calendar_headers_tags
818 818 unless @calendar_headers_tags_included
819 819 @calendar_headers_tags_included = true
820 820 content_for :header_tags do
821 821 start_of_week = case Setting.start_of_week.to_i
822 822 when 1
823 823 'Calendar._FD = 1;' # Monday
824 824 when 7
825 825 'Calendar._FD = 0;' # Sunday
826 826 else
827 827 '' # use language
828 828 end
829 829
830 830 javascript_include_tag('calendar/calendar') +
831 831 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
832 832 javascript_tag(start_of_week) +
833 833 javascript_include_tag('calendar/calendar-setup') +
834 834 stylesheet_link_tag('calendar')
835 835 end
836 836 end
837 837 end
838 838
839 839 def content_for(name, content = nil, &block)
840 840 @has_content ||= {}
841 841 @has_content[name] = true
842 842 super(name, content, &block)
843 843 end
844 844
845 845 def has_content?(name)
846 846 (@has_content && @has_content[name]) || false
847 847 end
848 848
849 849 # Returns the avatar image tag for the given +user+ if avatars are enabled
850 850 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
851 851 def avatar(user, options = { })
852 852 if Setting.gravatar_enabled?
853 853 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
854 854 email = nil
855 855 if user.respond_to?(:mail)
856 856 email = user.mail
857 857 elsif user.to_s =~ %r{<(.+?)>}
858 858 email = $1
859 859 end
860 860 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
861 861 else
862 862 ''
863 863 end
864 864 end
865 865
866 866 def favicon
867 867 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
868 868 end
869 869
870 870 private
871 871
872 872 def wiki_helper
873 873 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
874 874 extend helper
875 875 return self
876 876 end
877 877
878 878 def link_to_remote_content_update(text, url_params)
879 879 link_to_remote(text,
880 880 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
881 881 {:href => url_for(:params => url_params)}
882 882 )
883 883 end
884 884
885 885 end
@@ -1,791 +1,791
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 Project < ActiveRecord::Base
19 19 # Project statuses
20 20 STATUS_ACTIVE = 1
21 21 STATUS_ARCHIVED = 9
22 22
23 23 # Specific overidden Activities
24 24 has_many :time_entry_activities
25 25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 26 has_many :memberships, :class_name => 'Member'
27 27 has_many :member_principals, :class_name => 'Member',
28 28 :include => :principal,
29 29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 30 has_many :users, :through => :members
31 31 has_many :principals, :through => :member_principals, :source => :principal
32 32
33 33 has_many :enabled_modules, :dependent => :delete_all
34 34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 36 has_many :issue_changes, :through => :issues, :source => :journals
37 37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 38 has_many :time_entries, :dependent => :delete_all
39 39 has_many :queries, :dependent => :delete_all
40 40 has_many :documents, :dependent => :destroy
41 41 has_many :news, :dependent => :delete_all, :include => :author
42 42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 44 has_one :repository, :dependent => :destroy
45 45 has_many :changesets, :through => :repository
46 46 has_one :wiki, :dependent => :destroy
47 47 # Custom field for the project issues
48 48 has_and_belongs_to_many :issue_custom_fields,
49 49 :class_name => 'IssueCustomField',
50 50 :order => "#{CustomField.table_name}.position",
51 51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 52 :association_foreign_key => 'custom_field_id'
53 53
54 54 acts_as_nested_set :order => 'name'
55 55 acts_as_attachable :view_permission => :view_files,
56 56 :delete_permission => :manage_files
57 57
58 58 acts_as_customizable
59 59 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
60 60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
62 62 :author => nil
63 63
64 64 attr_protected :status, :enabled_module_names
65 65
66 66 validates_presence_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
67 validates_uniqueness_of :identifier
68 68 validates_associated :repository, :wiki
69 69 validates_length_of :name, :maximum => 30
70 70 validates_length_of :homepage, :maximum => 255
71 71 validates_length_of :identifier, :in => 1..20
72 72 # donwcase letters, digits, dashes but not digits only
73 73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 74 # reserved words
75 75 validates_exclusion_of :identifier, :in => %w( new )
76 76
77 77 before_destroy :delete_all_members, :destroy_children
78 78
79 79 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
80 80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 81 named_scope :all_public, { :conditions => { :is_public => true } }
82 82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83 83
84 84 def identifier=(identifier)
85 85 super unless identifier_frozen?
86 86 end
87 87
88 88 def identifier_frozen?
89 89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 90 end
91 91
92 92 # returns latest created projects
93 93 # non public projects will be returned only if user is a member of those
94 94 def self.latest(user=nil, count=5)
95 95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 96 end
97 97
98 98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 99 #
100 100 # Examples:
101 101 # Projects.visible_by(admin) => "projects.status = 1"
102 102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 103 def self.visible_by(user=nil)
104 104 user ||= User.current
105 105 if user && user.admin?
106 106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 107 elsif user && user.memberships.any?
108 108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
109 109 else
110 110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 111 end
112 112 end
113 113
114 114 def self.allowed_to_condition(user, permission, options={})
115 115 statements = []
116 116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 117 if perm = Redmine::AccessControl.permission(permission)
118 118 unless perm.project_module.nil?
119 119 # If the permission belongs to a project module, make sure the module is enabled
120 120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 121 end
122 122 end
123 123 if options[:project]
124 124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 127 end
128 128 if user.admin?
129 129 # no restriction
130 130 else
131 131 statements << "1=0"
132 132 if user.logged?
133 133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 135 end
136 136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 138 else
139 139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 140 # anonymous user allowed on public project
141 141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 142 end
143 143 end
144 144 end
145 145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 146 end
147 147
148 148 # Returns the Systemwide and project specific activities
149 149 def activities(include_inactive=false)
150 150 if include_inactive
151 151 return all_activities
152 152 else
153 153 return active_activities
154 154 end
155 155 end
156 156
157 157 # Will create a new Project specific Activity or update an existing one
158 158 #
159 159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 160 # does not successfully save.
161 161 def update_or_create_time_entry_activity(id, activity_hash)
162 162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 163 self.create_time_entry_activity_if_needed(activity_hash)
164 164 else
165 165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 166 activity.update_attributes(activity_hash) if activity
167 167 end
168 168 end
169 169
170 170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 171 #
172 172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 173 # does not successfully save.
174 174 def create_time_entry_activity_if_needed(activity)
175 175 if activity['parent_id']
176 176
177 177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 178 activity['name'] = parent_activity.name
179 179 activity['position'] = parent_activity.position
180 180
181 181 if Enumeration.overridding_change?(activity, parent_activity)
182 182 project_activity = self.time_entry_activities.create(activity)
183 183
184 184 if project_activity.new_record?
185 185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 186 else
187 187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 188 end
189 189 end
190 190 end
191 191 end
192 192
193 193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 194 #
195 195 # Examples:
196 196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 197 # project.project_condition(false) => "projects.id = 1"
198 198 def project_condition(with_subprojects)
199 199 cond = "#{Project.table_name}.id = #{id}"
200 200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 201 cond
202 202 end
203 203
204 204 def self.find(*args)
205 205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 206 project = find_by_identifier(*args)
207 207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 208 project
209 209 else
210 210 super
211 211 end
212 212 end
213 213
214 214 def to_param
215 215 # id is used for projects with a numeric identifier (compatibility)
216 216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 217 end
218 218
219 219 def active?
220 220 self.status == STATUS_ACTIVE
221 221 end
222 222
223 223 def archived?
224 224 self.status == STATUS_ARCHIVED
225 225 end
226 226
227 227 # Archives the project and its descendants
228 228 def archive
229 229 # Check that there is no issue of a non descendant project that is assigned
230 230 # to one of the project or descendant versions
231 231 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
232 232 if v_ids.any? && Issue.find(:first, :include => :project,
233 233 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
234 234 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
235 235 return false
236 236 end
237 237 Project.transaction do
238 238 archive!
239 239 end
240 240 true
241 241 end
242 242
243 243 # Unarchives the project
244 244 # All its ancestors must be active
245 245 def unarchive
246 246 return false if ancestors.detect {|a| !a.active?}
247 247 update_attribute :status, STATUS_ACTIVE
248 248 end
249 249
250 250 # Returns an array of projects the project can be moved to
251 251 # by the current user
252 252 def allowed_parents
253 253 return @allowed_parents if @allowed_parents
254 254 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
255 255 @allowed_parents = @allowed_parents - self_and_descendants
256 256 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
257 257 @allowed_parents << nil
258 258 end
259 259 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
260 260 @allowed_parents << parent
261 261 end
262 262 @allowed_parents
263 263 end
264 264
265 265 # Sets the parent of the project with authorization check
266 266 def set_allowed_parent!(p)
267 267 unless p.nil? || p.is_a?(Project)
268 268 if p.to_s.blank?
269 269 p = nil
270 270 else
271 271 p = Project.find_by_id(p)
272 272 return false unless p
273 273 end
274 274 end
275 275 if p.nil?
276 276 if !new_record? && allowed_parents.empty?
277 277 return false
278 278 end
279 279 elsif !allowed_parents.include?(p)
280 280 return false
281 281 end
282 282 set_parent!(p)
283 283 end
284 284
285 285 # Sets the parent of the project
286 286 # Argument can be either a Project, a String, a Fixnum or nil
287 287 def set_parent!(p)
288 288 unless p.nil? || p.is_a?(Project)
289 289 if p.to_s.blank?
290 290 p = nil
291 291 else
292 292 p = Project.find_by_id(p)
293 293 return false unless p
294 294 end
295 295 end
296 296 if p == parent && !p.nil?
297 297 # Nothing to do
298 298 true
299 299 elsif p.nil? || (p.active? && move_possible?(p))
300 300 # Insert the project so that target's children or root projects stay alphabetically sorted
301 301 sibs = (p.nil? ? self.class.roots : p.children)
302 302 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
303 303 if to_be_inserted_before
304 304 move_to_left_of(to_be_inserted_before)
305 305 elsif p.nil?
306 306 if sibs.empty?
307 307 # move_to_root adds the project in first (ie. left) position
308 308 move_to_root
309 309 else
310 310 move_to_right_of(sibs.last) unless self == sibs.last
311 311 end
312 312 else
313 313 # move_to_child_of adds the project in last (ie.right) position
314 314 move_to_child_of(p)
315 315 end
316 316 Issue.update_versions_from_hierarchy_change(self)
317 317 true
318 318 else
319 319 # Can not move to the given target
320 320 false
321 321 end
322 322 end
323 323
324 324 # Returns an array of the trackers used by the project and its active sub projects
325 325 def rolled_up_trackers
326 326 @rolled_up_trackers ||=
327 327 Tracker.find(:all, :include => :projects,
328 328 :select => "DISTINCT #{Tracker.table_name}.*",
329 329 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
330 330 :order => "#{Tracker.table_name}.position")
331 331 end
332 332
333 333 # Closes open and locked project versions that are completed
334 334 def close_completed_versions
335 335 Version.transaction do
336 336 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
337 337 if version.completed?
338 338 version.update_attribute(:status, 'closed')
339 339 end
340 340 end
341 341 end
342 342 end
343 343
344 344 # Returns a scope of the Versions on subprojects
345 345 def rolled_up_versions
346 346 @rolled_up_versions ||=
347 347 Version.scoped(:include => :project,
348 348 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
349 349 end
350 350
351 351 # Returns a scope of the Versions used by the project
352 352 def shared_versions
353 353 @shared_versions ||=
354 354 Version.scoped(:include => :project,
355 355 :conditions => "#{Project.table_name}.id = #{id}" +
356 356 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
357 357 " #{Version.table_name}.sharing = 'system'" +
358 358 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
359 359 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
360 360 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
361 361 "))")
362 362 end
363 363
364 364 # Returns a hash of project users grouped by role
365 365 def users_by_role
366 366 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
367 367 m.roles.each do |r|
368 368 h[r] ||= []
369 369 h[r] << m.user
370 370 end
371 371 h
372 372 end
373 373 end
374 374
375 375 # Deletes all project's members
376 376 def delete_all_members
377 377 me, mr = Member.table_name, MemberRole.table_name
378 378 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
379 379 Member.delete_all(['project_id = ?', id])
380 380 end
381 381
382 382 # Users issues can be assigned to
383 383 def assignable_users
384 384 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
385 385 end
386 386
387 387 # Returns the mail adresses of users that should be always notified on project events
388 388 def recipients
389 389 notified_users.collect {|user| user.mail}
390 390 end
391 391
392 392 # Returns the users that should be notified on project events
393 393 def notified_users
394 394 # TODO: User part should be extracted to User#notify_about?
395 395 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
396 396 end
397 397
398 398 # Returns an array of all custom fields enabled for project issues
399 399 # (explictly associated custom fields and custom fields enabled for all projects)
400 400 def all_issue_custom_fields
401 401 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
402 402 end
403 403
404 404 def project
405 405 self
406 406 end
407 407
408 408 def <=>(project)
409 409 name.downcase <=> project.name.downcase
410 410 end
411 411
412 412 def to_s
413 413 name
414 414 end
415 415
416 416 # Returns a short description of the projects (first lines)
417 417 def short_description(length = 255)
418 418 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
419 419 end
420 420
421 421 def css_classes
422 422 s = 'project'
423 423 s << ' root' if root?
424 424 s << ' child' if child?
425 425 s << (leaf? ? ' leaf' : ' parent')
426 426 s
427 427 end
428 428
429 429 # The earliest start date of a project, based on it's issues and versions
430 430 def start_date
431 431 if module_enabled?(:issue_tracking)
432 432 [
433 433 issues.minimum('start_date'),
434 434 shared_versions.collect(&:effective_date),
435 435 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
436 436 ].flatten.compact.min
437 437 end
438 438 end
439 439
440 440 # The latest due date of an issue or version
441 441 def due_date
442 442 if module_enabled?(:issue_tracking)
443 443 [
444 444 issues.maximum('due_date'),
445 445 shared_versions.collect(&:effective_date),
446 446 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
447 447 ].flatten.compact.max
448 448 end
449 449 end
450 450
451 451 def overdue?
452 452 active? && !due_date.nil? && (due_date < Date.today)
453 453 end
454 454
455 455 # Returns the percent completed for this project, based on the
456 456 # progress on it's versions.
457 457 def completed_percent(options={:include_subprojects => false})
458 458 if options.delete(:include_subprojects)
459 459 total = self_and_descendants.collect(&:completed_percent).sum
460 460
461 461 total / self_and_descendants.count
462 462 else
463 463 if versions.count > 0
464 464 total = versions.collect(&:completed_pourcent).sum
465 465
466 466 total / versions.count
467 467 else
468 468 100
469 469 end
470 470 end
471 471 end
472 472
473 473 # Return true if this project is allowed to do the specified action.
474 474 # action can be:
475 475 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
476 476 # * a permission Symbol (eg. :edit_project)
477 477 def allows_to?(action)
478 478 if action.is_a? Hash
479 479 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
480 480 else
481 481 allowed_permissions.include? action
482 482 end
483 483 end
484 484
485 485 def module_enabled?(module_name)
486 486 module_name = module_name.to_s
487 487 enabled_modules.detect {|m| m.name == module_name}
488 488 end
489 489
490 490 def enabled_module_names=(module_names)
491 491 if module_names && module_names.is_a?(Array)
492 492 module_names = module_names.collect(&:to_s)
493 493 # remove disabled modules
494 494 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
495 495 # add new modules
496 496 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
497 497 else
498 498 enabled_modules.clear
499 499 end
500 500 end
501 501
502 502 # Returns an array of projects that are in this project's hierarchy
503 503 #
504 504 # Example: parents, children, siblings
505 505 def hierarchy
506 506 parents = project.self_and_ancestors || []
507 507 descendants = project.descendants || []
508 508 project_hierarchy = parents | descendants # Set union
509 509 end
510 510
511 511 # Returns an auto-generated project identifier based on the last identifier used
512 512 def self.next_identifier
513 513 p = Project.find(:first, :order => 'created_on DESC')
514 514 p.nil? ? nil : p.identifier.to_s.succ
515 515 end
516 516
517 517 # Copies and saves the Project instance based on the +project+.
518 518 # Duplicates the source project's:
519 519 # * Wiki
520 520 # * Versions
521 521 # * Categories
522 522 # * Issues
523 523 # * Members
524 524 # * Queries
525 525 #
526 526 # Accepts an +options+ argument to specify what to copy
527 527 #
528 528 # Examples:
529 529 # project.copy(1) # => copies everything
530 530 # project.copy(1, :only => 'members') # => copies members only
531 531 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
532 532 def copy(project, options={})
533 533 project = project.is_a?(Project) ? project : Project.find(project)
534 534
535 535 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
536 536 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
537 537
538 538 Project.transaction do
539 539 if save
540 540 reload
541 541 to_be_copied.each do |name|
542 542 send "copy_#{name}", project
543 543 end
544 544 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
545 545 save
546 546 end
547 547 end
548 548 end
549 549
550 550
551 551 # Copies +project+ and returns the new instance. This will not save
552 552 # the copy
553 553 def self.copy_from(project)
554 554 begin
555 555 project = project.is_a?(Project) ? project : Project.find(project)
556 556 if project
557 557 # clear unique attributes
558 558 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
559 559 copy = Project.new(attributes)
560 560 copy.enabled_modules = project.enabled_modules
561 561 copy.trackers = project.trackers
562 562 copy.custom_values = project.custom_values.collect {|v| v.clone}
563 563 copy.issue_custom_fields = project.issue_custom_fields
564 564 return copy
565 565 else
566 566 return nil
567 567 end
568 568 rescue ActiveRecord::RecordNotFound
569 569 return nil
570 570 end
571 571 end
572 572
573 573 # Yields the given block for each project with its level in the tree
574 574 def self.project_tree(projects, &block)
575 575 ancestors = []
576 576 projects.sort_by(&:lft).each do |project|
577 577 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
578 578 ancestors.pop
579 579 end
580 580 yield project, ancestors.size
581 581 ancestors << project
582 582 end
583 583 end
584 584
585 585 private
586 586
587 587 # Destroys children before destroying self
588 588 def destroy_children
589 589 children.each do |child|
590 590 child.destroy
591 591 end
592 592 end
593 593
594 594 # Copies wiki from +project+
595 595 def copy_wiki(project)
596 596 # Check that the source project has a wiki first
597 597 unless project.wiki.nil?
598 598 self.wiki ||= Wiki.new
599 599 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
600 600 wiki_pages_map = {}
601 601 project.wiki.pages.each do |page|
602 602 # Skip pages without content
603 603 next if page.content.nil?
604 604 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
605 605 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
606 606 new_wiki_page.content = new_wiki_content
607 607 wiki.pages << new_wiki_page
608 608 wiki_pages_map[page.id] = new_wiki_page
609 609 end
610 610 wiki.save
611 611 # Reproduce page hierarchy
612 612 project.wiki.pages.each do |page|
613 613 if page.parent_id && wiki_pages_map[page.id]
614 614 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
615 615 wiki_pages_map[page.id].save
616 616 end
617 617 end
618 618 end
619 619 end
620 620
621 621 # Copies versions from +project+
622 622 def copy_versions(project)
623 623 project.versions.each do |version|
624 624 new_version = Version.new
625 625 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
626 626 self.versions << new_version
627 627 end
628 628 end
629 629
630 630 # Copies issue categories from +project+
631 631 def copy_issue_categories(project)
632 632 project.issue_categories.each do |issue_category|
633 633 new_issue_category = IssueCategory.new
634 634 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
635 635 self.issue_categories << new_issue_category
636 636 end
637 637 end
638 638
639 639 # Copies issues from +project+
640 640 def copy_issues(project)
641 641 # Stores the source issue id as a key and the copied issues as the
642 642 # value. Used to map the two togeather for issue relations.
643 643 issues_map = {}
644 644
645 645 # Get issues sorted by root_id, lft so that parent issues
646 646 # get copied before their children
647 647 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
648 648 new_issue = Issue.new
649 649 new_issue.copy_from(issue)
650 650 new_issue.project = self
651 651 # Reassign fixed_versions by name, since names are unique per
652 652 # project and the versions for self are not yet saved
653 653 if issue.fixed_version
654 654 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
655 655 end
656 656 # Reassign the category by name, since names are unique per
657 657 # project and the categories for self are not yet saved
658 658 if issue.category
659 659 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
660 660 end
661 661 # Parent issue
662 662 if issue.parent_id
663 663 if copied_parent = issues_map[issue.parent_id]
664 664 new_issue.parent_issue_id = copied_parent.id
665 665 end
666 666 end
667 667
668 668 self.issues << new_issue
669 669 issues_map[issue.id] = new_issue
670 670 end
671 671
672 672 # Relations after in case issues related each other
673 673 project.issues.each do |issue|
674 674 new_issue = issues_map[issue.id]
675 675
676 676 # Relations
677 677 issue.relations_from.each do |source_relation|
678 678 new_issue_relation = IssueRelation.new
679 679 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
680 680 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
681 681 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
682 682 new_issue_relation.issue_to = source_relation.issue_to
683 683 end
684 684 new_issue.relations_from << new_issue_relation
685 685 end
686 686
687 687 issue.relations_to.each do |source_relation|
688 688 new_issue_relation = IssueRelation.new
689 689 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
690 690 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
691 691 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
692 692 new_issue_relation.issue_from = source_relation.issue_from
693 693 end
694 694 new_issue.relations_to << new_issue_relation
695 695 end
696 696 end
697 697 end
698 698
699 699 # Copies members from +project+
700 700 def copy_members(project)
701 701 project.memberships.each do |member|
702 702 new_member = Member.new
703 703 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
704 704 # only copy non inherited roles
705 705 # inherited roles will be added when copying the group membership
706 706 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
707 707 next if role_ids.empty?
708 708 new_member.role_ids = role_ids
709 709 new_member.project = self
710 710 self.members << new_member
711 711 end
712 712 end
713 713
714 714 # Copies queries from +project+
715 715 def copy_queries(project)
716 716 project.queries.each do |query|
717 717 new_query = Query.new
718 718 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
719 719 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
720 720 new_query.project = self
721 721 self.queries << new_query
722 722 end
723 723 end
724 724
725 725 # Copies boards from +project+
726 726 def copy_boards(project)
727 727 project.boards.each do |board|
728 728 new_board = Board.new
729 729 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
730 730 new_board.project = self
731 731 self.boards << new_board
732 732 end
733 733 end
734 734
735 735 def allowed_permissions
736 736 @allowed_permissions ||= begin
737 737 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
738 738 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
739 739 end
740 740 end
741 741
742 742 def allowed_actions
743 743 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
744 744 end
745 745
746 746 # Returns all the active Systemwide and project specific activities
747 747 def active_activities
748 748 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
749 749
750 750 if overridden_activity_ids.empty?
751 751 return TimeEntryActivity.shared.active
752 752 else
753 753 return system_activities_and_project_overrides
754 754 end
755 755 end
756 756
757 757 # Returns all the Systemwide and project specific activities
758 758 # (inactive and active)
759 759 def all_activities
760 760 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
761 761
762 762 if overridden_activity_ids.empty?
763 763 return TimeEntryActivity.shared
764 764 else
765 765 return system_activities_and_project_overrides(true)
766 766 end
767 767 end
768 768
769 769 # Returns the systemwide active activities merged with the project specific overrides
770 770 def system_activities_and_project_overrides(include_inactive=false)
771 771 if include_inactive
772 772 return TimeEntryActivity.shared.
773 773 find(:all,
774 774 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
775 775 self.time_entry_activities
776 776 else
777 777 return TimeEntryActivity.shared.active.
778 778 find(:all,
779 779 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
780 780 self.time_entry_activities.active
781 781 end
782 782 end
783 783
784 784 # Archives subprojects recursively
785 785 def archive!
786 786 children.each do |subproject|
787 787 subproject.send :archive!
788 788 end
789 789 update_attribute :status, STATUS_ARCHIVED
790 790 end
791 791 end
@@ -1,1019 +1,1018
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 ProjectTest < ActiveSupport::TestCase
21 21 fixtures :all
22 22
23 23 def setup
24 24 @ecookbook = Project.find(1)
25 25 @ecookbook_sub1 = Project.find(3)
26 26 User.current = nil
27 27 end
28 28
29 29 should_validate_presence_of :name
30 30 should_validate_presence_of :identifier
31 31
32 should_validate_uniqueness_of :name
33 32 should_validate_uniqueness_of :identifier
34 33
35 34 context "associations" do
36 35 should_have_many :members
37 36 should_have_many :users, :through => :members
38 37 should_have_many :member_principals
39 38 should_have_many :principals, :through => :member_principals
40 39 should_have_many :enabled_modules
41 40 should_have_many :issues
42 41 should_have_many :issue_changes, :through => :issues
43 42 should_have_many :versions
44 43 should_have_many :time_entries
45 44 should_have_many :queries
46 45 should_have_many :documents
47 46 should_have_many :news
48 47 should_have_many :issue_categories
49 48 should_have_many :boards
50 49 should_have_many :changesets, :through => :repository
51 50
52 51 should_have_one :repository
53 52 should_have_one :wiki
54 53
55 54 should_have_and_belong_to_many :trackers
56 55 should_have_and_belong_to_many :issue_custom_fields
57 56 end
58 57
59 58 def test_truth
60 59 assert_kind_of Project, @ecookbook
61 60 assert_equal "eCookbook", @ecookbook.name
62 61 end
63 62
64 63 def test_update
65 64 assert_equal "eCookbook", @ecookbook.name
66 65 @ecookbook.name = "eCook"
67 66 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
68 67 @ecookbook.reload
69 68 assert_equal "eCook", @ecookbook.name
70 69 end
71 70
72 71 def test_validate_identifier
73 72 to_test = {"abc" => true,
74 73 "ab12" => true,
75 74 "ab-12" => true,
76 75 "12" => false,
77 76 "new" => false}
78 77
79 78 to_test.each do |identifier, valid|
80 79 p = Project.new
81 80 p.identifier = identifier
82 81 p.valid?
83 82 assert_equal valid, p.errors.on('identifier').nil?
84 83 end
85 84 end
86 85
87 86 def test_members_should_be_active_users
88 87 Project.all.each do |project|
89 88 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
90 89 end
91 90 end
92 91
93 92 def test_users_should_be_active_users
94 93 Project.all.each do |project|
95 94 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
96 95 end
97 96 end
98 97
99 98 def test_archive
100 99 user = @ecookbook.members.first.user
101 100 @ecookbook.archive
102 101 @ecookbook.reload
103 102
104 103 assert !@ecookbook.active?
105 104 assert @ecookbook.archived?
106 105 assert !user.projects.include?(@ecookbook)
107 106 # Subproject are also archived
108 107 assert !@ecookbook.children.empty?
109 108 assert @ecookbook.descendants.active.empty?
110 109 end
111 110
112 111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
113 112 # Assign an issue of a project to a version of a child project
114 113 Issue.find(4).update_attribute :fixed_version_id, 4
115 114
116 115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
117 116 assert_equal false, @ecookbook.archive
118 117 end
119 118 @ecookbook.reload
120 119 assert @ecookbook.active?
121 120 end
122 121
123 122 def test_unarchive
124 123 user = @ecookbook.members.first.user
125 124 @ecookbook.archive
126 125 # A subproject of an archived project can not be unarchived
127 126 assert !@ecookbook_sub1.unarchive
128 127
129 128 # Unarchive project
130 129 assert @ecookbook.unarchive
131 130 @ecookbook.reload
132 131 assert @ecookbook.active?
133 132 assert !@ecookbook.archived?
134 133 assert user.projects.include?(@ecookbook)
135 134 # Subproject can now be unarchived
136 135 @ecookbook_sub1.reload
137 136 assert @ecookbook_sub1.unarchive
138 137 end
139 138
140 139 def test_destroy
141 140 # 2 active members
142 141 assert_equal 2, @ecookbook.members.size
143 142 # and 1 is locked
144 143 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
145 144 # some boards
146 145 assert @ecookbook.boards.any?
147 146
148 147 @ecookbook.destroy
149 148 # make sure that the project non longer exists
150 149 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
151 150 # make sure related data was removed
152 151 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
153 152 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
154 153 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
155 154 end
156 155
157 156 def test_move_an_orphan_project_to_a_root_project
158 157 sub = Project.find(2)
159 158 sub.set_parent! @ecookbook
160 159 assert_equal @ecookbook.id, sub.parent.id
161 160 @ecookbook.reload
162 161 assert_equal 4, @ecookbook.children.size
163 162 end
164 163
165 164 def test_move_an_orphan_project_to_a_subproject
166 165 sub = Project.find(2)
167 166 assert sub.set_parent!(@ecookbook_sub1)
168 167 end
169 168
170 169 def test_move_a_root_project_to_a_project
171 170 sub = @ecookbook
172 171 assert sub.set_parent!(Project.find(2))
173 172 end
174 173
175 174 def test_should_not_move_a_project_to_its_children
176 175 sub = @ecookbook
177 176 assert !(sub.set_parent!(Project.find(3)))
178 177 end
179 178
180 179 def test_set_parent_should_add_roots_in_alphabetical_order
181 180 ProjectCustomField.delete_all
182 181 Project.delete_all
183 182 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
184 183 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
185 184 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
186 185 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
187 186
188 187 assert_equal 4, Project.count
189 188 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
190 189 end
191 190
192 191 def test_set_parent_should_add_children_in_alphabetical_order
193 192 ProjectCustomField.delete_all
194 193 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
195 194 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
196 195 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
197 196 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
198 197 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
199 198
200 199 parent.reload
201 200 assert_equal 4, parent.children.size
202 201 assert_equal parent.children.sort_by(&:name), parent.children
203 202 end
204 203
205 204 def test_rebuild_should_sort_children_alphabetically
206 205 ProjectCustomField.delete_all
207 206 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
208 207 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
209 208 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
210 209 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
211 210 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
212 211
213 212 Project.update_all("lft = NULL, rgt = NULL")
214 213 Project.rebuild!
215 214
216 215 parent.reload
217 216 assert_equal 4, parent.children.size
218 217 assert_equal parent.children.sort_by(&:name), parent.children
219 218 end
220 219
221 220
222 221 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
223 222 # Parent issue with a hierarchy project's fixed version
224 223 parent_issue = Issue.find(1)
225 224 parent_issue.update_attribute(:fixed_version_id, 4)
226 225 parent_issue.reload
227 226 assert_equal 4, parent_issue.fixed_version_id
228 227
229 228 # Should keep fixed versions for the issues
230 229 issue_with_local_fixed_version = Issue.find(5)
231 230 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
232 231 issue_with_local_fixed_version.reload
233 232 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
234 233
235 234 # Local issue with hierarchy fixed_version
236 235 issue_with_hierarchy_fixed_version = Issue.find(13)
237 236 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
238 237 issue_with_hierarchy_fixed_version.reload
239 238 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
240 239
241 240 # Move project out of the issue's hierarchy
242 241 moved_project = Project.find(3)
243 242 moved_project.set_parent!(Project.find(2))
244 243 parent_issue.reload
245 244 issue_with_local_fixed_version.reload
246 245 issue_with_hierarchy_fixed_version.reload
247 246
248 247 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
249 248 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
250 249 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
251 250 end
252 251
253 252 def test_parent
254 253 p = Project.find(6).parent
255 254 assert p.is_a?(Project)
256 255 assert_equal 5, p.id
257 256 end
258 257
259 258 def test_ancestors
260 259 a = Project.find(6).ancestors
261 260 assert a.first.is_a?(Project)
262 261 assert_equal [1, 5], a.collect(&:id)
263 262 end
264 263
265 264 def test_root
266 265 r = Project.find(6).root
267 266 assert r.is_a?(Project)
268 267 assert_equal 1, r.id
269 268 end
270 269
271 270 def test_children
272 271 c = Project.find(1).children
273 272 assert c.first.is_a?(Project)
274 273 assert_equal [5, 3, 4], c.collect(&:id)
275 274 end
276 275
277 276 def test_descendants
278 277 d = Project.find(1).descendants
279 278 assert d.first.is_a?(Project)
280 279 assert_equal [5, 6, 3, 4], d.collect(&:id)
281 280 end
282 281
283 282 def test_allowed_parents_should_be_empty_for_non_member_user
284 283 Role.non_member.add_permission!(:add_project)
285 284 user = User.find(9)
286 285 assert user.memberships.empty?
287 286 User.current = user
288 287 assert Project.new.allowed_parents.compact.empty?
289 288 end
290 289
291 290 def test_allowed_parents_with_add_subprojects_permission
292 291 Role.find(1).remove_permission!(:add_project)
293 292 Role.find(1).add_permission!(:add_subprojects)
294 293 User.current = User.find(2)
295 294 # new project
296 295 assert !Project.new.allowed_parents.include?(nil)
297 296 assert Project.new.allowed_parents.include?(Project.find(1))
298 297 # existing root project
299 298 assert Project.find(1).allowed_parents.include?(nil)
300 299 # existing child
301 300 assert Project.find(3).allowed_parents.include?(Project.find(1))
302 301 assert !Project.find(3).allowed_parents.include?(nil)
303 302 end
304 303
305 304 def test_allowed_parents_with_add_project_permission
306 305 Role.find(1).add_permission!(:add_project)
307 306 Role.find(1).remove_permission!(:add_subprojects)
308 307 User.current = User.find(2)
309 308 # new project
310 309 assert Project.new.allowed_parents.include?(nil)
311 310 assert !Project.new.allowed_parents.include?(Project.find(1))
312 311 # existing root project
313 312 assert Project.find(1).allowed_parents.include?(nil)
314 313 # existing child
315 314 assert Project.find(3).allowed_parents.include?(Project.find(1))
316 315 assert Project.find(3).allowed_parents.include?(nil)
317 316 end
318 317
319 318 def test_allowed_parents_with_add_project_and_subprojects_permission
320 319 Role.find(1).add_permission!(:add_project)
321 320 Role.find(1).add_permission!(:add_subprojects)
322 321 User.current = User.find(2)
323 322 # new project
324 323 assert Project.new.allowed_parents.include?(nil)
325 324 assert Project.new.allowed_parents.include?(Project.find(1))
326 325 # existing root project
327 326 assert Project.find(1).allowed_parents.include?(nil)
328 327 # existing child
329 328 assert Project.find(3).allowed_parents.include?(Project.find(1))
330 329 assert Project.find(3).allowed_parents.include?(nil)
331 330 end
332 331
333 332 def test_users_by_role
334 333 users_by_role = Project.find(1).users_by_role
335 334 assert_kind_of Hash, users_by_role
336 335 role = Role.find(1)
337 336 assert_kind_of Array, users_by_role[role]
338 337 assert users_by_role[role].include?(User.find(2))
339 338 end
340 339
341 340 def test_rolled_up_trackers
342 341 parent = Project.find(1)
343 342 parent.trackers = Tracker.find([1,2])
344 343 child = parent.children.find(3)
345 344
346 345 assert_equal [1, 2], parent.tracker_ids
347 346 assert_equal [2, 3], child.trackers.collect(&:id)
348 347
349 348 assert_kind_of Tracker, parent.rolled_up_trackers.first
350 349 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
351 350
352 351 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
353 352 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
354 353 end
355 354
356 355 def test_rolled_up_trackers_should_ignore_archived_subprojects
357 356 parent = Project.find(1)
358 357 parent.trackers = Tracker.find([1,2])
359 358 child = parent.children.find(3)
360 359 child.trackers = Tracker.find([1,3])
361 360 parent.children.each(&:archive)
362 361
363 362 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
364 363 end
365 364
366 365 context "#rolled_up_versions" do
367 366 setup do
368 367 @project = Project.generate!
369 368 @parent_version_1 = Version.generate!(:project => @project)
370 369 @parent_version_2 = Version.generate!(:project => @project)
371 370 end
372 371
373 372 should "include the versions for the current project" do
374 373 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
375 374 end
376 375
377 376 should "include versions for a subproject" do
378 377 @subproject = Project.generate!
379 378 @subproject.set_parent!(@project)
380 379 @subproject_version = Version.generate!(:project => @subproject)
381 380
382 381 assert_same_elements [
383 382 @parent_version_1,
384 383 @parent_version_2,
385 384 @subproject_version
386 385 ], @project.rolled_up_versions
387 386 end
388 387
389 388 should "include versions for a sub-subproject" do
390 389 @subproject = Project.generate!
391 390 @subproject.set_parent!(@project)
392 391 @sub_subproject = Project.generate!
393 392 @sub_subproject.set_parent!(@subproject)
394 393 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
395 394
396 395 @project.reload
397 396
398 397 assert_same_elements [
399 398 @parent_version_1,
400 399 @parent_version_2,
401 400 @sub_subproject_version
402 401 ], @project.rolled_up_versions
403 402 end
404 403
405 404
406 405 should "only check active projects" do
407 406 @subproject = Project.generate!
408 407 @subproject.set_parent!(@project)
409 408 @subproject_version = Version.generate!(:project => @subproject)
410 409 assert @subproject.archive
411 410
412 411 @project.reload
413 412
414 413 assert !@subproject.active?
415 414 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
416 415 end
417 416 end
418 417
419 418 def test_shared_versions_none_sharing
420 419 p = Project.find(5)
421 420 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
422 421 assert p.shared_versions.include?(v)
423 422 assert !p.children.first.shared_versions.include?(v)
424 423 assert !p.root.shared_versions.include?(v)
425 424 assert !p.siblings.first.shared_versions.include?(v)
426 425 assert !p.root.siblings.first.shared_versions.include?(v)
427 426 end
428 427
429 428 def test_shared_versions_descendants_sharing
430 429 p = Project.find(5)
431 430 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
432 431 assert p.shared_versions.include?(v)
433 432 assert p.children.first.shared_versions.include?(v)
434 433 assert !p.root.shared_versions.include?(v)
435 434 assert !p.siblings.first.shared_versions.include?(v)
436 435 assert !p.root.siblings.first.shared_versions.include?(v)
437 436 end
438 437
439 438 def test_shared_versions_hierarchy_sharing
440 439 p = Project.find(5)
441 440 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
442 441 assert p.shared_versions.include?(v)
443 442 assert p.children.first.shared_versions.include?(v)
444 443 assert p.root.shared_versions.include?(v)
445 444 assert !p.siblings.first.shared_versions.include?(v)
446 445 assert !p.root.siblings.first.shared_versions.include?(v)
447 446 end
448 447
449 448 def test_shared_versions_tree_sharing
450 449 p = Project.find(5)
451 450 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
452 451 assert p.shared_versions.include?(v)
453 452 assert p.children.first.shared_versions.include?(v)
454 453 assert p.root.shared_versions.include?(v)
455 454 assert p.siblings.first.shared_versions.include?(v)
456 455 assert !p.root.siblings.first.shared_versions.include?(v)
457 456 end
458 457
459 458 def test_shared_versions_system_sharing
460 459 p = Project.find(5)
461 460 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
462 461 assert p.shared_versions.include?(v)
463 462 assert p.children.first.shared_versions.include?(v)
464 463 assert p.root.shared_versions.include?(v)
465 464 assert p.siblings.first.shared_versions.include?(v)
466 465 assert p.root.siblings.first.shared_versions.include?(v)
467 466 end
468 467
469 468 def test_shared_versions
470 469 parent = Project.find(1)
471 470 child = parent.children.find(3)
472 471 private_child = parent.children.find(5)
473 472
474 473 assert_equal [1,2,3], parent.version_ids.sort
475 474 assert_equal [4], child.version_ids
476 475 assert_equal [6], private_child.version_ids
477 476 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
478 477
479 478 assert_equal 6, parent.shared_versions.size
480 479 parent.shared_versions.each do |version|
481 480 assert_kind_of Version, version
482 481 end
483 482
484 483 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
485 484 end
486 485
487 486 def test_shared_versions_should_ignore_archived_subprojects
488 487 parent = Project.find(1)
489 488 child = parent.children.find(3)
490 489 child.archive
491 490 parent.reload
492 491
493 492 assert_equal [1,2,3], parent.version_ids.sort
494 493 assert_equal [4], child.version_ids
495 494 assert !parent.shared_versions.collect(&:id).include?(4)
496 495 end
497 496
498 497 def test_shared_versions_visible_to_user
499 498 user = User.find(3)
500 499 parent = Project.find(1)
501 500 child = parent.children.find(5)
502 501
503 502 assert_equal [1,2,3], parent.version_ids.sort
504 503 assert_equal [6], child.version_ids
505 504
506 505 versions = parent.shared_versions.visible(user)
507 506
508 507 assert_equal 4, versions.size
509 508 versions.each do |version|
510 509 assert_kind_of Version, version
511 510 end
512 511
513 512 assert !versions.collect(&:id).include?(6)
514 513 end
515 514
516 515
517 516 def test_next_identifier
518 517 ProjectCustomField.delete_all
519 518 Project.create!(:name => 'last', :identifier => 'p2008040')
520 519 assert_equal 'p2008041', Project.next_identifier
521 520 end
522 521
523 522 def test_next_identifier_first_project
524 523 Project.delete_all
525 524 assert_nil Project.next_identifier
526 525 end
527 526
528 527
529 528 def test_enabled_module_names_should_not_recreate_enabled_modules
530 529 project = Project.find(1)
531 530 # Remove one module
532 531 modules = project.enabled_modules.slice(0..-2)
533 532 assert modules.any?
534 533 assert_difference 'EnabledModule.count', -1 do
535 534 project.enabled_module_names = modules.collect(&:name)
536 535 end
537 536 project.reload
538 537 # Ids should be preserved
539 538 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
540 539 end
541 540
542 541 def test_copy_from_existing_project
543 542 source_project = Project.find(1)
544 543 copied_project = Project.copy_from(1)
545 544
546 545 assert copied_project
547 546 # Cleared attributes
548 547 assert copied_project.id.blank?
549 548 assert copied_project.name.blank?
550 549 assert copied_project.identifier.blank?
551 550
552 551 # Duplicated attributes
553 552 assert_equal source_project.description, copied_project.description
554 553 assert_equal source_project.enabled_modules, copied_project.enabled_modules
555 554 assert_equal source_project.trackers, copied_project.trackers
556 555
557 556 # Default attributes
558 557 assert_equal 1, copied_project.status
559 558 end
560 559
561 560 def test_activities_should_use_the_system_activities
562 561 project = Project.find(1)
563 562 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
564 563 end
565 564
566 565
567 566 def test_activities_should_use_the_project_specific_activities
568 567 project = Project.find(1)
569 568 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
570 569 assert overridden_activity.save!
571 570
572 571 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
573 572 end
574 573
575 574 def test_activities_should_not_include_the_inactive_project_specific_activities
576 575 project = Project.find(1)
577 576 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
578 577 assert overridden_activity.save!
579 578
580 579 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
581 580 end
582 581
583 582 def test_activities_should_not_include_project_specific_activities_from_other_projects
584 583 project = Project.find(1)
585 584 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
586 585 assert overridden_activity.save!
587 586
588 587 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
589 588 end
590 589
591 590 def test_activities_should_handle_nils
592 591 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
593 592 TimeEntryActivity.delete_all
594 593
595 594 # No activities
596 595 project = Project.find(1)
597 596 assert project.activities.empty?
598 597
599 598 # No system, one overridden
600 599 assert overridden_activity.save!
601 600 project.reload
602 601 assert_equal [overridden_activity], project.activities
603 602 end
604 603
605 604 def test_activities_should_override_system_activities_with_project_activities
606 605 project = Project.find(1)
607 606 parent_activity = TimeEntryActivity.find(:first)
608 607 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
609 608 assert overridden_activity.save!
610 609
611 610 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
612 611 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
613 612 end
614 613
615 614 def test_activities_should_include_inactive_activities_if_specified
616 615 project = Project.find(1)
617 616 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
618 617 assert overridden_activity.save!
619 618
620 619 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
621 620 end
622 621
623 622 test 'activities should not include active System activities if the project has an override that is inactive' do
624 623 project = Project.find(1)
625 624 system_activity = TimeEntryActivity.find_by_name('Design')
626 625 assert system_activity.active?
627 626 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
628 627 assert overridden_activity.save!
629 628
630 629 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
631 630 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
632 631 end
633 632
634 633 def test_close_completed_versions
635 634 Version.update_all("status = 'open'")
636 635 project = Project.find(1)
637 636 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
638 637 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
639 638 project.close_completed_versions
640 639 project.reload
641 640 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
642 641 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
643 642 end
644 643
645 644 context "Project#copy" do
646 645 setup do
647 646 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
648 647 Project.destroy_all :identifier => "copy-test"
649 648 @source_project = Project.find(2)
650 649 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
651 650 @project.trackers = @source_project.trackers
652 651 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
653 652 end
654 653
655 654 should "copy issues" do
656 655 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
657 656 :subject => "copy issue status",
658 657 :tracker_id => 1,
659 658 :assigned_to_id => 2,
660 659 :project_id => @source_project.id)
661 660 assert @project.valid?
662 661 assert @project.issues.empty?
663 662 assert @project.copy(@source_project)
664 663
665 664 assert_equal @source_project.issues.size, @project.issues.size
666 665 @project.issues.each do |issue|
667 666 assert issue.valid?
668 667 assert ! issue.assigned_to.blank?
669 668 assert_equal @project, issue.project
670 669 end
671 670
672 671 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
673 672 assert copied_issue
674 673 assert copied_issue.status
675 674 assert_equal "Closed", copied_issue.status.name
676 675 end
677 676
678 677 should "change the new issues to use the copied version" do
679 678 User.current = User.find(1)
680 679 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
681 680 @source_project.versions << assigned_version
682 681 assert_equal 3, @source_project.versions.size
683 682 Issue.generate_for_project!(@source_project,
684 683 :fixed_version_id => assigned_version.id,
685 684 :subject => "change the new issues to use the copied version",
686 685 :tracker_id => 1,
687 686 :project_id => @source_project.id)
688 687
689 688 assert @project.copy(@source_project)
690 689 @project.reload
691 690 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
692 691
693 692 assert copied_issue
694 693 assert copied_issue.fixed_version
695 694 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
696 695 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
697 696 end
698 697
699 698 should "copy issue relations" do
700 699 Setting.cross_project_issue_relations = '1'
701 700
702 701 second_issue = Issue.generate!(:status_id => 5,
703 702 :subject => "copy issue relation",
704 703 :tracker_id => 1,
705 704 :assigned_to_id => 2,
706 705 :project_id => @source_project.id)
707 706 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
708 707 :issue_to => second_issue,
709 708 :relation_type => "relates")
710 709 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
711 710 :issue_to => second_issue,
712 711 :relation_type => "duplicates")
713 712
714 713 assert @project.copy(@source_project)
715 714 assert_equal @source_project.issues.count, @project.issues.count
716 715 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
717 716 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
718 717
719 718 # First issue with a relation on project
720 719 assert_equal 1, copied_issue.relations.size, "Relation not copied"
721 720 copied_relation = copied_issue.relations.first
722 721 assert_equal "relates", copied_relation.relation_type
723 722 assert_equal copied_second_issue.id, copied_relation.issue_to_id
724 723 assert_not_equal source_relation.id, copied_relation.id
725 724
726 725 # Second issue with a cross project relation
727 726 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
728 727 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
729 728 assert_equal "duplicates", copied_relation.relation_type
730 729 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
731 730 assert_not_equal source_relation_cross_project.id, copied_relation.id
732 731 end
733 732
734 733 should "copy memberships" do
735 734 assert @project.valid?
736 735 assert @project.members.empty?
737 736 assert @project.copy(@source_project)
738 737
739 738 assert_equal @source_project.memberships.size, @project.memberships.size
740 739 @project.memberships.each do |membership|
741 740 assert membership
742 741 assert_equal @project, membership.project
743 742 end
744 743 end
745 744
746 745 should "copy project specific queries" do
747 746 assert @project.valid?
748 747 assert @project.queries.empty?
749 748 assert @project.copy(@source_project)
750 749
751 750 assert_equal @source_project.queries.size, @project.queries.size
752 751 @project.queries.each do |query|
753 752 assert query
754 753 assert_equal @project, query.project
755 754 end
756 755 end
757 756
758 757 should "copy versions" do
759 758 @source_project.versions << Version.generate!
760 759 @source_project.versions << Version.generate!
761 760
762 761 assert @project.versions.empty?
763 762 assert @project.copy(@source_project)
764 763
765 764 assert_equal @source_project.versions.size, @project.versions.size
766 765 @project.versions.each do |version|
767 766 assert version
768 767 assert_equal @project, version.project
769 768 end
770 769 end
771 770
772 771 should "copy wiki" do
773 772 assert_difference 'Wiki.count' do
774 773 assert @project.copy(@source_project)
775 774 end
776 775
777 776 assert @project.wiki
778 777 assert_not_equal @source_project.wiki, @project.wiki
779 778 assert_equal "Start page", @project.wiki.start_page
780 779 end
781 780
782 781 should "copy wiki pages and content with hierarchy" do
783 782 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
784 783 assert @project.copy(@source_project)
785 784 end
786 785
787 786 assert @project.wiki
788 787 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
789 788
790 789 @project.wiki.pages.each do |wiki_page|
791 790 assert wiki_page.content
792 791 assert !@source_project.wiki.pages.include?(wiki_page)
793 792 end
794 793
795 794 parent = @project.wiki.find_page('Parent_page')
796 795 child1 = @project.wiki.find_page('Child_page_1')
797 796 child2 = @project.wiki.find_page('Child_page_2')
798 797 assert_equal parent, child1.parent
799 798 assert_equal parent, child2.parent
800 799 end
801 800
802 801 should "copy issue categories" do
803 802 assert @project.copy(@source_project)
804 803
805 804 assert_equal 2, @project.issue_categories.size
806 805 @project.issue_categories.each do |issue_category|
807 806 assert !@source_project.issue_categories.include?(issue_category)
808 807 end
809 808 end
810 809
811 810 should "copy boards" do
812 811 assert @project.copy(@source_project)
813 812
814 813 assert_equal 1, @project.boards.size
815 814 @project.boards.each do |board|
816 815 assert !@source_project.boards.include?(board)
817 816 end
818 817 end
819 818
820 819 should "change the new issues to use the copied issue categories" do
821 820 issue = Issue.find(4)
822 821 issue.update_attribute(:category_id, 3)
823 822
824 823 assert @project.copy(@source_project)
825 824
826 825 @project.issues.each do |issue|
827 826 assert issue.category
828 827 assert_equal "Stock management", issue.category.name # Same name
829 828 assert_not_equal IssueCategory.find(3), issue.category # Different record
830 829 end
831 830 end
832 831
833 832 should "limit copy with :only option" do
834 833 assert @project.members.empty?
835 834 assert @project.issue_categories.empty?
836 835 assert @source_project.issues.any?
837 836
838 837 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
839 838
840 839 assert @project.members.any?
841 840 assert @project.issue_categories.any?
842 841 assert @project.issues.empty?
843 842 end
844 843
845 844 end
846 845
847 846 context "#start_date" do
848 847 setup do
849 848 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
850 849 @project = Project.generate!(:identifier => 'test0')
851 850 @project.trackers << Tracker.generate!
852 851 end
853 852
854 853 should "be nil if there are no issues on the project" do
855 854 assert_nil @project.start_date
856 855 end
857 856
858 857 should "be nil if issue tracking is disabled" do
859 858 Issue.generate_for_project!(@project, :start_date => Date.today)
860 859 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
861 860 @project.reload
862 861
863 862 assert_nil @project.start_date
864 863 end
865 864
866 865 should "be tested when issues have no start date"
867 866
868 867 should "be the earliest start date of it's issues" do
869 868 early = 7.days.ago.to_date
870 869 Issue.generate_for_project!(@project, :start_date => Date.today)
871 870 Issue.generate_for_project!(@project, :start_date => early)
872 871
873 872 assert_equal early, @project.start_date
874 873 end
875 874
876 875 end
877 876
878 877 context "#due_date" do
879 878 setup do
880 879 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
881 880 @project = Project.generate!(:identifier => 'test0')
882 881 @project.trackers << Tracker.generate!
883 882 end
884 883
885 884 should "be nil if there are no issues on the project" do
886 885 assert_nil @project.due_date
887 886 end
888 887
889 888 should "be nil if issue tracking is disabled" do
890 889 Issue.generate_for_project!(@project, :due_date => Date.today)
891 890 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
892 891 @project.reload
893 892
894 893 assert_nil @project.due_date
895 894 end
896 895
897 896 should "be tested when issues have no due date"
898 897
899 898 should "be the latest due date of it's issues" do
900 899 future = 7.days.from_now.to_date
901 900 Issue.generate_for_project!(@project, :due_date => future)
902 901 Issue.generate_for_project!(@project, :due_date => Date.today)
903 902
904 903 assert_equal future, @project.due_date
905 904 end
906 905
907 906 should "be the latest due date of it's versions" do
908 907 future = 7.days.from_now.to_date
909 908 @project.versions << Version.generate!(:effective_date => future)
910 909 @project.versions << Version.generate!(:effective_date => Date.today)
911 910
912 911
913 912 assert_equal future, @project.due_date
914 913
915 914 end
916 915
917 916 should "pick the latest date from it's issues and versions" do
918 917 future = 7.days.from_now.to_date
919 918 far_future = 14.days.from_now.to_date
920 919 Issue.generate_for_project!(@project, :due_date => far_future)
921 920 @project.versions << Version.generate!(:effective_date => future)
922 921
923 922 assert_equal far_future, @project.due_date
924 923 end
925 924
926 925 end
927 926
928 927 context "Project#completed_percent" do
929 928 setup do
930 929 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
931 930 @project = Project.generate!(:identifier => 'test0')
932 931 @project.trackers << Tracker.generate!
933 932 end
934 933
935 934 context "no versions" do
936 935 should "be 100" do
937 936 assert_equal 100, @project.completed_percent
938 937 end
939 938 end
940 939
941 940 context "with versions" do
942 941 should "return 0 if the versions have no issues" do
943 942 Version.generate!(:project => @project)
944 943 Version.generate!(:project => @project)
945 944
946 945 assert_equal 0, @project.completed_percent
947 946 end
948 947
949 948 should "return 100 if the version has only closed issues" do
950 949 v1 = Version.generate!(:project => @project)
951 950 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
952 951 v2 = Version.generate!(:project => @project)
953 952 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
954 953
955 954 assert_equal 100, @project.completed_percent
956 955 end
957 956
958 957 should "return the averaged completed percent of the versions (not weighted)" do
959 958 v1 = Version.generate!(:project => @project)
960 959 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
961 960 v2 = Version.generate!(:project => @project)
962 961 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
963 962
964 963 assert_equal 50, @project.completed_percent
965 964 end
966 965
967 966 end
968 967 end
969 968
970 969 context "#notified_users" do
971 970 setup do
972 971 @project = Project.generate!
973 972 @role = Role.generate!
974 973
975 974 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
976 975 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
977 976
978 977 @all_events_user = User.generate!(:mail_notification => 'all')
979 978 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
980 979
981 980 @no_events_user = User.generate!(:mail_notification => 'none')
982 981 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
983 982
984 983 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
985 984 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
986 985
987 986 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
988 987 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
989 988
990 989 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
991 990 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
992 991 end
993 992
994 993 should "include members with a mail notification" do
995 994 assert @project.notified_users.include?(@user_with_membership_notification)
996 995 end
997 996
998 997 should "include users with the 'all' notification option" do
999 998 assert @project.notified_users.include?(@all_events_user)
1000 999 end
1001 1000
1002 1001 should "not include users with the 'none' notification option" do
1003 1002 assert !@project.notified_users.include?(@no_events_user)
1004 1003 end
1005 1004
1006 1005 should "not include users with the 'only_my_events' notification option" do
1007 1006 assert !@project.notified_users.include?(@only_my_events_user)
1008 1007 end
1009 1008
1010 1009 should "not include users with the 'only_assigned' notification option" do
1011 1010 assert !@project.notified_users.include?(@only_assigned_user)
1012 1011 end
1013 1012
1014 1013 should "not include users with the 'only_owner' notification option" do
1015 1014 assert !@project.notified_users.include?(@only_owned_user)
1016 1015 end
1017 1016 end
1018 1017
1019 1018 end
General Comments 0
You need to be logged in to leave comments. Login now