##// END OF EJS Templates
Merged r2262, r2341 and r2486 from trunk....
Jean-Philippe Lang -
r2450:57d10ed89396
parent child
Show More
@@ -1,627 +1,627
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include GravatarHelper::PublicMethods
26 26
27 27 extend Forwardable
28 28 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 29
30 30 def current_role
31 31 @current_role ||= User.current.role_for_project(@project)
32 32 end
33 33
34 34 # Return true if user is authorized for controller/action, otherwise false
35 35 def authorize_for(controller, action)
36 36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 37 end
38 38
39 39 # Display a link if user is authorized
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 # Display a link to user's account page
51 51 def link_to_user(user, options={})
52 52 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
53 53 end
54 54
55 55 def link_to_issue(issue, options={})
56 56 options[:class] ||= ''
57 57 options[:class] << ' issue'
58 58 options[:class] << ' closed' if issue.closed?
59 59 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
60 60 end
61 61
62 62 # Generates a link to an attachment.
63 63 # Options:
64 64 # * :text - Link text (default to attachment filename)
65 65 # * :download - Force download (default: false)
66 66 def link_to_attachment(attachment, options={})
67 67 text = options.delete(:text) || attachment.filename
68 68 action = options.delete(:download) ? 'download' : 'show'
69 69
70 70 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
71 71 end
72 72
73 73 def toggle_link(name, id, options={})
74 74 onclick = "Element.toggle('#{id}'); "
75 75 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
76 76 onclick << "return false;"
77 77 link_to(name, "#", :onclick => onclick)
78 78 end
79 79
80 80 def image_to_function(name, function, html_options = {})
81 81 html_options.symbolize_keys!
82 82 tag(:input, html_options.merge({
83 83 :type => "image", :src => image_path(name),
84 84 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
85 85 }))
86 86 end
87 87
88 88 def prompt_to_remote(name, text, param, url, html_options = {})
89 89 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
90 90 link_to name, {}, html_options
91 91 end
92 92
93 93 def format_date(date)
94 94 return nil unless date
95 95 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
96 96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
97 97 date.strftime(@date_format)
98 98 end
99 99
100 100 def format_time(time, include_date = true)
101 101 return nil unless time
102 102 time = time.to_time if time.is_a?(String)
103 103 zone = User.current.time_zone
104 104 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
105 105 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
106 106 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
107 107 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
108 108 end
109 109
110 110 def format_activity_title(text)
111 111 h(truncate_single_line(text, 100))
112 112 end
113 113
114 114 def format_activity_day(date)
115 115 date == Date.today ? l(:label_today).titleize : format_date(date)
116 116 end
117 117
118 118 def format_activity_description(text)
119 119 h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
120 120 end
121 121
122 122 def distance_of_date_in_words(from_date, to_date = 0)
123 123 from_date = from_date.to_date if from_date.respond_to?(:to_date)
124 124 to_date = to_date.to_date if to_date.respond_to?(:to_date)
125 125 distance_in_days = (to_date - from_date).abs
126 126 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
127 127 end
128 128
129 129 def due_date_distance_in_words(date)
130 130 if date
131 131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
132 132 end
133 133 end
134 134
135 135 def render_page_hierarchy(pages, node=nil)
136 136 content = ''
137 137 if pages[node]
138 138 content << "<ul class=\"pages-hierarchy\">\n"
139 139 pages[node].each do |page|
140 140 content << "<li>"
141 141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
142 142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
143 143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
144 144 content << "</li>\n"
145 145 end
146 146 content << "</ul>\n"
147 147 end
148 148 content
149 149 end
150 150
151 151 # Renders flash messages
152 152 def render_flash_messages
153 153 s = ''
154 154 flash.each do |k,v|
155 155 s << content_tag('div', v, :class => "flash #{k}")
156 156 end
157 157 s
158 158 end
159 159
160 160 # Truncates and returns the string as a single line
161 161 def truncate_single_line(string, *args)
162 162 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
163 163 end
164 164
165 165 def html_hours(text)
166 166 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
167 167 end
168 168
169 169 def authoring(created, author, options={})
170 170 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
171 171 link_to(distance_of_time_in_words(Time.now, created),
172 172 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
173 173 :title => format_time(created))
174 174 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
175 175 l(options[:label] || :label_added_time_by, author_tag, time_tag)
176 176 end
177 177
178 178 def l_or_humanize(s, options={})
179 179 k = "#{options[:prefix]}#{s}".to_sym
180 180 l_has_string?(k) ? l(k) : s.to_s.humanize
181 181 end
182 182
183 183 def day_name(day)
184 184 l(:general_day_names).split(',')[day-1]
185 185 end
186 186
187 187 def month_name(month)
188 188 l(:actionview_datehelper_select_month_names).split(',')[month-1]
189 189 end
190 190
191 191 def syntax_highlight(name, content)
192 192 type = CodeRay::FileType[name]
193 193 type ? CodeRay.scan(content, type).html : h(content)
194 194 end
195 195
196 196 def to_path_param(path)
197 197 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
198 198 end
199 199
200 200 def pagination_links_full(paginator, count=nil, options={})
201 201 page_param = options.delete(:page_param) || :page
202 202 url_param = params.dup
203 203 # don't reuse params if filters are present
204 204 url_param.clear if url_param.has_key?(:set_filter)
205 205
206 206 html = ''
207 207 html << link_to_remote(('&#171; ' + l(:label_previous)),
208 208 {:update => 'content',
209 209 :url => url_param.merge(page_param => paginator.current.previous),
210 210 :complete => 'window.scrollTo(0,0)'},
211 211 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
212 212
213 213 html << (pagination_links_each(paginator, options) do |n|
214 214 link_to_remote(n.to_s,
215 215 {:url => {:params => url_param.merge(page_param => n)},
216 216 :update => 'content',
217 217 :complete => 'window.scrollTo(0,0)'},
218 218 {:href => url_for(:params => url_param.merge(page_param => n))})
219 219 end || '')
220 220
221 221 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
222 222 {:update => 'content',
223 223 :url => url_param.merge(page_param => paginator.current.next),
224 224 :complete => 'window.scrollTo(0,0)'},
225 225 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
226 226
227 227 unless count.nil?
228 228 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
229 229 end
230 230
231 231 html
232 232 end
233 233
234 234 def per_page_links(selected=nil)
235 235 url_param = params.dup
236 236 url_param.clear if url_param.has_key?(:set_filter)
237 237
238 238 links = Setting.per_page_options_array.collect do |n|
239 239 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
240 240 {:href => url_for(url_param.merge(:per_page => n))})
241 241 end
242 242 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
243 243 end
244 244
245 245 def breadcrumb(*args)
246 246 elements = args.flatten
247 247 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
248 248 end
249 249
250 250 def html_title(*args)
251 251 if args.empty?
252 252 title = []
253 253 title << @project.name if @project
254 254 title += @html_title if @html_title
255 255 title << Setting.app_title
256 256 title.compact.join(' - ')
257 257 else
258 258 @html_title ||= []
259 259 @html_title += args
260 260 end
261 261 end
262 262
263 263 def accesskey(s)
264 264 Redmine::AccessKeys.key_for s
265 265 end
266 266
267 267 # Formats text according to system settings.
268 268 # 2 ways to call this method:
269 269 # * with a String: textilizable(text, options)
270 270 # * with an object and one of its attribute: textilizable(issue, :description, options)
271 271 def textilizable(*args)
272 272 options = args.last.is_a?(Hash) ? args.pop : {}
273 273 case args.size
274 274 when 1
275 275 obj = options[:object]
276 276 text = args.shift
277 277 when 2
278 278 obj = args.shift
279 279 text = obj.send(args.shift).to_s
280 280 else
281 281 raise ArgumentError, 'invalid arguments to textilizable'
282 282 end
283 283 return '' if text.blank?
284 284
285 285 only_path = options.delete(:only_path) == false ? false : true
286 286
287 287 # when using an image link, try to use an attachment, if possible
288 288 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
289 289
290 290 if attachments
291 291 attachments = attachments.sort_by(&:created_on).reverse
292 292 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
293 293 style = $1
294 294 filename = $6.downcase
295 295 # search for the picture in attachments
296 296 if found = attachments.detect { |att| att.filename.downcase == filename }
297 297 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
298 298 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
299 299 alt = desc.blank? ? nil : "(#{desc})"
300 300 "!#{style}#{image_url}#{alt}!"
301 301 else
302 302 m
303 303 end
304 304 end
305 305 end
306 306
307 307 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
308 308
309 309 # different methods for formatting wiki links
310 310 case options[:wiki_links]
311 311 when :local
312 312 # used for local links to html files
313 313 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
314 314 when :anchor
315 315 # used for single-file wiki export
316 316 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
317 317 else
318 318 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
319 319 end
320 320
321 321 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
322 322
323 323 # Wiki links
324 324 #
325 325 # Examples:
326 326 # [[mypage]]
327 327 # [[mypage|mytext]]
328 328 # wiki links can refer other project wikis, using project name or identifier:
329 329 # [[project:]] -> wiki starting page
330 330 # [[project:|mytext]]
331 331 # [[project:mypage]]
332 332 # [[project:mypage|mytext]]
333 333 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
334 334 link_project = project
335 335 esc, all, page, title = $1, $2, $3, $5
336 336 if esc.nil?
337 337 if page =~ /^([^\:]+)\:(.*)$/
338 338 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
339 339 page = $2
340 340 title ||= $1 if page.blank?
341 341 end
342 342
343 343 if link_project && link_project.wiki
344 344 # extract anchor
345 345 anchor = nil
346 346 if page =~ /^(.+?)\#(.+)$/
347 347 page, anchor = $1, $2
348 348 end
349 349 # check if page exists
350 350 wiki_page = link_project.wiki.find_page(page)
351 351 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
352 352 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
353 353 else
354 354 # project or wiki doesn't exist
355 355 title || page
356 356 end
357 357 else
358 358 all
359 359 end
360 360 end
361 361
362 362 # Redmine links
363 363 #
364 364 # Examples:
365 365 # Issues:
366 366 # #52 -> Link to issue #52
367 367 # Changesets:
368 368 # r52 -> Link to revision 52
369 369 # commit:a85130f -> Link to scmid starting with a85130f
370 370 # Documents:
371 371 # document#17 -> Link to document with id 17
372 372 # document:Greetings -> Link to the document with title "Greetings"
373 373 # document:"Some document" -> Link to the document with title "Some document"
374 374 # Versions:
375 375 # version#3 -> Link to version with id 3
376 376 # version:1.0.0 -> Link to version named "1.0.0"
377 377 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
378 378 # Attachments:
379 379 # attachment:file.zip -> Link to the attachment of the current object named file.zip
380 380 # Source files:
381 381 # source:some/file -> Link to the file located at /some/file in the project's repository
382 382 # source:some/file@52 -> Link to the file's revision 52
383 383 # source:some/file#L120 -> Link to line 120 of the file
384 384 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
385 385 # export:some/file -> Force the download of the file
386 386 # Forum messages:
387 387 # message#1218 -> Link to message with id 1218
388 388 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
389 389 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
390 390 link = nil
391 391 if esc.nil?
392 392 if prefix.nil? && sep == 'r'
393 393 if project && (changeset = project.changesets.find_by_revision(oid))
394 394 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
395 395 :class => 'changeset',
396 396 :title => truncate_single_line(changeset.comments, 100))
397 397 end
398 398 elsif sep == '#'
399 399 oid = oid.to_i
400 400 case prefix
401 401 when nil
402 402 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
403 403 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
404 404 :class => (issue.closed? ? 'issue closed' : 'issue'),
405 405 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
406 406 link = content_tag('del', link) if issue.closed?
407 407 end
408 408 when 'document'
409 409 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
410 410 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
411 411 :class => 'document'
412 412 end
413 413 when 'version'
414 414 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
415 415 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
416 416 :class => 'version'
417 417 end
418 418 when 'message'
419 419 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
420 420 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
421 421 :controller => 'messages',
422 422 :action => 'show',
423 423 :board_id => message.board,
424 424 :id => message.root,
425 425 :anchor => (message.parent ? "message-#{message.id}" : nil)},
426 426 :class => 'message'
427 427 end
428 428 end
429 429 elsif sep == ':'
430 430 # removes the double quotes if any
431 431 name = oid.gsub(%r{^"(.*)"$}, "\\1")
432 432 case prefix
433 433 when 'document'
434 434 if project && document = project.documents.find_by_title(name)
435 435 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
436 436 :class => 'document'
437 437 end
438 438 when 'version'
439 439 if project && version = project.versions.find_by_name(name)
440 440 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
441 441 :class => 'version'
442 442 end
443 443 when 'commit'
444 444 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
445 445 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
446 446 :class => 'changeset',
447 447 :title => truncate_single_line(changeset.comments, 100)
448 448 end
449 449 when 'source', 'export'
450 450 if project && project.repository
451 451 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
452 452 path, rev, anchor = $1, $3, $5
453 453 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
454 454 :path => to_path_param(path),
455 455 :rev => rev,
456 456 :anchor => anchor,
457 457 :format => (prefix == 'export' ? 'raw' : nil)},
458 458 :class => (prefix == 'export' ? 'source download' : 'source')
459 459 end
460 460 when 'attachment'
461 461 if attachments && attachment = attachments.detect {|a| a.filename == name }
462 462 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
463 463 :class => 'attachment'
464 464 end
465 465 end
466 466 end
467 467 end
468 468 leading + (link || "#{prefix}#{sep}#{oid}")
469 469 end
470 470
471 471 text
472 472 end
473 473
474 474 # Same as Rails' simple_format helper without using paragraphs
475 475 def simple_format_without_paragraph(text)
476 476 text.to_s.
477 477 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
478 478 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
479 479 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
480 480 end
481 481
482 482 def error_messages_for(object_name, options = {})
483 483 options = options.symbolize_keys
484 484 object = instance_variable_get("@#{object_name}")
485 485 if object && !object.errors.empty?
486 486 # build full_messages here with controller current language
487 487 full_messages = []
488 488 object.errors.each do |attr, msg|
489 489 next if msg.nil?
490 msg = msg.first if msg.is_a? Array
490 msg = [msg] unless msg.is_a?(Array)
491 491 if attr == "base"
492 full_messages << l(msg)
492 full_messages << l(*msg)
493 493 else
494 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
494 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(*msg) unless attr == "custom_values"
495 495 end
496 496 end
497 497 # retrieve custom values error messages
498 498 if object.errors[:custom_values]
499 499 object.custom_values.each do |v|
500 500 v.errors.each do |attr, msg|
501 501 next if msg.nil?
502 msg = msg.first if msg.is_a? Array
503 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
502 msg = [msg] unless msg.is_a?(Array)
503 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(*msg)
504 504 end
505 505 end
506 506 end
507 507 content_tag("div",
508 508 content_tag(
509 509 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
510 510 ) +
511 511 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
512 512 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
513 513 )
514 514 else
515 515 ""
516 516 end
517 517 end
518 518
519 519 def lang_options_for_select(blank=true)
520 520 (blank ? [["(auto)", ""]] : []) +
521 521 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
522 522 end
523 523
524 524 def label_tag_for(name, option_tags = nil, options = {})
525 525 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
526 526 content_tag("label", label_text)
527 527 end
528 528
529 529 def labelled_tabular_form_for(name, object, options, &proc)
530 530 options[:html] ||= {}
531 531 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
532 532 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
533 533 end
534 534
535 535 def back_url_hidden_field_tag
536 536 back_url = params[:back_url] || request.env['HTTP_REFERER']
537 537 back_url = CGI.unescape(back_url.to_s)
538 538 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
539 539 end
540 540
541 541 def check_all_links(form_name)
542 542 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
543 543 " | " +
544 544 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
545 545 end
546 546
547 547 def progress_bar(pcts, options={})
548 548 pcts = [pcts, pcts] unless pcts.is_a?(Array)
549 549 pcts[1] = pcts[1] - pcts[0]
550 550 pcts << (100 - pcts[1] - pcts[0])
551 551 width = options[:width] || '100px;'
552 552 legend = options[:legend] || ''
553 553 content_tag('table',
554 554 content_tag('tr',
555 555 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
556 556 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
557 557 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
558 558 ), :class => 'progress', :style => "width: #{width};") +
559 559 content_tag('p', legend, :class => 'pourcent')
560 560 end
561 561
562 562 def context_menu_link(name, url, options={})
563 563 options[:class] ||= ''
564 564 if options.delete(:selected)
565 565 options[:class] << ' icon-checked disabled'
566 566 options[:disabled] = true
567 567 end
568 568 if options.delete(:disabled)
569 569 options.delete(:method)
570 570 options.delete(:confirm)
571 571 options.delete(:onclick)
572 572 options[:class] << ' disabled'
573 573 url = '#'
574 574 end
575 575 link_to name, url, options
576 576 end
577 577
578 578 def calendar_for(field_id)
579 579 include_calendar_headers_tags
580 580 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
581 581 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
582 582 end
583 583
584 584 def include_calendar_headers_tags
585 585 unless @calendar_headers_tags_included
586 586 @calendar_headers_tags_included = true
587 587 content_for :header_tags do
588 588 javascript_include_tag('calendar/calendar') +
589 589 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
590 590 javascript_include_tag('calendar/calendar-setup') +
591 591 stylesheet_link_tag('calendar')
592 592 end
593 593 end
594 594 end
595 595
596 596 def content_for(name, content = nil, &block)
597 597 @has_content ||= {}
598 598 @has_content[name] = true
599 599 super(name, content, &block)
600 600 end
601 601
602 602 def has_content?(name)
603 603 (@has_content && @has_content[name]) || false
604 604 end
605 605
606 606 # Returns the avatar image tag for the given +user+ if avatars are enabled
607 607 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
608 608 def avatar(user, options = { })
609 609 if Setting.gravatar_enabled?
610 610 email = nil
611 611 if user.respond_to?(:mail)
612 612 email = user.mail
613 613 elsif user.to_s =~ %r{<(.+?)>}
614 614 email = $1
615 615 end
616 616 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
617 617 end
618 618 end
619 619
620 620 private
621 621
622 622 def wiki_helper
623 623 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
624 624 extend helper
625 625 return self
626 626 end
627 627 end
@@ -1,66 +1,66
1 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 3 <head>
4 4 <title><%=h html_title %></title>
5 5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 7 <meta name="keywords" content="issue,bug,tracker" />
8 8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 9 <%= javascript_include_tag :defaults %>
10 10 <%= heads_for_wiki_formatter %>
11 11 <!--[if IE]>
12 12 <style type="text/css">
13 13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
14 14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
15 15 </style>
16 16 <![endif]-->
17 17 <%= call_hook :view_layouts_base_html_head %>
18 18 <!-- page specific tags -->
19 19 <%= yield :header_tags -%>
20 20 </head>
21 21 <body>
22 22 <div id="wrapper">
23 23 <div id="top-menu">
24 24 <div id="account">
25 25 <%= render_menu :account_menu -%>
26 26 </div>
27 27 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}", :id => 'loggedas') if User.current.logged? %>
28 28 <%= render_menu :top_menu -%>
29 29 </div>
30 30
31 31 <div id="header">
32 32 <div id="quick-search">
33 33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
34 34 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
35 35 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
36 36 <% end %>
37 37 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
38 38 </div>
39 39
40 40 <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
41 41
42 42 <div id="main-menu">
43 43 <%= render_main_menu(@project) %>
44 44 </div>
45 45 </div>
46 46
47 47 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
48 48 <div id="sidebar">
49 49 <%= yield :sidebar %>
50 50 </div>
51 51
52 52 <div id="content">
53 53 <%= render_flash_messages %>
54 54 <%= yield %>
55 55 </div>
56 56 </div>
57 57
58 58 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
59 59
60 60 <div id="footer">
61 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2008 Jean-Philippe Lang
61 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2009 Jean-Philippe Lang
62 62 </div>
63 63 </div>
64 64 <%= call_hook :view_layouts_base_body_bottom %>
65 65 </body>
66 66 </html>
@@ -1,865 +1,868
1 1 == Redmine changelog
2 2
3 3 Redmine - project management software
4 4 Copyright (C) 2006-2009 Jean-Philippe Lang
5 5 http://www.redmine.org/
6 6
7 7
8 8 == 2009-xx-xx v0.8.2
9 9
10 10 * Send an email to the user when an administrator activates a registered user
11 11 * Strip keywords from received email body
12 * Footer updated to 2009
13 * Fixed: exporting an issue with attachments to PDF raises an error
14 * Fixed: "too few arguments" error may occur on activerecord error translation
12 15
13 16
14 17 == 2009-02-15 v0.8.1
15 18
16 19 * Select watchers on new issue form
17 20 * Issue description is no longer a required field
18 21 * Files module: ability to add files without version
19 22 * Jump to the current tab when using the project quick-jump combo
20 23 * Display a warning if some attachments were not saved
21 24 * Import custom fields values from emails on issue creation
22 25 * Show view/annotate/download links on entry and annotate views
23 26 * Admin Info Screen: Display if plugin assets directory is writable
24 27 * Adds a 'Create and continue' button on the new issue form
25 28 * IMAP: add options to move received emails
26 29 * Do not show Category field when categories are not defined
27 30 * Lower the project identifier limit to a minimum of two characters
28 31 * Add "closed" html class to closed entries in issue list
29 32 * Fixed: broken redirect URL on login failure
30 33 * Fixed: Deleted files are shown when using Darcs
31 34 * Fixed: Darcs adapter works on Win32 only
32 35 * Fixed: syntax highlight doesn't appear in new ticket preview
33 36 * Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets
34 37 * Fixed: no error is raised when entering invalid hours on the issue update form
35 38 * Fixed: Details time log report CSV export doesn't honour date format from settings
36 39 * Fixed: invalid css classes on issue details
37 40 * Fixed: Trac importer creates duplicate custom values
38 41 * Fixed: inline attached image should not match partial filename
39 42
40 43
41 44 == 2008-12-30 v0.8.0
42 45
43 46 * Setting added in order to limit the number of diff lines that should be displayed
44 47 * Makes logged-in username in topbar linking to
45 48 * Mail handler: strip tags when receiving a html-only email
46 49 * Mail handler: add watchers before sending notification
47 50 * Adds a css class (overdue) to overdue issues on issue lists and detail views
48 51 * Fixed: project activity truncated after viewing user's activity
49 52 * Fixed: email address entered for password recovery shouldn't be case-sensitive
50 53 * Fixed: default flag removed when editing a default enumeration
51 54 * Fixed: default category ignored when adding a document
52 55 * Fixed: error on repository user mapping when a repository username is blank
53 56 * Fixed: Firefox cuts off large diffs
54 57 * Fixed: CVS browser should not show dead revisions (deleted files)
55 58 * Fixed: escape double-quotes in image titles
56 59 * Fixed: escape textarea content when editing a issue note
57 60 * Fixed: JS error on context menu with IE
58 61 * Fixed: bold syntax around single character in series doesn't work
59 62 * Fixed several XSS vulnerabilities
60 63 * Fixed a SQL injection vulnerability
61 64
62 65
63 66 == 2008-12-07 v0.8.0-rc1
64 67
65 68 * Wiki page protection
66 69 * Wiki page hierarchy. Parent page can be assigned on the Rename screen
67 70 * Adds support for issue creation via email
68 71 * Adds support for free ticket filtering and custom queries on Gantt chart and calendar
69 72 * Cross-project search
70 73 * Ability to search a project and its subprojects
71 74 * Ability to search the projects the user belongs to
72 75 * Adds custom fields on time entries
73 76 * Adds boolean and list custom fields for time entries as criteria on time report
74 77 * Cross-project time reports
75 78 * Display latest user's activity on account/show view
76 79 * Show last connexion time on user's page
77 80 * Obfuscates email address on user's account page using javascript
78 81 * wiki TOC rendered as an unordered list
79 82 * Adds the ability to search for a user on the administration users list
80 83 * Adds the ability to search for a project name or identifier on the administration projects list
81 84 * Redirect user to the previous page after logging in
82 85 * Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users
83 86 * Adds permissions for viewing the watcher list and adding new watchers on the issue detail view
84 87 * Adds permissions to let users edit and/or delete their messages
85 88 * Link to activity view when displaying dates
86 89 * Hide Redmine version in atom feeds and pdf properties
87 90 * Maps repository users to Redmine users. Users with same username or email are automatically mapped. Mapping can be manually adjusted in repository settings. Multiple usernames can be mapped to the same Redmine user.
88 91 * Sort users by their display names so that user dropdown lists are sorted alphabetically
89 92 * Adds estimated hours to issue filters
90 93 * Switch order of current and previous revisions in side-by-side diff
91 94 * Render the commit changes list as a tree
92 95 * Adds watch/unwatch functionality at forum topic level
93 96 * When moving an issue to another project, reassign it to the category with same name if any
94 97 * Adds child_pages macro for wiki pages
95 98 * Use GET instead of POST on roadmap (#718), gantt and calendar forms
96 99 * Search engine: display total results count and count by result type
97 100 * Email delivery configuration moved to an unversioned YAML file (config/email.yml, see the sample file)
98 101 * Adds icons on search results
99 102 * Adds 'Edit' link on account/show for admin users
100 103 * Adds Lock/Unlock/Activate link on user edit screen
101 104 * Adds user count in status drop down on admin user list
102 105 * Adds multi-levels blockquotes support by using > at the beginning of lines
103 106 * Adds a Reply link to each issue note
104 107 * Adds plain text only option for mail notifications
105 108 * Gravatar support for issue detail, user grid, and activity stream (disabled by default)
106 109 * Adds 'Delete wiki pages attachments' permission
107 110 * Show the most recent file when displaying an inline image
108 111 * Makes permission screens localized
109 112 * AuthSource list: display associated users count and disable 'Delete' buton if any
110 113 * Make the 'duplicates of' relation asymmetric
111 114 * Adds username to the password reminder email
112 115 * Adds links to forum messages using message#id syntax
113 116 * Allow same name for custom fields on different object types
114 117 * One-click bulk edition using the issue list context menu within the same project
115 118 * Adds support for commit logs reencoding to UTF-8 before insertion in the database. Source encoding of commit logs can be selected in Application settings -> Repositories.
116 119 * Adds checkboxes toggle links on permissions report
117 120 * Adds Trac-Like anchors on wiki headings
118 121 * Adds support for wiki links with anchor
119 122 * Adds category to the issue context menu
120 123 * Adds a workflow overview screen
121 124 * Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename
122 125 * Dots allowed in custom field name
123 126 * Adds posts quoting functionality
124 127 * Adds an option to generate sequential project identifiers
125 128 * Adds mailto link on the user administration list
126 129 * Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value
127 130 * Gantt chart: display issues that don't have a due date if they are assigned to a version with a date
128 131 * Change projects homepage limit to 255 chars
129 132 * Improved on-the-fly account creation. If some attributes are missing (eg. not present in the LDAP) or are invalid, the registration form is displayed so that the user is able to fill or fix these attributes
130 133 * Adds "please select" to activity select box if no activity is set as default
131 134 * Do not silently ignore timelog validation failure on issue edit
132 135 * Adds a rake task to send reminder emails
133 136 * Allow empty cells in wiki tables
134 137 * Makes wiki text formatter pluggable
135 138 * Adds back textile acronyms support
136 139 * Remove pre tag attributes
137 140 * Plugin hooks
138 141 * Pluggable admin menu
139 142 * Plugins can provide activity content
140 143 * Moves plugin list to its own administration menu item
141 144 * Adds url and author_url plugin attributes
142 145 * Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version
143 146 * Adds atom feed on time entries details
144 147 * Adds project name to issues feed title
145 148 * Adds a css class on menu items in order to apply item specific styles (eg. icons)
146 149 * Adds a Redmine plugin generators
147 150 * Adds timelog link to the issue context menu
148 151 * Adds links to the user page on various views
149 152 * Turkish translation by Ismail Sezen
150 153 * Catalan translation
151 154 * Vietnamese translation
152 155 * Slovak translation
153 156 * Better naming of activity feed if only one kind of event is displayed
154 157 * Enable syntax highlight on issues, messages and news
155 158 * Add target version to the issue list context menu
156 159 * Hide 'Target version' filter if no version is defined
157 160 * Add filters on cross-project issue list for custom fields marked as 'For all projects'
158 161 * Turn ftp urls into links
159 162 * Hiding the View Differences button when a wiki page's history only has one version
160 163 * Messages on a Board can now be sorted by the number of replies
161 164 * Adds a class ('me') to events of the activity view created by current user
162 165 * Strip pre/code tags content from activity view events
163 166 * Display issue notes in the activity view
164 167 * Adds links to changesets atom feed on repository browser
165 168 * Track project and tracker changes in issue history
166 169 * Adds anchor to atom feed messages links
167 170 * Adds a key in lang files to set the decimal separator (point or comma) in csv exports
168 171 * Makes importer work with Trac 0.8.x
169 172 * Upgraded to Prototype 1.6.0.1
170 173 * File viewer for attached text files
171 174 * Menu mapper: add support for :before, :after and :last options to #push method and add #delete method
172 175 * Removed inconsistent revision numbers on diff view
173 176 * CVS: add support for modules names with spaces
174 177 * Log the user in after registration if account activation is not needed
175 178 * Mercurial adapter improvements
176 179 * Trac importer: read session_attribute table to find user's email and real name
177 180 * Ability to disable unused SCM adapters in application settings
178 181 * Adds Filesystem adapter
179 182 * Clear changesets and changes with raw sql when deleting a repository for performance
180 183 * Redmine.pm now uses the 'commit access' permission defined in Redmine
181 184 * Reposman can create any type of scm (--scm option)
182 185 * Reposman creates a repository if the 'repository' module is enabled at project level only
183 186 * Display svn properties in the browser, svn >= 1.5.0 only
184 187 * Reduces memory usage when importing large git repositories
185 188 * Wider SVG graphs in repository stats
186 189 * SubversionAdapter#entries performance improvement
187 190 * SCM browser: ability to download raw unified diffs
188 191 * More detailed error message in log when scm command fails
189 192 * Adds support for file viewing with Darcs 2.0+
190 193 * Check that git changeset is not in the database before creating it
191 194 * Unified diff viewer for attached files with .patch or .diff extension
192 195 * File size display with Bazaar repositories
193 196 * Git adapter: use commit time instead of author time
194 197 * Prettier url for changesets
195 198 * Makes changes link to entries on the revision view
196 199 * Adds a field on the repository view to browse at specific revision
197 200 * Adds new projects atom feed
198 201 * Added rake tasks to generate rcov code coverage reports
199 202 * Add Redcloth's :block_markdown_rule to allow horizontal rules in wiki
200 203 * Show the project hierarchy in the drop down list for new membership on user administration screen
201 204 * Split user edit screen into tabs
202 205 * Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead
203 206 * Fixed: Roadmap crashes when a version has a due date > 2037
204 207 * Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen
205 208 * Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory
206 209 * Fixed: logtime entry duplicated when edited from parent project
207 210 * Fixed: wrong digest for text files under Windows
208 211 * Fixed: associated revisions are displayed in wrong order on issue view
209 212 * Fixed: Git Adapter date parsing ignores timezone
210 213 * Fixed: Printing long roadmap doesn't split across pages
211 214 * Fixes custom fields display order at several places
212 215 * Fixed: urls containing @ are parsed as email adress by the wiki formatter
213 216 * Fixed date filters accuracy with SQLite
214 217 * Fixed: tokens not escaped in highlight_tokens regexp
215 218 * Fixed Bazaar shared repository browsing
216 219 * Fixes platform determination under JRuby
217 220 * Fixed: Estimated time in issue's journal should be rounded to two decimals
218 221 * Fixed: 'search titles only' box ignored after one search is done on titles only
219 222 * Fixed: non-ASCII subversion path can't be displayed
220 223 * Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format
221 224 * Fixed: document listing shows on "my page" when viewing documents is disabled for the role
222 225 * Fixed: Latest news appear on the homepage for projects with the News module disabled
223 226 * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled
224 227 * Fixed: the default status is lost when reordering issue statuses
225 228 * Fixes error with Postgresql and non-UTF8 commit logs
226 229 * Fixed: textile footnotes no longer work
227 230 * Fixed: http links containing parentheses fail to reder correctly
228 231 * Fixed: GitAdapter#get_rev should use current branch instead of hardwiring master
229 232
230 233
231 234 == 2008-07-06 v0.7.3
232 235
233 236 * Allow dot in firstnames and lastnames
234 237 * Add project name to cross-project Atom feeds
235 238 * Encoding set to utf8 in example database.yml
236 239 * HTML titles on forums related views
237 240 * Fixed: various XSS vulnerabilities
238 241 * Fixed: Entourage (and some old client) fails to correctly render notification styles
239 242 * Fixed: Fixed: timelog redirects inappropriately when :back_url is blank
240 243 * Fixed: wrong relative paths to images in wiki_syntax.html
241 244
242 245
243 246 == 2008-06-15 v0.7.2
244 247
245 248 * "New Project" link on Projects page
246 249 * Links to repository directories on the repo browser
247 250 * Move status to front in Activity View
248 251 * Remove edit step from Status context menu
249 252 * Fixed: No way to do textile horizontal rule
250 253 * Fixed: Repository: View differences doesn't work
251 254 * Fixed: attachement's name maybe invalid.
252 255 * Fixed: Error when creating a new issue
253 256 * Fixed: NoMethodError on @available_filters.has_key?
254 257 * Fixed: Check All / Uncheck All in Email Settings
255 258 * Fixed: "View differences" of one file at /repositories/revision/ fails
256 259 * Fixed: Column width in "my page"
257 260 * Fixed: private subprojects are listed on Issues view
258 261 * Fixed: Textile: bold, italics, underline, etc... not working after parentheses
259 262 * Fixed: Update issue form: comment field from log time end out of screen
260 263 * Fixed: Editing role: "issue can be assigned to this role" out of box
261 264 * Fixed: Unable use angular braces after include word
262 265 * Fixed: Using '*' as keyword for repository referencing keywords doesn't work
263 266 * Fixed: Subversion repository "View differences" on each file rise ERROR
264 267 * Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root
265 268 * Fixed: It is possible to lock out the last admin account
266 269 * Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access
267 270 * Fixed: Issue number display clipped on 'my issues'
268 271 * Fixed: Roadmap version list links not carrying state
269 272 * Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default
270 273 * Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master"
271 274 * Fixed: browser's language subcodes ignored
272 275 * Fixed: Error on project selection with numeric (only) identifier.
273 276 * Fixed: Link to PDF doesn't work after creating new issue
274 277 * Fixed: "Replies" should not be shown on forum threads that are locked
275 278 * Fixed: SVN errors lead to svn username/password being displayed to end users (security issue)
276 279 * Fixed: http links containing hashes don't display correct
277 280 * Fixed: Allow ampersands in Enumeration names
278 281 * Fixed: Atom link on saved query does not include query_id
279 282 * Fixed: Logtime info lost when there's an error updating an issue
280 283 * Fixed: TOC does not parse colorization markups
281 284 * Fixed: CVS: add support for modules names with spaces
282 285 * Fixed: Bad rendering on projects/add
283 286 * Fixed: exception when viewing differences on cvs
284 287 * Fixed: export issue to pdf will messup when use Chinese language
285 288 * Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant
286 289 * Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE
287 290 * Fixed: Importing from trac : some wiki links are messed
288 291 * Fixed: Incorrect weekend definition in Hebrew calendar locale
289 292 * Fixed: Atom feeds don't provide author section for repository revisions
290 293 * Fixed: In Activity views, changesets titles can be multiline while they should not
291 294 * Fixed: Ignore unreadable subversion directories (read disabled using authz)
292 295 * Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets
293 296 * Fixed: Close statement handler in Redmine.pm
294 297
295 298
296 299 == 2008-05-04 v0.7.1
297 300
298 301 * Thai translation added (Gampol Thitinilnithi)
299 302 * Translations updates
300 303 * Escape HTML comment tags
301 304 * Prevent "can't convert nil into String" error when :sort_order param is not present
302 305 * Fixed: Updating tickets add a time log with zero hours
303 306 * Fixed: private subprojects names are revealed on the project overview
304 307 * Fixed: Search for target version of "none" fails with postgres 8.3
305 308 * Fixed: Home, Logout, Login links shouldn't be absolute links
306 309 * Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects
307 310 * Fixed: error when using upcase language name in coderay
308 311 * Fixed: error on Trac import when :due attribute is nil
309 312
310 313
311 314 == 2008-04-28 v0.7.0
312 315
313 316 * Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
314 317 * Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list.
315 318 * Add predefined date ranges to the time report
316 319 * Time report can be done at issue level
317 320 * Various timelog report enhancements
318 321 * Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30
319 322 * Display the context menu above and/or to the left of the click if needed
320 323 * Make the admin project files list sortable
321 324 * Mercurial: display working directory files sizes unless browsing a specific revision
322 325 * Preserve status filter and page number when using lock/unlock/activate links on the users list
323 326 * Redmine.pm support for LDAP authentication
324 327 * Better error message and AR errors in log for failed LDAP on-the-fly user creation
325 328 * Redirected user to where he is coming from after logging hours
326 329 * Warn user that subprojects are also deleted when deleting a project
327 330 * Include subprojects versions on calendar and gantt
328 331 * Notify project members when a message is posted if they want to receive notifications
329 332 * Fixed: Feed content limit setting has no effect
330 333 * Fixed: Priorities not ordered when displayed as a filter in issue list
331 334 * Fixed: can not display attached images inline in message replies
332 335 * Fixed: Boards are not deleted when project is deleted
333 336 * Fixed: trying to preview a new issue raises an exception with postgresql
334 337 * Fixed: single file 'View difference' links do not work because of duplicate slashes in url
335 338 * Fixed: inline image not displayed when including a wiki page
336 339 * Fixed: CVS duplicate key violation
337 340 * Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues
338 341 * Fixed: custom field filters behaviour
339 342 * Fixed: Postgresql 8.3 compatibility
340 343 * Fixed: Links to repository directories don't work
341 344
342 345
343 346 == 2008-03-29 v0.7.0-rc1
344 347
345 348 * Overall activity view and feed added, link is available on the project list
346 349 * Git VCS support
347 350 * Rails 2.0 sessions cookie store compatibility
348 351 * Use project identifiers in urls instead of ids
349 352 * Default configuration data can now be loaded from the administration screen
350 353 * Administration settings screen split to tabs (email notifications options moved to 'Settings')
351 354 * Project description is now unlimited and optional
352 355 * Wiki annotate view
353 356 * Escape HTML tag in textile content
354 357 * Add Redmine links to documents, versions, attachments and repository files
355 358 * New setting to specify how many objects should be displayed on paginated lists. There are 2 ways to select a set of issues on the issue list:
356 359 * by using checkbox and/or the little pencil that will select/unselect all issues
357 360 * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues
358 361 * Context menu disabled on links so that the default context menu of the browser is displayed when right-clicking on a link (click anywhere else on the row to display the context menu)
359 362 * User display format is now configurable in administration settings
360 363 * Issue list now supports bulk edit/move/delete (for a set of issues that belong to the same project)
361 364 * Merged 'change status', 'edit issue' and 'add note' actions:
362 365 * Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status
363 366 * 'Change issue status' permission removed. To change an issue status, a user just needs to have either 'Edit' or 'Add note' permissions and some workflow transitions allowed
364 367 * Details by assignees on issue summary view
365 368 * 'New issue' link in the main menu (accesskey 7). The drop-down lists to add an issue on the project overview and the issue list are removed
366 369 * Change status select box default to current status
367 370 * Preview for issue notes, news and messages
368 371 * Optional description for attachments
369 372 * 'Fixed version' label changed to 'Target version'
370 373 * Let the user choose when deleting issues with reported hours to:
371 374 * delete the hours
372 375 * assign the hours to the project
373 376 * reassign the hours to another issue
374 377 * Date range filter and pagination on time entries detail view
375 378 * Propagate time tracking to the parent project
376 379 * Switch added on the project activity view to include subprojects
377 380 * Display total estimated and spent hours on the version detail view
378 381 * Weekly time tracking block for 'My page'
379 382 * Permissions to edit time entries
380 383 * Include subprojects on the issue list, calendar, gantt and timelog by default (can be turned off is administration settings)
381 384 * Roadmap enhancements (separate related issues from wiki contents, leading h1 in version wiki pages is hidden, smaller wiki headings)
382 385 * Make versions with same date sorted by name
383 386 * Allow issue list to be sorted by target version
384 387 * Related changesets messages displayed on the issue details view
385 388 * Create a journal and send an email when an issue is closed by commit
386 389 * Add 'Author' to the available columns for the issue list
387 390 * More appropriate default sort order on sortable columns
388 391 * Add issue subject to the time entries view and issue subject, description and tracker to the csv export
389 392 * Permissions to edit issue notes
390 393 * Display date/time instead of date on files list
391 394 * Do not show Roadmap menu item if the project doesn't define any versions
392 395 * Allow longer version names (60 chars)
393 396 * Ability to copy an existing workflow when creating a new role
394 397 * Display custom fields in two columns on the issue form
395 398 * Added 'estimated time' in the csv export of the issue list
396 399 * Display the last 30 days on the activity view rather than the current month (number of days can be configured in the application settings)
397 400 * Setting for whether new projects should be public by default
398 401 * User preference to choose how comments/replies are displayed: in chronological or reverse chronological order
399 402 * Added default value for custom fields
400 403 * Added tabindex property on wiki toolbar buttons (to easily move from field to field using the tab key)
401 404 * Redirect to issue page after creating a new issue
402 405 * Wiki toolbar improvements (mainly for Firefox)
403 406 * Display wiki syntax quick ref link on all wiki textareas
404 407 * Display links to Atom feeds
405 408 * Breadcrumb nav for the forums
406 409 * Show replies when choosing to display messages in the activity
407 410 * Added 'include' macro to include another wiki page
408 411 * RedmineWikiFormatting page available as a static HTML file locally
409 412 * Wrap diff content
410 413 * Strip out email address from authors in repository screens
411 414 * Highlight the current item of the main menu
412 415 * Added simple syntax highlighters for php and java languages
413 416 * Do not show empty diffs
414 417 * Show explicit error message when the scm command failed (eg. when svn binary is not available)
415 418 * Lithuanian translation added (Sergej Jegorov)
416 419 * Ukrainan translation added (Natalia Konovka & Mykhaylo Sorochan)
417 420 * Danish translation added (Mads Vestergaard)
418 421 * Added i18n support to the jstoolbar and various settings screen
419 422 * RedCloth's glyphs no longer user
420 423 * New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/)
421 424 * The following menus can now be extended by plugins: top_menu, account_menu, application_menu
422 425 * Added a simple rake task to fetch changesets from the repositories: rake redmine:fetch_changesets
423 426 * Remove hardcoded "Redmine" strings in account related emails and use application title instead
424 427 * Mantis importer preserve bug ids
425 428 * Trac importer: Trac guide wiki pages skipped
426 429 * Trac importer: wiki attachments migration added
427 430 * Trac importer: support database schema for Trac migration
428 431 * Trac importer: support CamelCase links
429 432 * Removes the Redmine version from the footer (can be viewed on admin -> info)
430 433 * Rescue and display an error message when trying to delete a role that is in use
431 434 * Add various 'X-Redmine' headers to email notifications: X-Redmine-Host, X-Redmine-Site, X-Redmine-Project, X-Redmine-Issue-Id, -Author, -Assignee, X-Redmine-Topic-Id
432 435 * Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs
433 436 * Fixed: Gantt and calendar not properly refreshed (fragment caching removed)
434 437 * Fixed: Textile image with style attribute cause internal server error
435 438 * Fixed: wiki TOC not rendered properly when used in an issue or document description
436 439 * Fixed: 'has already been taken' error message on username and email fields if left empty
437 440 * Fixed: non-ascii attachement filename with IE
438 441 * Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed
439 442 * Fixed: search for all words doesn't work
440 443 * Fixed: Do not show sticky and locked checkboxes when replying to a message
441 444 * Fixed: Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank
442 445 * Fixed: Date custom fields not displayed as specified in application settings
443 446 * Fixed: titles not escaped in the activity view
444 447 * Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context
445 448 * Fixed: on calendar, gantt and in the tracker filter on the issue list, only active trackers of the project (and its sub projects) should be available
446 449 * Fixed: locked users should not receive email notifications
447 450 * Fixed: custom field selection is not saved when unchecking them all on project settings
448 451 * Fixed: can not lock a topic when creating it
449 452 * Fixed: Incorrect filtering for unset values when using 'is not' filter
450 453 * Fixed: PostgreSQL issues_seq_id not updated when using Trac importer
451 454 * Fixed: ajax pagination does not scroll up
452 455 * Fixed: error when uploading a file with no content-type specified by the browser
453 456 * Fixed: wiki and changeset links not displayed when previewing issue description or notes
454 457 * Fixed: 'LdapError: no bind result' error when authenticating
455 458 * Fixed: 'LdapError: invalid binding information' when no username/password are set on the LDAP account
456 459 * Fixed: CVS repository doesn't work if port is used in the url
457 460 * Fixed: Email notifications: host name is missing in generated links
458 461 * Fixed: Email notifications: referenced changesets, wiki pages, attachments... are not turned into links
459 462 * Fixed: Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed
460 463 * Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console
461 464 * Fixed: Do not send an email with no recipient, cc or bcc
462 465 * Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues.
463 466 * Fixed: Mercurial browsing under unix-like os and for directory depth > 2
464 467 * Fixed: Wiki links with pipe can not be used in wiki tables
465 468 * Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets
466 469 * Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql
467 470
468 471
469 472 == 2008-03-12 v0.6.4
470 473
471 474 * Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects
472 475 * Fixed: potential LDAP authentication security flaw
473 476 * Fixed: context submenus on the issue list don't show up with IE6.
474 477 * Fixed: Themes are not applied with Rails 2.0
475 478 * Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil
476 479 * Fixed: Mercurial repository browsing
477 480 * Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails
478 481 * Fixed: not null constraints not removed with Postgresql
479 482 * Doctype set to transitional
480 483
481 484
482 485 == 2007-12-18 v0.6.3
483 486
484 487 * Fixed: upload doesn't work in 'Files' section
485 488
486 489
487 490 == 2007-12-16 v0.6.2
488 491
489 492 * Search engine: issue custom fields can now be searched
490 493 * News comments are now textilized
491 494 * Updated Japanese translation (Satoru Kurashiki)
492 495 * Updated Chinese translation (Shortie Lo)
493 496 * Fixed Rails 2.0 compatibility bugs:
494 497 * Unable to create a wiki
495 498 * Gantt and calendar error
496 499 * Trac importer error (readonly? is defined by ActiveRecord)
497 500 * Fixed: 'assigned to me' filter broken
498 501 * Fixed: crash when validation fails on issue edition with no custom fields
499 502 * Fixed: reposman "can't find group" error
500 503 * Fixed: 'LDAP account password is too long' error when leaving the field empty on creation
501 504 * Fixed: empty lines when displaying repository files with Windows style eol
502 505 * Fixed: missing body closing tag in repository annotate and entry views
503 506
504 507
505 508 == 2007-12-10 v0.6.1
506 509
507 510 * Rails 2.0 compatibility
508 511 * Custom fields can now be displayed as columns on the issue list
509 512 * Added version details view (accessible from the roadmap)
510 513 * Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account)
511 514 * Added per-project tracker selection. Trackers can be selected on project settings
512 515 * Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums
513 516 * Forums: messages can now be edited/deleted (explicit permissions need to be given)
514 517 * Forums: topics can be locked so that no reply can be added
515 518 * Forums: topics can be marked as sticky so that they always appear at the top of the list
516 519 * Forums: attachments can now be added to replies
517 520 * Added time zone support
518 521 * Added a setting to choose the account activation strategy (available in application settings)
519 522 * Added 'Classic' theme (inspired from the v0.51 design)
520 523 * Added an alternate theme which provides issue list colorization based on issues priority
521 524 * Added Bazaar SCM adapter
522 525 * Added Annotate/Blame view in the repository browser (except for Darcs SCM)
523 526 * Diff style (inline or side by side) automatically saved as a user preference
524 527 * Added issues status changes on the activity view (by Cyril Mougel)
525 528 * Added forums topics on the activity view (disabled by default)
526 529 * Added an option on 'My account' for users who don't want to be notified of changes that they make
527 530 * Trac importer now supports mysql and postgresql databases
528 531 * Trac importer improvements (by Mat Trudel)
529 532 * 'fixed version' field can now be displayed on the issue list
530 533 * Added a couple of new formats for the 'date format' setting
531 534 * Added Traditional Chinese translation (by Shortie Lo)
532 535 * Added Russian translation (iGor kMeta)
533 536 * Project name format limitation removed (name can now contain any character)
534 537 * Project identifier maximum length changed from 12 to 20
535 538 * Changed the maximum length of LDAP account to 255 characters
536 539 * Removed the 12 characters limit on passwords
537 540 * Added wiki macros support
538 541 * Performance improvement on workflow setup screen
539 542 * More detailed html title on several views
540 543 * Custom fields can now be reordered
541 544 * Search engine: search can be restricted to an exact phrase by using quotation marks
542 545 * Added custom fields marked as 'For all projects' to the csv export of the cross project issue list
543 546 * Email notifications are now sent as Blind carbon copy by default
544 547 * Fixed: all members (including non active) should be deleted when deleting a project
545 548 * Fixed: Error on wiki syntax link (accessible from wiki/edit)
546 549 * Fixed: 'quick jump to a revision' form on the revisions list
547 550 * Fixed: error on admin/info if there's more than 1 plugin installed
548 551 * Fixed: svn or ldap password can be found in clear text in the html source in editing mode
549 552 * Fixed: 'Assigned to' drop down list is not sorted
550 553 * Fixed: 'View all issues' link doesn't work on issues/show
551 554 * Fixed: error on account/register when validation fails
552 555 * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter'
553 556 * Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt)
554 557 * Fixed: Wrong feed URLs on the home page
555 558 * Fixed: Update of time entry fails when the issue has been moved to an other project
556 559 * Fixed: Error when moving an issue without changing its tracker (Postgresql)
557 560 * Fixed: Changes not recorded when using :pserver string (CVS adapter)
558 561 * Fixed: admin should be able to move issues to any project
559 562 * Fixed: adding an attachment is not possible when changing the status of an issue
560 563 * Fixed: No mime-types in documents/files downloading
561 564 * Fixed: error when sorting the messages if there's only one board for the project
562 565 * Fixed: 'me' doesn't appear in the drop down filters on a project issue list.
563 566
564 567 == 2007-11-04 v0.6.0
565 568
566 569 * Permission model refactoring.
567 570 * Permissions: there are now 2 builtin roles that can be used to specify permissions given to other users than members of projects
568 571 * Permissions: some permissions (eg. browse the repository) can be removed for certain roles
569 572 * Permissions: modules (eg. issue tracking, news, documents...) can be enabled/disabled at project level
570 573 * Added Mantis and Trac importers
571 574 * New application layout
572 575 * Added "Bulk edit" functionality on the issue list
573 576 * More flexible mail notifications settings at user level
574 577 * Added AJAX based context menu on the project issue list that provide shortcuts for editing, re-assigning, changing the status or the priority, moving or deleting an issue
575 578 * Added the hability to copy an issue. It can be done from the "issue/show" view or from the context menu on the issue list
576 579 * Added the ability to customize issue list columns (at application level or for each saved query)
577 580 * Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap
578 581 * Added the ability to rename wiki pages (specific permission required)
579 582 * Search engines now supports pagination. Results are sorted in reverse chronological order
580 583 * Added "Estimated hours" attribute on issues
581 584 * A category with assigned issue can now be deleted. 2 options are proposed: remove assignments or reassign issues to another category
582 585 * Forum notifications are now also sent to the authors of the thread, even if they donοΏ½t watch the board
583 586 * Added an application setting to specify the application protocol (http or https) used to generate urls in emails
584 587 * Gantt chart: now starts at the current month by default
585 588 * Gantt chart: month count and zoom factor are automatically saved as user preferences
586 589 * Wiki links can now refer to other project wikis
587 590 * Added wiki index by date
588 591 * Added preview on add/edit issue form
589 592 * Emails footer can now be customized from the admin interface (Admin -> Email notifications)
590 593 * Default encodings for repository files can now be set in application settings (used to convert files content and diff to UTF-8 so that theyοΏ½re properly displayed)
591 594 * Calendar: first day of week can now be set in lang files
592 595 * Automatic closing of duplicate issues
593 596 * Added a cross-project issue list
594 597 * AJAXified the SCM browser (tree view)
595 598 * Pretty URL for the repository browser (Cyril Mougel)
596 599 * Search engine: added a checkbox to search titles only
597 600 * Added "% done" in the filter list
598 601 * Enumerations: values can now be reordered and a default value can be specified (eg. default issue priority)
599 602 * Added some accesskeys
600 603 * Added "Float" as a custom field format
601 604 * Added basic Theme support
602 605 * Added the ability to set the οΏ½done ratioοΏ½ of issues fixed by commit (Nikolay Solakov)
603 606 * Added custom fields in issue related mail notifications
604 607 * Email notifications are now sent in plain text and html
605 608 * Gantt chart can now be exported to a graphic file (png). This functionality is only available if RMagick is installed.
606 609 * Added syntax highlightment for repository files and wiki
607 610 * Improved automatic Redmine links
608 611 * Added automatic table of content support on wiki pages
609 612 * Added radio buttons on the documents list to sort documents by category, date, title or author
610 613 * Added basic plugin support, with a sample plugin
611 614 * Added a link to add a new category when creating or editing an issue
612 615 * Added a "Assignable" boolean on the Role model. If unchecked, issues can not be assigned to users having this role.
613 616 * Added an option to be able to relate issues in different projects
614 617 * Added the ability to move issues (to another project) without changing their trackers.
615 618 * Atom feeds added on project activity, news and changesets
616 619 * Added the ability to reset its own RSS access key
617 620 * Main project list now displays root projects with their subprojects
618 621 * Added anchor links to issue notes
619 622 * Added reposman Ruby version. This script can now register created repositories in Redmine (Nicolas Chuche)
620 623 * Issue notes are now included in search
621 624 * Added email sending test functionality
622 625 * Added LDAPS support for LDAP authentication
623 626 * Removed hard-coded URLs in mail templates
624 627 * Subprojects are now grouped by projects in the navigation drop-down menu
625 628 * Added a new value for date filters: this week
626 629 * Added cache for application settings
627 630 * Added Polish translation (Tomasz Gawryl)
628 631 * Added Czech translation (Jan Kadlecek)
629 632 * Added Romanian translation (Csongor Bartus)
630 633 * Added Hebrew translation (Bob Builder)
631 634 * Added Serbian translation (Dragan Matic)
632 635 * Added Korean translation (Choi Jong Yoon)
633 636 * Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations
634 637 * Performance improvement on calendar and gantt
635 638 * Fixed: wiki preview doesnοΏ½t work on long entries
636 639 * Fixed: queries with multiple custom fields return no result
637 640 * Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters
638 641 * Fixed: URL with ~ broken in wiki formatting
639 642 * Fixed: some quotation marks are rendered as strange characters in pdf
640 643
641 644
642 645 == 2007-07-15 v0.5.1
643 646
644 647 * per project forums added
645 648 * added the ability to archive projects
646 649 * added οΏ½WatchοΏ½ functionality on issues. It allows users to receive notifications about issue changes
647 650 * custom fields for issues can now be used as filters on issue list
648 651 * added per user custom queries
649 652 * commit messages are now scanned for referenced or fixed issue IDs (keywords defined in Admin -> Settings)
650 653 * projects list now shows the list of public projects and private projects for which the user is a member
651 654 * versions can now be created with no date
652 655 * added issue count details for versions on Reports view
653 656 * added time report, by member/activity/tracker/version and year/month/week for the selected period
654 657 * each category can now be associated to a user, so that new issues in that category are automatically assigned to that user
655 658 * added autologin feature (disabled by default)
656 659 * optimistic locking added for wiki edits
657 660 * added wiki diff
658 661 * added the ability to destroy wiki pages (requires permission)
659 662 * a wiki page can now be attached to each version, and displayed on the roadmap
660 663 * attachments can now be added to wiki pages (original patch by Pavol Murin) and displayed online
661 664 * added an option to see all versions in the roadmap view (including completed ones)
662 665 * added basic issue relations
663 666 * added the ability to log time when changing an issue status
664 667 * account information can now be sent to the user when creating an account
665 668 * author and assignee of an issue always receive notifications (even if they turned of mail notifications)
666 669 * added a quick search form in page header
667 670 * added 'me' value for 'assigned to' and 'author' query filters
668 671 * added a link on revision screen to see the entire diff for the revision
669 672 * added last commit message for each entry in repository browser
670 673 * added the ability to view a file diff with free to/from revision selection.
671 674 * text files can now be viewed online when browsing the repository
672 675 * added basic support for other SCM: CVS (Ralph Vater), Mercurial and Darcs
673 676 * added fragment caching for svn diffs
674 677 * added fragment caching for calendar and gantt views
675 678 * login field automatically focused on login form
676 679 * subproject name displayed on issue list, calendar and gantt
677 680 * added an option to choose the date format: language based or ISO 8601
678 681 * added a simple mail handler. It lets users add notes to an existing issue by replying to the initial notification email.
679 682 * a 403 error page is now displayed (instead of a blank page) when trying to access a protected page
680 683 * added portuguese translation (Joao Carlos Clementoni)
681 684 * added partial online help japanese translation (Ken Date)
682 685 * added bulgarian translation (Nikolay Solakov)
683 686 * added dutch translation (Linda van den Brink)
684 687 * added swedish translation (Thomas Habets)
685 688 * italian translation update (Alessio Spadaro)
686 689 * japanese translation update (Satoru Kurashiki)
687 690 * fixed: error on history atom feed when thereοΏ½s no notes on an issue change
688 691 * fixed: error in journalizing an issue with longtext custom fields (Postgresql)
689 692 * fixed: creation of Oracle schema
690 693 * fixed: last day of the month not included in project activity
691 694 * fixed: files with an apostrophe in their names can't be accessed in SVN repository
692 695 * fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000)
693 696 * fixed: open/closed issue counts are always 0 on reports view (postgresql)
694 697 * fixed: date query filters (wrong results and sql error with postgresql)
695 698 * fixed: confidentiality issue on account/show (private project names displayed to anyone)
696 699 * fixed: Long text custom fields displayed without line breaks
697 700 * fixed: Error when editing the wokflow after deleting a status
698 701 * fixed: SVN commit dates are now stored as local time
699 702
700 703
701 704 == 2007-04-11 v0.5.0
702 705
703 706 * added per project Wiki
704 707 * added rss/atom feeds at project level (custom queries can be used as feeds)
705 708 * added search engine (search in issues, news, commits, wiki pages, documents)
706 709 * simple time tracking functionality added
707 710 * added version due dates on calendar and gantt
708 711 * added subprojects issue count on project Reports page
709 712 * added the ability to copy an existing workflow when creating a new tracker
710 713 * added the ability to include subprojects on calendar and gantt
711 714 * added the ability to select trackers to display on calendar and gantt (Jeffrey Jones)
712 715 * added side by side svn diff view (Cyril Mougel)
713 716 * added back subproject filter on issue list
714 717 * added permissions report in admin area
715 718 * added a status filter on users list
716 719 * support for password-protected SVN repositories
717 720 * SVN commits are now stored in the database
718 721 * added simple svn statistics SVG graphs
719 722 * progress bars for roadmap versions (Nick Read)
720 723 * issue history now shows file uploads and deletions
721 724 * #id patterns are turned into links to issues in descriptions and commit messages
722 725 * japanese translation added (Satoru Kurashiki)
723 726 * chinese simplified translation added (Andy Wu)
724 727 * italian translation added (Alessio Spadaro)
725 728 * added scripts to manage SVN repositories creation and user access control using ssh+svn (Nicolas Chuche)
726 729 * better calendar rendering time
727 730 * fixed migration scripts to work with mysql 5 running in strict mode
728 731 * fixed: error when clicking "add" with no block selected on my/page_layout
729 732 * fixed: hard coded links in navigation bar
730 733 * fixed: table_name pre/suffix support
731 734
732 735
733 736 == 2007-02-18 v0.4.2
734 737
735 738 * Rails 1.2 is now required
736 739 * settings are now stored in the database and editable through the application in: Admin -> Settings (config_custom.rb is no longer used)
737 740 * added project roadmap view
738 741 * mail notifications added when a document, a file or an attachment is added
739 742 * tooltips added on Gantt chart and calender to view the details of the issues
740 743 * ability to set the sort order for roles, trackers, issue statuses
741 744 * added missing fields to csv export: priority, start date, due date, done ratio
742 745 * added total number of issues per tracker on project overview
743 746 * all icons replaced (new icons are based on GPL icon set: "KDE Crystal Diamond 2.5" -by paolino- and "kNeu! Alpha v0.1" -by Pablo Fabregat-)
744 747 * added back "fixed version" field on issue screen and in filters
745 748 * project settings screen split in 4 tabs
746 749 * custom fields screen split in 3 tabs (one for each kind of custom field)
747 750 * multiple issues pdf export now rendered as a table
748 751 * added a button on users/list to manually activate an account
749 752 * added a setting option to disable "password lost" functionality
750 753 * added a setting option to set max number of issues in csv/pdf exports
751 754 * fixed: subprojects count is always 0 on projects list
752 755 * fixed: locked users are proposed when adding a member to a project
753 756 * fixed: setting an issue status as default status leads to an sql error with SQLite
754 757 * fixed: unable to delete an issue status even if it's not used yet
755 758 * fixed: filters ignored when exporting a predefined query to csv/pdf
756 759 * fixed: crash when french "issue_edit" email notification is sent
757 760 * fixed: hide mail preference not saved (my/account)
758 761 * fixed: crash when a new user try to edit its "my page" layout
759 762
760 763
761 764 == 2007-01-03 v0.4.1
762 765
763 766 * fixed: emails have no recipient when one of the project members has notifications disabled
764 767
765 768
766 769 == 2007-01-02 v0.4.0
767 770
768 771 * simple SVN browser added (just needs svn binaries in PATH)
769 772 * comments can now be added on news
770 773 * "my page" is now customizable
771 774 * more powerfull and savable filters for issues lists
772 775 * improved issues change history
773 776 * new functionality: move an issue to another project or tracker
774 777 * new functionality: add a note to an issue
775 778 * new report: project activity
776 779 * "start date" and "% done" fields added on issues
777 780 * project calendar added
778 781 * gantt chart added (exportable to pdf)
779 782 * single/multiple issues pdf export added
780 783 * issues reports improvements
781 784 * multiple file upload for issues, documents and files
782 785 * option to set maximum size of uploaded files
783 786 * textile formating of issue and news descritions (RedCloth required)
784 787 * integration of DotClear jstoolbar for textile formatting
785 788 * calendar date picker for date fields (LGPL DHTML Calendar http://sourceforge.net/projects/jscalendar)
786 789 * new filter in issues list: Author
787 790 * ajaxified paginators
788 791 * news rss feed added
789 792 * option to set number of results per page on issues list
790 793 * localized csv separator (comma/semicolon)
791 794 * csv output encoded to ISO-8859-1
792 795 * user custom field displayed on account/show
793 796 * default configuration improved (default roles, trackers, status, permissions and workflows)
794 797 * language for default configuration data can now be chosen when running 'load_default_data' task
795 798 * javascript added on custom field form to show/hide fields according to the format of custom field
796 799 * fixed: custom fields not in csv exports
797 800 * fixed: project settings now displayed according to user's permissions
798 801 * fixed: application error when no version is selected on projects/add_file
799 802 * fixed: public actions not authorized for members of non public projects
800 803 * fixed: non public projects were shown on welcome screen even if current user is not a member
801 804
802 805
803 806 == 2006-10-08 v0.3.0
804 807
805 808 * user authentication against multiple LDAP (optional)
806 809 * token based "lost password" functionality
807 810 * user self-registration functionality (optional)
808 811 * custom fields now available for issues, users and projects
809 812 * new custom field format "text" (displayed as a textarea field)
810 813 * project & administration drop down menus in navigation bar for quicker access
811 814 * text formatting is preserved for long text fields (issues, projects and news descriptions)
812 815 * urls and emails are turned into clickable links in long text fields
813 816 * "due date" field added on issues
814 817 * tracker selection filter added on change log
815 818 * Localization plugin replaced with GLoc 1.1.0 (iconv required)
816 819 * error messages internationalization
817 820 * german translation added (thanks to Karim Trott)
818 821 * data locking for issues to prevent update conflicts (using ActiveRecord builtin optimistic locking)
819 822 * new filter in issues list: "Fixed version"
820 823 * active filters are displayed with colored background on issues list
821 824 * custom configuration is now defined in config/config_custom.rb
822 825 * user object no more stored in session (only user_id)
823 826 * news summary field is no longer required
824 827 * tables and forms redesign
825 828 * Fixed: boolean custom field not working
826 829 * Fixed: error messages for custom fields are not displayed
827 830 * Fixed: invalid custom fields should have a red border
828 831 * Fixed: custom fields values are not validated on issue update
829 832 * Fixed: unable to choose an empty value for 'List' custom fields
830 833 * Fixed: no issue categories sorting
831 834 * Fixed: incorrect versions sorting
832 835
833 836
834 837 == 2006-07-12 - v0.2.2
835 838
836 839 * Fixed: bug in "issues list"
837 840
838 841
839 842 == 2006-07-09 - v0.2.1
840 843
841 844 * new databases supported: Oracle, PostgreSQL, SQL Server
842 845 * projects/subprojects hierarchy (1 level of subprojects only)
843 846 * environment information display in admin/info
844 847 * more filter options in issues list (rev6)
845 848 * default language based on browser settings (Accept-Language HTTP header)
846 849 * issues list exportable to CSV (rev6)
847 850 * simple_format and auto_link on long text fields
848 851 * more data validations
849 852 * Fixed: error when all mail notifications are unchecked in admin/mail_options
850 853 * Fixed: all project news are displayed on project summary
851 854 * Fixed: Can't change user password in users/edit
852 855 * Fixed: Error on tables creation with PostgreSQL (rev5)
853 856 * Fixed: SQL error in "issue reports" view with PostgreSQL (rev5)
854 857
855 858
856 859 == 2006-06-25 - v0.1.0
857 860
858 861 * multiple users/multiple projects
859 862 * role based access control
860 863 * issue tracking system
861 864 * fully customizable workflow
862 865 * documents/files repository
863 866 * email notifications on issue creation and update
864 867 * multilanguage support (except for error messages):english, french, spanish
865 868 * online manual in french (unfinished)
@@ -1,459 +1,461
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'iconv'
19 19 require 'rfpdf/chinese'
20 20
21 21 module Redmine
22 22 module Export
23 23 module PDF
24 include ActionView::Helpers::NumberHelper
25
24 26 class IFPDF < FPDF
25 27 include GLoc
26 28 attr_accessor :footer_date
27 29
28 30 def initialize(lang)
29 31 super()
30 32 set_language_if_valid lang
31 33 case current_language.to_s
32 34 when 'ja'
33 35 extend(PDF_Japanese)
34 36 AddSJISFont()
35 37 @font_for_content = 'SJIS'
36 38 @font_for_footer = 'SJIS'
37 39 when 'zh'
38 40 extend(PDF_Chinese)
39 41 AddGBFont()
40 42 @font_for_content = 'GB'
41 43 @font_for_footer = 'GB'
42 44 when 'zh-tw'
43 45 extend(PDF_Chinese)
44 46 AddBig5Font()
45 47 @font_for_content = 'Big5'
46 48 @font_for_footer = 'Big5'
47 49 else
48 50 @font_for_content = 'Arial'
49 51 @font_for_footer = 'Helvetica'
50 52 end
51 53 SetCreator(Redmine::Info.app_name)
52 54 SetFont(@font_for_content)
53 55 end
54 56
55 57 def SetFontStyle(style, size)
56 58 SetFont(@font_for_content, style, size)
57 59 end
58 60
59 61 def SetTitle(txt)
60 62 txt = begin
61 63 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
62 64 hextxt = "<FEFF" # FEFF is BOM
63 65 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
64 66 hextxt << ">"
65 67 rescue
66 68 txt
67 69 end || ''
68 70 super(txt)
69 71 end
70 72
71 73 def textstring(s)
72 74 # Format a text string
73 75 if s =~ /^</ # This means the string is hex-dumped.
74 76 return s
75 77 else
76 78 return '('+escape(s)+')'
77 79 end
78 80 end
79 81
80 82 def Cell(w,h=0,txt='',border=0,ln=0,align='',fill=0,link='')
81 83 @ic ||= Iconv.new(l(:general_pdf_encoding), 'UTF-8')
82 84 # these quotation marks are not correctly rendered in the pdf
83 85 txt = txt.gsub(/[Ò€œÒ€�]/, '"') if txt
84 86 txt = begin
85 87 # 0x5c char handling
86 88 txtar = txt.split('\\')
87 89 txtar << '' if txt[-1] == ?\\
88 90 txtar.collect {|x| @ic.iconv(x)}.join('\\').gsub(/\\/, "\\\\\\\\")
89 91 rescue
90 92 txt
91 93 end || ''
92 94 super w,h,txt,border,ln,align,fill,link
93 95 end
94 96
95 97 def Footer
96 98 SetFont(@font_for_footer, 'I', 8)
97 99 SetY(-15)
98 100 SetX(15)
99 101 Cell(0, 5, @footer_date, 0, 0, 'L')
100 102 SetY(-15)
101 103 SetX(-30)
102 104 Cell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
103 105 end
104 106 end
105 107
106 108 # Returns a PDF string of a list of issues
107 109 def issues_to_pdf(issues, project)
108 110 pdf = IFPDF.new(current_language)
109 111 title = project ? "#{project} - #{l(:label_issue_plural)}" : "#{l(:label_issue_plural)}"
110 112 pdf.SetTitle(title)
111 113 pdf.AliasNbPages
112 114 pdf.footer_date = format_date(Date.today)
113 115 pdf.AddPage("L")
114 116 row_height = 7
115 117
116 118 # title
117 119 pdf.SetFontStyle('B',11)
118 120 pdf.Cell(190,10, title)
119 121 pdf.Ln
120 122
121 123 # headers
122 124 pdf.SetFontStyle('B',10)
123 125 pdf.SetFillColor(230, 230, 230)
124 126 pdf.Cell(15, row_height, "#", 0, 0, 'L', 1)
125 127 pdf.Cell(30, row_height, l(:field_tracker), 0, 0, 'L', 1)
126 128 pdf.Cell(30, row_height, l(:field_status), 0, 0, 'L', 1)
127 129 pdf.Cell(30, row_height, l(:field_priority), 0, 0, 'L', 1)
128 130 pdf.Cell(40, row_height, l(:field_assigned_to), 0, 0, 'L', 1)
129 131 pdf.Cell(25, row_height, l(:field_updated_on), 0, 0, 'L', 1)
130 132 pdf.Cell(0, row_height, l(:field_subject), 0, 0, 'L', 1)
131 133 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
132 134 pdf.Ln
133 135 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
134 136 pdf.SetY(pdf.GetY() + 1)
135 137
136 138 # rows
137 139 pdf.SetFontStyle('',9)
138 140 pdf.SetFillColor(255, 255, 255)
139 141 issues.each do |issue|
140 142 pdf.Cell(15, row_height, issue.id.to_s, 0, 0, 'L', 1)
141 143 pdf.Cell(30, row_height, issue.tracker.name, 0, 0, 'L', 1)
142 144 pdf.Cell(30, row_height, issue.status.name, 0, 0, 'L', 1)
143 145 pdf.Cell(30, row_height, issue.priority.name, 0, 0, 'L', 1)
144 146 pdf.Cell(40, row_height, issue.assigned_to ? issue.assigned_to.to_s : '', 0, 0, 'L', 1)
145 147 pdf.Cell(25, row_height, format_date(issue.updated_on), 0, 0, 'L', 1)
146 148 pdf.MultiCell(0, row_height, (project == issue.project ? issue.subject : "#{issue.project} - #{issue.subject}"))
147 149 pdf.Line(10, pdf.GetY, 287, pdf.GetY)
148 150 pdf.SetY(pdf.GetY() + 1)
149 151 end
150 152 pdf.Output
151 153 end
152 154
153 155 # Returns a PDF string of a single issue
154 156 def issue_to_pdf(issue)
155 157 pdf = IFPDF.new(current_language)
156 158 pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
157 159 pdf.AliasNbPages
158 160 pdf.footer_date = format_date(Date.today)
159 161 pdf.AddPage
160 162
161 163 pdf.SetFontStyle('B',11)
162 164 pdf.Cell(190,10, "#{issue.project} - #{issue.tracker} # #{issue.id}: #{issue.subject}")
163 165 pdf.Ln
164 166
165 167 y0 = pdf.GetY
166 168
167 169 pdf.SetFontStyle('B',9)
168 170 pdf.Cell(35,5, l(:field_status) + ":","LT")
169 171 pdf.SetFontStyle('',9)
170 172 pdf.Cell(60,5, issue.status.to_s,"RT")
171 173 pdf.SetFontStyle('B',9)
172 174 pdf.Cell(35,5, l(:field_priority) + ":","LT")
173 175 pdf.SetFontStyle('',9)
174 176 pdf.Cell(60,5, issue.priority.to_s,"RT")
175 177 pdf.Ln
176 178
177 179 pdf.SetFontStyle('B',9)
178 180 pdf.Cell(35,5, l(:field_author) + ":","L")
179 181 pdf.SetFontStyle('',9)
180 182 pdf.Cell(60,5, issue.author.to_s,"R")
181 183 pdf.SetFontStyle('B',9)
182 184 pdf.Cell(35,5, l(:field_category) + ":","L")
183 185 pdf.SetFontStyle('',9)
184 186 pdf.Cell(60,5, issue.category.to_s,"R")
185 187 pdf.Ln
186 188
187 189 pdf.SetFontStyle('B',9)
188 190 pdf.Cell(35,5, l(:field_created_on) + ":","L")
189 191 pdf.SetFontStyle('',9)
190 192 pdf.Cell(60,5, format_date(issue.created_on),"R")
191 193 pdf.SetFontStyle('B',9)
192 194 pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
193 195 pdf.SetFontStyle('',9)
194 196 pdf.Cell(60,5, issue.assigned_to.to_s,"R")
195 197 pdf.Ln
196 198
197 199 pdf.SetFontStyle('B',9)
198 200 pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
199 201 pdf.SetFontStyle('',9)
200 202 pdf.Cell(60,5, format_date(issue.updated_on),"RB")
201 203 pdf.SetFontStyle('B',9)
202 204 pdf.Cell(35,5, l(:field_due_date) + ":","LB")
203 205 pdf.SetFontStyle('',9)
204 206 pdf.Cell(60,5, format_date(issue.due_date),"RB")
205 207 pdf.Ln
206 208
207 209 for custom_value in issue.custom_values
208 210 pdf.SetFontStyle('B',9)
209 211 pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
210 212 pdf.SetFontStyle('',9)
211 213 pdf.MultiCell(155,5, (show_value custom_value),"R")
212 214 end
213 215
214 216 pdf.SetFontStyle('B',9)
215 217 pdf.Cell(35,5, l(:field_subject) + ":","LTB")
216 218 pdf.SetFontStyle('',9)
217 219 pdf.Cell(155,5, issue.subject,"RTB")
218 220 pdf.Ln
219 221
220 222 pdf.SetFontStyle('B',9)
221 223 pdf.Cell(35,5, l(:field_description) + ":")
222 224 pdf.SetFontStyle('',9)
223 225 pdf.MultiCell(155,5, @issue.description,"BR")
224 226
225 227 pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
226 228 pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
227 229 pdf.Ln
228 230
229 231 if issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
230 232 pdf.SetFontStyle('B',9)
231 233 pdf.Cell(190,5, l(:label_associated_revisions), "B")
232 234 pdf.Ln
233 235 for changeset in issue.changesets
234 236 pdf.SetFontStyle('B',8)
235 237 pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.author.to_s)
236 238 pdf.Ln
237 239 unless changeset.comments.blank?
238 240 pdf.SetFontStyle('',8)
239 241 pdf.MultiCell(190,5, changeset.comments)
240 242 end
241 243 pdf.Ln
242 244 end
243 245 end
244 246
245 247 pdf.SetFontStyle('B',9)
246 248 pdf.Cell(190,5, l(:label_history), "B")
247 249 pdf.Ln
248 250 for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
249 251 pdf.SetFontStyle('B',8)
250 252 pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
251 253 pdf.Ln
252 254 pdf.SetFontStyle('I',8)
253 255 for detail in journal.details
254 256 pdf.Cell(190,5, "- " + show_detail(detail, true))
255 257 pdf.Ln
256 258 end
257 259 if journal.notes?
258 260 pdf.SetFontStyle('',8)
259 261 pdf.MultiCell(190,5, journal.notes)
260 262 end
261 263 pdf.Ln
262 264 end
263 265
264 266 if issue.attachments.any?
265 267 pdf.SetFontStyle('B',9)
266 268 pdf.Cell(190,5, l(:label_attachment_plural), "B")
267 269 pdf.Ln
268 270 for attachment in issue.attachments
269 271 pdf.SetFontStyle('',8)
270 272 pdf.Cell(80,5, attachment.filename)
271 273 pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
272 274 pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
273 275 pdf.Cell(65,5, attachment.author.name,0,0,"R")
274 276 pdf.Ln
275 277 end
276 278 end
277 279 pdf.Output
278 280 end
279 281
280 282 # Returns a PDF string of a gantt chart
281 283 def gantt_to_pdf(gantt, project)
282 284 pdf = IFPDF.new(current_language)
283 285 pdf.SetTitle("#{l(:label_gantt)} #{project}")
284 286 pdf.AliasNbPages
285 287 pdf.footer_date = format_date(Date.today)
286 288 pdf.AddPage("L")
287 289 pdf.SetFontStyle('B',12)
288 290 pdf.SetX(15)
289 291 pdf.Cell(70, 20, project.to_s)
290 292 pdf.Ln
291 293 pdf.SetFontStyle('B',9)
292 294
293 295 subject_width = 70
294 296 header_heigth = 5
295 297
296 298 headers_heigth = header_heigth
297 299 show_weeks = false
298 300 show_days = false
299 301
300 302 if gantt.months < 7
301 303 show_weeks = true
302 304 headers_heigth = 2*header_heigth
303 305 if gantt.months < 3
304 306 show_days = true
305 307 headers_heigth = 3*header_heigth
306 308 end
307 309 end
308 310
309 311 g_width = 210
310 312 zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
311 313 g_height = 120
312 314 t_height = g_height + headers_heigth
313 315
314 316 y_start = pdf.GetY
315 317
316 318 # Months headers
317 319 month_f = gantt.date_from
318 320 left = subject_width
319 321 height = header_heigth
320 322 gantt.months.times do
321 323 width = ((month_f >> 1) - month_f) * zoom
322 324 pdf.SetY(y_start)
323 325 pdf.SetX(left)
324 326 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
325 327 left = left + width
326 328 month_f = month_f >> 1
327 329 end
328 330
329 331 # Weeks headers
330 332 if show_weeks
331 333 left = subject_width
332 334 height = header_heigth
333 335 if gantt.date_from.cwday == 1
334 336 # gantt.date_from is monday
335 337 week_f = gantt.date_from
336 338 else
337 339 # find next monday after gantt.date_from
338 340 week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
339 341 width = (7 - gantt.date_from.cwday + 1) * zoom-1
340 342 pdf.SetY(y_start + header_heigth)
341 343 pdf.SetX(left)
342 344 pdf.Cell(width + 1, height, "", "LTR")
343 345 left = left + width+1
344 346 end
345 347 while week_f <= gantt.date_to
346 348 width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
347 349 pdf.SetY(y_start + header_heigth)
348 350 pdf.SetX(left)
349 351 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
350 352 left = left + width
351 353 week_f = week_f+7
352 354 end
353 355 end
354 356
355 357 # Days headers
356 358 if show_days
357 359 left = subject_width
358 360 height = header_heigth
359 361 wday = gantt.date_from.cwday
360 362 pdf.SetFontStyle('B',7)
361 363 (gantt.date_to - gantt.date_from + 1).to_i.times do
362 364 width = zoom
363 365 pdf.SetY(y_start + 2 * header_heigth)
364 366 pdf.SetX(left)
365 367 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
366 368 left = left + width
367 369 wday = wday + 1
368 370 wday = 1 if wday > 7
369 371 end
370 372 end
371 373
372 374 pdf.SetY(y_start)
373 375 pdf.SetX(15)
374 376 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
375 377
376 378 # Tasks
377 379 top = headers_heigth + y_start
378 380 pdf.SetFontStyle('B',7)
379 381 gantt.events.each do |i|
380 382 pdf.SetY(top)
381 383 pdf.SetX(15)
382 384
383 385 if i.is_a? Issue
384 386 pdf.Cell(subject_width-15, 5, "#{i.tracker} #{i.id}: #{i.subject}".sub(/^(.{30}[^\s]*\s).*$/, '\1 (...)'), "LR")
385 387 else
386 388 pdf.Cell(subject_width-15, 5, "#{l(:label_version)}: #{i.name}", "LR")
387 389 end
388 390
389 391 pdf.SetY(top)
390 392 pdf.SetX(subject_width)
391 393 pdf.Cell(g_width, 5, "", "LR")
392 394
393 395 pdf.SetY(top+1.5)
394 396
395 397 if i.is_a? Issue
396 398 i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
397 399 i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
398 400
399 401 i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
400 402 i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
401 403 i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
402 404
403 405 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
404 406
405 407 i_left = ((i_start_date - gantt.date_from)*zoom)
406 408 i_width = ((i_end_date - i_start_date + 1)*zoom)
407 409 d_width = ((i_done_date - i_start_date)*zoom)
408 410 l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
409 411 l_width ||= 0
410 412
411 413 pdf.SetX(subject_width + i_left)
412 414 pdf.SetFillColor(200,200,200)
413 415 pdf.Cell(i_width, 2, "", 0, 0, "", 1)
414 416
415 417 if l_width > 0
416 418 pdf.SetY(top+1.5)
417 419 pdf.SetX(subject_width + i_left)
418 420 pdf.SetFillColor(255,100,100)
419 421 pdf.Cell(l_width, 2, "", 0, 0, "", 1)
420 422 end
421 423 if d_width > 0
422 424 pdf.SetY(top+1.5)
423 425 pdf.SetX(subject_width + i_left)
424 426 pdf.SetFillColor(100,100,255)
425 427 pdf.Cell(d_width, 2, "", 0, 0, "", 1)
426 428 end
427 429
428 430 pdf.SetY(top+1.5)
429 431 pdf.SetX(subject_width + i_left + i_width)
430 432 pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
431 433 else
432 434 i_left = ((i.start_date - gantt.date_from)*zoom)
433 435
434 436 pdf.SetX(subject_width + i_left)
435 437 pdf.SetFillColor(50,200,50)
436 438 pdf.Cell(2, 2, "", 0, 0, "", 1)
437 439
438 440 pdf.SetY(top+1.5)
439 441 pdf.SetX(subject_width + i_left + 3)
440 442 pdf.Cell(30, 2, "#{i.name}")
441 443 end
442 444
443 445 top = top + 5
444 446 pdf.SetDrawColor(200, 200, 200)
445 447 pdf.Line(15, top, subject_width+g_width, top)
446 448 if pdf.GetY() > 180
447 449 pdf.AddPage("L")
448 450 top = 20
449 451 pdf.Line(15, top, subject_width+g_width, top)
450 452 end
451 453 pdf.SetDrawColor(0, 0, 0)
452 454 end
453 455
454 456 pdf.Line(15, top, subject_width+g_width, top)
455 457 pdf.Output
456 458 end
457 459 end
458 460 end
459 461 end
@@ -1,786 +1,786
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < Test::Unit::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :issues,
30 30 :issue_statuses,
31 31 :versions,
32 32 :trackers,
33 33 :projects_trackers,
34 34 :issue_categories,
35 35 :enabled_modules,
36 36 :enumerations,
37 37 :attachments,
38 38 :workflows,
39 39 :custom_fields,
40 40 :custom_values,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index
54 54 get :index
55 55 assert_response :success
56 56 assert_template 'index.rhtml'
57 57 assert_not_nil assigns(:issues)
58 58 assert_nil assigns(:project)
59 59 assert_tag :tag => 'a', :content => /Can't print recipes/
60 60 assert_tag :tag => 'a', :content => /Subproject issue/
61 61 # private projects hidden
62 62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
63 63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
64 64 end
65 65
66 66 def test_index_should_not_list_issues_when_module_disabled
67 67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
68 68 get :index
69 69 assert_response :success
70 70 assert_template 'index.rhtml'
71 71 assert_not_nil assigns(:issues)
72 72 assert_nil assigns(:project)
73 73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
74 74 assert_tag :tag => 'a', :content => /Subproject issue/
75 75 end
76 76
77 77 def test_index_with_project
78 78 Setting.display_subprojects_issues = 0
79 79 get :index, :project_id => 1
80 80 assert_response :success
81 81 assert_template 'index.rhtml'
82 82 assert_not_nil assigns(:issues)
83 83 assert_tag :tag => 'a', :content => /Can't print recipes/
84 84 assert_no_tag :tag => 'a', :content => /Subproject issue/
85 85 end
86 86
87 87 def test_index_with_project_and_subprojects
88 88 Setting.display_subprojects_issues = 1
89 89 get :index, :project_id => 1
90 90 assert_response :success
91 91 assert_template 'index.rhtml'
92 92 assert_not_nil assigns(:issues)
93 93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 94 assert_tag :tag => 'a', :content => /Subproject issue/
95 95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
96 96 end
97 97
98 98 def test_index_with_project_and_subprojects_should_show_private_subprojects
99 99 @request.session[:user_id] = 2
100 100 Setting.display_subprojects_issues = 1
101 101 get :index, :project_id => 1
102 102 assert_response :success
103 103 assert_template 'index.rhtml'
104 104 assert_not_nil assigns(:issues)
105 105 assert_tag :tag => 'a', :content => /Can't print recipes/
106 106 assert_tag :tag => 'a', :content => /Subproject issue/
107 107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
108 108 end
109 109
110 110 def test_index_with_project_and_filter
111 111 get :index, :project_id => 1, :set_filter => 1
112 112 assert_response :success
113 113 assert_template 'index.rhtml'
114 114 assert_not_nil assigns(:issues)
115 115 end
116 116
117 117 def test_index_csv_with_project
118 118 get :index, :format => 'csv'
119 119 assert_response :success
120 120 assert_not_nil assigns(:issues)
121 121 assert_equal 'text/csv', @response.content_type
122 122
123 123 get :index, :project_id => 1, :format => 'csv'
124 124 assert_response :success
125 125 assert_not_nil assigns(:issues)
126 126 assert_equal 'text/csv', @response.content_type
127 127 end
128 128
129 129 def test_index_pdf
130 130 get :index, :format => 'pdf'
131 131 assert_response :success
132 132 assert_not_nil assigns(:issues)
133 133 assert_equal 'application/pdf', @response.content_type
134 134
135 135 get :index, :project_id => 1, :format => 'pdf'
136 136 assert_response :success
137 137 assert_not_nil assigns(:issues)
138 138 assert_equal 'application/pdf', @response.content_type
139 139 end
140 140
141 141 def test_index_sort
142 142 get :index, :sort_key => 'tracker'
143 143 assert_response :success
144 144
145 145 sort_params = @request.session['issuesindex_sort']
146 146 assert sort_params.is_a?(Hash)
147 147 assert_equal 'tracker', sort_params[:key]
148 148 assert_equal 'ASC', sort_params[:order]
149 149 end
150 150
151 151 def test_gantt
152 152 get :gantt, :project_id => 1
153 153 assert_response :success
154 154 assert_template 'gantt.rhtml'
155 155 assert_not_nil assigns(:gantt)
156 156 events = assigns(:gantt).events
157 157 assert_not_nil events
158 158 # Issue with start and due dates
159 159 i = Issue.find(1)
160 160 assert_not_nil i.due_date
161 161 assert events.include?(Issue.find(1))
162 162 # Issue with without due date but targeted to a version with date
163 163 i = Issue.find(2)
164 164 assert_nil i.due_date
165 165 assert events.include?(i)
166 166 end
167 167
168 168 def test_cross_project_gantt
169 169 get :gantt
170 170 assert_response :success
171 171 assert_template 'gantt.rhtml'
172 172 assert_not_nil assigns(:gantt)
173 173 events = assigns(:gantt).events
174 174 assert_not_nil events
175 175 end
176 176
177 177 def test_gantt_export_to_pdf
178 178 get :gantt, :project_id => 1, :format => 'pdf'
179 179 assert_response :success
180 180 assert_equal 'application/pdf', @response.content_type
181 181 assert @response.body.starts_with?('%PDF')
182 182 assert_not_nil assigns(:gantt)
183 183 end
184 184
185 185 def test_cross_project_gantt_export_to_pdf
186 186 get :gantt, :format => 'pdf'
187 187 assert_response :success
188 188 assert_equal 'application/pdf', @response.content_type
189 189 assert @response.body.starts_with?('%PDF')
190 190 assert_not_nil assigns(:gantt)
191 191 end
192 192
193 193 if Object.const_defined?(:Magick)
194 194 def test_gantt_image
195 195 get :gantt, :project_id => 1, :format => 'png'
196 196 assert_response :success
197 197 assert_equal 'image/png', @response.content_type
198 198 end
199 199 else
200 200 puts "RMagick not installed. Skipping tests !!!"
201 201 end
202 202
203 203 def test_calendar
204 204 get :calendar, :project_id => 1
205 205 assert_response :success
206 206 assert_template 'calendar'
207 207 assert_not_nil assigns(:calendar)
208 208 end
209 209
210 210 def test_cross_project_calendar
211 211 get :calendar
212 212 assert_response :success
213 213 assert_template 'calendar'
214 214 assert_not_nil assigns(:calendar)
215 215 end
216 216
217 217 def test_changes
218 218 get :changes, :project_id => 1
219 219 assert_response :success
220 220 assert_not_nil assigns(:journals)
221 221 assert_equal 'application/atom+xml', @response.content_type
222 222 end
223 223
224 224 def test_show_by_anonymous
225 225 get :show, :id => 1
226 226 assert_response :success
227 227 assert_template 'show.rhtml'
228 228 assert_not_nil assigns(:issue)
229 229 assert_equal Issue.find(1), assigns(:issue)
230 230
231 231 # anonymous role is allowed to add a note
232 232 assert_tag :tag => 'form',
233 233 :descendant => { :tag => 'fieldset',
234 234 :child => { :tag => 'legend',
235 235 :content => /Notes/ } }
236 236 end
237 237
238 238 def test_show_by_manager
239 239 @request.session[:user_id] = 2
240 240 get :show, :id => 1
241 241 assert_response :success
242 242
243 243 assert_tag :tag => 'form',
244 244 :descendant => { :tag => 'fieldset',
245 245 :child => { :tag => 'legend',
246 246 :content => /Change properties/ } },
247 247 :descendant => { :tag => 'fieldset',
248 248 :child => { :tag => 'legend',
249 249 :content => /Log time/ } },
250 250 :descendant => { :tag => 'fieldset',
251 251 :child => { :tag => 'legend',
252 252 :content => /Notes/ } }
253 253 end
254 254
255 255 def test_show_export_to_pdf
256 get :show, :id => 1, :format => 'pdf'
256 get :show, :id => 3, :format => 'pdf'
257 257 assert_response :success
258 258 assert_equal 'application/pdf', @response.content_type
259 259 assert @response.body.starts_with?('%PDF')
260 260 assert_not_nil assigns(:issue)
261 261 end
262 262
263 263 def test_get_new
264 264 @request.session[:user_id] = 2
265 265 get :new, :project_id => 1, :tracker_id => 1
266 266 assert_response :success
267 267 assert_template 'new'
268 268
269 269 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
270 270 :value => 'Default string' }
271 271 end
272 272
273 273 def test_get_new_without_tracker_id
274 274 @request.session[:user_id] = 2
275 275 get :new, :project_id => 1
276 276 assert_response :success
277 277 assert_template 'new'
278 278
279 279 issue = assigns(:issue)
280 280 assert_not_nil issue
281 281 assert_equal Project.find(1).trackers.first, issue.tracker
282 282 end
283 283
284 284 def test_update_new_form
285 285 @request.session[:user_id] = 2
286 286 xhr :post, :new, :project_id => 1,
287 287 :issue => {:tracker_id => 2,
288 288 :subject => 'This is the test_new issue',
289 289 :description => 'This is the description',
290 290 :priority_id => 5}
291 291 assert_response :success
292 292 assert_template 'new'
293 293 end
294 294
295 295 def test_post_new
296 296 @request.session[:user_id] = 2
297 297 post :new, :project_id => 1,
298 298 :issue => {:tracker_id => 3,
299 299 :subject => 'This is the test_new issue',
300 300 :description => 'This is the description',
301 301 :priority_id => 5,
302 302 :estimated_hours => '',
303 303 :custom_field_values => {'2' => 'Value for field 2'}}
304 304 assert_redirected_to 'issues/show'
305 305
306 306 issue = Issue.find_by_subject('This is the test_new issue')
307 307 assert_not_nil issue
308 308 assert_equal 2, issue.author_id
309 309 assert_equal 3, issue.tracker_id
310 310 assert_nil issue.estimated_hours
311 311 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
312 312 assert_not_nil v
313 313 assert_equal 'Value for field 2', v.value
314 314 end
315 315
316 316 def test_post_new_and_continue
317 317 @request.session[:user_id] = 2
318 318 post :new, :project_id => 1,
319 319 :issue => {:tracker_id => 3,
320 320 :subject => 'This is first issue',
321 321 :priority_id => 5},
322 322 :continue => ''
323 323 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
324 324 end
325 325
326 326 def test_post_new_without_custom_fields_param
327 327 @request.session[:user_id] = 2
328 328 post :new, :project_id => 1,
329 329 :issue => {:tracker_id => 1,
330 330 :subject => 'This is the test_new issue',
331 331 :description => 'This is the description',
332 332 :priority_id => 5}
333 333 assert_redirected_to 'issues/show'
334 334 end
335 335
336 336 def test_post_new_with_required_custom_field_and_without_custom_fields_param
337 337 field = IssueCustomField.find_by_name('Database')
338 338 field.update_attribute(:is_required, true)
339 339
340 340 @request.session[:user_id] = 2
341 341 post :new, :project_id => 1,
342 342 :issue => {:tracker_id => 1,
343 343 :subject => 'This is the test_new issue',
344 344 :description => 'This is the description',
345 345 :priority_id => 5}
346 346 assert_response :success
347 347 assert_template 'new'
348 348 issue = assigns(:issue)
349 349 assert_not_nil issue
350 350 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
351 351 end
352 352
353 353 def test_post_new_with_watchers
354 354 @request.session[:user_id] = 2
355 355 ActionMailer::Base.deliveries.clear
356 356
357 357 assert_difference 'Watcher.count', 2 do
358 358 post :new, :project_id => 1,
359 359 :issue => {:tracker_id => 1,
360 360 :subject => 'This is a new issue with watchers',
361 361 :description => 'This is the description',
362 362 :priority_id => 5,
363 363 :watcher_user_ids => ['2', '3']}
364 364 end
365 365 assert_redirected_to 'issues/show'
366 366
367 367 issue = Issue.find_by_subject('This is a new issue with watchers')
368 368 # Watchers added
369 369 assert_equal [2, 3], issue.watcher_user_ids.sort
370 370 assert issue.watched_by?(User.find(3))
371 371 # Watchers notified
372 372 mail = ActionMailer::Base.deliveries.last
373 373 assert_kind_of TMail::Mail, mail
374 374 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
375 375 end
376 376
377 377 def test_post_should_preserve_fields_values_on_validation_failure
378 378 @request.session[:user_id] = 2
379 379 post :new, :project_id => 1,
380 380 :issue => {:tracker_id => 1,
381 381 # empty subject
382 382 :subject => '',
383 383 :description => 'This is a description',
384 384 :priority_id => 6,
385 385 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
386 386 assert_response :success
387 387 assert_template 'new'
388 388
389 389 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
390 390 :content => 'This is a description'
391 391 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
392 392 :child => { :tag => 'option', :attributes => { :selected => 'selected',
393 393 :value => '6' },
394 394 :content => 'High' }
395 395 # Custom fields
396 396 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
397 397 :child => { :tag => 'option', :attributes => { :selected => 'selected',
398 398 :value => 'Oracle' },
399 399 :content => 'Oracle' }
400 400 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
401 401 :value => 'Value for field 2'}
402 402 end
403 403
404 404 def test_copy_issue
405 405 @request.session[:user_id] = 2
406 406 get :new, :project_id => 1, :copy_from => 1
407 407 assert_template 'new'
408 408 assert_not_nil assigns(:issue)
409 409 orig = Issue.find(1)
410 410 assert_equal orig.subject, assigns(:issue).subject
411 411 end
412 412
413 413 def test_get_edit
414 414 @request.session[:user_id] = 2
415 415 get :edit, :id => 1
416 416 assert_response :success
417 417 assert_template 'edit'
418 418 assert_not_nil assigns(:issue)
419 419 assert_equal Issue.find(1), assigns(:issue)
420 420 end
421 421
422 422 def test_get_edit_with_params
423 423 @request.session[:user_id] = 2
424 424 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
425 425 assert_response :success
426 426 assert_template 'edit'
427 427
428 428 issue = assigns(:issue)
429 429 assert_not_nil issue
430 430
431 431 assert_equal 5, issue.status_id
432 432 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
433 433 :child => { :tag => 'option',
434 434 :content => 'Closed',
435 435 :attributes => { :selected => 'selected' } }
436 436
437 437 assert_equal 7, issue.priority_id
438 438 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
439 439 :child => { :tag => 'option',
440 440 :content => 'Urgent',
441 441 :attributes => { :selected => 'selected' } }
442 442 end
443 443
444 444 def test_reply_to_issue
445 445 @request.session[:user_id] = 2
446 446 get :reply, :id => 1
447 447 assert_response :success
448 448 assert_select_rjs :show, "update"
449 449 end
450 450
451 451 def test_reply_to_note
452 452 @request.session[:user_id] = 2
453 453 get :reply, :id => 1, :journal_id => 2
454 454 assert_response :success
455 455 assert_select_rjs :show, "update"
456 456 end
457 457
458 458 def test_post_edit_without_custom_fields_param
459 459 @request.session[:user_id] = 2
460 460 ActionMailer::Base.deliveries.clear
461 461
462 462 issue = Issue.find(1)
463 463 assert_equal '125', issue.custom_value_for(2).value
464 464 old_subject = issue.subject
465 465 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
466 466
467 467 assert_difference('Journal.count') do
468 468 assert_difference('JournalDetail.count', 2) do
469 469 post :edit, :id => 1, :issue => {:subject => new_subject,
470 470 :priority_id => '6',
471 471 :category_id => '1' # no change
472 472 }
473 473 end
474 474 end
475 475 assert_redirected_to 'issues/show/1'
476 476 issue.reload
477 477 assert_equal new_subject, issue.subject
478 478 # Make sure custom fields were not cleared
479 479 assert_equal '125', issue.custom_value_for(2).value
480 480
481 481 mail = ActionMailer::Base.deliveries.last
482 482 assert_kind_of TMail::Mail, mail
483 483 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
484 484 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
485 485 end
486 486
487 487 def test_post_edit_with_custom_field_change
488 488 @request.session[:user_id] = 2
489 489 issue = Issue.find(1)
490 490 assert_equal '125', issue.custom_value_for(2).value
491 491
492 492 assert_difference('Journal.count') do
493 493 assert_difference('JournalDetail.count', 3) do
494 494 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
495 495 :priority_id => '6',
496 496 :category_id => '1', # no change
497 497 :custom_field_values => { '2' => 'New custom value' }
498 498 }
499 499 end
500 500 end
501 501 assert_redirected_to 'issues/show/1'
502 502 issue.reload
503 503 assert_equal 'New custom value', issue.custom_value_for(2).value
504 504
505 505 mail = ActionMailer::Base.deliveries.last
506 506 assert_kind_of TMail::Mail, mail
507 507 assert mail.body.include?("Searchable field changed from 125 to New custom value")
508 508 end
509 509
510 510 def test_post_edit_with_status_and_assignee_change
511 511 issue = Issue.find(1)
512 512 assert_equal 1, issue.status_id
513 513 @request.session[:user_id] = 2
514 514 assert_difference('TimeEntry.count', 0) do
515 515 post :edit,
516 516 :id => 1,
517 517 :issue => { :status_id => 2, :assigned_to_id => 3 },
518 518 :notes => 'Assigned to dlopper',
519 519 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
520 520 end
521 521 assert_redirected_to 'issues/show/1'
522 522 issue.reload
523 523 assert_equal 2, issue.status_id
524 524 j = issue.journals.find(:first, :order => 'id DESC')
525 525 assert_equal 'Assigned to dlopper', j.notes
526 526 assert_equal 2, j.details.size
527 527
528 528 mail = ActionMailer::Base.deliveries.last
529 529 assert mail.body.include?("Status changed from New to Assigned")
530 530 end
531 531
532 532 def test_post_edit_with_note_only
533 533 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
534 534 # anonymous user
535 535 post :edit,
536 536 :id => 1,
537 537 :notes => notes
538 538 assert_redirected_to 'issues/show/1'
539 539 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
540 540 assert_equal notes, j.notes
541 541 assert_equal 0, j.details.size
542 542 assert_equal User.anonymous, j.user
543 543
544 544 mail = ActionMailer::Base.deliveries.last
545 545 assert mail.body.include?(notes)
546 546 end
547 547
548 548 def test_post_edit_with_note_and_spent_time
549 549 @request.session[:user_id] = 2
550 550 spent_hours_before = Issue.find(1).spent_hours
551 551 assert_difference('TimeEntry.count') do
552 552 post :edit,
553 553 :id => 1,
554 554 :notes => '2.5 hours added',
555 555 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
556 556 end
557 557 assert_redirected_to 'issues/show/1'
558 558
559 559 issue = Issue.find(1)
560 560
561 561 j = issue.journals.find(:first, :order => 'id DESC')
562 562 assert_equal '2.5 hours added', j.notes
563 563 assert_equal 0, j.details.size
564 564
565 565 t = issue.time_entries.find(:first, :order => 'id DESC')
566 566 assert_not_nil t
567 567 assert_equal 2.5, t.hours
568 568 assert_equal spent_hours_before + 2.5, issue.spent_hours
569 569 end
570 570
571 571 def test_post_edit_with_attachment_only
572 572 set_tmp_attachments_directory
573 573
574 574 # Delete all fixtured journals, a race condition can occur causing the wrong
575 575 # journal to get fetched in the next find.
576 576 Journal.delete_all
577 577
578 578 # anonymous user
579 579 post :edit,
580 580 :id => 1,
581 581 :notes => '',
582 582 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
583 583 assert_redirected_to 'issues/show/1'
584 584 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
585 585 assert j.notes.blank?
586 586 assert_equal 1, j.details.size
587 587 assert_equal 'testfile.txt', j.details.first.value
588 588 assert_equal User.anonymous, j.user
589 589
590 590 mail = ActionMailer::Base.deliveries.last
591 591 assert mail.body.include?('testfile.txt')
592 592 end
593 593
594 594 def test_post_edit_with_no_change
595 595 issue = Issue.find(1)
596 596 issue.journals.clear
597 597 ActionMailer::Base.deliveries.clear
598 598
599 599 post :edit,
600 600 :id => 1,
601 601 :notes => ''
602 602 assert_redirected_to 'issues/show/1'
603 603
604 604 issue.reload
605 605 assert issue.journals.empty?
606 606 # No email should be sent
607 607 assert ActionMailer::Base.deliveries.empty?
608 608 end
609 609
610 610 def test_post_edit_with_invalid_spent_time
611 611 @request.session[:user_id] = 2
612 612 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
613 613
614 614 assert_no_difference('Journal.count') do
615 615 post :edit,
616 616 :id => 1,
617 617 :notes => notes,
618 618 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
619 619 end
620 620 assert_response :success
621 621 assert_template 'edit'
622 622
623 623 assert_tag :textarea, :attributes => { :name => 'notes' },
624 624 :content => notes
625 625 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
626 626 end
627 627
628 628 def test_bulk_edit
629 629 @request.session[:user_id] = 2
630 630 # update issues priority
631 631 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
632 632 assert_response 302
633 633 # check that the issues were updated
634 634 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
635 635 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
636 636 end
637 637
638 638 def test_bulk_unassign
639 639 assert_not_nil Issue.find(2).assigned_to
640 640 @request.session[:user_id] = 2
641 641 # unassign issues
642 642 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
643 643 assert_response 302
644 644 # check that the issues were updated
645 645 assert_nil Issue.find(2).assigned_to
646 646 end
647 647
648 648 def test_move_one_issue_to_another_project
649 649 @request.session[:user_id] = 1
650 650 post :move, :id => 1, :new_project_id => 2
651 651 assert_redirected_to 'projects/ecookbook/issues'
652 652 assert_equal 2, Issue.find(1).project_id
653 653 end
654 654
655 655 def test_bulk_move_to_another_project
656 656 @request.session[:user_id] = 1
657 657 post :move, :ids => [1, 2], :new_project_id => 2
658 658 assert_redirected_to 'projects/ecookbook/issues'
659 659 # Issues moved to project 2
660 660 assert_equal 2, Issue.find(1).project_id
661 661 assert_equal 2, Issue.find(2).project_id
662 662 # No tracker change
663 663 assert_equal 1, Issue.find(1).tracker_id
664 664 assert_equal 2, Issue.find(2).tracker_id
665 665 end
666 666
667 667 def test_bulk_move_to_another_tracker
668 668 @request.session[:user_id] = 1
669 669 post :move, :ids => [1, 2], :new_tracker_id => 2
670 670 assert_redirected_to 'projects/ecookbook/issues'
671 671 assert_equal 2, Issue.find(1).tracker_id
672 672 assert_equal 2, Issue.find(2).tracker_id
673 673 end
674 674
675 675 def test_context_menu_one_issue
676 676 @request.session[:user_id] = 2
677 677 get :context_menu, :ids => [1]
678 678 assert_response :success
679 679 assert_template 'context_menu'
680 680 assert_tag :tag => 'a', :content => 'Edit',
681 681 :attributes => { :href => '/issues/edit/1',
682 682 :class => 'icon-edit' }
683 683 assert_tag :tag => 'a', :content => 'Closed',
684 684 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
685 685 :class => '' }
686 686 assert_tag :tag => 'a', :content => 'Immediate',
687 687 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
688 688 :class => '' }
689 689 assert_tag :tag => 'a', :content => 'Dave Lopper',
690 690 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
691 691 :class => '' }
692 692 assert_tag :tag => 'a', :content => 'Copy',
693 693 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
694 694 :class => 'icon-copy' }
695 695 assert_tag :tag => 'a', :content => 'Move',
696 696 :attributes => { :href => '/issues/move?ids%5B%5D=1',
697 697 :class => 'icon-move' }
698 698 assert_tag :tag => 'a', :content => 'Delete',
699 699 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
700 700 :class => 'icon-del' }
701 701 end
702 702
703 703 def test_context_menu_one_issue_by_anonymous
704 704 get :context_menu, :ids => [1]
705 705 assert_response :success
706 706 assert_template 'context_menu'
707 707 assert_tag :tag => 'a', :content => 'Delete',
708 708 :attributes => { :href => '#',
709 709 :class => 'icon-del disabled' }
710 710 end
711 711
712 712 def test_context_menu_multiple_issues_of_same_project
713 713 @request.session[:user_id] = 2
714 714 get :context_menu, :ids => [1, 2]
715 715 assert_response :success
716 716 assert_template 'context_menu'
717 717 assert_tag :tag => 'a', :content => 'Edit',
718 718 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
719 719 :class => 'icon-edit' }
720 720 assert_tag :tag => 'a', :content => 'Immediate',
721 721 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
722 722 :class => '' }
723 723 assert_tag :tag => 'a', :content => 'Dave Lopper',
724 724 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
725 725 :class => '' }
726 726 assert_tag :tag => 'a', :content => 'Move',
727 727 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
728 728 :class => 'icon-move' }
729 729 assert_tag :tag => 'a', :content => 'Delete',
730 730 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
731 731 :class => 'icon-del' }
732 732 end
733 733
734 734 def test_context_menu_multiple_issues_of_different_project
735 735 @request.session[:user_id] = 2
736 736 get :context_menu, :ids => [1, 2, 4]
737 737 assert_response :success
738 738 assert_template 'context_menu'
739 739 assert_tag :tag => 'a', :content => 'Delete',
740 740 :attributes => { :href => '#',
741 741 :class => 'icon-del disabled' }
742 742 end
743 743
744 744 def test_destroy_issue_with_no_time_entries
745 745 assert_nil TimeEntry.find_by_issue_id(2)
746 746 @request.session[:user_id] = 2
747 747 post :destroy, :id => 2
748 748 assert_redirected_to 'projects/ecookbook/issues'
749 749 assert_nil Issue.find_by_id(2)
750 750 end
751 751
752 752 def test_destroy_issues_with_time_entries
753 753 @request.session[:user_id] = 2
754 754 post :destroy, :ids => [1, 3]
755 755 assert_response :success
756 756 assert_template 'destroy'
757 757 assert_not_nil assigns(:hours)
758 758 assert Issue.find_by_id(1) && Issue.find_by_id(3)
759 759 end
760 760
761 761 def test_destroy_issues_and_destroy_time_entries
762 762 @request.session[:user_id] = 2
763 763 post :destroy, :ids => [1, 3], :todo => 'destroy'
764 764 assert_redirected_to 'projects/ecookbook/issues'
765 765 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
766 766 assert_nil TimeEntry.find_by_id([1, 2])
767 767 end
768 768
769 769 def test_destroy_issues_and_assign_time_entries_to_project
770 770 @request.session[:user_id] = 2
771 771 post :destroy, :ids => [1, 3], :todo => 'nullify'
772 772 assert_redirected_to 'projects/ecookbook/issues'
773 773 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
774 774 assert_nil TimeEntry.find(1).issue_id
775 775 assert_nil TimeEntry.find(2).issue_id
776 776 end
777 777
778 778 def test_destroy_issues_and_reassign_time_entries_to_another_issue
779 779 @request.session[:user_id] = 2
780 780 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
781 781 assert_redirected_to 'projects/ecookbook/issues'
782 782 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
783 783 assert_equal 2, TimeEntry.find(1).issue_id
784 784 assert_equal 2, TimeEntry.find(2).issue_id
785 785 end
786 786 end
General Comments 0
You need to be logged in to leave comments. Login now