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