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