##// END OF EJS Templates
Merged r2797, r2804, r2814, r2820, r2837, r2838 from trunk....
Jean-Philippe Lang -
r2767:3d0fbea9fdfd
parent child
Show More
@@ -0,0 +1,43
1 Return-Path: <jsmith@somenet.foo>
2 Received: from osiris ([127.0.0.1])
3 by OSIRIS
4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
6 From: "John Smith" <jsmith@somenet.foo>
7 To: <redmine@somenet.foo>
8 Subject: New ticket on a given project
9 Date: Sun, 22 Jun 2008 12:28:07 +0200
10 MIME-Version: 1.0
11 Content-Type: text/plain;
12 format=flowed;
13 charset="iso-8859-1";
14 reply-type=original
15 Content-Transfer-Encoding: 7bit
16 X-Priority: 3
17 X-MSMail-Priority: Normal
18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20
21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 platea dictumst.
32
33 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
34 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
35 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
36 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
37 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
38 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
39
40 Project : onlinestore
41 Tracker: Feature request
42 category : Stock management
43 priority: Urgent
@@ -1,108 +1,109
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class NewsController < ApplicationController
19 19 before_filter :find_news, :except => [:new, :index, :preview]
20 20 before_filter :find_project, :only => [:new, :preview]
21 21 before_filter :authorize, :except => [:index, :preview]
22 22 before_filter :find_optional_project, :only => :index
23 23 accept_key_auth :index
24 24
25 25 def index
26 26 @news_pages, @newss = paginate :news,
27 27 :per_page => 10,
28 28 :conditions => (@project ? {:project_id => @project.id} : Project.visible_by(User.current)),
29 29 :include => [:author, :project],
30 30 :order => "#{News.table_name}.created_on DESC"
31 31 respond_to do |format|
32 32 format.html { render :layout => false if request.xhr? }
33 33 format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
34 34 end
35 35 end
36 36
37 37 def show
38 38 @comments = @news.comments
39 39 @comments.reverse! if User.current.wants_comments_in_reverse_order?
40 40 end
41 41
42 42 def new
43 43 @news = News.new(:project => @project, :author => User.current)
44 44 if request.post?
45 45 @news.attributes = params[:news]
46 46 if @news.save
47 47 flash[:notice] = l(:notice_successful_create)
48 48 Mailer.deliver_news_added(@news) if Setting.notified_events.include?('news_added')
49 49 redirect_to :controller => 'news', :action => 'index', :project_id => @project
50 50 end
51 51 end
52 52 end
53 53
54 54 def edit
55 55 if request.post? and @news.update_attributes(params[:news])
56 56 flash[:notice] = l(:notice_successful_update)
57 57 redirect_to :action => 'show', :id => @news
58 58 end
59 59 end
60 60
61 61 def add_comment
62 62 @comment = Comment.new(params[:comment])
63 63 @comment.author = User.current
64 64 if @news.comments << @comment
65 65 flash[:notice] = l(:label_comment_added)
66 66 redirect_to :action => 'show', :id => @news
67 67 else
68 show
68 69 render :action => 'show'
69 70 end
70 71 end
71 72
72 73 def destroy_comment
73 74 @news.comments.find(params[:comment_id]).destroy
74 75 redirect_to :action => 'show', :id => @news
75 76 end
76 77
77 78 def destroy
78 79 @news.destroy
79 80 redirect_to :action => 'index', :project_id => @project
80 81 end
81 82
82 83 def preview
83 84 @text = (params[:news] ? params[:news][:description] : nil)
84 85 render :partial => 'common/preview'
85 86 end
86 87
87 88 private
88 89 def find_news
89 90 @news = News.find(params[:id])
90 91 @project = @news.project
91 92 rescue ActiveRecord::RecordNotFound
92 93 render_404
93 94 end
94 95
95 96 def find_project
96 97 @project = Project.find(params[:project_id])
97 98 rescue ActiveRecord::RecordNotFound
98 99 render_404
99 100 end
100 101
101 102 def find_optional_project
102 103 return true unless params[:project_id]
103 104 @project = Project.find(params[:project_id])
104 105 authorize
105 106 rescue ActiveRecord::RecordNotFound
106 107 render_404
107 108 end
108 109 end
@@ -1,627 +1,627
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'coderay'
19 19 require 'coderay/helpers/file_type'
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include GravatarHelper::PublicMethods
26 26
27 27 extend Forwardable
28 28 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 29
30 30 def current_role
31 31 @current_role ||= User.current.role_for_project(@project)
32 32 end
33 33
34 34 # Return true if user is authorized for controller/action, otherwise false
35 35 def authorize_for(controller, action)
36 36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 37 end
38 38
39 39 # Display a link if user is authorized
40 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 42 end
43 43
44 44 # Display a link to remote if user is authorized
45 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 46 url = options[:url] || {}
47 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 48 end
49 49
50 50 # Display a link to user's account page
51 51 def link_to_user(user, options={})
52 52 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
53 53 end
54 54
55 55 def link_to_issue(issue, options={})
56 56 options[:class] ||= ''
57 57 options[:class] << ' issue'
58 58 options[:class] << ' closed' if issue.closed?
59 59 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
60 60 end
61 61
62 62 # Generates a link to an attachment.
63 63 # Options:
64 64 # * :text - Link text (default to attachment filename)
65 65 # * :download - Force download (default: false)
66 66 def link_to_attachment(attachment, options={})
67 67 text = options.delete(:text) || attachment.filename
68 68 action = options.delete(:download) ? 'download' : 'show'
69 69
70 70 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
71 71 end
72 72
73 73 def toggle_link(name, id, options={})
74 74 onclick = "Element.toggle('#{id}'); "
75 75 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
76 76 onclick << "return false;"
77 77 link_to(name, "#", :onclick => onclick)
78 78 end
79 79
80 80 def image_to_function(name, function, html_options = {})
81 81 html_options.symbolize_keys!
82 82 tag(:input, html_options.merge({
83 83 :type => "image", :src => image_path(name),
84 84 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
85 85 }))
86 86 end
87 87
88 88 def prompt_to_remote(name, text, param, url, html_options = {})
89 89 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
90 90 link_to name, {}, html_options
91 91 end
92 92
93 93 def format_date(date)
94 94 return nil unless date
95 95 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
96 96 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
97 97 date.strftime(@date_format)
98 98 end
99 99
100 100 def format_time(time, include_date = true)
101 101 return nil unless time
102 102 time = time.to_time if time.is_a?(String)
103 103 zone = User.current.time_zone
104 104 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
105 105 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
106 106 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
107 107 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
108 108 end
109 109
110 110 def format_activity_title(text)
111 111 h(truncate_single_line(text, 100))
112 112 end
113 113
114 114 def format_activity_day(date)
115 115 date == Date.today ? l(:label_today).titleize : format_date(date)
116 116 end
117 117
118 118 def format_activity_description(text)
119 119 h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
120 120 end
121 121
122 122 def distance_of_date_in_words(from_date, to_date = 0)
123 123 from_date = from_date.to_date if from_date.respond_to?(:to_date)
124 124 to_date = to_date.to_date if to_date.respond_to?(:to_date)
125 125 distance_in_days = (to_date - from_date).abs
126 126 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
127 127 end
128 128
129 129 def due_date_distance_in_words(date)
130 130 if date
131 131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
132 132 end
133 133 end
134 134
135 135 def render_page_hierarchy(pages, node=nil)
136 136 content = ''
137 137 if pages[node]
138 138 content << "<ul class=\"pages-hierarchy\">\n"
139 139 pages[node].each do |page|
140 140 content << "<li>"
141 141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
142 142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
143 143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
144 144 content << "</li>\n"
145 145 end
146 146 content << "</ul>\n"
147 147 end
148 148 content
149 149 end
150 150
151 151 # Renders flash messages
152 152 def render_flash_messages
153 153 s = ''
154 154 flash.each do |k,v|
155 155 s << content_tag('div', v, :class => "flash #{k}")
156 156 end
157 157 s
158 158 end
159 159
160 160 # Truncates and returns the string as a single line
161 161 def truncate_single_line(string, *args)
162 162 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
163 163 end
164 164
165 165 def html_hours(text)
166 166 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
167 167 end
168 168
169 169 def authoring(created, author, options={})
170 170 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
171 171 link_to(distance_of_time_in_words(Time.now, created),
172 172 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
173 173 :title => format_time(created))
174 174 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
175 175 l(options[:label] || :label_added_time_by, author_tag, time_tag)
176 176 end
177 177
178 178 def l_or_humanize(s, options={})
179 179 k = "#{options[:prefix]}#{s}".to_sym
180 180 l_has_string?(k) ? l(k) : s.to_s.humanize
181 181 end
182 182
183 183 def day_name(day)
184 184 l(:general_day_names).split(',')[day-1]
185 185 end
186 186
187 187 def month_name(month)
188 188 l(:actionview_datehelper_select_month_names).split(',')[month-1]
189 189 end
190 190
191 191 def syntax_highlight(name, content)
192 192 type = CodeRay::FileType[name]
193 193 type ? CodeRay.scan(content, type).html : h(content)
194 194 end
195 195
196 196 def to_path_param(path)
197 197 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
198 198 end
199 199
200 200 def pagination_links_full(paginator, count=nil, options={})
201 201 page_param = options.delete(:page_param) || :page
202 202 url_param = params.dup
203 203 # don't reuse params if filters are present
204 204 url_param.clear if url_param.has_key?(:set_filter)
205 205
206 206 html = ''
207 207 html << link_to_remote(('&#171; ' + l(:label_previous)),
208 208 {:update => 'content',
209 209 :url => url_param.merge(page_param => paginator.current.previous),
210 210 :complete => 'window.scrollTo(0,0)'},
211 211 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
212 212
213 213 html << (pagination_links_each(paginator, options) do |n|
214 214 link_to_remote(n.to_s,
215 215 {:url => {:params => url_param.merge(page_param => n)},
216 216 :update => 'content',
217 217 :complete => 'window.scrollTo(0,0)'},
218 218 {:href => url_for(:params => url_param.merge(page_param => n))})
219 219 end || '')
220 220
221 221 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
222 222 {:update => 'content',
223 223 :url => url_param.merge(page_param => paginator.current.next),
224 224 :complete => 'window.scrollTo(0,0)'},
225 225 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
226 226
227 227 unless count.nil?
228 228 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
229 229 end
230 230
231 231 html
232 232 end
233 233
234 234 def per_page_links(selected=nil)
235 235 url_param = params.dup
236 236 url_param.clear if url_param.has_key?(:set_filter)
237 237
238 238 links = Setting.per_page_options_array.collect do |n|
239 239 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
240 240 {:href => url_for(url_param.merge(:per_page => n))})
241 241 end
242 242 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
243 243 end
244 244
245 245 def breadcrumb(*args)
246 246 elements = args.flatten
247 247 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
248 248 end
249 249
250 250 def html_title(*args)
251 251 if args.empty?
252 252 title = []
253 253 title << @project.name if @project
254 254 title += @html_title if @html_title
255 255 title << Setting.app_title
256 256 title.compact.join(' - ')
257 257 else
258 258 @html_title ||= []
259 259 @html_title += args
260 260 end
261 261 end
262 262
263 263 def accesskey(s)
264 264 Redmine::AccessKeys.key_for s
265 265 end
266 266
267 267 # Formats text according to system settings.
268 268 # 2 ways to call this method:
269 269 # * with a String: textilizable(text, options)
270 270 # * with an object and one of its attribute: textilizable(issue, :description, options)
271 271 def textilizable(*args)
272 272 options = args.last.is_a?(Hash) ? args.pop : {}
273 273 case args.size
274 274 when 1
275 275 obj = options[:object]
276 276 text = args.shift
277 277 when 2
278 278 obj = args.shift
279 279 text = obj.send(args.shift).to_s
280 280 else
281 281 raise ArgumentError, 'invalid arguments to textilizable'
282 282 end
283 283 return '' if text.blank?
284 284
285 285 only_path = options.delete(:only_path) == false ? false : true
286 286
287 287 # when using an image link, try to use an attachment, if possible
288 288 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
289 289
290 290 if attachments
291 291 attachments = attachments.sort_by(&:created_on).reverse
292 292 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
293 293 style = $1
294 294 filename = $6.downcase
295 295 # search for the picture in attachments
296 296 if found = attachments.detect { |att| att.filename.downcase == filename }
297 297 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
298 298 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
299 299 alt = desc.blank? ? nil : "(#{desc})"
300 300 "!#{style}#{image_url}#{alt}!"
301 301 else
302 302 m
303 303 end
304 304 end
305 305 end
306 306
307 307 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
308 308
309 309 # different methods for formatting wiki links
310 310 case options[:wiki_links]
311 311 when :local
312 312 # used for local links to html files
313 313 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
314 314 when :anchor
315 315 # used for single-file wiki export
316 316 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
317 317 else
318 318 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
319 319 end
320 320
321 321 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
322 322
323 323 # Wiki links
324 324 #
325 325 # Examples:
326 326 # [[mypage]]
327 327 # [[mypage|mytext]]
328 328 # wiki links can refer other project wikis, using project name or identifier:
329 329 # [[project:]] -> wiki starting page
330 330 # [[project:|mytext]]
331 331 # [[project:mypage]]
332 332 # [[project:mypage|mytext]]
333 333 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
334 334 link_project = project
335 335 esc, all, page, title = $1, $2, $3, $5
336 336 if esc.nil?
337 337 if page =~ /^([^\:]+)\:(.*)$/
338 338 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
339 339 page = $2
340 340 title ||= $1 if page.blank?
341 341 end
342 342
343 343 if link_project && link_project.wiki
344 344 # extract anchor
345 345 anchor = nil
346 346 if page =~ /^(.+?)\#(.+)$/
347 347 page, anchor = $1, $2
348 348 end
349 349 # check if page exists
350 350 wiki_page = link_project.wiki.find_page(page)
351 351 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
352 352 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
353 353 else
354 354 # project or wiki doesn't exist
355 355 title || page
356 356 end
357 357 else
358 358 all
359 359 end
360 360 end
361 361
362 362 # Redmine links
363 363 #
364 364 # Examples:
365 365 # Issues:
366 366 # #52 -> Link to issue #52
367 367 # Changesets:
368 368 # r52 -> Link to revision 52
369 369 # commit:a85130f -> Link to scmid starting with a85130f
370 370 # Documents:
371 371 # document#17 -> Link to document with id 17
372 372 # document:Greetings -> Link to the document with title "Greetings"
373 373 # document:"Some document" -> Link to the document with title "Some document"
374 374 # Versions:
375 375 # version#3 -> Link to version with id 3
376 376 # version:1.0.0 -> Link to version named "1.0.0"
377 377 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
378 378 # Attachments:
379 379 # attachment:file.zip -> Link to the attachment of the current object named file.zip
380 380 # Source files:
381 381 # source:some/file -> Link to the file located at /some/file in the project's repository
382 382 # source:some/file@52 -> Link to the file's revision 52
383 383 # source:some/file#L120 -> Link to line 120 of the file
384 384 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
385 385 # export:some/file -> Force the download of the file
386 386 # Forum messages:
387 387 # message#1218 -> Link to message with id 1218
388 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 389 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
390 390 link = nil
391 391 if esc.nil?
392 392 if prefix.nil? && sep == 'r'
393 393 if project && (changeset = project.changesets.find_by_revision(oid))
394 394 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
395 395 :class => 'changeset',
396 396 :title => truncate_single_line(changeset.comments, 100))
397 397 end
398 398 elsif sep == '#'
399 399 oid = oid.to_i
400 400 case prefix
401 401 when nil
402 402 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
403 403 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
404 404 :class => (issue.closed? ? 'issue closed' : 'issue'),
405 405 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
406 406 link = content_tag('del', link) if issue.closed?
407 407 end
408 408 when 'document'
409 409 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
410 410 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
411 411 :class => 'document'
412 412 end
413 413 when 'version'
414 414 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
415 415 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
416 416 :class => 'version'
417 417 end
418 418 when 'message'
419 419 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
420 420 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
421 421 :controller => 'messages',
422 422 :action => 'show',
423 423 :board_id => message.board,
424 424 :id => message.root,
425 425 :anchor => (message.parent ? "message-#{message.id}" : nil)},
426 426 :class => 'message'
427 427 end
428 428 end
429 429 elsif sep == ':'
430 430 # removes the double quotes if any
431 431 name = oid.gsub(%r{^"(.*)"$}, "\\1")
432 432 case prefix
433 433 when 'document'
434 434 if project && document = project.documents.find_by_title(name)
435 435 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
436 436 :class => 'document'
437 437 end
438 438 when 'version'
439 439 if project && version = project.versions.find_by_name(name)
440 440 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
441 441 :class => 'version'
442 442 end
443 443 when 'commit'
444 444 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
445 445 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
446 446 :class => 'changeset',
447 447 :title => truncate_single_line(changeset.comments, 100)
448 448 end
449 449 when 'source', 'export'
450 450 if project && project.repository
451 451 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
452 452 path, rev, anchor = $1, $3, $5
453 453 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
454 454 :path => to_path_param(path),
455 455 :rev => rev,
456 456 :anchor => anchor,
457 457 :format => (prefix == 'export' ? 'raw' : nil)},
458 458 :class => (prefix == 'export' ? 'source download' : 'source')
459 459 end
460 460 when 'attachment'
461 461 if attachments && attachment = attachments.detect {|a| a.filename == name }
462 462 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
463 463 :class => 'attachment'
464 464 end
465 465 end
466 466 end
467 467 end
468 468 leading + (link || "#{prefix}#{sep}#{oid}")
469 469 end
470 470
471 471 text
472 472 end
473 473
474 474 # Same as Rails' simple_format helper without using paragraphs
475 475 def simple_format_without_paragraph(text)
476 476 text.to_s.
477 477 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
478 478 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
479 479 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
480 480 end
481 481
482 482 def error_messages_for(object_name, options = {})
483 483 options = options.symbolize_keys
484 484 object = instance_variable_get("@#{object_name}")
485 485 if object && !object.errors.empty?
486 486 # build full_messages here with controller current language
487 487 full_messages = []
488 488 object.errors.each do |attr, msg|
489 489 next if msg.nil?
490 490 msg = [msg] unless msg.is_a?(Array)
491 491 if attr == "base"
492 492 full_messages << l(*msg)
493 493 else
494 494 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(*msg) unless attr == "custom_values"
495 495 end
496 496 end
497 497 # retrieve custom values error messages
498 498 if object.errors[:custom_values]
499 499 object.custom_values.each do |v|
500 500 v.errors.each do |attr, msg|
501 501 next if msg.nil?
502 502 msg = [msg] unless msg.is_a?(Array)
503 503 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(*msg)
504 504 end
505 505 end
506 506 end
507 507 content_tag("div",
508 508 content_tag(
509 509 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
510 510 ) +
511 511 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
512 512 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
513 513 )
514 514 else
515 515 ""
516 516 end
517 517 end
518 518
519 519 def lang_options_for_select(blank=true)
520 520 (blank ? [["(auto)", ""]] : []) +
521 521 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
522 522 end
523 523
524 524 def label_tag_for(name, option_tags = nil, options = {})
525 525 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
526 526 content_tag("label", label_text)
527 527 end
528 528
529 529 def labelled_tabular_form_for(name, object, options, &proc)
530 530 options[:html] ||= {}
531 531 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
532 532 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
533 533 end
534 534
535 535 def back_url_hidden_field_tag
536 536 back_url = params[:back_url] || request.env['HTTP_REFERER']
537 537 back_url = CGI.unescape(back_url.to_s)
538 538 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
539 539 end
540 540
541 541 def check_all_links(form_name)
542 542 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
543 543 " | " +
544 544 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
545 545 end
546 546
547 547 def progress_bar(pcts, options={})
548 548 pcts = [pcts, pcts] unless pcts.is_a?(Array)
549 549 pcts[1] = pcts[1] - pcts[0]
550 550 pcts << (100 - pcts[1] - pcts[0])
551 551 width = options[:width] || '100px;'
552 552 legend = options[:legend] || ''
553 553 content_tag('table',
554 554 content_tag('tr',
555 555 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
556 556 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
557 557 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
558 558 ), :class => 'progress', :style => "width: #{width};") +
559 559 content_tag('p', legend, :class => 'pourcent')
560 560 end
561 561
562 562 def context_menu_link(name, url, options={})
563 563 options[:class] ||= ''
564 564 if options.delete(:selected)
565 565 options[:class] << ' icon-checked disabled'
566 566 options[:disabled] = true
567 567 end
568 568 if options.delete(:disabled)
569 569 options.delete(:method)
570 570 options.delete(:confirm)
571 571 options.delete(:onclick)
572 572 options[:class] << ' disabled'
573 573 url = '#'
574 574 end
575 575 link_to name, url, options
576 576 end
577 577
578 578 def calendar_for(field_id)
579 579 include_calendar_headers_tags
580 580 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
581 581 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
582 582 end
583 583
584 584 def include_calendar_headers_tags
585 585 unless @calendar_headers_tags_included
586 586 @calendar_headers_tags_included = true
587 587 content_for :header_tags do
588 588 javascript_include_tag('calendar/calendar') +
589 589 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
590 590 javascript_include_tag('calendar/calendar-setup') +
591 591 stylesheet_link_tag('calendar')
592 592 end
593 593 end
594 594 end
595 595
596 596 def content_for(name, content = nil, &block)
597 597 @has_content ||= {}
598 598 @has_content[name] = true
599 599 super(name, content, &block)
600 600 end
601 601
602 602 def has_content?(name)
603 603 (@has_content && @has_content[name]) || false
604 604 end
605 605
606 606 # Returns the avatar image tag for the given +user+ if avatars are enabled
607 607 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
608 608 def avatar(user, options = { })
609 609 if Setting.gravatar_enabled?
610 610 email = nil
611 611 if user.respond_to?(:mail)
612 612 email = user.mail
613 613 elsif user.to_s =~ %r{<(.+?)>}
614 614 email = $1
615 615 end
616 616 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
617 617 end
618 618 end
619 619
620 620 private
621 621
622 622 def wiki_helper
623 623 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
624 624 extend helper
625 625 return self
626 626 end
627 627 end
@@ -1,200 +1,200
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MailHandler < ActionMailer::Base
19 19 include ActionView::Helpers::SanitizeHelper
20 20
21 21 class UnauthorizedAction < StandardError; end
22 22 class MissingInformation < StandardError; end
23 23
24 24 attr_reader :email, :user
25 25
26 26 def self.receive(email, options={})
27 27 @@handler_options = options.dup
28 28
29 29 @@handler_options[:issue] ||= {}
30 30
31 31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 32 @@handler_options[:allow_override] ||= []
33 33 # Project needs to be overridable if not specified
34 34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 35 # Status overridable by default
36 36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 37 super email
38 38 end
39 39
40 40 # Processes incoming emails
41 41 def receive(email)
42 42 @email = email
43 43 @user = User.active.find(:first, :conditions => ["LOWER(mail) = ?", email.from.to_a.first.to_s.strip.downcase])
44 44 unless @user
45 45 # Unknown user => the email is ignored
46 46 # TODO: ability to create the user's account
47 47 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
48 48 return false
49 49 end
50 50 User.current = @user
51 51 dispatch
52 52 end
53 53
54 54 private
55 55
56 56 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
57 57
58 58 def dispatch
59 59 if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
60 60 receive_issue_update(m[1].to_i)
61 61 else
62 62 receive_issue
63 63 end
64 64 rescue ActiveRecord::RecordInvalid => e
65 65 # TODO: send a email to the user
66 66 logger.error e.message if logger
67 67 false
68 68 rescue MissingInformation => e
69 69 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
70 70 false
71 71 rescue UnauthorizedAction => e
72 72 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
73 73 false
74 74 end
75 75
76 76 # Creates a new issue
77 77 def receive_issue
78 78 project = target_project
79 79 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
80 80 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
81 81 priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
82 82 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
83 83
84 84 # check permission
85 85 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
86 86 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
87 87 # check workflow
88 88 if status && issue.new_statuses_allowed_to(user).include?(status)
89 89 issue.status = status
90 90 end
91 91 issue.subject = email.subject.chomp.toutf8
92 92 issue.description = plain_text_body
93 93 # custom fields
94 94 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
95 95 if value = get_keyword(c.name, :override => true)
96 96 h[c.id] = value
97 97 end
98 98 h
99 99 end
100 100 issue.save!
101 101 add_attachments(issue)
102 102 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
103 103 # add To and Cc as watchers
104 104 add_watchers(issue)
105 105 # send notification after adding watchers so that they can reply to Redmine
106 106 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
107 107 issue
108 108 end
109 109
110 110 def target_project
111 111 # TODO: other ways to specify project:
112 112 # * parse the email To field
113 113 # * specific project (eg. Setting.mail_handler_target_project)
114 114 target = Project.find_by_identifier(get_keyword(:project))
115 115 raise MissingInformation.new('Unable to determine target project') if target.nil?
116 116 target
117 117 end
118 118
119 119 # Adds a note to an existing issue
120 120 def receive_issue_update(issue_id)
121 121 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
122 122
123 123 issue = Issue.find_by_id(issue_id)
124 124 return unless issue
125 125 # check permission
126 126 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
127 127 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
128 128
129 129 # add the note
130 130 journal = issue.init_journal(user, plain_text_body)
131 131 add_attachments(issue)
132 132 # check workflow
133 133 if status && issue.new_statuses_allowed_to(user).include?(status)
134 134 issue.status = status
135 135 end
136 136 issue.save!
137 137 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
138 138 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
139 139 journal
140 140 end
141 141
142 142 def add_attachments(obj)
143 143 if email.has_attachments?
144 144 email.attachments.each do |attachment|
145 145 Attachment.create(:container => obj,
146 146 :file => attachment,
147 147 :author => user,
148 148 :content_type => attachment.content_type)
149 149 end
150 150 end
151 151 end
152 152
153 153 # Adds To and Cc as watchers of the given object if the sender has the
154 154 # appropriate permission
155 155 def add_watchers(obj)
156 156 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
157 157 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
158 158 unless addresses.empty?
159 159 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
160 160 watchers.each {|w| obj.add_watcher(w)}
161 161 end
162 162 end
163 163 end
164 164
165 165 def get_keyword(attr, options={})
166 166 @keywords ||= {}
167 167 if @keywords.has_key?(attr)
168 168 @keywords[attr]
169 169 else
170 170 @keywords[attr] = begin
171 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}:[ \t]*(.+)\s*$/i, '')
171 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr}[ \t]*:[ \t]*(.+)\s*$/i, '')
172 172 $1.strip
173 173 elsif !@@handler_options[:issue][attr].blank?
174 174 @@handler_options[:issue][attr]
175 175 end
176 176 end
177 177 end
178 178 end
179 179
180 180 # Returns the text/plain part of the email
181 181 # If not found (eg. HTML-only email), returns the body with tags removed
182 182 def plain_text_body
183 183 return @plain_text_body unless @plain_text_body.nil?
184 184 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
185 185 if parts.empty?
186 186 parts << @email
187 187 end
188 188 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
189 189 if plain_text_part.nil?
190 190 # no text/plain part found, assuming html-only email
191 191 # strip html tags and remove doctype directive
192 192 @plain_text_body = strip_tags(@email.body.to_s)
193 193 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
194 194 else
195 195 @plain_text_body = plain_text_part.body.to_s
196 196 end
197 197 @plain_text_body.strip!
198 198 @plain_text_body
199 199 end
200 200 end
@@ -1,9 +1,9
1 1 <p><%= l(:mail_body_reminder, @issues.size, @days) %></p>
2 2
3 3 <ul>
4 4 <% @issues.each do |issue| -%>
5 <li><%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %></li>
5 <li><%=h issue.project %> - <%=link_to("#{issue.tracker} ##{issue.id}", :controller => 'issues', :action => 'show', :id => issue, :only_path => false)%>: <%=h issue.subject %></li>
6 6 <% end -%>
7 7 </ul>
8 8
9 9 <p><%= link_to l(:label_issue_view_all), @issues_url %></p>
@@ -1,30 +1,30
1 1 <h2><%= l(:label_index_by_date) %></h2>
2 2
3 3 <% if @pages.empty? %>
4 4 <p class="nodata"><%= l(:label_no_data) %></p>
5 5 <% end %>
6 6
7 7 <% @pages_by_date.keys.sort.reverse.each do |date| %>
8 8 <h3><%= format_date(date) %></h3>
9 9 <ul>
10 10 <% @pages_by_date[date].each do |page| %>
11 11 <li><%= link_to page.pretty_title, :action => 'index', :page => page.title %></li>
12 12 <% end %>
13 13 </ul>
14 14 <% end %>
15 15
16 16 <% content_for :sidebar do %>
17 17 <%= render :partial => 'sidebar' %>
18 18 <% end %>
19 19
20 20 <% unless @pages.empty? %>
21 21 <p class="other-formats">
22 22 <%= l(:label_export_to) %>
23 <span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
23 <span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
24 24 <span><%= link_to 'HTML', {:action => 'special', :page => 'export'}, :class => 'html' %></span>
25 25 </p>
26 26 <% end %>
27 27
28 28 <% content_for :header_tags do %>
29 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %>
29 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :format => 'atom', :key => User.current.rss_key) %>
30 30 <% end %>
@@ -1,23 +1,23
1 1 <h2><%= l(:label_index_by_title) %></h2>
2 2
3 3 <% if @pages.empty? %>
4 4 <p class="nodata"><%= l(:label_no_data) %></p>
5 5 <% end %>
6 6
7 7 <%= render_page_hierarchy(@pages_by_parent_id) %>
8 8
9 9 <% content_for :sidebar do %>
10 10 <%= render :partial => 'sidebar' %>
11 11 <% end %>
12 12
13 13 <% unless @pages.empty? %>
14 14 <p class="other-formats">
15 15 <%= l(:label_export_to) %>
16 <span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
16 <span><%= link_to 'Atom', {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
17 17 <span><%= link_to 'HTML', {:action => 'special', :page => 'export'} %></span>
18 18 </p>
19 19 <% end %>
20 20
21 21 <% content_for :header_tags do %>
22 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %>
22 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_edits => 1, :format => 'atom', :key => User.current.rss_key) %>
23 23 <% end %>
@@ -1,917 +1,922
1 1 == Redmine changelog
2 2
3 3 Redmine - project management software
4 4 Copyright (C) 2006-2009 Jean-Philippe Lang
5 5 http://www.redmine.org/
6 6
7 7
8 8 == 2009-xx-xx v0.8.5
9 9
10 * Incoming mail handler : Allow spaces between keywords and colon
11 * Do not require a non-word character after a comma in Redmine links
12 * Include issue hyperlinks in reminder emails
13 * Fixed: 500 Internal Server Error is raised if add an empty comment to the news
14 * Fixes: Atom links for wiki pages are not correct
10 15 * Fixed: Atom feeds leak email address
11 16
12 17
13 18 == 2009-05-17 v0.8.4
14 19
15 20 * Allow textile mailto links
16 21 * Fixed: memory consumption when uploading file
17 22 * Fixed: Mercurial integration doesn't work if Redmine is installed in folder path containing space
18 23 * Fixed: an error is raised when no tab is available on project settings
19 24 * Fixed: insert image macro corrupts urls with excalamation marks
20 25 * Fixed: error on cross-project gantt PNG export
21 26 * Fixed: self and alternate links in atom feeds do not respect Atom specs
22 27 * Fixed: accept any svn tunnel scheme in repository URL
23 28 * Fixed: issues/show should accept user's rss key
24 29 * Fixed: consistency of custom fields display on the issue detail view
25 30 * Fixed: wiki comments length validation is missing
26 31 * Fixed: weak autologin token generation algorithm causes duplicate tokens
27 32
28 33
29 34 == 2009-04-05 v0.8.3
30 35
31 36 * Separate project field and subject in cross-project issue view
32 37 * Ability to set language for redmine:load_default_data task using REDMINE_LANG environment variable
33 38 * Rescue Redmine::DefaultData::DataAlreadyLoaded in redmine:load_default_data task
34 39 * CSS classes to highlight own and assigned issues
35 40 * Hide "New file" link on wiki pages from printing
36 41 * Flush buffer when asking for language in redmine:load_default_data task
37 42 * Minimum project identifier length set to 1
38 43 * Include headers so that emails don't trigger vacation auto-responders
39 44 * Fixed: Time entries csv export links for all projects are malformed
40 45 * Fixed: Files without Version aren't visible in the Activity page
41 46 * Fixed: Commit logs are centered in the repo browser
42 47 * Fixed: News summary field content is not searchable
43 48 * Fixed: Journal#save has a wrong signature
44 49 * Fixed: Email footer signature convention
45 50 * Fixed: Timelog report do not show time for non-versioned issues
46 51
47 52
48 53 == 2009-03-07 v0.8.2
49 54
50 55 * Send an email to the user when an administrator activates a registered user
51 56 * Strip keywords from received email body
52 57 * Footer updated to 2009
53 58 * Show RSS-link even when no issues is found
54 59 * One click filter action in activity view
55 60 * Clickable/linkable line #'s while browsing the repo or viewing a file
56 61 * Links to versions on files list
57 62 * Added request and controller objects to the hooks by default
58 63 * Fixed: exporting an issue with attachments to PDF raises an error
59 64 * Fixed: "too few arguments" error may occur on activerecord error translation
60 65 * Fixed: "Default columns Displayed on the Issues list" setting is not easy to read
61 66 * Fixed: visited links to closed tickets are not striked through with IE6
62 67 * Fixed: MailHandler#plain_text_body returns nil if there was nothing to strip
63 68 * Fixed: MailHandler raises an error when processing an email without From header
64 69
65 70
66 71 == 2009-02-15 v0.8.1
67 72
68 73 * Select watchers on new issue form
69 74 * Issue description is no longer a required field
70 75 * Files module: ability to add files without version
71 76 * Jump to the current tab when using the project quick-jump combo
72 77 * Display a warning if some attachments were not saved
73 78 * Import custom fields values from emails on issue creation
74 79 * Show view/annotate/download links on entry and annotate views
75 80 * Admin Info Screen: Display if plugin assets directory is writable
76 81 * Adds a 'Create and continue' button on the new issue form
77 82 * IMAP: add options to move received emails
78 83 * Do not show Category field when categories are not defined
79 84 * Lower the project identifier limit to a minimum of two characters
80 85 * Add "closed" html class to closed entries in issue list
81 86 * Fixed: broken redirect URL on login failure
82 87 * Fixed: Deleted files are shown when using Darcs
83 88 * Fixed: Darcs adapter works on Win32 only
84 89 * Fixed: syntax highlight doesn't appear in new ticket preview
85 90 * Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets
86 91 * Fixed: no error is raised when entering invalid hours on the issue update form
87 92 * Fixed: Details time log report CSV export doesn't honour date format from settings
88 93 * Fixed: invalid css classes on issue details
89 94 * Fixed: Trac importer creates duplicate custom values
90 95 * Fixed: inline attached image should not match partial filename
91 96
92 97
93 98 == 2008-12-30 v0.8.0
94 99
95 100 * Setting added in order to limit the number of diff lines that should be displayed
96 101 * Makes logged-in username in topbar linking to
97 102 * Mail handler: strip tags when receiving a html-only email
98 103 * Mail handler: add watchers before sending notification
99 104 * Adds a css class (overdue) to overdue issues on issue lists and detail views
100 105 * Fixed: project activity truncated after viewing user's activity
101 106 * Fixed: email address entered for password recovery shouldn't be case-sensitive
102 107 * Fixed: default flag removed when editing a default enumeration
103 108 * Fixed: default category ignored when adding a document
104 109 * Fixed: error on repository user mapping when a repository username is blank
105 110 * Fixed: Firefox cuts off large diffs
106 111 * Fixed: CVS browser should not show dead revisions (deleted files)
107 112 * Fixed: escape double-quotes in image titles
108 113 * Fixed: escape textarea content when editing a issue note
109 114 * Fixed: JS error on context menu with IE
110 115 * Fixed: bold syntax around single character in series doesn't work
111 116 * Fixed several XSS vulnerabilities
112 117 * Fixed a SQL injection vulnerability
113 118
114 119
115 120 == 2008-12-07 v0.8.0-rc1
116 121
117 122 * Wiki page protection
118 123 * Wiki page hierarchy. Parent page can be assigned on the Rename screen
119 124 * Adds support for issue creation via email
120 125 * Adds support for free ticket filtering and custom queries on Gantt chart and calendar
121 126 * Cross-project search
122 127 * Ability to search a project and its subprojects
123 128 * Ability to search the projects the user belongs to
124 129 * Adds custom fields on time entries
125 130 * Adds boolean and list custom fields for time entries as criteria on time report
126 131 * Cross-project time reports
127 132 * Display latest user's activity on account/show view
128 133 * Show last connexion time on user's page
129 134 * Obfuscates email address on user's account page using javascript
130 135 * wiki TOC rendered as an unordered list
131 136 * Adds the ability to search for a user on the administration users list
132 137 * Adds the ability to search for a project name or identifier on the administration projects list
133 138 * Redirect user to the previous page after logging in
134 139 * Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users
135 140 * Adds permissions for viewing the watcher list and adding new watchers on the issue detail view
136 141 * Adds permissions to let users edit and/or delete their messages
137 142 * Link to activity view when displaying dates
138 143 * Hide Redmine version in atom feeds and pdf properties
139 144 * 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.
140 145 * Sort users by their display names so that user dropdown lists are sorted alphabetically
141 146 * Adds estimated hours to issue filters
142 147 * Switch order of current and previous revisions in side-by-side diff
143 148 * Render the commit changes list as a tree
144 149 * Adds watch/unwatch functionality at forum topic level
145 150 * When moving an issue to another project, reassign it to the category with same name if any
146 151 * Adds child_pages macro for wiki pages
147 152 * Use GET instead of POST on roadmap (#718), gantt and calendar forms
148 153 * Search engine: display total results count and count by result type
149 154 * Email delivery configuration moved to an unversioned YAML file (config/email.yml, see the sample file)
150 155 * Adds icons on search results
151 156 * Adds 'Edit' link on account/show for admin users
152 157 * Adds Lock/Unlock/Activate link on user edit screen
153 158 * Adds user count in status drop down on admin user list
154 159 * Adds multi-levels blockquotes support by using > at the beginning of lines
155 160 * Adds a Reply link to each issue note
156 161 * Adds plain text only option for mail notifications
157 162 * Gravatar support for issue detail, user grid, and activity stream (disabled by default)
158 163 * Adds 'Delete wiki pages attachments' permission
159 164 * Show the most recent file when displaying an inline image
160 165 * Makes permission screens localized
161 166 * AuthSource list: display associated users count and disable 'Delete' buton if any
162 167 * Make the 'duplicates of' relation asymmetric
163 168 * Adds username to the password reminder email
164 169 * Adds links to forum messages using message#id syntax
165 170 * Allow same name for custom fields on different object types
166 171 * One-click bulk edition using the issue list context menu within the same project
167 172 * 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.
168 173 * Adds checkboxes toggle links on permissions report
169 174 * Adds Trac-Like anchors on wiki headings
170 175 * Adds support for wiki links with anchor
171 176 * Adds category to the issue context menu
172 177 * Adds a workflow overview screen
173 178 * Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename
174 179 * Dots allowed in custom field name
175 180 * Adds posts quoting functionality
176 181 * Adds an option to generate sequential project identifiers
177 182 * Adds mailto link on the user administration list
178 183 * Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value
179 184 * Gantt chart: display issues that don't have a due date if they are assigned to a version with a date
180 185 * Change projects homepage limit to 255 chars
181 186 * 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
182 187 * Adds "please select" to activity select box if no activity is set as default
183 188 * Do not silently ignore timelog validation failure on issue edit
184 189 * Adds a rake task to send reminder emails
185 190 * Allow empty cells in wiki tables
186 191 * Makes wiki text formatter pluggable
187 192 * Adds back textile acronyms support
188 193 * Remove pre tag attributes
189 194 * Plugin hooks
190 195 * Pluggable admin menu
191 196 * Plugins can provide activity content
192 197 * Moves plugin list to its own administration menu item
193 198 * Adds url and author_url plugin attributes
194 199 * Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version
195 200 * Adds atom feed on time entries details
196 201 * Adds project name to issues feed title
197 202 * Adds a css class on menu items in order to apply item specific styles (eg. icons)
198 203 * Adds a Redmine plugin generators
199 204 * Adds timelog link to the issue context menu
200 205 * Adds links to the user page on various views
201 206 * Turkish translation by Ismail Sezen
202 207 * Catalan translation
203 208 * Vietnamese translation
204 209 * Slovak translation
205 210 * Better naming of activity feed if only one kind of event is displayed
206 211 * Enable syntax highlight on issues, messages and news
207 212 * Add target version to the issue list context menu
208 213 * Hide 'Target version' filter if no version is defined
209 214 * Add filters on cross-project issue list for custom fields marked as 'For all projects'
210 215 * Turn ftp urls into links
211 216 * Hiding the View Differences button when a wiki page's history only has one version
212 217 * Messages on a Board can now be sorted by the number of replies
213 218 * Adds a class ('me') to events of the activity view created by current user
214 219 * Strip pre/code tags content from activity view events
215 220 * Display issue notes in the activity view
216 221 * Adds links to changesets atom feed on repository browser
217 222 * Track project and tracker changes in issue history
218 223 * Adds anchor to atom feed messages links
219 224 * Adds a key in lang files to set the decimal separator (point or comma) in csv exports
220 225 * Makes importer work with Trac 0.8.x
221 226 * Upgraded to Prototype 1.6.0.1
222 227 * File viewer for attached text files
223 228 * Menu mapper: add support for :before, :after and :last options to #push method and add #delete method
224 229 * Removed inconsistent revision numbers on diff view
225 230 * CVS: add support for modules names with spaces
226 231 * Log the user in after registration if account activation is not needed
227 232 * Mercurial adapter improvements
228 233 * Trac importer: read session_attribute table to find user's email and real name
229 234 * Ability to disable unused SCM adapters in application settings
230 235 * Adds Filesystem adapter
231 236 * Clear changesets and changes with raw sql when deleting a repository for performance
232 237 * Redmine.pm now uses the 'commit access' permission defined in Redmine
233 238 * Reposman can create any type of scm (--scm option)
234 239 * Reposman creates a repository if the 'repository' module is enabled at project level only
235 240 * Display svn properties in the browser, svn >= 1.5.0 only
236 241 * Reduces memory usage when importing large git repositories
237 242 * Wider SVG graphs in repository stats
238 243 * SubversionAdapter#entries performance improvement
239 244 * SCM browser: ability to download raw unified diffs
240 245 * More detailed error message in log when scm command fails
241 246 * Adds support for file viewing with Darcs 2.0+
242 247 * Check that git changeset is not in the database before creating it
243 248 * Unified diff viewer for attached files with .patch or .diff extension
244 249 * File size display with Bazaar repositories
245 250 * Git adapter: use commit time instead of author time
246 251 * Prettier url for changesets
247 252 * Makes changes link to entries on the revision view
248 253 * Adds a field on the repository view to browse at specific revision
249 254 * Adds new projects atom feed
250 255 * Added rake tasks to generate rcov code coverage reports
251 256 * Add Redcloth's :block_markdown_rule to allow horizontal rules in wiki
252 257 * Show the project hierarchy in the drop down list for new membership on user administration screen
253 258 * Split user edit screen into tabs
254 259 * Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead
255 260 * Fixed: Roadmap crashes when a version has a due date > 2037
256 261 * Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen
257 262 * Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory
258 263 * Fixed: logtime entry duplicated when edited from parent project
259 264 * Fixed: wrong digest for text files under Windows
260 265 * Fixed: associated revisions are displayed in wrong order on issue view
261 266 * Fixed: Git Adapter date parsing ignores timezone
262 267 * Fixed: Printing long roadmap doesn't split across pages
263 268 * Fixes custom fields display order at several places
264 269 * Fixed: urls containing @ are parsed as email adress by the wiki formatter
265 270 * Fixed date filters accuracy with SQLite
266 271 * Fixed: tokens not escaped in highlight_tokens regexp
267 272 * Fixed Bazaar shared repository browsing
268 273 * Fixes platform determination under JRuby
269 274 * Fixed: Estimated time in issue's journal should be rounded to two decimals
270 275 * Fixed: 'search titles only' box ignored after one search is done on titles only
271 276 * Fixed: non-ASCII subversion path can't be displayed
272 277 * Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format
273 278 * Fixed: document listing shows on "my page" when viewing documents is disabled for the role
274 279 * Fixed: Latest news appear on the homepage for projects with the News module disabled
275 280 * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled
276 281 * Fixed: the default status is lost when reordering issue statuses
277 282 * Fixes error with Postgresql and non-UTF8 commit logs
278 283 * Fixed: textile footnotes no longer work
279 284 * Fixed: http links containing parentheses fail to reder correctly
280 285 * Fixed: GitAdapter#get_rev should use current branch instead of hardwiring master
281 286
282 287
283 288 == 2008-07-06 v0.7.3
284 289
285 290 * Allow dot in firstnames and lastnames
286 291 * Add project name to cross-project Atom feeds
287 292 * Encoding set to utf8 in example database.yml
288 293 * HTML titles on forums related views
289 294 * Fixed: various XSS vulnerabilities
290 295 * Fixed: Entourage (and some old client) fails to correctly render notification styles
291 296 * Fixed: Fixed: timelog redirects inappropriately when :back_url is blank
292 297 * Fixed: wrong relative paths to images in wiki_syntax.html
293 298
294 299
295 300 == 2008-06-15 v0.7.2
296 301
297 302 * "New Project" link on Projects page
298 303 * Links to repository directories on the repo browser
299 304 * Move status to front in Activity View
300 305 * Remove edit step from Status context menu
301 306 * Fixed: No way to do textile horizontal rule
302 307 * Fixed: Repository: View differences doesn't work
303 308 * Fixed: attachement's name maybe invalid.
304 309 * Fixed: Error when creating a new issue
305 310 * Fixed: NoMethodError on @available_filters.has_key?
306 311 * Fixed: Check All / Uncheck All in Email Settings
307 312 * Fixed: "View differences" of one file at /repositories/revision/ fails
308 313 * Fixed: Column width in "my page"
309 314 * Fixed: private subprojects are listed on Issues view
310 315 * Fixed: Textile: bold, italics, underline, etc... not working after parentheses
311 316 * Fixed: Update issue form: comment field from log time end out of screen
312 317 * Fixed: Editing role: "issue can be assigned to this role" out of box
313 318 * Fixed: Unable use angular braces after include word
314 319 * Fixed: Using '*' as keyword for repository referencing keywords doesn't work
315 320 * Fixed: Subversion repository "View differences" on each file rise ERROR
316 321 * Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root
317 322 * Fixed: It is possible to lock out the last admin account
318 323 * Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access
319 324 * Fixed: Issue number display clipped on 'my issues'
320 325 * Fixed: Roadmap version list links not carrying state
321 326 * Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default
322 327 * Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master"
323 328 * Fixed: browser's language subcodes ignored
324 329 * Fixed: Error on project selection with numeric (only) identifier.
325 330 * Fixed: Link to PDF doesn't work after creating new issue
326 331 * Fixed: "Replies" should not be shown on forum threads that are locked
327 332 * Fixed: SVN errors lead to svn username/password being displayed to end users (security issue)
328 333 * Fixed: http links containing hashes don't display correct
329 334 * Fixed: Allow ampersands in Enumeration names
330 335 * Fixed: Atom link on saved query does not include query_id
331 336 * Fixed: Logtime info lost when there's an error updating an issue
332 337 * Fixed: TOC does not parse colorization markups
333 338 * Fixed: CVS: add support for modules names with spaces
334 339 * Fixed: Bad rendering on projects/add
335 340 * Fixed: exception when viewing differences on cvs
336 341 * Fixed: export issue to pdf will messup when use Chinese language
337 342 * Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant
338 343 * Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE
339 344 * Fixed: Importing from trac : some wiki links are messed
340 345 * Fixed: Incorrect weekend definition in Hebrew calendar locale
341 346 * Fixed: Atom feeds don't provide author section for repository revisions
342 347 * Fixed: In Activity views, changesets titles can be multiline while they should not
343 348 * Fixed: Ignore unreadable subversion directories (read disabled using authz)
344 349 * Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets
345 350 * Fixed: Close statement handler in Redmine.pm
346 351
347 352
348 353 == 2008-05-04 v0.7.1
349 354
350 355 * Thai translation added (Gampol Thitinilnithi)
351 356 * Translations updates
352 357 * Escape HTML comment tags
353 358 * Prevent "can't convert nil into String" error when :sort_order param is not present
354 359 * Fixed: Updating tickets add a time log with zero hours
355 360 * Fixed: private subprojects names are revealed on the project overview
356 361 * Fixed: Search for target version of "none" fails with postgres 8.3
357 362 * Fixed: Home, Logout, Login links shouldn't be absolute links
358 363 * Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects
359 364 * Fixed: error when using upcase language name in coderay
360 365 * Fixed: error on Trac import when :due attribute is nil
361 366
362 367
363 368 == 2008-04-28 v0.7.0
364 369
365 370 * Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
366 371 * Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list.
367 372 * Add predefined date ranges to the time report
368 373 * Time report can be done at issue level
369 374 * Various timelog report enhancements
370 375 * Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30
371 376 * Display the context menu above and/or to the left of the click if needed
372 377 * Make the admin project files list sortable
373 378 * Mercurial: display working directory files sizes unless browsing a specific revision
374 379 * Preserve status filter and page number when using lock/unlock/activate links on the users list
375 380 * Redmine.pm support for LDAP authentication
376 381 * Better error message and AR errors in log for failed LDAP on-the-fly user creation
377 382 * Redirected user to where he is coming from after logging hours
378 383 * Warn user that subprojects are also deleted when deleting a project
379 384 * Include subprojects versions on calendar and gantt
380 385 * Notify project members when a message is posted if they want to receive notifications
381 386 * Fixed: Feed content limit setting has no effect
382 387 * Fixed: Priorities not ordered when displayed as a filter in issue list
383 388 * Fixed: can not display attached images inline in message replies
384 389 * Fixed: Boards are not deleted when project is deleted
385 390 * Fixed: trying to preview a new issue raises an exception with postgresql
386 391 * Fixed: single file 'View difference' links do not work because of duplicate slashes in url
387 392 * Fixed: inline image not displayed when including a wiki page
388 393 * Fixed: CVS duplicate key violation
389 394 * Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues
390 395 * Fixed: custom field filters behaviour
391 396 * Fixed: Postgresql 8.3 compatibility
392 397 * Fixed: Links to repository directories don't work
393 398
394 399
395 400 == 2008-03-29 v0.7.0-rc1
396 401
397 402 * Overall activity view and feed added, link is available on the project list
398 403 * Git VCS support
399 404 * Rails 2.0 sessions cookie store compatibility
400 405 * Use project identifiers in urls instead of ids
401 406 * Default configuration data can now be loaded from the administration screen
402 407 * Administration settings screen split to tabs (email notifications options moved to 'Settings')
403 408 * Project description is now unlimited and optional
404 409 * Wiki annotate view
405 410 * Escape HTML tag in textile content
406 411 * Add Redmine links to documents, versions, attachments and repository files
407 412 * 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:
408 413 * by using checkbox and/or the little pencil that will select/unselect all issues
409 414 * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues
410 415 * 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)
411 416 * User display format is now configurable in administration settings
412 417 * Issue list now supports bulk edit/move/delete (for a set of issues that belong to the same project)
413 418 * Merged 'change status', 'edit issue' and 'add note' actions:
414 419 * Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status
415 420 * '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
416 421 * Details by assignees on issue summary view
417 422 * '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
418 423 * Change status select box default to current status
419 424 * Preview for issue notes, news and messages
420 425 * Optional description for attachments
421 426 * 'Fixed version' label changed to 'Target version'
422 427 * Let the user choose when deleting issues with reported hours to:
423 428 * delete the hours
424 429 * assign the hours to the project
425 430 * reassign the hours to another issue
426 431 * Date range filter and pagination on time entries detail view
427 432 * Propagate time tracking to the parent project
428 433 * Switch added on the project activity view to include subprojects
429 434 * Display total estimated and spent hours on the version detail view
430 435 * Weekly time tracking block for 'My page'
431 436 * Permissions to edit time entries
432 437 * Include subprojects on the issue list, calendar, gantt and timelog by default (can be turned off is administration settings)
433 438 * Roadmap enhancements (separate related issues from wiki contents, leading h1 in version wiki pages is hidden, smaller wiki headings)
434 439 * Make versions with same date sorted by name
435 440 * Allow issue list to be sorted by target version
436 441 * Related changesets messages displayed on the issue details view
437 442 * Create a journal and send an email when an issue is closed by commit
438 443 * Add 'Author' to the available columns for the issue list
439 444 * More appropriate default sort order on sortable columns
440 445 * Add issue subject to the time entries view and issue subject, description and tracker to the csv export
441 446 * Permissions to edit issue notes
442 447 * Display date/time instead of date on files list
443 448 * Do not show Roadmap menu item if the project doesn't define any versions
444 449 * Allow longer version names (60 chars)
445 450 * Ability to copy an existing workflow when creating a new role
446 451 * Display custom fields in two columns on the issue form
447 452 * Added 'estimated time' in the csv export of the issue list
448 453 * Display the last 30 days on the activity view rather than the current month (number of days can be configured in the application settings)
449 454 * Setting for whether new projects should be public by default
450 455 * User preference to choose how comments/replies are displayed: in chronological or reverse chronological order
451 456 * Added default value for custom fields
452 457 * Added tabindex property on wiki toolbar buttons (to easily move from field to field using the tab key)
453 458 * Redirect to issue page after creating a new issue
454 459 * Wiki toolbar improvements (mainly for Firefox)
455 460 * Display wiki syntax quick ref link on all wiki textareas
456 461 * Display links to Atom feeds
457 462 * Breadcrumb nav for the forums
458 463 * Show replies when choosing to display messages in the activity
459 464 * Added 'include' macro to include another wiki page
460 465 * RedmineWikiFormatting page available as a static HTML file locally
461 466 * Wrap diff content
462 467 * Strip out email address from authors in repository screens
463 468 * Highlight the current item of the main menu
464 469 * Added simple syntax highlighters for php and java languages
465 470 * Do not show empty diffs
466 471 * Show explicit error message when the scm command failed (eg. when svn binary is not available)
467 472 * Lithuanian translation added (Sergej Jegorov)
468 473 * Ukrainan translation added (Natalia Konovka & Mykhaylo Sorochan)
469 474 * Danish translation added (Mads Vestergaard)
470 475 * Added i18n support to the jstoolbar and various settings screen
471 476 * RedCloth's glyphs no longer user
472 477 * New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/)
473 478 * The following menus can now be extended by plugins: top_menu, account_menu, application_menu
474 479 * Added a simple rake task to fetch changesets from the repositories: rake redmine:fetch_changesets
475 480 * Remove hardcoded "Redmine" strings in account related emails and use application title instead
476 481 * Mantis importer preserve bug ids
477 482 * Trac importer: Trac guide wiki pages skipped
478 483 * Trac importer: wiki attachments migration added
479 484 * Trac importer: support database schema for Trac migration
480 485 * Trac importer: support CamelCase links
481 486 * Removes the Redmine version from the footer (can be viewed on admin -> info)
482 487 * Rescue and display an error message when trying to delete a role that is in use
483 488 * 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
484 489 * Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs
485 490 * Fixed: Gantt and calendar not properly refreshed (fragment caching removed)
486 491 * Fixed: Textile image with style attribute cause internal server error
487 492 * Fixed: wiki TOC not rendered properly when used in an issue or document description
488 493 * Fixed: 'has already been taken' error message on username and email fields if left empty
489 494 * Fixed: non-ascii attachement filename with IE
490 495 * Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed
491 496 * Fixed: search for all words doesn't work
492 497 * Fixed: Do not show sticky and locked checkboxes when replying to a message
493 498 * Fixed: Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank
494 499 * Fixed: Date custom fields not displayed as specified in application settings
495 500 * Fixed: titles not escaped in the activity view
496 501 * Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context
497 502 * 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
498 503 * Fixed: locked users should not receive email notifications
499 504 * Fixed: custom field selection is not saved when unchecking them all on project settings
500 505 * Fixed: can not lock a topic when creating it
501 506 * Fixed: Incorrect filtering for unset values when using 'is not' filter
502 507 * Fixed: PostgreSQL issues_seq_id not updated when using Trac importer
503 508 * Fixed: ajax pagination does not scroll up
504 509 * Fixed: error when uploading a file with no content-type specified by the browser
505 510 * Fixed: wiki and changeset links not displayed when previewing issue description or notes
506 511 * Fixed: 'LdapError: no bind result' error when authenticating
507 512 * Fixed: 'LdapError: invalid binding information' when no username/password are set on the LDAP account
508 513 * Fixed: CVS repository doesn't work if port is used in the url
509 514 * Fixed: Email notifications: host name is missing in generated links
510 515 * Fixed: Email notifications: referenced changesets, wiki pages, attachments... are not turned into links
511 516 * Fixed: Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed
512 517 * Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console
513 518 * Fixed: Do not send an email with no recipient, cc or bcc
514 519 * Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues.
515 520 * Fixed: Mercurial browsing under unix-like os and for directory depth > 2
516 521 * Fixed: Wiki links with pipe can not be used in wiki tables
517 522 * Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets
518 523 * Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql
519 524
520 525
521 526 == 2008-03-12 v0.6.4
522 527
523 528 * Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects
524 529 * Fixed: potential LDAP authentication security flaw
525 530 * Fixed: context submenus on the issue list don't show up with IE6.
526 531 * Fixed: Themes are not applied with Rails 2.0
527 532 * Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil
528 533 * Fixed: Mercurial repository browsing
529 534 * Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails
530 535 * Fixed: not null constraints not removed with Postgresql
531 536 * Doctype set to transitional
532 537
533 538
534 539 == 2007-12-18 v0.6.3
535 540
536 541 * Fixed: upload doesn't work in 'Files' section
537 542
538 543
539 544 == 2007-12-16 v0.6.2
540 545
541 546 * Search engine: issue custom fields can now be searched
542 547 * News comments are now textilized
543 548 * Updated Japanese translation (Satoru Kurashiki)
544 549 * Updated Chinese translation (Shortie Lo)
545 550 * Fixed Rails 2.0 compatibility bugs:
546 551 * Unable to create a wiki
547 552 * Gantt and calendar error
548 553 * Trac importer error (readonly? is defined by ActiveRecord)
549 554 * Fixed: 'assigned to me' filter broken
550 555 * Fixed: crash when validation fails on issue edition with no custom fields
551 556 * Fixed: reposman "can't find group" error
552 557 * Fixed: 'LDAP account password is too long' error when leaving the field empty on creation
553 558 * Fixed: empty lines when displaying repository files with Windows style eol
554 559 * Fixed: missing body closing tag in repository annotate and entry views
555 560
556 561
557 562 == 2007-12-10 v0.6.1
558 563
559 564 * Rails 2.0 compatibility
560 565 * Custom fields can now be displayed as columns on the issue list
561 566 * Added version details view (accessible from the roadmap)
562 567 * Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account)
563 568 * Added per-project tracker selection. Trackers can be selected on project settings
564 569 * Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums
565 570 * Forums: messages can now be edited/deleted (explicit permissions need to be given)
566 571 * Forums: topics can be locked so that no reply can be added
567 572 * Forums: topics can be marked as sticky so that they always appear at the top of the list
568 573 * Forums: attachments can now be added to replies
569 574 * Added time zone support
570 575 * Added a setting to choose the account activation strategy (available in application settings)
571 576 * Added 'Classic' theme (inspired from the v0.51 design)
572 577 * Added an alternate theme which provides issue list colorization based on issues priority
573 578 * Added Bazaar SCM adapter
574 579 * Added Annotate/Blame view in the repository browser (except for Darcs SCM)
575 580 * Diff style (inline or side by side) automatically saved as a user preference
576 581 * Added issues status changes on the activity view (by Cyril Mougel)
577 582 * Added forums topics on the activity view (disabled by default)
578 583 * Added an option on 'My account' for users who don't want to be notified of changes that they make
579 584 * Trac importer now supports mysql and postgresql databases
580 585 * Trac importer improvements (by Mat Trudel)
581 586 * 'fixed version' field can now be displayed on the issue list
582 587 * Added a couple of new formats for the 'date format' setting
583 588 * Added Traditional Chinese translation (by Shortie Lo)
584 589 * Added Russian translation (iGor kMeta)
585 590 * Project name format limitation removed (name can now contain any character)
586 591 * Project identifier maximum length changed from 12 to 20
587 592 * Changed the maximum length of LDAP account to 255 characters
588 593 * Removed the 12 characters limit on passwords
589 594 * Added wiki macros support
590 595 * Performance improvement on workflow setup screen
591 596 * More detailed html title on several views
592 597 * Custom fields can now be reordered
593 598 * Search engine: search can be restricted to an exact phrase by using quotation marks
594 599 * Added custom fields marked as 'For all projects' to the csv export of the cross project issue list
595 600 * Email notifications are now sent as Blind carbon copy by default
596 601 * Fixed: all members (including non active) should be deleted when deleting a project
597 602 * Fixed: Error on wiki syntax link (accessible from wiki/edit)
598 603 * Fixed: 'quick jump to a revision' form on the revisions list
599 604 * Fixed: error on admin/info if there's more than 1 plugin installed
600 605 * Fixed: svn or ldap password can be found in clear text in the html source in editing mode
601 606 * Fixed: 'Assigned to' drop down list is not sorted
602 607 * Fixed: 'View all issues' link doesn't work on issues/show
603 608 * Fixed: error on account/register when validation fails
604 609 * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter'
605 610 * Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt)
606 611 * Fixed: Wrong feed URLs on the home page
607 612 * Fixed: Update of time entry fails when the issue has been moved to an other project
608 613 * Fixed: Error when moving an issue without changing its tracker (Postgresql)
609 614 * Fixed: Changes not recorded when using :pserver string (CVS adapter)
610 615 * Fixed: admin should be able to move issues to any project
611 616 * Fixed: adding an attachment is not possible when changing the status of an issue
612 617 * Fixed: No mime-types in documents/files downloading
613 618 * Fixed: error when sorting the messages if there's only one board for the project
614 619 * Fixed: 'me' doesn't appear in the drop down filters on a project issue list.
615 620
616 621 == 2007-11-04 v0.6.0
617 622
618 623 * Permission model refactoring.
619 624 * Permissions: there are now 2 builtin roles that can be used to specify permissions given to other users than members of projects
620 625 * Permissions: some permissions (eg. browse the repository) can be removed for certain roles
621 626 * Permissions: modules (eg. issue tracking, news, documents...) can be enabled/disabled at project level
622 627 * Added Mantis and Trac importers
623 628 * New application layout
624 629 * Added "Bulk edit" functionality on the issue list
625 630 * More flexible mail notifications settings at user level
626 631 * 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
627 632 * 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
628 633 * Added the ability to customize issue list columns (at application level or for each saved query)
629 634 * Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap
630 635 * Added the ability to rename wiki pages (specific permission required)
631 636 * Search engines now supports pagination. Results are sorted in reverse chronological order
632 637 * Added "Estimated hours" attribute on issues
633 638 * A category with assigned issue can now be deleted. 2 options are proposed: remove assignments or reassign issues to another category
634 639 * Forum notifications are now also sent to the authors of the thread, even if they donοΏ½t watch the board
635 640 * Added an application setting to specify the application protocol (http or https) used to generate urls in emails
636 641 * Gantt chart: now starts at the current month by default
637 642 * Gantt chart: month count and zoom factor are automatically saved as user preferences
638 643 * Wiki links can now refer to other project wikis
639 644 * Added wiki index by date
640 645 * Added preview on add/edit issue form
641 646 * Emails footer can now be customized from the admin interface (Admin -> Email notifications)
642 647 * 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)
643 648 * Calendar: first day of week can now be set in lang files
644 649 * Automatic closing of duplicate issues
645 650 * Added a cross-project issue list
646 651 * AJAXified the SCM browser (tree view)
647 652 * Pretty URL for the repository browser (Cyril Mougel)
648 653 * Search engine: added a checkbox to search titles only
649 654 * Added "% done" in the filter list
650 655 * Enumerations: values can now be reordered and a default value can be specified (eg. default issue priority)
651 656 * Added some accesskeys
652 657 * Added "Float" as a custom field format
653 658 * Added basic Theme support
654 659 * Added the ability to set the οΏ½done ratioοΏ½ of issues fixed by commit (Nikolay Solakov)
655 660 * Added custom fields in issue related mail notifications
656 661 * Email notifications are now sent in plain text and html
657 662 * Gantt chart can now be exported to a graphic file (png). This functionality is only available if RMagick is installed.
658 663 * Added syntax highlightment for repository files and wiki
659 664 * Improved automatic Redmine links
660 665 * Added automatic table of content support on wiki pages
661 666 * Added radio buttons on the documents list to sort documents by category, date, title or author
662 667 * Added basic plugin support, with a sample plugin
663 668 * Added a link to add a new category when creating or editing an issue
664 669 * Added a "Assignable" boolean on the Role model. If unchecked, issues can not be assigned to users having this role.
665 670 * Added an option to be able to relate issues in different projects
666 671 * Added the ability to move issues (to another project) without changing their trackers.
667 672 * Atom feeds added on project activity, news and changesets
668 673 * Added the ability to reset its own RSS access key
669 674 * Main project list now displays root projects with their subprojects
670 675 * Added anchor links to issue notes
671 676 * Added reposman Ruby version. This script can now register created repositories in Redmine (Nicolas Chuche)
672 677 * Issue notes are now included in search
673 678 * Added email sending test functionality
674 679 * Added LDAPS support for LDAP authentication
675 680 * Removed hard-coded URLs in mail templates
676 681 * Subprojects are now grouped by projects in the navigation drop-down menu
677 682 * Added a new value for date filters: this week
678 683 * Added cache for application settings
679 684 * Added Polish translation (Tomasz Gawryl)
680 685 * Added Czech translation (Jan Kadlecek)
681 686 * Added Romanian translation (Csongor Bartus)
682 687 * Added Hebrew translation (Bob Builder)
683 688 * Added Serbian translation (Dragan Matic)
684 689 * Added Korean translation (Choi Jong Yoon)
685 690 * Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations
686 691 * Performance improvement on calendar and gantt
687 692 * Fixed: wiki preview doesnοΏ½t work on long entries
688 693 * Fixed: queries with multiple custom fields return no result
689 694 * Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters
690 695 * Fixed: URL with ~ broken in wiki formatting
691 696 * Fixed: some quotation marks are rendered as strange characters in pdf
692 697
693 698
694 699 == 2007-07-15 v0.5.1
695 700
696 701 * per project forums added
697 702 * added the ability to archive projects
698 703 * added οΏ½WatchοΏ½ functionality on issues. It allows users to receive notifications about issue changes
699 704 * custom fields for issues can now be used as filters on issue list
700 705 * added per user custom queries
701 706 * commit messages are now scanned for referenced or fixed issue IDs (keywords defined in Admin -> Settings)
702 707 * projects list now shows the list of public projects and private projects for which the user is a member
703 708 * versions can now be created with no date
704 709 * added issue count details for versions on Reports view
705 710 * added time report, by member/activity/tracker/version and year/month/week for the selected period
706 711 * each category can now be associated to a user, so that new issues in that category are automatically assigned to that user
707 712 * added autologin feature (disabled by default)
708 713 * optimistic locking added for wiki edits
709 714 * added wiki diff
710 715 * added the ability to destroy wiki pages (requires permission)
711 716 * a wiki page can now be attached to each version, and displayed on the roadmap
712 717 * attachments can now be added to wiki pages (original patch by Pavol Murin) and displayed online
713 718 * added an option to see all versions in the roadmap view (including completed ones)
714 719 * added basic issue relations
715 720 * added the ability to log time when changing an issue status
716 721 * account information can now be sent to the user when creating an account
717 722 * author and assignee of an issue always receive notifications (even if they turned of mail notifications)
718 723 * added a quick search form in page header
719 724 * added 'me' value for 'assigned to' and 'author' query filters
720 725 * added a link on revision screen to see the entire diff for the revision
721 726 * added last commit message for each entry in repository browser
722 727 * added the ability to view a file diff with free to/from revision selection.
723 728 * text files can now be viewed online when browsing the repository
724 729 * added basic support for other SCM: CVS (Ralph Vater), Mercurial and Darcs
725 730 * added fragment caching for svn diffs
726 731 * added fragment caching for calendar and gantt views
727 732 * login field automatically focused on login form
728 733 * subproject name displayed on issue list, calendar and gantt
729 734 * added an option to choose the date format: language based or ISO 8601
730 735 * added a simple mail handler. It lets users add notes to an existing issue by replying to the initial notification email.
731 736 * a 403 error page is now displayed (instead of a blank page) when trying to access a protected page
732 737 * added portuguese translation (Joao Carlos Clementoni)
733 738 * added partial online help japanese translation (Ken Date)
734 739 * added bulgarian translation (Nikolay Solakov)
735 740 * added dutch translation (Linda van den Brink)
736 741 * added swedish translation (Thomas Habets)
737 742 * italian translation update (Alessio Spadaro)
738 743 * japanese translation update (Satoru Kurashiki)
739 744 * fixed: error on history atom feed when thereοΏ½s no notes on an issue change
740 745 * fixed: error in journalizing an issue with longtext custom fields (Postgresql)
741 746 * fixed: creation of Oracle schema
742 747 * fixed: last day of the month not included in project activity
743 748 * fixed: files with an apostrophe in their names can't be accessed in SVN repository
744 749 * fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000)
745 750 * fixed: open/closed issue counts are always 0 on reports view (postgresql)
746 751 * fixed: date query filters (wrong results and sql error with postgresql)
747 752 * fixed: confidentiality issue on account/show (private project names displayed to anyone)
748 753 * fixed: Long text custom fields displayed without line breaks
749 754 * fixed: Error when editing the wokflow after deleting a status
750 755 * fixed: SVN commit dates are now stored as local time
751 756
752 757
753 758 == 2007-04-11 v0.5.0
754 759
755 760 * added per project Wiki
756 761 * added rss/atom feeds at project level (custom queries can be used as feeds)
757 762 * added search engine (search in issues, news, commits, wiki pages, documents)
758 763 * simple time tracking functionality added
759 764 * added version due dates on calendar and gantt
760 765 * added subprojects issue count on project Reports page
761 766 * added the ability to copy an existing workflow when creating a new tracker
762 767 * added the ability to include subprojects on calendar and gantt
763 768 * added the ability to select trackers to display on calendar and gantt (Jeffrey Jones)
764 769 * added side by side svn diff view (Cyril Mougel)
765 770 * added back subproject filter on issue list
766 771 * added permissions report in admin area
767 772 * added a status filter on users list
768 773 * support for password-protected SVN repositories
769 774 * SVN commits are now stored in the database
770 775 * added simple svn statistics SVG graphs
771 776 * progress bars for roadmap versions (Nick Read)
772 777 * issue history now shows file uploads and deletions
773 778 * #id patterns are turned into links to issues in descriptions and commit messages
774 779 * japanese translation added (Satoru Kurashiki)
775 780 * chinese simplified translation added (Andy Wu)
776 781 * italian translation added (Alessio Spadaro)
777 782 * added scripts to manage SVN repositories creation and user access control using ssh+svn (Nicolas Chuche)
778 783 * better calendar rendering time
779 784 * fixed migration scripts to work with mysql 5 running in strict mode
780 785 * fixed: error when clicking "add" with no block selected on my/page_layout
781 786 * fixed: hard coded links in navigation bar
782 787 * fixed: table_name pre/suffix support
783 788
784 789
785 790 == 2007-02-18 v0.4.2
786 791
787 792 * Rails 1.2 is now required
788 793 * settings are now stored in the database and editable through the application in: Admin -> Settings (config_custom.rb is no longer used)
789 794 * added project roadmap view
790 795 * mail notifications added when a document, a file or an attachment is added
791 796 * tooltips added on Gantt chart and calender to view the details of the issues
792 797 * ability to set the sort order for roles, trackers, issue statuses
793 798 * added missing fields to csv export: priority, start date, due date, done ratio
794 799 * added total number of issues per tracker on project overview
795 800 * 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-)
796 801 * added back "fixed version" field on issue screen and in filters
797 802 * project settings screen split in 4 tabs
798 803 * custom fields screen split in 3 tabs (one for each kind of custom field)
799 804 * multiple issues pdf export now rendered as a table
800 805 * added a button on users/list to manually activate an account
801 806 * added a setting option to disable "password lost" functionality
802 807 * added a setting option to set max number of issues in csv/pdf exports
803 808 * fixed: subprojects count is always 0 on projects list
804 809 * fixed: locked users are proposed when adding a member to a project
805 810 * fixed: setting an issue status as default status leads to an sql error with SQLite
806 811 * fixed: unable to delete an issue status even if it's not used yet
807 812 * fixed: filters ignored when exporting a predefined query to csv/pdf
808 813 * fixed: crash when french "issue_edit" email notification is sent
809 814 * fixed: hide mail preference not saved (my/account)
810 815 * fixed: crash when a new user try to edit its "my page" layout
811 816
812 817
813 818 == 2007-01-03 v0.4.1
814 819
815 820 * fixed: emails have no recipient when one of the project members has notifications disabled
816 821
817 822
818 823 == 2007-01-02 v0.4.0
819 824
820 825 * simple SVN browser added (just needs svn binaries in PATH)
821 826 * comments can now be added on news
822 827 * "my page" is now customizable
823 828 * more powerfull and savable filters for issues lists
824 829 * improved issues change history
825 830 * new functionality: move an issue to another project or tracker
826 831 * new functionality: add a note to an issue
827 832 * new report: project activity
828 833 * "start date" and "% done" fields added on issues
829 834 * project calendar added
830 835 * gantt chart added (exportable to pdf)
831 836 * single/multiple issues pdf export added
832 837 * issues reports improvements
833 838 * multiple file upload for issues, documents and files
834 839 * option to set maximum size of uploaded files
835 840 * textile formating of issue and news descritions (RedCloth required)
836 841 * integration of DotClear jstoolbar for textile formatting
837 842 * calendar date picker for date fields (LGPL DHTML Calendar http://sourceforge.net/projects/jscalendar)
838 843 * new filter in issues list: Author
839 844 * ajaxified paginators
840 845 * news rss feed added
841 846 * option to set number of results per page on issues list
842 847 * localized csv separator (comma/semicolon)
843 848 * csv output encoded to ISO-8859-1
844 849 * user custom field displayed on account/show
845 850 * default configuration improved (default roles, trackers, status, permissions and workflows)
846 851 * language for default configuration data can now be chosen when running 'load_default_data' task
847 852 * javascript added on custom field form to show/hide fields according to the format of custom field
848 853 * fixed: custom fields not in csv exports
849 854 * fixed: project settings now displayed according to user's permissions
850 855 * fixed: application error when no version is selected on projects/add_file
851 856 * fixed: public actions not authorized for members of non public projects
852 857 * fixed: non public projects were shown on welcome screen even if current user is not a member
853 858
854 859
855 860 == 2006-10-08 v0.3.0
856 861
857 862 * user authentication against multiple LDAP (optional)
858 863 * token based "lost password" functionality
859 864 * user self-registration functionality (optional)
860 865 * custom fields now available for issues, users and projects
861 866 * new custom field format "text" (displayed as a textarea field)
862 867 * project & administration drop down menus in navigation bar for quicker access
863 868 * text formatting is preserved for long text fields (issues, projects and news descriptions)
864 869 * urls and emails are turned into clickable links in long text fields
865 870 * "due date" field added on issues
866 871 * tracker selection filter added on change log
867 872 * Localization plugin replaced with GLoc 1.1.0 (iconv required)
868 873 * error messages internationalization
869 874 * german translation added (thanks to Karim Trott)
870 875 * data locking for issues to prevent update conflicts (using ActiveRecord builtin optimistic locking)
871 876 * new filter in issues list: "Fixed version"
872 877 * active filters are displayed with colored background on issues list
873 878 * custom configuration is now defined in config/config_custom.rb
874 879 * user object no more stored in session (only user_id)
875 880 * news summary field is no longer required
876 881 * tables and forms redesign
877 882 * Fixed: boolean custom field not working
878 883 * Fixed: error messages for custom fields are not displayed
879 884 * Fixed: invalid custom fields should have a red border
880 885 * Fixed: custom fields values are not validated on issue update
881 886 * Fixed: unable to choose an empty value for 'List' custom fields
882 887 * Fixed: no issue categories sorting
883 888 * Fixed: incorrect versions sorting
884 889
885 890
886 891 == 2006-07-12 - v0.2.2
887 892
888 893 * Fixed: bug in "issues list"
889 894
890 895
891 896 == 2006-07-09 - v0.2.1
892 897
893 898 * new databases supported: Oracle, PostgreSQL, SQL Server
894 899 * projects/subprojects hierarchy (1 level of subprojects only)
895 900 * environment information display in admin/info
896 901 * more filter options in issues list (rev6)
897 902 * default language based on browser settings (Accept-Language HTTP header)
898 903 * issues list exportable to CSV (rev6)
899 904 * simple_format and auto_link on long text fields
900 905 * more data validations
901 906 * Fixed: error when all mail notifications are unchecked in admin/mail_options
902 907 * Fixed: all project news are displayed on project summary
903 908 * Fixed: Can't change user password in users/edit
904 909 * Fixed: Error on tables creation with PostgreSQL (rev5)
905 910 * Fixed: SQL error in "issue reports" view with PostgreSQL (rev5)
906 911
907 912
908 913 == 2006-06-25 - v0.1.0
909 914
910 915 * multiple users/multiple projects
911 916 * role based access control
912 917 * issue tracking system
913 918 * fully customizable workflow
914 919 * documents/files repository
915 920 * email notifications on issue creation and update
916 921 * multilanguage support (except for error messages):english, french, spanish
917 922 * online manual in french (unfinished)
@@ -1,147 +1,156
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'news_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class NewsController; def rescue_action(e) raise e end; end
23 23
24 24 class NewsControllerTest < Test::Unit::TestCase
25 25 fixtures :projects, :users, :roles, :members, :enabled_modules, :news, :comments
26 26
27 27 def setup
28 28 @controller = NewsController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 end
33 33
34 34 def test_index
35 35 get :index
36 36 assert_response :success
37 37 assert_template 'index'
38 38 assert_not_nil assigns(:newss)
39 39 assert_nil assigns(:project)
40 40 end
41 41
42 42 def test_index_with_project
43 43 get :index, :project_id => 1
44 44 assert_response :success
45 45 assert_template 'index'
46 46 assert_not_nil assigns(:newss)
47 47 end
48 48
49 49 def test_show
50 50 get :show, :id => 1
51 51 assert_response :success
52 52 assert_template 'show'
53 53 assert_tag :tag => 'h2', :content => /eCookbook first release/
54 54 end
55 55
56 56 def test_show_not_found
57 57 get :show, :id => 999
58 58 assert_response 404
59 59 end
60 60
61 61 def test_get_new
62 62 @request.session[:user_id] = 2
63 63 get :new, :project_id => 1
64 64 assert_response :success
65 65 assert_template 'new'
66 66 end
67 67
68 68 def test_post_new
69 69 @request.session[:user_id] = 2
70 70 post :new, :project_id => 1, :news => { :title => 'NewsControllerTest',
71 71 :description => 'This is the description',
72 72 :summary => '' }
73 73 assert_redirected_to 'projects/ecookbook/news'
74 74
75 75 news = News.find_by_title('NewsControllerTest')
76 76 assert_not_nil news
77 77 assert_equal 'This is the description', news.description
78 78 assert_equal User.find(2), news.author
79 79 assert_equal Project.find(1), news.project
80 80 end
81 81
82 82 def test_get_edit
83 83 @request.session[:user_id] = 2
84 84 get :edit, :id => 1
85 85 assert_response :success
86 86 assert_template 'edit'
87 87 end
88 88
89 89 def test_post_edit
90 90 @request.session[:user_id] = 2
91 91 post :edit, :id => 1, :news => { :description => 'Description changed by test_post_edit' }
92 92 assert_redirected_to 'news/show/1'
93 93 news = News.find(1)
94 94 assert_equal 'Description changed by test_post_edit', news.description
95 95 end
96 96
97 97 def test_post_new_with_validation_failure
98 98 @request.session[:user_id] = 2
99 99 post :new, :project_id => 1, :news => { :title => '',
100 100 :description => 'This is the description',
101 101 :summary => '' }
102 102 assert_response :success
103 103 assert_template 'new'
104 104 assert_not_nil assigns(:news)
105 105 assert assigns(:news).new_record?
106 106 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' },
107 107 :content => /1 error/
108 108 end
109 109
110 110 def test_add_comment
111 111 @request.session[:user_id] = 2
112 112 post :add_comment, :id => 1, :comment => { :comments => 'This is a NewsControllerTest comment' }
113 113 assert_redirected_to 'news/show/1'
114 114
115 115 comment = News.find(1).comments.find(:first, :order => 'created_on DESC')
116 116 assert_not_nil comment
117 117 assert_equal 'This is a NewsControllerTest comment', comment.comments
118 118 assert_equal User.find(2), comment.author
119 119 end
120 120
121 def test_empty_comment_should_not_be_added
122 @request.session[:user_id] = 2
123 assert_no_difference 'Comment.count' do
124 post :add_comment, :id => 1, :comment => { :comments => '' }
125 assert_response :success
126 assert_template 'show'
127 end
128 end
129
121 130 def test_destroy_comment
122 131 comments_count = News.find(1).comments.size
123 132 @request.session[:user_id] = 2
124 133 post :destroy_comment, :id => 1, :comment_id => 2
125 134 assert_redirected_to 'news/show/1'
126 135 assert_nil Comment.find_by_id(2)
127 136 assert_equal comments_count - 1, News.find(1).comments.size
128 137 end
129 138
130 139 def test_destroy
131 140 @request.session[:user_id] = 2
132 141 post :destroy, :id => 1
133 142 assert_redirected_to 'projects/ecookbook/news'
134 143 assert_nil News.find_by_id(1)
135 144 end
136 145
137 146 def test_preview
138 147 get :preview, :project_id => 1,
139 148 :news => {:title => '',
140 149 :description => 'News description',
141 150 :summary => ''}
142 151 assert_response :success
143 152 assert_template 'common/_preview'
144 153 assert_tag :tag => 'fieldset', :attributes => { :class => 'preview' },
145 154 :content => /News description/
146 155 end
147 156 end
@@ -1,455 +1,460
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 fixtures :projects, :roles, :enabled_modules, :users,
24 24 :repositories, :changesets,
25 25 :trackers, :issue_statuses, :issues, :versions, :documents,
26 26 :wikis, :wiki_pages, :wiki_contents,
27 27 :boards, :messages,
28 28 :attachments
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 def test_auto_links
35 35 to_test = {
36 36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
37 37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
38 38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
40 40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
41 41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
42 42 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
43 43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
44 44 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
45 45 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
46 46 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
47 47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
48 48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
49 49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
50 50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
51 51 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
52 52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
53 53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
54 54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
55 55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
56 56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
57 57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
58 58 # two exclamation marks
59 59 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
60 60 }
61 61 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
62 62 end
63 63
64 64 def test_auto_mailto
65 65 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
66 66 textilizable('test@foo.bar')
67 67 end
68 68
69 69 def test_inline_images
70 70 to_test = {
71 71 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
72 72 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
73 73 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
74 74 # inline styles should be stripped
75 75 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
76 76 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
77 77 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
78 78 }
79 79 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
80 80 end
81 81
82 82 def test_acronyms
83 83 to_test = {
84 84 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
85 85 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>',
86 86 }
87 87 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
88 88
89 89 end
90 90
91 91 def test_attached_images
92 92 to_test = {
93 93 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
94 94 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
95 95 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
96 96 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />'
97 97 }
98 98 attachments = Attachment.find(:all)
99 99 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
100 100 end
101 101
102 102 def test_textile_external_links
103 103 to_test = {
104 104 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
105 105 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
106 106 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
107 107 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
108 108 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
109 109 # no multiline link text
110 110 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test",
111 111 # mailto link
112 112 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
113 113 # two exclamation marks
114 114 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
115 115 }
116 116 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
117 117 end
118 118
119 119 def test_redmine_links
120 120 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
121 121 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
122 122
123 123 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
124 124 :class => 'changeset', :title => 'My very first commit')
125 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
126 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
125 127
126 128 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
127 129 :class => 'document')
128 130
129 131 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
130 132 :class => 'version')
131 133
132 134 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
133 135
134 136 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
135 137 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
136 138
137 139 to_test = {
138 140 # tickets
139 141 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
140 142 # changesets
141 143 'r1' => changeset_link,
144 'r1.' => "#{changeset_link}.",
145 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
146 'r1,r2' => "#{changeset_link},#{changeset_link2}",
142 147 # documents
143 148 'document#1' => document_link,
144 149 'document:"Test document"' => document_link,
145 150 # versions
146 151 'version#2' => version_link,
147 152 'version:1.0' => version_link,
148 153 'version:"1.0"' => version_link,
149 154 # source
150 155 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
151 156 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
152 157 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
153 158 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
154 159 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
155 160 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
156 161 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
157 162 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
158 163 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
159 164 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
160 165 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
161 166 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
162 167 # message
163 168 'message#4' => link_to('Post 2', message_url, :class => 'message'),
164 169 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
165 170 # escaping
166 171 '!#3.' => '#3.',
167 172 '!r1' => 'r1',
168 173 '!document#1' => 'document#1',
169 174 '!document:"Test document"' => 'document:"Test document"',
170 175 '!version#2' => 'version#2',
171 176 '!version:1.0' => 'version:1.0',
172 177 '!version:"1.0"' => 'version:"1.0"',
173 178 '!source:/some/file' => 'source:/some/file',
174 179 # invalid expressions
175 180 'source:' => 'source:',
176 181 # url hash
177 182 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
178 183 }
179 184 @project = Project.find(1)
180 185 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
181 186 end
182 187
183 188 def test_wiki_links
184 189 to_test = {
185 190 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
186 191 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
187 192 # link with anchor
188 193 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
189 194 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
190 195 # page that doesn't exist
191 196 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
192 197 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
193 198 # link to another project wiki
194 199 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
195 200 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
196 201 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
197 202 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
198 203 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
199 204 # striked through link
200 205 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
201 206 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
202 207 # escaping
203 208 '![[Another page|Page]]' => '[[Another page|Page]]',
204 209 }
205 210 @project = Project.find(1)
206 211 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
207 212 end
208 213
209 214 def test_html_tags
210 215 to_test = {
211 216 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
212 217 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
213 218 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
214 219 # do not escape pre/code tags
215 220 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
216 221 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
217 222 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
218 223 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
219 224 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
220 225 # remove attributes except class
221 226 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
222 227 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
223 228 }
224 229 to_test.each { |text, result| assert_equal result, textilizable(text) }
225 230 end
226 231
227 232 def test_allowed_html_tags
228 233 to_test = {
229 234 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
230 235 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
231 236 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
232 237 }
233 238 to_test.each { |text, result| assert_equal result, textilizable(text) }
234 239 end
235 240
236 241 def syntax_highlight
237 242 raw = <<-RAW
238 243 <pre><code class="ruby">
239 244 # Some ruby code here
240 245 </pre></code>
241 246 RAW
242 247
243 248 expected = <<-EXPECTED
244 249 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
245 250 </pre></code>
246 251 EXPECTED
247 252
248 253 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
249 254 end
250 255
251 256 def test_wiki_links_in_tables
252 257 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
253 258 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
254 259 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
255 260 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
256 261 }
257 262 @project = Project.find(1)
258 263 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
259 264 end
260 265
261 266 def test_text_formatting
262 267 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
263 268 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
264 269 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
265 270 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
266 271 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
267 272 }
268 273 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
269 274 end
270 275
271 276 def test_wiki_horizontal_rule
272 277 assert_equal '<hr />', textilizable('---')
273 278 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
274 279 end
275 280
276 281 def test_acronym
277 282 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
278 283 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
279 284 end
280 285
281 286 def test_footnotes
282 287 raw = <<-RAW
283 288 This is some text[1].
284 289
285 290 fn1. This is the foot note
286 291 RAW
287 292
288 293 expected = <<-EXPECTED
289 294 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
290 295 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
291 296 EXPECTED
292 297
293 298 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
294 299 end
295 300
296 301 def test_table_of_content
297 302 raw = <<-RAW
298 303 {{toc}}
299 304
300 305 h1. Title
301 306
302 307 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
303 308
304 309 h2. Subtitle
305 310
306 311 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
307 312
308 313 h2. Subtitle with %{color:red}red text%
309 314
310 315 h1. Another title
311 316
312 317 RAW
313 318
314 319 expected = '<ul class="toc">' +
315 320 '<li class="heading1"><a href="#Title">Title</a></li>' +
316 321 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
317 322 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
318 323 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
319 324 '</ul>'
320 325
321 326 assert textilizable(raw).gsub("\n", "").include?(expected)
322 327 end
323 328
324 329 def test_blockquote
325 330 # orig raw text
326 331 raw = <<-RAW
327 332 John said:
328 333 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
329 334 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
330 335 > * Donec odio lorem,
331 336 > * sagittis ac,
332 337 > * malesuada in,
333 338 > * adipiscing eu, dolor.
334 339 >
335 340 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
336 341 > Proin a tellus. Nam vel neque.
337 342
338 343 He's right.
339 344 RAW
340 345
341 346 # expected html
342 347 expected = <<-EXPECTED
343 348 <p>John said:</p>
344 349 <blockquote>
345 350 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
346 351 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
347 352 <ul>
348 353 <li>Donec odio lorem,</li>
349 354 <li>sagittis ac,</li>
350 355 <li>malesuada in,</li>
351 356 <li>adipiscing eu, dolor.</li>
352 357 </ul>
353 358 <blockquote>
354 359 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
355 360 </blockquote>
356 361 <p>Proin a tellus. Nam vel neque.</p>
357 362 </blockquote>
358 363 <p>He's right.</p>
359 364 EXPECTED
360 365
361 366 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
362 367 end
363 368
364 369 def test_table
365 370 raw = <<-RAW
366 371 This is a table with empty cells:
367 372
368 373 |cell11|cell12||
369 374 |cell21||cell23|
370 375 |cell31|cell32|cell33|
371 376 RAW
372 377
373 378 expected = <<-EXPECTED
374 379 <p>This is a table with empty cells:</p>
375 380
376 381 <table>
377 382 <tr><td>cell11</td><td>cell12</td><td></td></tr>
378 383 <tr><td>cell21</td><td></td><td>cell23</td></tr>
379 384 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
380 385 </table>
381 386 EXPECTED
382 387
383 388 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
384 389 end
385 390
386 391 def test_default_formatter
387 392 Setting.text_formatting = 'unknown'
388 393 text = 'a *link*: http://www.example.net/'
389 394 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
390 395 Setting.text_formatting = 'textile'
391 396 end
392 397
393 398 def test_date_format_default
394 399 today = Date.today
395 400 Setting.date_format = ''
396 401 assert_equal l_date(today), format_date(today)
397 402 end
398 403
399 404 def test_date_format
400 405 today = Date.today
401 406 Setting.date_format = '%d %m %Y'
402 407 assert_equal today.strftime('%d %m %Y'), format_date(today)
403 408 end
404 409
405 410 def test_time_format_default
406 411 now = Time.now
407 412 Setting.date_format = ''
408 413 Setting.time_format = ''
409 414 assert_equal l_datetime(now), format_time(now)
410 415 assert_equal l_time(now), format_time(now, false)
411 416 end
412 417
413 418 def test_time_format
414 419 now = Time.now
415 420 Setting.date_format = '%d %m %Y'
416 421 Setting.time_format = '%H %M'
417 422 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
418 423 assert_equal now.strftime('%H %M'), format_time(now, false)
419 424 end
420 425
421 426 def test_utc_time_format
422 427 now = Time.now.utc
423 428 Setting.date_format = '%d %m %Y'
424 429 Setting.time_format = '%H %M'
425 430 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
426 431 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
427 432 end
428 433
429 434 def test_due_date_distance_in_words
430 435 to_test = { Date.today => 'Due in 0 days',
431 436 Date.today + 1 => 'Due in 1 day',
432 437 Date.today + 100 => 'Due in 100 days',
433 438 Date.today + 20000 => 'Due in 20000 days',
434 439 Date.today - 1 => '1 day late',
435 440 Date.today - 100 => '100 days late',
436 441 Date.today - 20000 => '20000 days late',
437 442 }
438 443 to_test.each do |date, expected|
439 444 assert_equal expected, due_date_distance_in_words(date)
440 445 end
441 446 end
442 447
443 448 def test_avatar
444 449 # turn on avatars
445 450 Setting.gravatar_enabled = '1'
446 451 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
447 452 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
448 453 assert_nil avatar('jsmith')
449 454 assert_nil avatar(nil)
450 455
451 456 # turn off avatars
452 457 Setting.gravatar_enabled = '0'
453 458 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
454 459 end
455 460 end
@@ -1,169 +1,184
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class MailHandlerTest < Test::Unit::TestCase
21 21 fixtures :users, :projects,
22 22 :enabled_modules,
23 23 :roles,
24 24 :members,
25 25 :issues,
26 26 :issue_statuses,
27 27 :workflows,
28 28 :trackers,
29 29 :projects_trackers,
30 30 :enumerations,
31 31 :issue_categories,
32 32 :custom_fields,
33 33 :custom_fields_trackers
34 34
35 35 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
36 36
37 37 def setup
38 38 ActionMailer::Base.deliveries.clear
39 39 end
40 40
41 41 def test_add_issue
42 42 # This email contains: 'Project: onlinestore'
43 43 issue = submit_email('ticket_on_given_project.eml')
44 44 assert issue.is_a?(Issue)
45 45 assert !issue.new_record?
46 46 issue.reload
47 47 assert_equal 'New ticket on a given project', issue.subject
48 48 assert_equal User.find_by_login('jsmith'), issue.author
49 49 assert_equal Project.find(2), issue.project
50 50 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
51 51 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
52 52 # keywords should be removed from the email body
53 53 assert !issue.description.match(/^Project:/i)
54 54 assert !issue.description.match(/^Status:/i)
55 55 end
56 56
57 57 def test_add_issue_with_status
58 58 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
59 59 issue = submit_email('ticket_on_given_project.eml')
60 60 assert issue.is_a?(Issue)
61 61 assert !issue.new_record?
62 62 issue.reload
63 63 assert_equal Project.find(2), issue.project
64 64 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
65 65 end
66 66
67 67 def test_add_issue_with_attributes_override
68 68 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
69 69 assert issue.is_a?(Issue)
70 70 assert !issue.new_record?
71 71 issue.reload
72 72 assert_equal 'New ticket on a given project', issue.subject
73 73 assert_equal User.find_by_login('jsmith'), issue.author
74 74 assert_equal Project.find(2), issue.project
75 75 assert_equal 'Feature request', issue.tracker.to_s
76 76 assert_equal 'Stock management', issue.category.to_s
77 77 assert_equal 'Urgent', issue.priority.to_s
78 78 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
79 79 end
80 80
81 81 def test_add_issue_with_partial_attributes_override
82 82 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
83 83 assert issue.is_a?(Issue)
84 84 assert !issue.new_record?
85 85 issue.reload
86 86 assert_equal 'New ticket on a given project', issue.subject
87 87 assert_equal User.find_by_login('jsmith'), issue.author
88 88 assert_equal Project.find(2), issue.project
89 89 assert_equal 'Feature request', issue.tracker.to_s
90 90 assert_nil issue.category
91 91 assert_equal 'High', issue.priority.to_s
92 92 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
93 93 end
94 94
95 def test_add_issue_with_spaces_between_attribute_and_separator
96 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
97 assert issue.is_a?(Issue)
98 assert !issue.new_record?
99 issue.reload
100 assert_equal 'New ticket on a given project', issue.subject
101 assert_equal User.find_by_login('jsmith'), issue.author
102 assert_equal Project.find(2), issue.project
103 assert_equal 'Feature request', issue.tracker.to_s
104 assert_equal 'Stock management', issue.category.to_s
105 assert_equal 'Urgent', issue.priority.to_s
106 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
107 end
108
109
95 110 def test_add_issue_with_attachment_to_specific_project
96 111 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
97 112 assert issue.is_a?(Issue)
98 113 assert !issue.new_record?
99 114 issue.reload
100 115 assert_equal 'Ticket created by email with attachment', issue.subject
101 116 assert_equal User.find_by_login('jsmith'), issue.author
102 117 assert_equal Project.find(2), issue.project
103 118 assert_equal 'This is a new ticket with attachments', issue.description
104 119 # Attachment properties
105 120 assert_equal 1, issue.attachments.size
106 121 assert_equal 'Paella.jpg', issue.attachments.first.filename
107 122 assert_equal 'image/jpeg', issue.attachments.first.content_type
108 123 assert_equal 10790, issue.attachments.first.filesize
109 124 end
110 125
111 126 def test_add_issue_with_custom_fields
112 127 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
113 128 assert issue.is_a?(Issue)
114 129 assert !issue.new_record?
115 130 issue.reload
116 131 assert_equal 'New ticket with custom field values', issue.subject
117 132 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
118 133 assert !issue.description.match(/^searchable field:/i)
119 134 end
120 135
121 136 def test_add_issue_with_cc
122 137 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
123 138 assert issue.is_a?(Issue)
124 139 assert !issue.new_record?
125 140 issue.reload
126 141 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
127 142 assert_equal 1, issue.watchers.size
128 143 end
129 144
130 145 def test_add_issue_without_from_header
131 146 Role.anonymous.add_permission!(:add_issues)
132 147 assert_equal false, submit_email('ticket_without_from_header.eml')
133 148 end
134 149
135 150 def test_add_issue_note
136 151 journal = submit_email('ticket_reply.eml')
137 152 assert journal.is_a?(Journal)
138 153 assert_equal User.find_by_login('jsmith'), journal.user
139 154 assert_equal Issue.find(2), journal.journalized
140 155 assert_match /This is reply/, journal.notes
141 156 end
142 157
143 158 def test_add_issue_note_with_status_change
144 159 # This email contains: 'Status: Resolved'
145 160 journal = submit_email('ticket_reply_with_status.eml')
146 161 assert journal.is_a?(Journal)
147 162 issue = Issue.find(journal.issue.id)
148 163 assert_equal User.find_by_login('jsmith'), journal.user
149 164 assert_equal Issue.find(2), journal.journalized
150 165 assert_match /This is reply/, journal.notes
151 166 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
152 167 end
153 168
154 169 def test_should_strip_tags_of_html_only_emails
155 170 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
156 171 assert issue.is_a?(Issue)
157 172 assert !issue.new_record?
158 173 issue.reload
159 174 assert_equal 'HTML email', issue.subject
160 175 assert_equal 'This is a html-only email.', issue.description
161 176 end
162 177
163 178 private
164 179
165 180 def submit_email(filename, options={})
166 181 raw = IO.read(File.join(FIXTURES_PATH, filename))
167 182 MailHandler.receive(raw, options)
168 183 end
169 184 end
General Comments 0
You need to be logged in to leave comments. Login now