##// END OF EJS Templates
Merged r2768, r2773, r2774, r2775, r2796 from trunk....
Jean-Philippe Lang -
r2768:94488269d1c3
parent child
Show More
@@ -1,125 +1,127
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 MessagesController < ApplicationController
18 class MessagesController < ApplicationController
19 menu_item :boards
19 menu_item :boards
20 before_filter :find_board, :only => [:new, :preview]
20 before_filter :find_board, :only => [:new, :preview]
21 before_filter :find_message, :except => [:new, :preview]
21 before_filter :find_message, :except => [:new, :preview]
22 before_filter :authorize, :except => [:preview, :edit, :destroy]
22 before_filter :authorize, :except => [:preview, :edit, :destroy]
23
23
24 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
24 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
25 verify :xhr => true, :only => :quote
25 verify :xhr => true, :only => :quote
26
26
27 helper :watchers
27 helper :watchers
28 helper :attachments
28 helper :attachments
29 include AttachmentsHelper
29 include AttachmentsHelper
30
30
31 # Show a topic and its replies
31 # Show a topic and its replies
32 def show
32 def show
33 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
33 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
34 @replies.reverse! if User.current.wants_comments_in_reverse_order?
34 @replies.reverse! if User.current.wants_comments_in_reverse_order?
35 @reply = Message.new(:subject => "RE: #{@message.subject}")
35 @reply = Message.new(:subject => "RE: #{@message.subject}")
36 render :action => "show", :layout => false if request.xhr?
36 render :action => "show", :layout => false if request.xhr?
37 end
37 end
38
38
39 # Create a new topic
39 # Create a new topic
40 def new
40 def new
41 @message = Message.new(params[:message])
41 @message = Message.new(params[:message])
42 @message.author = User.current
42 @message.author = User.current
43 @message.board = @board
43 @message.board = @board
44 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
44 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
45 @message.locked = params[:message]['locked']
45 @message.locked = params[:message]['locked']
46 @message.sticky = params[:message]['sticky']
46 @message.sticky = params[:message]['sticky']
47 end
47 end
48 if request.post? && @message.save
48 if request.post? && @message.save
49 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
49 attach_files(@message, params[:attachments])
50 attach_files(@message, params[:attachments])
50 redirect_to :action => 'show', :id => @message
51 redirect_to :action => 'show', :id => @message
51 end
52 end
52 end
53 end
53
54
54 # Reply to a topic
55 # Reply to a topic
55 def reply
56 def reply
56 @reply = Message.new(params[:reply])
57 @reply = Message.new(params[:reply])
57 @reply.author = User.current
58 @reply.author = User.current
58 @reply.board = @board
59 @reply.board = @board
59 @topic.children << @reply
60 @topic.children << @reply
60 if !@reply.new_record?
61 if !@reply.new_record?
62 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
61 attach_files(@reply, params[:attachments])
63 attach_files(@reply, params[:attachments])
62 end
64 end
63 redirect_to :action => 'show', :id => @topic
65 redirect_to :action => 'show', :id => @topic
64 end
66 end
65
67
66 # Edit a message
68 # Edit a message
67 def edit
69 def edit
68 render_403 and return false unless @message.editable_by?(User.current)
70 render_403 and return false unless @message.editable_by?(User.current)
69 if params[:message]
71 if params[:message]
70 @message.locked = params[:message]['locked']
72 @message.locked = params[:message]['locked']
71 @message.sticky = params[:message]['sticky']
73 @message.sticky = params[:message]['sticky']
72 end
74 end
73 if request.post? && @message.update_attributes(params[:message])
75 if request.post? && @message.update_attributes(params[:message])
74 attach_files(@message, params[:attachments])
76 attach_files(@message, params[:attachments])
75 flash[:notice] = l(:notice_successful_update)
77 flash[:notice] = l(:notice_successful_update)
76 redirect_to :action => 'show', :id => @topic
78 redirect_to :action => 'show', :id => @topic
77 end
79 end
78 end
80 end
79
81
80 # Delete a messages
82 # Delete a messages
81 def destroy
83 def destroy
82 render_403 and return false unless @message.destroyable_by?(User.current)
84 render_403 and return false unless @message.destroyable_by?(User.current)
83 @message.destroy
85 @message.destroy
84 redirect_to @message.parent.nil? ?
86 redirect_to @message.parent.nil? ?
85 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
87 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
86 { :action => 'show', :id => @message.parent }
88 { :action => 'show', :id => @message.parent }
87 end
89 end
88
90
89 def quote
91 def quote
90 user = @message.author
92 user = @message.author
91 text = @message.content
93 text = @message.content
92 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
94 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
93 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
95 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
94 render(:update) { |page|
96 render(:update) { |page|
95 page.<< "$('message_content').value = \"#{content}\";"
97 page.<< "$('message_content').value = \"#{content}\";"
96 page.show 'reply'
98 page.show 'reply'
97 page << "Form.Element.focus('message_content');"
99 page << "Form.Element.focus('message_content');"
98 page << "Element.scrollTo('reply');"
100 page << "Element.scrollTo('reply');"
99 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
101 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
100 }
102 }
101 end
103 end
102
104
103 def preview
105 def preview
104 message = @board.messages.find_by_id(params[:id])
106 message = @board.messages.find_by_id(params[:id])
105 @attachements = message.attachments if message
107 @attachements = message.attachments if message
106 @text = (params[:message] || params[:reply])[:content]
108 @text = (params[:message] || params[:reply])[:content]
107 render :partial => 'common/preview'
109 render :partial => 'common/preview'
108 end
110 end
109
111
110 private
112 private
111 def find_message
113 def find_message
112 find_board
114 find_board
113 @message = @board.messages.find(params[:id], :include => :parent)
115 @message = @board.messages.find(params[:id], :include => :parent)
114 @topic = @message.root
116 @topic = @message.root
115 rescue ActiveRecord::RecordNotFound
117 rescue ActiveRecord::RecordNotFound
116 render_404
118 render_404
117 end
119 end
118
120
119 def find_board
121 def find_board
120 @board = Board.find(params[:board_id], :include => :project)
122 @board = Board.find(params[:board_id], :include => :project)
121 @project = @board.project
123 @project = @board.project
122 rescue ActiveRecord::RecordNotFound
124 rescue ActiveRecord::RecordNotFound
123 render_404
125 render_404
124 end
126 end
125 end
127 end
@@ -1,290 +1,293
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 TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
21 before_filter :find_optional_project, :only => [:report, :details]
21 before_filter :find_optional_project, :only => [:report, :details]
22
22
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
24
24
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27 helper :issues
27 helper :issues
28 include TimelogHelper
28 include TimelogHelper
29 helper :custom_fields
29 helper :custom_fields
30 include CustomFieldsHelper
30 include CustomFieldsHelper
31
31
32 def report
32 def report
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
34 :klass => Project,
34 :klass => Project,
35 :label => :label_project},
35 :label => :label_project},
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
37 :klass => Version,
37 :klass => Version,
38 :label => :label_version},
38 :label => :label_version},
39 'category' => {:sql => "#{Issue.table_name}.category_id",
39 'category' => {:sql => "#{Issue.table_name}.category_id",
40 :klass => IssueCategory,
40 :klass => IssueCategory,
41 :label => :field_category},
41 :label => :field_category},
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
43 :klass => User,
43 :klass => User,
44 :label => :label_member},
44 :label => :label_member},
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
46 :klass => Tracker,
46 :klass => Tracker,
47 :label => :label_tracker},
47 :label => :label_tracker},
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
49 :klass => Enumeration,
49 :klass => Enumeration,
50 :label => :label_activity},
50 :label => :label_activity},
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
52 :klass => Issue,
52 :klass => Issue,
53 :label => :label_issue}
53 :label => :label_issue}
54 }
54 }
55
55
56 # Add list and boolean custom fields as available criterias
56 # Add list and boolean custom fields as available criterias
57 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
57 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
58 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
58 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
59 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
59 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
60 :format => cf.field_format,
60 :format => cf.field_format,
61 :label => cf.name}
61 :label => cf.name}
62 end if @project
62 end if @project
63
63
64 # Add list and boolean time entry custom fields
64 # Add list and boolean time entry custom fields
65 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
65 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
66 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
66 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
67 :format => cf.field_format,
67 :format => cf.field_format,
68 :label => cf.name}
68 :label => cf.name}
69 end
69 end
70
70
71 @criterias = params[:criterias] || []
71 @criterias = params[:criterias] || []
72 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
72 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
73 @criterias.uniq!
73 @criterias.uniq!
74 @criterias = @criterias[0,3]
74 @criterias = @criterias[0,3]
75
75
76 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
76 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
77
77
78 retrieve_date_range
78 retrieve_date_range
79
79
80 unless @criterias.empty?
80 unless @criterias.empty?
81 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
81 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
82 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
82 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
83
83
84 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
84 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
85 sql << " FROM #{TimeEntry.table_name}"
85 sql << " FROM #{TimeEntry.table_name}"
86 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
86 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
87 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
87 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
88 sql << " WHERE"
88 sql << " WHERE"
89 sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
89 sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
90 sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
90 sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
91 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
91 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
92 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
92 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
93
93
94 @hours = ActiveRecord::Base.connection.select_all(sql)
94 @hours = ActiveRecord::Base.connection.select_all(sql)
95
95
96 @hours.each do |row|
96 @hours.each do |row|
97 case @columns
97 case @columns
98 when 'year'
98 when 'year'
99 row['year'] = row['tyear']
99 row['year'] = row['tyear']
100 when 'month'
100 when 'month'
101 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
101 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
102 when 'week'
102 when 'week'
103 row['week'] = "#{row['tyear']}-#{row['tweek']}"
103 row['week'] = "#{row['tyear']}-#{row['tweek']}"
104 when 'day'
104 when 'day'
105 row['day'] = "#{row['spent_on']}"
105 row['day'] = "#{row['spent_on']}"
106 end
106 end
107 end
107 end
108
108
109 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
109 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
110
110
111 @periods = []
111 @periods = []
112 # Date#at_beginning_of_ not supported in Rails 1.2.x
112 # Date#at_beginning_of_ not supported in Rails 1.2.x
113 date_from = @from.to_time
113 date_from = @from.to_time
114 # 100 columns max
114 # 100 columns max
115 while date_from <= @to.to_time && @periods.length < 100
115 while date_from <= @to.to_time && @periods.length < 100
116 case @columns
116 case @columns
117 when 'year'
117 when 'year'
118 @periods << "#{date_from.year}"
118 @periods << "#{date_from.year}"
119 date_from = (date_from + 1.year).at_beginning_of_year
119 date_from = (date_from + 1.year).at_beginning_of_year
120 when 'month'
120 when 'month'
121 @periods << "#{date_from.year}-#{date_from.month}"
121 @periods << "#{date_from.year}-#{date_from.month}"
122 date_from = (date_from + 1.month).at_beginning_of_month
122 date_from = (date_from + 1.month).at_beginning_of_month
123 when 'week'
123 when 'week'
124 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
124 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
125 date_from = (date_from + 7.day).at_beginning_of_week
125 date_from = (date_from + 7.day).at_beginning_of_week
126 when 'day'
126 when 'day'
127 @periods << "#{date_from.to_date}"
127 @periods << "#{date_from.to_date}"
128 date_from = date_from + 1.day
128 date_from = date_from + 1.day
129 end
129 end
130 end
130 end
131 end
131 end
132
132
133 respond_to do |format|
133 respond_to do |format|
134 format.html { render :layout => !request.xhr? }
134 format.html { render :layout => !request.xhr? }
135 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
135 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
136 end
136 end
137 end
137 end
138
138
139 def details
139 def details
140 sort_init 'spent_on', 'desc'
140 sort_init 'spent_on', 'desc'
141 sort_update 'spent_on' => 'spent_on',
141 sort_update 'spent_on' => 'spent_on',
142 'user' => 'user_id',
142 'user' => 'user_id',
143 'activity' => 'activity_id',
143 'activity' => 'activity_id',
144 'project' => "#{Project.table_name}.name",
144 'project' => "#{Project.table_name}.name",
145 'issue' => 'issue_id',
145 'issue' => 'issue_id',
146 'hours' => 'hours'
146 'hours' => 'hours'
147
147
148 cond = ARCondition.new
148 cond = ARCondition.new
149 if @project.nil?
149 if @project.nil?
150 cond << Project.allowed_to_condition(User.current, :view_time_entries)
150 cond << Project.allowed_to_condition(User.current, :view_time_entries)
151 elsif @issue.nil?
151 elsif @issue.nil?
152 cond << @project.project_condition(Setting.display_subprojects_issues?)
152 cond << @project.project_condition(Setting.display_subprojects_issues?)
153 else
153 else
154 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
154 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
155 end
155 end
156
156
157 retrieve_date_range
157 retrieve_date_range
158 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
158 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
159
159
160 TimeEntry.visible_by(User.current) do
160 TimeEntry.visible_by(User.current) do
161 respond_to do |format|
161 respond_to do |format|
162 format.html {
162 format.html {
163 # Paginate results
163 # Paginate results
164 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
164 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
165 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
165 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
166 @entries = TimeEntry.find(:all,
166 @entries = TimeEntry.find(:all,
167 :include => [:project, :activity, :user, {:issue => :tracker}],
167 :include => [:project, :activity, :user, {:issue => :tracker}],
168 :conditions => cond.conditions,
168 :conditions => cond.conditions,
169 :order => sort_clause,
169 :order => sort_clause,
170 :limit => @entry_pages.items_per_page,
170 :limit => @entry_pages.items_per_page,
171 :offset => @entry_pages.current.offset)
171 :offset => @entry_pages.current.offset)
172 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
172 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
173
173
174 render :layout => !request.xhr?
174 render :layout => !request.xhr?
175 }
175 }
176 format.atom {
176 format.atom {
177 entries = TimeEntry.find(:all,
177 entries = TimeEntry.find(:all,
178 :include => [:project, :activity, :user, {:issue => :tracker}],
178 :include => [:project, :activity, :user, {:issue => :tracker}],
179 :conditions => cond.conditions,
179 :conditions => cond.conditions,
180 :order => "#{TimeEntry.table_name}.created_on DESC",
180 :order => "#{TimeEntry.table_name}.created_on DESC",
181 :limit => Setting.feeds_limit.to_i)
181 :limit => Setting.feeds_limit.to_i)
182 render_feed(entries, :title => l(:label_spent_time))
182 render_feed(entries, :title => l(:label_spent_time))
183 }
183 }
184 format.csv {
184 format.csv {
185 # Export all entries
185 # Export all entries
186 @entries = TimeEntry.find(:all,
186 @entries = TimeEntry.find(:all,
187 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
187 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
188 :conditions => cond.conditions,
188 :conditions => cond.conditions,
189 :order => sort_clause)
189 :order => sort_clause)
190 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
190 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
191 }
191 }
192 end
192 end
193 end
193 end
194 end
194 end
195
195
196 def edit
196 def edit
197 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
197 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
198 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
198 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
199 @time_entry.attributes = params[:time_entry]
199 @time_entry.attributes = params[:time_entry]
200
201 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
202
200 if request.post? and @time_entry.save
203 if request.post? and @time_entry.save
201 flash[:notice] = l(:notice_successful_update)
204 flash[:notice] = l(:notice_successful_update)
202 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
205 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
203 return
206 return
204 end
207 end
205 end
208 end
206
209
207 def destroy
210 def destroy
208 render_404 and return unless @time_entry
211 render_404 and return unless @time_entry
209 render_403 and return unless @time_entry.editable_by?(User.current)
212 render_403 and return unless @time_entry.editable_by?(User.current)
210 @time_entry.destroy
213 @time_entry.destroy
211 flash[:notice] = l(:notice_successful_delete)
214 flash[:notice] = l(:notice_successful_delete)
212 redirect_to :back
215 redirect_to :back
213 rescue ::ActionController::RedirectBackError
216 rescue ::ActionController::RedirectBackError
214 redirect_to :action => 'details', :project_id => @time_entry.project
217 redirect_to :action => 'details', :project_id => @time_entry.project
215 end
218 end
216
219
217 private
220 private
218 def find_project
221 def find_project
219 if params[:id]
222 if params[:id]
220 @time_entry = TimeEntry.find(params[:id])
223 @time_entry = TimeEntry.find(params[:id])
221 @project = @time_entry.project
224 @project = @time_entry.project
222 elsif params[:issue_id]
225 elsif params[:issue_id]
223 @issue = Issue.find(params[:issue_id])
226 @issue = Issue.find(params[:issue_id])
224 @project = @issue.project
227 @project = @issue.project
225 elsif params[:project_id]
228 elsif params[:project_id]
226 @project = Project.find(params[:project_id])
229 @project = Project.find(params[:project_id])
227 else
230 else
228 render_404
231 render_404
229 return false
232 return false
230 end
233 end
231 rescue ActiveRecord::RecordNotFound
234 rescue ActiveRecord::RecordNotFound
232 render_404
235 render_404
233 end
236 end
234
237
235 def find_optional_project
238 def find_optional_project
236 if !params[:issue_id].blank?
239 if !params[:issue_id].blank?
237 @issue = Issue.find(params[:issue_id])
240 @issue = Issue.find(params[:issue_id])
238 @project = @issue.project
241 @project = @issue.project
239 elsif !params[:project_id].blank?
242 elsif !params[:project_id].blank?
240 @project = Project.find(params[:project_id])
243 @project = Project.find(params[:project_id])
241 end
244 end
242 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
245 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
243 end
246 end
244
247
245 # Retrieves the date range based on predefined ranges or specific from/to param dates
248 # Retrieves the date range based on predefined ranges or specific from/to param dates
246 def retrieve_date_range
249 def retrieve_date_range
247 @free_period = false
250 @free_period = false
248 @from, @to = nil, nil
251 @from, @to = nil, nil
249
252
250 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
253 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
251 case params[:period].to_s
254 case params[:period].to_s
252 when 'today'
255 when 'today'
253 @from = @to = Date.today
256 @from = @to = Date.today
254 when 'yesterday'
257 when 'yesterday'
255 @from = @to = Date.today - 1
258 @from = @to = Date.today - 1
256 when 'current_week'
259 when 'current_week'
257 @from = Date.today - (Date.today.cwday - 1)%7
260 @from = Date.today - (Date.today.cwday - 1)%7
258 @to = @from + 6
261 @to = @from + 6
259 when 'last_week'
262 when 'last_week'
260 @from = Date.today - 7 - (Date.today.cwday - 1)%7
263 @from = Date.today - 7 - (Date.today.cwday - 1)%7
261 @to = @from + 6
264 @to = @from + 6
262 when '7_days'
265 when '7_days'
263 @from = Date.today - 7
266 @from = Date.today - 7
264 @to = Date.today
267 @to = Date.today
265 when 'current_month'
268 when 'current_month'
266 @from = Date.civil(Date.today.year, Date.today.month, 1)
269 @from = Date.civil(Date.today.year, Date.today.month, 1)
267 @to = (@from >> 1) - 1
270 @to = (@from >> 1) - 1
268 when 'last_month'
271 when 'last_month'
269 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
272 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
270 @to = (@from >> 1) - 1
273 @to = (@from >> 1) - 1
271 when '30_days'
274 when '30_days'
272 @from = Date.today - 30
275 @from = Date.today - 30
273 @to = Date.today
276 @to = Date.today
274 when 'current_year'
277 when 'current_year'
275 @from = Date.civil(Date.today.year, 1, 1)
278 @from = Date.civil(Date.today.year, 1, 1)
276 @to = Date.civil(Date.today.year, 12, 31)
279 @to = Date.civil(Date.today.year, 12, 31)
277 end
280 end
278 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
281 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
279 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
282 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
280 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
283 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
281 @free_period = true
284 @free_period = true
282 else
285 else
283 # default
286 # default
284 end
287 end
285
288
286 @from, @to = @to, @from if @from && @to && @from > @to
289 @from, @to = @to, @from if @from && @to && @from > @to
287 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
290 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
288 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
291 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
289 end
292 end
290 end
293 end
@@ -1,211 +1,212
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 'diff'
18 require 'diff'
19
19
20 class WikiController < ApplicationController
20 class WikiController < ApplicationController
21 before_filter :find_wiki, :authorize
21 before_filter :find_wiki, :authorize
22 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
22 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
23
23
24 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
24 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
25
25
26 helper :attachments
26 helper :attachments
27 include AttachmentsHelper
27 include AttachmentsHelper
28
28
29 # display a page (in editing mode if it doesn't exist)
29 # display a page (in editing mode if it doesn't exist)
30 def index
30 def index
31 page_title = params[:page]
31 page_title = params[:page]
32 @page = @wiki.find_or_new_page(page_title)
32 @page = @wiki.find_or_new_page(page_title)
33 if @page.new_record?
33 if @page.new_record?
34 if User.current.allowed_to?(:edit_wiki_pages, @project)
34 if User.current.allowed_to?(:edit_wiki_pages, @project)
35 edit
35 edit
36 render :action => 'edit'
36 render :action => 'edit'
37 else
37 else
38 render_404
38 render_404
39 end
39 end
40 return
40 return
41 end
41 end
42 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
42 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
43 # Redirects user to the current version if he's not allowed to view previous versions
43 # Redirects user to the current version if he's not allowed to view previous versions
44 redirect_to :version => nil
44 redirect_to :version => nil
45 return
45 return
46 end
46 end
47 @content = @page.content_for_version(params[:version])
47 @content = @page.content_for_version(params[:version])
48 if params[:export] == 'html'
48 if params[:export] == 'html'
49 export = render_to_string :action => 'export', :layout => false
49 export = render_to_string :action => 'export', :layout => false
50 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
50 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
51 return
51 return
52 elsif params[:export] == 'txt'
52 elsif params[:export] == 'txt'
53 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
53 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
54 return
54 return
55 end
55 end
56 @editable = editable?
56 @editable = editable?
57 render :action => 'show'
57 render :action => 'show'
58 end
58 end
59
59
60 # edit an existing page or a new one
60 # edit an existing page or a new one
61 def edit
61 def edit
62 @page = @wiki.find_or_new_page(params[:page])
62 @page = @wiki.find_or_new_page(params[:page])
63 return render_403 unless editable?
63 return render_403 unless editable?
64 @page.content = WikiContent.new(:page => @page) if @page.new_record?
64 @page.content = WikiContent.new(:page => @page) if @page.new_record?
65
65
66 @content = @page.content_for_version(params[:version])
66 @content = @page.content_for_version(params[:version])
67 @content.text = initial_page_content(@page) if @content.text.blank?
67 @content.text = initial_page_content(@page) if @content.text.blank?
68 # don't keep previous comment
68 # don't keep previous comment
69 @content.comments = nil
69 @content.comments = nil
70 if request.get?
70 if request.get?
71 # To prevent StaleObjectError exception when reverting to a previous version
71 # To prevent StaleObjectError exception when reverting to a previous version
72 @content.version = @page.content.version
72 @content.version = @page.content.version
73 else
73 else
74 if !@page.new_record? && @content.text == params[:content][:text]
74 if !@page.new_record? && @content.text == params[:content][:text]
75 # don't save if text wasn't changed
75 # don't save if text wasn't changed
76 redirect_to :action => 'index', :id => @project, :page => @page.title
76 redirect_to :action => 'index', :id => @project, :page => @page.title
77 return
77 return
78 end
78 end
79 #@content.text = params[:content][:text]
79 #@content.text = params[:content][:text]
80 #@content.comments = params[:content][:comments]
80 #@content.comments = params[:content][:comments]
81 @content.attributes = params[:content]
81 @content.attributes = params[:content]
82 @content.author = User.current
82 @content.author = User.current
83 # if page is new @page.save will also save content, but not if page isn't a new record
83 # if page is new @page.save will also save content, but not if page isn't a new record
84 if (@page.new_record? ? @page.save : @content.save)
84 if (@page.new_record? ? @page.save : @content.save)
85 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
85 redirect_to :action => 'index', :id => @project, :page => @page.title
86 redirect_to :action => 'index', :id => @project, :page => @page.title
86 end
87 end
87 end
88 end
88 rescue ActiveRecord::StaleObjectError
89 rescue ActiveRecord::StaleObjectError
89 # Optimistic locking exception
90 # Optimistic locking exception
90 flash[:error] = l(:notice_locking_conflict)
91 flash[:error] = l(:notice_locking_conflict)
91 end
92 end
92
93
93 # rename a page
94 # rename a page
94 def rename
95 def rename
95 return render_403 unless editable?
96 return render_403 unless editable?
96 @page.redirect_existing_links = true
97 @page.redirect_existing_links = true
97 # used to display the *original* title if some AR validation errors occur
98 # used to display the *original* title if some AR validation errors occur
98 @original_title = @page.pretty_title
99 @original_title = @page.pretty_title
99 if request.post? && @page.update_attributes(params[:wiki_page])
100 if request.post? && @page.update_attributes(params[:wiki_page])
100 flash[:notice] = l(:notice_successful_update)
101 flash[:notice] = l(:notice_successful_update)
101 redirect_to :action => 'index', :id => @project, :page => @page.title
102 redirect_to :action => 'index', :id => @project, :page => @page.title
102 end
103 end
103 end
104 end
104
105
105 def protect
106 def protect
106 @page.update_attribute :protected, params[:protected]
107 @page.update_attribute :protected, params[:protected]
107 redirect_to :action => 'index', :id => @project, :page => @page.title
108 redirect_to :action => 'index', :id => @project, :page => @page.title
108 end
109 end
109
110
110 # show page history
111 # show page history
111 def history
112 def history
112 @version_count = @page.content.versions.count
113 @version_count = @page.content.versions.count
113 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
114 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
114 # don't load text
115 # don't load text
115 @versions = @page.content.versions.find :all,
116 @versions = @page.content.versions.find :all,
116 :select => "id, author_id, comments, updated_on, version",
117 :select => "id, author_id, comments, updated_on, version",
117 :order => 'version DESC',
118 :order => 'version DESC',
118 :limit => @version_pages.items_per_page + 1,
119 :limit => @version_pages.items_per_page + 1,
119 :offset => @version_pages.current.offset
120 :offset => @version_pages.current.offset
120
121
121 render :layout => false if request.xhr?
122 render :layout => false if request.xhr?
122 end
123 end
123
124
124 def diff
125 def diff
125 @diff = @page.diff(params[:version], params[:version_from])
126 @diff = @page.diff(params[:version], params[:version_from])
126 render_404 unless @diff
127 render_404 unless @diff
127 end
128 end
128
129
129 def annotate
130 def annotate
130 @annotate = @page.annotate(params[:version])
131 @annotate = @page.annotate(params[:version])
131 render_404 unless @annotate
132 render_404 unless @annotate
132 end
133 end
133
134
134 # remove a wiki page and its history
135 # remove a wiki page and its history
135 def destroy
136 def destroy
136 return render_403 unless editable?
137 return render_403 unless editable?
137 @page.destroy
138 @page.destroy
138 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
139 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
139 end
140 end
140
141
141 # display special pages
142 # display special pages
142 def special
143 def special
143 page_title = params[:page].downcase
144 page_title = params[:page].downcase
144 case page_title
145 case page_title
145 # show pages index, sorted by title
146 # show pages index, sorted by title
146 when 'page_index', 'date_index'
147 when 'page_index', 'date_index'
147 # eager load information about last updates, without loading text
148 # eager load information about last updates, without loading text
148 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
149 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
149 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
150 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
150 :order => 'title'
151 :order => 'title'
151 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
152 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
152 @pages_by_parent_id = @pages.group_by(&:parent_id)
153 @pages_by_parent_id = @pages.group_by(&:parent_id)
153 # export wiki to a single html file
154 # export wiki to a single html file
154 when 'export'
155 when 'export'
155 @pages = @wiki.pages.find :all, :order => 'title'
156 @pages = @wiki.pages.find :all, :order => 'title'
156 export = render_to_string :action => 'export_multiple', :layout => false
157 export = render_to_string :action => 'export_multiple', :layout => false
157 send_data(export, :type => 'text/html', :filename => "wiki.html")
158 send_data(export, :type => 'text/html', :filename => "wiki.html")
158 return
159 return
159 else
160 else
160 # requested special page doesn't exist, redirect to default page
161 # requested special page doesn't exist, redirect to default page
161 redirect_to :action => 'index', :id => @project, :page => nil and return
162 redirect_to :action => 'index', :id => @project, :page => nil and return
162 end
163 end
163 render :action => "special_#{page_title}"
164 render :action => "special_#{page_title}"
164 end
165 end
165
166
166 def preview
167 def preview
167 page = @wiki.find_page(params[:page])
168 page = @wiki.find_page(params[:page])
168 # page is nil when previewing a new page
169 # page is nil when previewing a new page
169 return render_403 unless page.nil? || editable?(page)
170 return render_403 unless page.nil? || editable?(page)
170 if page
171 if page
171 @attachements = page.attachments
172 @attachements = page.attachments
172 @previewed = page.content
173 @previewed = page.content
173 end
174 end
174 @text = params[:content][:text]
175 @text = params[:content][:text]
175 render :partial => 'common/preview'
176 render :partial => 'common/preview'
176 end
177 end
177
178
178 def add_attachment
179 def add_attachment
179 return render_403 unless editable?
180 return render_403 unless editable?
180 attach_files(@page, params[:attachments])
181 attach_files(@page, params[:attachments])
181 redirect_to :action => 'index', :page => @page.title
182 redirect_to :action => 'index', :page => @page.title
182 end
183 end
183
184
184 private
185 private
185
186
186 def find_wiki
187 def find_wiki
187 @project = Project.find(params[:id])
188 @project = Project.find(params[:id])
188 @wiki = @project.wiki
189 @wiki = @project.wiki
189 render_404 unless @wiki
190 render_404 unless @wiki
190 rescue ActiveRecord::RecordNotFound
191 rescue ActiveRecord::RecordNotFound
191 render_404
192 render_404
192 end
193 end
193
194
194 # Finds the requested page and returns a 404 error if it doesn't exist
195 # Finds the requested page and returns a 404 error if it doesn't exist
195 def find_existing_page
196 def find_existing_page
196 @page = @wiki.find_page(params[:page])
197 @page = @wiki.find_page(params[:page])
197 render_404 if @page.nil?
198 render_404 if @page.nil?
198 end
199 end
199
200
200 # Returns true if the current user is allowed to edit the page, otherwise false
201 # Returns true if the current user is allowed to edit the page, otherwise false
201 def editable?(page = @page)
202 def editable?(page = @page)
202 page.editable_by?(User.current)
203 page.editable_by?(User.current)
203 end
204 end
204
205
205 # Returns the default content of a new wiki page
206 # Returns the default content of a new wiki page
206 def initial_page_content(page)
207 def initial_page_content(page)
207 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
208 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
208 extend helper unless self.instance_of?(helper)
209 extend helper unless self.instance_of?(helper)
209 helper.instance_method(:initial_page_content).bind(self).call(page)
210 helper.instance_method(:initial_page_content).bind(self).call(page)
210 end
211 end
211 end
212 end
@@ -1,155 +1,157
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'iconv'
18 require 'iconv'
19
19
20 class Changeset < ActiveRecord::Base
20 class Changeset < ActiveRecord::Base
21 belongs_to :repository
21 belongs_to :repository
22 belongs_to :user
22 belongs_to :user
23 has_many :changes, :dependent => :delete_all
23 has_many :changes, :dependent => :delete_all
24 has_and_belongs_to_many :issues
24 has_and_belongs_to_many :issues
25
25
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.comments.blank? ? '' : (': ' + o.comments))},
27 :description => :comments,
27 :description => :comments,
28 :datetime => :committed_on,
28 :datetime => :committed_on,
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
30
30
31 acts_as_searchable :columns => 'comments',
31 acts_as_searchable :columns => 'comments',
32 :include => {:repository => :project},
32 :include => {:repository => :project},
33 :project_key => "#{Repository.table_name}.project_id",
33 :project_key => "#{Repository.table_name}.project_id",
34 :date_column => 'committed_on'
34 :date_column => 'committed_on'
35
35
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 :author_key => :user_id,
37 :author_key => :user_id,
38 :find_options => {:include => {:repository => :project}}
38 :find_options => {:include => {:repository => :project}}
39
39
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 validates_uniqueness_of :revision, :scope => :repository_id
41 validates_uniqueness_of :revision, :scope => :repository_id
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43
43
44 def revision=(r)
44 def revision=(r)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
46 end
46 end
47
47
48 def comments=(comment)
48 def comments=(comment)
49 write_attribute(:comments, Changeset.normalize_comments(comment))
49 write_attribute(:comments, Changeset.normalize_comments(comment))
50 end
50 end
51
51
52 def committed_on=(date)
52 def committed_on=(date)
53 self.commit_date = date
53 self.commit_date = date
54 super
54 super
55 end
55 end
56
56
57 def project
57 def project
58 repository.project
58 repository.project
59 end
59 end
60
60
61 def author
61 def author
62 user || committer.to_s.split('<').first
62 user || committer.to_s.split('<').first
63 end
63 end
64
64
65 def before_create
65 def before_create
66 self.user = repository.find_committer_user(committer)
66 self.user = repository.find_committer_user(committer)
67 end
67 end
68
68
69 def after_create
69 def after_create
70 scan_comment_for_issue_ids
70 scan_comment_for_issue_ids
71 end
71 end
72 require 'pp'
72 require 'pp'
73
73
74 def scan_comment_for_issue_ids
74 def scan_comment_for_issue_ids
75 return if comments.blank?
75 return if comments.blank?
76 # keywords used to reference issues
76 # keywords used to reference issues
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
78 # keywords used to fix issues
78 # keywords used to fix issues
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
80 # status and optional done ratio applied
80 # status and optional done ratio applied
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
83
83
84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
85 return if kw_regexp.blank?
85 return if kw_regexp.blank?
86
86
87 referenced_issues = []
87 referenced_issues = []
88
88
89 if ref_keywords.delete('*')
89 if ref_keywords.delete('*')
90 # find any issue ID in the comments
90 # find any issue ID in the comments
91 target_issue_ids = []
91 target_issue_ids = []
92 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
92 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
94 end
94 end
95
95
96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
97 action = match[0]
97 action = match[0]
98 target_issue_ids = match[1].scan(/\d+/)
98 target_issue_ids = match[1].scan(/\d+/)
99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
100 if fix_status && fix_keywords.include?(action.downcase)
100 if fix_status && fix_keywords.include?(action.downcase)
101 # update status of issues
101 # update status of issues
102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
103 target_issues.each do |issue|
103 target_issues.each do |issue|
104 # the issue may have been updated by the closure of another one (eg. duplicate)
104 # the issue may have been updated by the closure of another one (eg. duplicate)
105 issue.reload
105 issue.reload
106 # don't change the status is the issue is closed
106 # don't change the status is the issue is closed
107 next if issue.status.is_closed?
107 next if issue.status.is_closed?
108 csettext = "r#{self.revision}"
108 csettext = "r#{self.revision}"
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
110 csettext = "commit:\"#{self.scmid}\""
110 csettext = "commit:\"#{self.scmid}\""
111 end
111 end
112 journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
112 journal = issue.init_journal(user || User.anonymous, l(:text_status_changed_by_changeset, csettext))
113 issue.status = fix_status
113 issue.status = fix_status
114 issue.done_ratio = done_ratio if done_ratio
114 issue.done_ratio = done_ratio if done_ratio
115 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
116 { :changeset => self, :issue => issue })
115 issue.save
117 issue.save
116 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
118 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
117 end
119 end
118 end
120 end
119 referenced_issues += target_issues
121 referenced_issues += target_issues
120 end
122 end
121
123
122 self.issues = referenced_issues.uniq
124 self.issues = referenced_issues.uniq
123 end
125 end
124
126
125 # Returns the previous changeset
127 # Returns the previous changeset
126 def previous
128 def previous
127 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
129 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
128 end
130 end
129
131
130 # Returns the next changeset
132 # Returns the next changeset
131 def next
133 def next
132 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
134 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
133 end
135 end
134
136
135 # Strips and reencodes a commit log before insertion into the database
137 # Strips and reencodes a commit log before insertion into the database
136 def self.normalize_comments(str)
138 def self.normalize_comments(str)
137 to_utf8(str.to_s.strip)
139 to_utf8(str.to_s.strip)
138 end
140 end
139
141
140 private
142 private
141
143
142
144
143 def self.to_utf8(str)
145 def self.to_utf8(str)
144 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
146 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
145 encoding = Setting.commit_logs_encoding.to_s.strip
147 encoding = Setting.commit_logs_encoding.to_s.strip
146 unless encoding.blank? || encoding == 'UTF-8'
148 unless encoding.blank? || encoding == 'UTF-8'
147 begin
149 begin
148 return Iconv.conv('UTF-8', encoding, str)
150 return Iconv.conv('UTF-8', encoding, str)
149 rescue Iconv::Failure
151 rescue Iconv::Failure
150 # do nothing here
152 # do nothing here
151 end
153 end
152 end
154 end
153 str
155 str
154 end
156 end
155 end
157 end
@@ -1,410 +1,410
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :default_order
19 attr_accessor :name, :sortable, :default_order
20 include GLoc
20 include GLoc
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.default_order = options[:default_order]
25 self.default_order = options[:default_order]
26 end
26 end
27
27
28 def caption
28 def caption
29 set_language_if_valid(User.current.language)
29 set_language_if_valid(User.current.language)
30 l("field_#{name}")
30 l("field_#{name}")
31 end
31 end
32 end
32 end
33
33
34 class QueryCustomFieldColumn < QueryColumn
34 class QueryCustomFieldColumn < QueryColumn
35
35
36 def initialize(custom_field)
36 def initialize(custom_field)
37 self.name = "cf_#{custom_field.id}".to_sym
37 self.name = "cf_#{custom_field.id}".to_sym
38 self.sortable = false
38 self.sortable = false
39 @cf = custom_field
39 @cf = custom_field
40 end
40 end
41
41
42 def caption
42 def caption
43 @cf.name
43 @cf.name
44 end
44 end
45
45
46 def custom_field
46 def custom_field
47 @cf
47 @cf
48 end
48 end
49 end
49 end
50
50
51 class Query < ActiveRecord::Base
51 class Query < ActiveRecord::Base
52 belongs_to :project
52 belongs_to :project
53 belongs_to :user
53 belongs_to :user
54 serialize :filters
54 serialize :filters
55 serialize :column_names
55 serialize :column_names
56
56
57 attr_protected :project_id, :user_id
57 attr_protected :project_id, :user_id
58
58
59 validates_presence_of :name, :on => :save
59 validates_presence_of :name, :on => :save
60 validates_length_of :name, :maximum => 255
60 validates_length_of :name, :maximum => 255
61
61
62 @@operators = { "=" => :label_equals,
62 @@operators = { "=" => :label_equals,
63 "!" => :label_not_equals,
63 "!" => :label_not_equals,
64 "o" => :label_open_issues,
64 "o" => :label_open_issues,
65 "c" => :label_closed_issues,
65 "c" => :label_closed_issues,
66 "!*" => :label_none,
66 "!*" => :label_none,
67 "*" => :label_all,
67 "*" => :label_all,
68 ">=" => '>=',
68 ">=" => '>=',
69 "<=" => '<=',
69 "<=" => '<=',
70 "<t+" => :label_in_less_than,
70 "<t+" => :label_in_less_than,
71 ">t+" => :label_in_more_than,
71 ">t+" => :label_in_more_than,
72 "t+" => :label_in,
72 "t+" => :label_in,
73 "t" => :label_today,
73 "t" => :label_today,
74 "w" => :label_this_week,
74 "w" => :label_this_week,
75 ">t-" => :label_less_than_ago,
75 ">t-" => :label_less_than_ago,
76 "<t-" => :label_more_than_ago,
76 "<t-" => :label_more_than_ago,
77 "t-" => :label_ago,
77 "t-" => :label_ago,
78 "~" => :label_contains,
78 "~" => :label_contains,
79 "!~" => :label_not_contains }
79 "!~" => :label_not_contains }
80
80
81 cattr_reader :operators
81 cattr_reader :operators
82
82
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
83 @@operators_by_filter_type = { :list => [ "=", "!" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
84 :list_status => [ "o", "=", "!", "c", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
85 :list_optional => [ "=", "!", "!*", "*" ],
86 :list_subprojects => [ "*", "!*", "=" ],
86 :list_subprojects => [ "*", "!*", "=" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
87 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
88 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
89 :string => [ "=", "~", "!", "!~" ],
89 :string => [ "=", "~", "!", "!~" ],
90 :text => [ "~", "!~" ],
90 :text => [ "~", "!~" ],
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
91 :integer => [ "=", ">=", "<=", "!*", "*" ] }
92
92
93 cattr_reader :operators_by_filter_type
93 cattr_reader :operators_by_filter_type
94
94
95 @@available_columns = [
95 @@available_columns = [
96 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"),
96 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name"),
97 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
97 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position"),
98 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
98 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position"),
99 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
99 QueryColumn.new(:priority, :sortable => "#{Enumeration.table_name}.position", :default_order => 'desc'),
100 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
100 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
101 QueryColumn.new(:author),
101 QueryColumn.new(:author),
102 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
102 QueryColumn.new(:assigned_to, :sortable => "#{User.table_name}.lastname"),
103 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
103 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
104 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
104 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name"),
105 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
105 QueryColumn.new(:fixed_version, :sortable => "#{Version.table_name}.effective_date", :default_order => 'desc'),
106 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
106 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
107 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
107 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
108 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
108 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
109 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
109 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio"),
110 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
110 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
111 ]
111 ]
112 cattr_reader :available_columns
112 cattr_reader :available_columns
113
113
114 def initialize(attributes = nil)
114 def initialize(attributes = nil)
115 super attributes
115 super attributes
116 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
116 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
117 set_language_if_valid(User.current.language)
117 set_language_if_valid(User.current.language)
118 end
118 end
119
119
120 def after_initialize
120 def after_initialize
121 # Store the fact that project is nil (used in #editable_by?)
121 # Store the fact that project is nil (used in #editable_by?)
122 @is_for_all = project.nil?
122 @is_for_all = project.nil?
123 end
123 end
124
124
125 def validate
125 def validate
126 filters.each_key do |field|
126 filters.each_key do |field|
127 errors.add label_for(field), :activerecord_error_blank unless
127 errors.add label_for(field), :activerecord_error_blank unless
128 # filter requires one or more values
128 # filter requires one or more values
129 (values_for(field) and !values_for(field).first.blank?) or
129 (values_for(field) and !values_for(field).first.blank?) or
130 # filter doesn't require any value
130 # filter doesn't require any value
131 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
131 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
132 end if filters
132 end if filters
133 end
133 end
134
134
135 def editable_by?(user)
135 def editable_by?(user)
136 return false unless user
136 return false unless user
137 # Admin can edit them all and regular users can edit their private queries
137 # Admin can edit them all and regular users can edit their private queries
138 return true if user.admin? || (!is_public && self.user_id == user.id)
138 return true if user.admin? || (!is_public && self.user_id == user.id)
139 # Members can not edit public queries that are for all project (only admin is allowed to)
139 # Members can not edit public queries that are for all project (only admin is allowed to)
140 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
140 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
141 end
141 end
142
142
143 def available_filters
143 def available_filters
144 return @available_filters if @available_filters
144 return @available_filters if @available_filters
145
145
146 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
146 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
147
147
148 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
148 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
149 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
149 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
150 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
150 "priority_id" => { :type => :list, :order => 3, :values => Enumeration.find(:all, :conditions => ['opt=?','IPRI'], :order => 'position').collect{|s| [s.name, s.id.to_s] } },
151 "subject" => { :type => :text, :order => 8 },
151 "subject" => { :type => :text, :order => 8 },
152 "created_on" => { :type => :date_past, :order => 9 },
152 "created_on" => { :type => :date_past, :order => 9 },
153 "updated_on" => { :type => :date_past, :order => 10 },
153 "updated_on" => { :type => :date_past, :order => 10 },
154 "start_date" => { :type => :date, :order => 11 },
154 "start_date" => { :type => :date, :order => 11 },
155 "due_date" => { :type => :date, :order => 12 },
155 "due_date" => { :type => :date, :order => 12 },
156 "estimated_hours" => { :type => :integer, :order => 13 },
156 "estimated_hours" => { :type => :integer, :order => 13 },
157 "done_ratio" => { :type => :integer, :order => 14 }}
157 "done_ratio" => { :type => :integer, :order => 14 }}
158
158
159 user_values = []
159 user_values = []
160 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
160 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
161 if project
161 if project
162 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
162 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
163 else
163 else
164 # members of the user's projects
164 # members of the user's projects
165 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
165 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
166 end
166 end
167 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
167 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
168 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
168 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
169
169
170 if project
170 if project
171 # project specific filters
171 # project specific filters
172 unless @project.issue_categories.empty?
172 unless @project.issue_categories.empty?
173 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
173 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
174 end
174 end
175 unless @project.versions.empty?
175 unless @project.versions.empty?
176 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
176 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
177 end
177 end
178 unless @project.active_children.empty?
178 unless @project.active_children.empty?
179 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
179 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
180 end
180 end
181 add_custom_fields_filters(@project.all_issue_custom_fields)
181 add_custom_fields_filters(@project.all_issue_custom_fields)
182 else
182 else
183 # global filters for cross project issue list
183 # global filters for cross project issue list
184 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
184 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
185 end
185 end
186 @available_filters
186 @available_filters
187 end
187 end
188
188
189 def add_filter(field, operator, values)
189 def add_filter(field, operator, values)
190 # values must be an array
190 # values must be an array
191 return unless values and values.is_a? Array # and !values.first.empty?
191 return unless values and values.is_a? Array # and !values.first.empty?
192 # check if field is defined as an available filter
192 # check if field is defined as an available filter
193 if available_filters.has_key? field
193 if available_filters.has_key? field
194 filter_options = available_filters[field]
194 filter_options = available_filters[field]
195 # check if operator is allowed for that filter
195 # check if operator is allowed for that filter
196 #if @@operators_by_filter_type[filter_options[:type]].include? operator
196 #if @@operators_by_filter_type[filter_options[:type]].include? operator
197 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
197 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
198 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
198 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
199 #end
199 #end
200 filters[field] = {:operator => operator, :values => values }
200 filters[field] = {:operator => operator, :values => values }
201 end
201 end
202 end
202 end
203
203
204 def add_short_filter(field, expression)
204 def add_short_filter(field, expression)
205 return unless expression
205 return unless expression
206 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
206 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
207 add_filter field, (parms[0] || "="), [parms[1] || ""]
207 add_filter field, (parms[0] || "="), [parms[1] || ""]
208 end
208 end
209
209
210 def has_filter?(field)
210 def has_filter?(field)
211 filters and filters[field]
211 filters and filters[field]
212 end
212 end
213
213
214 def operator_for(field)
214 def operator_for(field)
215 has_filter?(field) ? filters[field][:operator] : nil
215 has_filter?(field) ? filters[field][:operator] : nil
216 end
216 end
217
217
218 def values_for(field)
218 def values_for(field)
219 has_filter?(field) ? filters[field][:values] : nil
219 has_filter?(field) ? filters[field][:values] : nil
220 end
220 end
221
221
222 def label_for(field)
222 def label_for(field)
223 label = available_filters[field][:name] if available_filters.has_key?(field)
223 label = available_filters[field][:name] if available_filters.has_key?(field)
224 label ||= field.gsub(/\_id$/, "")
224 label ||= field.gsub(/\_id$/, "")
225 end
225 end
226
226
227 def available_columns
227 def available_columns
228 return @available_columns if @available_columns
228 return @available_columns if @available_columns
229 @available_columns = Query.available_columns
229 @available_columns = Query.available_columns
230 @available_columns += (project ?
230 @available_columns += (project ?
231 project.all_issue_custom_fields :
231 project.all_issue_custom_fields :
232 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
232 IssueCustomField.find(:all, :conditions => {:is_for_all => true})
233 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
233 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
234 end
234 end
235
235
236 def columns
236 def columns
237 if has_default_columns?
237 if has_default_columns?
238 available_columns.select do |c|
238 available_columns.select do |c|
239 # Adds the project column by default for cross-project lists
239 # Adds the project column by default for cross-project lists
240 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
240 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
241 end
241 end
242 else
242 else
243 # preserve the column_names order
243 # preserve the column_names order
244 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
244 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
245 end
245 end
246 end
246 end
247
247
248 def column_names=(names)
248 def column_names=(names)
249 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
249 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
250 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
250 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
251 write_attribute(:column_names, names)
251 write_attribute(:column_names, names)
252 end
252 end
253
253
254 def has_column?(column)
254 def has_column?(column)
255 column_names && column_names.include?(column.name)
255 column_names && column_names.include?(column.name)
256 end
256 end
257
257
258 def has_default_columns?
258 def has_default_columns?
259 column_names.nil? || column_names.empty?
259 column_names.nil? || column_names.empty?
260 end
260 end
261
261
262 def project_statement
262 def project_statement
263 project_clauses = []
263 project_clauses = []
264 if project && !@project.active_children.empty?
264 if project && !@project.active_children.empty?
265 ids = [project.id]
265 ids = [project.id]
266 if has_filter?("subproject_id")
266 if has_filter?("subproject_id")
267 case operator_for("subproject_id")
267 case operator_for("subproject_id")
268 when '='
268 when '='
269 # include the selected subprojects
269 # include the selected subprojects
270 ids += values_for("subproject_id").each(&:to_i)
270 ids += values_for("subproject_id").each(&:to_i)
271 when '!*'
271 when '!*'
272 # main project only
272 # main project only
273 else
273 else
274 # all subprojects
274 # all subprojects
275 ids += project.child_ids
275 ids += project.child_ids
276 end
276 end
277 elsif Setting.display_subprojects_issues?
277 elsif Setting.display_subprojects_issues?
278 ids += project.child_ids
278 ids += project.child_ids
279 end
279 end
280 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
280 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
281 elsif project
281 elsif project
282 project_clauses << "#{Project.table_name}.id = %d" % project.id
282 project_clauses << "#{Project.table_name}.id = %d" % project.id
283 end
283 end
284 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
284 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
285 project_clauses.join(' AND ')
285 project_clauses.join(' AND ')
286 end
286 end
287
287
288 def statement
288 def statement
289 # filters clauses
289 # filters clauses
290 filters_clauses = []
290 filters_clauses = []
291 filters.each_key do |field|
291 filters.each_key do |field|
292 next if field == "subproject_id"
292 next if field == "subproject_id"
293 v = values_for(field).clone
293 v = values_for(field).clone
294 next unless v and !v.empty?
294 next unless v and !v.empty?
295
295
296 sql = ''
296 sql = ''
297 is_custom_filter = false
297 is_custom_filter = false
298 if field =~ /^cf_(\d+)$/
298 if field =~ /^cf_(\d+)$/
299 # custom field
299 # custom field
300 db_table = CustomValue.table_name
300 db_table = CustomValue.table_name
301 db_field = 'value'
301 db_field = 'value'
302 is_custom_filter = true
302 is_custom_filter = true
303 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
303 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
304 else
304 else
305 # regular field
305 # regular field
306 db_table = Issue.table_name
306 db_table = Issue.table_name
307 db_field = field
307 db_field = field
308 sql << '('
308 sql << '('
309 end
309 end
310
310
311 # "me" value subsitution
311 # "me" value subsitution
312 if %w(assigned_to_id author_id).include?(field)
312 if %w(assigned_to_id author_id).include?(field)
313 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
313 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
314 end
314 end
315
315
316 sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
316 sql = sql + sql_for_field(field, v, db_table, db_field, is_custom_filter)
317
317
318 sql << ')'
318 sql << ')'
319 filters_clauses << sql
319 filters_clauses << sql
320 end if filters and valid?
320 end if filters and valid?
321
321
322 (filters_clauses << project_statement).join(' AND ')
322 (filters_clauses << project_statement).join(' AND ')
323 end
323 end
324
324
325 private
325 private
326
326
327 # Helper method to generate the WHERE sql for a +field+ with a +value+
327 # Helper method to generate the WHERE sql for a +field+ with a +value+
328 def sql_for_field(field, value, db_table, db_field, is_custom_filter)
328 def sql_for_field(field, value, db_table, db_field, is_custom_filter)
329 sql = ''
329 sql = ''
330 case operator_for field
330 case operator_for field
331 when "="
331 when "="
332 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
332 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
333 when "!"
333 when "!"
334 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
334 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
335 when "!*"
335 when "!*"
336 sql = "#{db_table}.#{db_field} IS NULL"
336 sql = "#{db_table}.#{db_field} IS NULL"
337 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
337 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
338 when "*"
338 when "*"
339 sql = "#{db_table}.#{db_field} IS NOT NULL"
339 sql = "#{db_table}.#{db_field} IS NOT NULL"
340 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
340 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
341 when ">="
341 when ">="
342 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
342 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
343 when "<="
343 when "<="
344 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
344 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
345 when "o"
345 when "o"
346 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
346 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
347 when "c"
347 when "c"
348 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
348 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
349 when ">t-"
349 when ">t-"
350 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
350 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
351 when "<t-"
351 when "<t-"
352 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
352 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
353 when "t-"
353 when "t-"
354 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
354 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
355 when ">t+"
355 when ">t+"
356 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
356 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
357 when "<t+"
357 when "<t+"
358 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
358 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
359 when "t+"
359 when "t+"
360 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
360 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
361 when "t"
361 when "t"
362 sql = date_range_clause(db_table, db_field, 0, 0)
362 sql = date_range_clause(db_table, db_field, 0, 0)
363 when "w"
363 when "w"
364 from = l(:general_first_day_of_week) == '7' ?
364 from = l(:general_first_day_of_week) == '7' ?
365 # week starts on sunday
365 # week starts on sunday
366 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
366 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
367 # week starts on monday (Rails default)
367 # week starts on monday (Rails default)
368 Time.now.at_beginning_of_week
368 Time.now.at_beginning_of_week
369 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
369 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
370 when "~"
370 when "~"
371 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
371 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
372 when "!~"
372 when "!~"
373 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
373 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
374 end
374 end
375
375
376 return sql
376 return sql
377 end
377 end
378
378
379 def add_custom_fields_filters(custom_fields)
379 def add_custom_fields_filters(custom_fields)
380 @available_filters ||= {}
380 @available_filters ||= {}
381
381
382 custom_fields.select(&:is_filter?).each do |field|
382 custom_fields.select(&:is_filter?).each do |field|
383 case field.field_format
383 case field.field_format
384 when "text"
384 when "text"
385 options = { :type => :text, :order => 20 }
385 options = { :type => :text, :order => 20 }
386 when "list"
386 when "list"
387 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
387 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
388 when "date"
388 when "date"
389 options = { :type => :date, :order => 20 }
389 options = { :type => :date, :order => 20 }
390 when "bool"
390 when "bool"
391 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
391 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
392 else
392 else
393 options = { :type => :string, :order => 20 }
393 options = { :type => :string, :order => 20 }
394 end
394 end
395 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
395 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
396 end
396 end
397 end
397 end
398
398
399 # Returns a SQL clause for a date or datetime field.
399 # Returns a SQL clause for a date or datetime field.
400 def date_range_clause(table, field, from, to)
400 def date_range_clause(table, field, from, to)
401 s = []
401 s = []
402 if from
402 if from
403 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
403 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
404 end
404 end
405 if to
405 if to
406 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
406 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
407 end
407 end
408 s.join(' AND ')
408 s.join(' AND ')
409 end
409 end
410 end
410 end
@@ -1,31 +1,31
1 xml.instruct!
1 xml.instruct!
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3 xml.title truncate_single_line(@title, 100)
3 xml.title truncate_single_line(@title, 100)
4 xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false))
4 xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false))
5 xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil))
5 xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil))
6 xml.id url_for(:controller => 'welcome', :only_path => false)
6 xml.id url_for(:controller => 'welcome', :only_path => false)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
8 xml.author { xml.name "#{Setting.app_title}" }
8 xml.author { xml.name "#{Setting.app_title}" }
9 xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
9 xml.generator(:uri => Redmine::Info.url) { xml.text! Redmine::Info.app_name; }
10 @items.each do |item|
10 @items.each do |item|
11 xml.entry do
11 xml.entry do
12 url = url_for(item.event_url(:only_path => false))
12 url = url_for(item.event_url(:only_path => false))
13 if @project
13 if @project
14 xml.title truncate_single_line(item.event_title, 100)
14 xml.title truncate_single_line(item.event_title, 100)
15 else
15 else
16 xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100)
16 xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100)
17 end
17 end
18 xml.link "rel" => "alternate", "href" => url
18 xml.link "rel" => "alternate", "href" => url
19 xml.id url
19 xml.id url
20 xml.updated item.event_datetime.xmlschema
20 xml.updated item.event_datetime.xmlschema
21 author = item.event_author if item.respond_to?(:event_author)
21 author = item.event_author if item.respond_to?(:event_author)
22 xml.author do
22 xml.author do
23 xml.name(author)
23 xml.name(author)
24 xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail
24 xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail
25 end if author
25 end if author
26 xml.content "type" => "html" do
26 xml.content "type" => "html" do
27 xml.text! textilizable(item.event_description)
27 xml.text! textilizable(item, :event_description, :only_path => false)
28 end
28 end
29 end
29 end
30 end
30 end
31 end
31 end
@@ -1,30 +1,30
1 xml.instruct!
1 xml.instruct!
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3 xml.title @title
3 xml.title @title
4 xml.link "rel" => "self", "href" => url_for(:format => 'atom', :key => User.current.rss_key, :only_path => false)
4 xml.link "rel" => "self", "href" => url_for(:format => 'atom', :key => User.current.rss_key, :only_path => false)
5 xml.link "rel" => "alternate", "href" => home_url(:only_path => false)
5 xml.link "rel" => "alternate", "href" => home_url(:only_path => false)
6 xml.id url_for(:controller => 'welcome', :only_path => false)
6 xml.id url_for(:controller => 'welcome', :only_path => false)
7 xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema)
7 xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema)
8 xml.author { xml.name "#{Setting.app_title}" }
8 xml.author { xml.name "#{Setting.app_title}" }
9 @journals.each do |change|
9 @journals.each do |change|
10 issue = change.issue
10 issue = change.issue
11 xml.entry do
11 xml.entry do
12 xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}"
12 xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}"
13 xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false)
13 xml.link "rel" => "alternate", "href" => url_for(:controller => 'issues' , :action => 'show', :id => issue, :only_path => false)
14 xml.id url_for(:controller => 'issues' , :action => 'show', :id => issue, :journal_id => change, :only_path => false)
14 xml.id url_for(:controller => 'issues' , :action => 'show', :id => issue, :journal_id => change, :only_path => false)
15 xml.updated change.created_on.xmlschema
15 xml.updated change.created_on.xmlschema
16 xml.author do
16 xml.author do
17 xml.name change.user.name
17 xml.name change.user.name
18 xml.email(change.user.mail) if change.user.is_a?(User) && !change.user.mail.blank? && !change.user.pref.hide_mail
18 xml.email(change.user.mail) if change.user.is_a?(User) && !change.user.mail.blank? && !change.user.pref.hide_mail
19 end
19 end
20 xml.content "type" => "html" do
20 xml.content "type" => "html" do
21 xml.text! '<ul>'
21 xml.text! '<ul>'
22 change.details.each do |detail|
22 change.details.each do |detail|
23 xml.text! '<li>' + show_detail(detail, false) + '</li>'
23 xml.text! '<li>' + show_detail(detail, false) + '</li>'
24 end
24 end
25 xml.text! '</ul>'
25 xml.text! '</ul>'
26 xml.text! textilizable(change.notes) unless change.notes.blank?
26 xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank?
27 end
27 end
28 end
28 end
29 end
29 end
30 end No newline at end of file
30 end
@@ -1,922 +1,924
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
10 * Incoming mail handler : Allow spaces between keywords and colon
11 * Do not require a non-word character after a comma in Redmine links
11 * Do not require a non-word character after a comma in Redmine links
12 * Include issue hyperlinks in reminder emails
12 * Include issue hyperlinks in reminder emails
13 * Fixed: 500 Internal Server Error is raised if add an empty comment to the news
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
14 * Fixed: Atom links for wiki pages are not correct
15 * Fixed: Atom feeds leak email address
15 * Fixed: Atom feeds leak email address
16 * Fixed: Case sensitivity in Issue filtering
17 * Fixed: When reading RSS feed, the inline-embedded images are not properly shown
16
18
17
19
18 == 2009-05-17 v0.8.4
20 == 2009-05-17 v0.8.4
19
21
20 * Allow textile mailto links
22 * Allow textile mailto links
21 * Fixed: memory consumption when uploading file
23 * Fixed: memory consumption when uploading file
22 * Fixed: Mercurial integration doesn't work if Redmine is installed in folder path containing space
24 * Fixed: Mercurial integration doesn't work if Redmine is installed in folder path containing space
23 * Fixed: an error is raised when no tab is available on project settings
25 * Fixed: an error is raised when no tab is available on project settings
24 * Fixed: insert image macro corrupts urls with excalamation marks
26 * Fixed: insert image macro corrupts urls with excalamation marks
25 * Fixed: error on cross-project gantt PNG export
27 * Fixed: error on cross-project gantt PNG export
26 * Fixed: self and alternate links in atom feeds do not respect Atom specs
28 * Fixed: self and alternate links in atom feeds do not respect Atom specs
27 * Fixed: accept any svn tunnel scheme in repository URL
29 * Fixed: accept any svn tunnel scheme in repository URL
28 * Fixed: issues/show should accept user's rss key
30 * Fixed: issues/show should accept user's rss key
29 * Fixed: consistency of custom fields display on the issue detail view
31 * Fixed: consistency of custom fields display on the issue detail view
30 * Fixed: wiki comments length validation is missing
32 * Fixed: wiki comments length validation is missing
31 * Fixed: weak autologin token generation algorithm causes duplicate tokens
33 * Fixed: weak autologin token generation algorithm causes duplicate tokens
32
34
33
35
34 == 2009-04-05 v0.8.3
36 == 2009-04-05 v0.8.3
35
37
36 * Separate project field and subject in cross-project issue view
38 * Separate project field and subject in cross-project issue view
37 * Ability to set language for redmine:load_default_data task using REDMINE_LANG environment variable
39 * Ability to set language for redmine:load_default_data task using REDMINE_LANG environment variable
38 * Rescue Redmine::DefaultData::DataAlreadyLoaded in redmine:load_default_data task
40 * Rescue Redmine::DefaultData::DataAlreadyLoaded in redmine:load_default_data task
39 * CSS classes to highlight own and assigned issues
41 * CSS classes to highlight own and assigned issues
40 * Hide "New file" link on wiki pages from printing
42 * Hide "New file" link on wiki pages from printing
41 * Flush buffer when asking for language in redmine:load_default_data task
43 * Flush buffer when asking for language in redmine:load_default_data task
42 * Minimum project identifier length set to 1
44 * Minimum project identifier length set to 1
43 * Include headers so that emails don't trigger vacation auto-responders
45 * Include headers so that emails don't trigger vacation auto-responders
44 * Fixed: Time entries csv export links for all projects are malformed
46 * Fixed: Time entries csv export links for all projects are malformed
45 * Fixed: Files without Version aren't visible in the Activity page
47 * Fixed: Files without Version aren't visible in the Activity page
46 * Fixed: Commit logs are centered in the repo browser
48 * Fixed: Commit logs are centered in the repo browser
47 * Fixed: News summary field content is not searchable
49 * Fixed: News summary field content is not searchable
48 * Fixed: Journal#save has a wrong signature
50 * Fixed: Journal#save has a wrong signature
49 * Fixed: Email footer signature convention
51 * Fixed: Email footer signature convention
50 * Fixed: Timelog report do not show time for non-versioned issues
52 * Fixed: Timelog report do not show time for non-versioned issues
51
53
52
54
53 == 2009-03-07 v0.8.2
55 == 2009-03-07 v0.8.2
54
56
55 * Send an email to the user when an administrator activates a registered user
57 * Send an email to the user when an administrator activates a registered user
56 * Strip keywords from received email body
58 * Strip keywords from received email body
57 * Footer updated to 2009
59 * Footer updated to 2009
58 * Show RSS-link even when no issues is found
60 * Show RSS-link even when no issues is found
59 * One click filter action in activity view
61 * One click filter action in activity view
60 * Clickable/linkable line #'s while browsing the repo or viewing a file
62 * Clickable/linkable line #'s while browsing the repo or viewing a file
61 * Links to versions on files list
63 * Links to versions on files list
62 * Added request and controller objects to the hooks by default
64 * Added request and controller objects to the hooks by default
63 * Fixed: exporting an issue with attachments to PDF raises an error
65 * Fixed: exporting an issue with attachments to PDF raises an error
64 * Fixed: "too few arguments" error may occur on activerecord error translation
66 * Fixed: "too few arguments" error may occur on activerecord error translation
65 * Fixed: "Default columns Displayed on the Issues list" setting is not easy to read
67 * Fixed: "Default columns Displayed on the Issues list" setting is not easy to read
66 * Fixed: visited links to closed tickets are not striked through with IE6
68 * Fixed: visited links to closed tickets are not striked through with IE6
67 * Fixed: MailHandler#plain_text_body returns nil if there was nothing to strip
69 * Fixed: MailHandler#plain_text_body returns nil if there was nothing to strip
68 * Fixed: MailHandler raises an error when processing an email without From header
70 * Fixed: MailHandler raises an error when processing an email without From header
69
71
70
72
71 == 2009-02-15 v0.8.1
73 == 2009-02-15 v0.8.1
72
74
73 * Select watchers on new issue form
75 * Select watchers on new issue form
74 * Issue description is no longer a required field
76 * Issue description is no longer a required field
75 * Files module: ability to add files without version
77 * Files module: ability to add files without version
76 * Jump to the current tab when using the project quick-jump combo
78 * Jump to the current tab when using the project quick-jump combo
77 * Display a warning if some attachments were not saved
79 * Display a warning if some attachments were not saved
78 * Import custom fields values from emails on issue creation
80 * Import custom fields values from emails on issue creation
79 * Show view/annotate/download links on entry and annotate views
81 * Show view/annotate/download links on entry and annotate views
80 * Admin Info Screen: Display if plugin assets directory is writable
82 * Admin Info Screen: Display if plugin assets directory is writable
81 * Adds a 'Create and continue' button on the new issue form
83 * Adds a 'Create and continue' button on the new issue form
82 * IMAP: add options to move received emails
84 * IMAP: add options to move received emails
83 * Do not show Category field when categories are not defined
85 * Do not show Category field when categories are not defined
84 * Lower the project identifier limit to a minimum of two characters
86 * Lower the project identifier limit to a minimum of two characters
85 * Add "closed" html class to closed entries in issue list
87 * Add "closed" html class to closed entries in issue list
86 * Fixed: broken redirect URL on login failure
88 * Fixed: broken redirect URL on login failure
87 * Fixed: Deleted files are shown when using Darcs
89 * Fixed: Deleted files are shown when using Darcs
88 * Fixed: Darcs adapter works on Win32 only
90 * Fixed: Darcs adapter works on Win32 only
89 * Fixed: syntax highlight doesn't appear in new ticket preview
91 * Fixed: syntax highlight doesn't appear in new ticket preview
90 * Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets
92 * Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets
91 * Fixed: no error is raised when entering invalid hours on the issue update form
93 * Fixed: no error is raised when entering invalid hours on the issue update form
92 * Fixed: Details time log report CSV export doesn't honour date format from settings
94 * Fixed: Details time log report CSV export doesn't honour date format from settings
93 * Fixed: invalid css classes on issue details
95 * Fixed: invalid css classes on issue details
94 * Fixed: Trac importer creates duplicate custom values
96 * Fixed: Trac importer creates duplicate custom values
95 * Fixed: inline attached image should not match partial filename
97 * Fixed: inline attached image should not match partial filename
96
98
97
99
98 == 2008-12-30 v0.8.0
100 == 2008-12-30 v0.8.0
99
101
100 * Setting added in order to limit the number of diff lines that should be displayed
102 * Setting added in order to limit the number of diff lines that should be displayed
101 * Makes logged-in username in topbar linking to
103 * Makes logged-in username in topbar linking to
102 * Mail handler: strip tags when receiving a html-only email
104 * Mail handler: strip tags when receiving a html-only email
103 * Mail handler: add watchers before sending notification
105 * Mail handler: add watchers before sending notification
104 * Adds a css class (overdue) to overdue issues on issue lists and detail views
106 * Adds a css class (overdue) to overdue issues on issue lists and detail views
105 * Fixed: project activity truncated after viewing user's activity
107 * Fixed: project activity truncated after viewing user's activity
106 * Fixed: email address entered for password recovery shouldn't be case-sensitive
108 * Fixed: email address entered for password recovery shouldn't be case-sensitive
107 * Fixed: default flag removed when editing a default enumeration
109 * Fixed: default flag removed when editing a default enumeration
108 * Fixed: default category ignored when adding a document
110 * Fixed: default category ignored when adding a document
109 * Fixed: error on repository user mapping when a repository username is blank
111 * Fixed: error on repository user mapping when a repository username is blank
110 * Fixed: Firefox cuts off large diffs
112 * Fixed: Firefox cuts off large diffs
111 * Fixed: CVS browser should not show dead revisions (deleted files)
113 * Fixed: CVS browser should not show dead revisions (deleted files)
112 * Fixed: escape double-quotes in image titles
114 * Fixed: escape double-quotes in image titles
113 * Fixed: escape textarea content when editing a issue note
115 * Fixed: escape textarea content when editing a issue note
114 * Fixed: JS error on context menu with IE
116 * Fixed: JS error on context menu with IE
115 * Fixed: bold syntax around single character in series doesn't work
117 * Fixed: bold syntax around single character in series doesn't work
116 * Fixed several XSS vulnerabilities
118 * Fixed several XSS vulnerabilities
117 * Fixed a SQL injection vulnerability
119 * Fixed a SQL injection vulnerability
118
120
119
121
120 == 2008-12-07 v0.8.0-rc1
122 == 2008-12-07 v0.8.0-rc1
121
123
122 * Wiki page protection
124 * Wiki page protection
123 * Wiki page hierarchy. Parent page can be assigned on the Rename screen
125 * Wiki page hierarchy. Parent page can be assigned on the Rename screen
124 * Adds support for issue creation via email
126 * Adds support for issue creation via email
125 * Adds support for free ticket filtering and custom queries on Gantt chart and calendar
127 * Adds support for free ticket filtering and custom queries on Gantt chart and calendar
126 * Cross-project search
128 * Cross-project search
127 * Ability to search a project and its subprojects
129 * Ability to search a project and its subprojects
128 * Ability to search the projects the user belongs to
130 * Ability to search the projects the user belongs to
129 * Adds custom fields on time entries
131 * Adds custom fields on time entries
130 * Adds boolean and list custom fields for time entries as criteria on time report
132 * Adds boolean and list custom fields for time entries as criteria on time report
131 * Cross-project time reports
133 * Cross-project time reports
132 * Display latest user's activity on account/show view
134 * Display latest user's activity on account/show view
133 * Show last connexion time on user's page
135 * Show last connexion time on user's page
134 * Obfuscates email address on user's account page using javascript
136 * Obfuscates email address on user's account page using javascript
135 * wiki TOC rendered as an unordered list
137 * wiki TOC rendered as an unordered list
136 * Adds the ability to search for a user on the administration users list
138 * Adds the ability to search for a user on the administration users list
137 * Adds the ability to search for a project name or identifier on the administration projects list
139 * Adds the ability to search for a project name or identifier on the administration projects list
138 * Redirect user to the previous page after logging in
140 * Redirect user to the previous page after logging in
139 * Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users
141 * Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users
140 * Adds permissions for viewing the watcher list and adding new watchers on the issue detail view
142 * Adds permissions for viewing the watcher list and adding new watchers on the issue detail view
141 * Adds permissions to let users edit and/or delete their messages
143 * Adds permissions to let users edit and/or delete their messages
142 * Link to activity view when displaying dates
144 * Link to activity view when displaying dates
143 * Hide Redmine version in atom feeds and pdf properties
145 * Hide Redmine version in atom feeds and pdf properties
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.
146 * 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.
145 * Sort users by their display names so that user dropdown lists are sorted alphabetically
147 * Sort users by their display names so that user dropdown lists are sorted alphabetically
146 * Adds estimated hours to issue filters
148 * Adds estimated hours to issue filters
147 * Switch order of current and previous revisions in side-by-side diff
149 * Switch order of current and previous revisions in side-by-side diff
148 * Render the commit changes list as a tree
150 * Render the commit changes list as a tree
149 * Adds watch/unwatch functionality at forum topic level
151 * Adds watch/unwatch functionality at forum topic level
150 * When moving an issue to another project, reassign it to the category with same name if any
152 * When moving an issue to another project, reassign it to the category with same name if any
151 * Adds child_pages macro for wiki pages
153 * Adds child_pages macro for wiki pages
152 * Use GET instead of POST on roadmap (#718), gantt and calendar forms
154 * Use GET instead of POST on roadmap (#718), gantt and calendar forms
153 * Search engine: display total results count and count by result type
155 * Search engine: display total results count and count by result type
154 * Email delivery configuration moved to an unversioned YAML file (config/email.yml, see the sample file)
156 * Email delivery configuration moved to an unversioned YAML file (config/email.yml, see the sample file)
155 * Adds icons on search results
157 * Adds icons on search results
156 * Adds 'Edit' link on account/show for admin users
158 * Adds 'Edit' link on account/show for admin users
157 * Adds Lock/Unlock/Activate link on user edit screen
159 * Adds Lock/Unlock/Activate link on user edit screen
158 * Adds user count in status drop down on admin user list
160 * Adds user count in status drop down on admin user list
159 * Adds multi-levels blockquotes support by using > at the beginning of lines
161 * Adds multi-levels blockquotes support by using > at the beginning of lines
160 * Adds a Reply link to each issue note
162 * Adds a Reply link to each issue note
161 * Adds plain text only option for mail notifications
163 * Adds plain text only option for mail notifications
162 * Gravatar support for issue detail, user grid, and activity stream (disabled by default)
164 * Gravatar support for issue detail, user grid, and activity stream (disabled by default)
163 * Adds 'Delete wiki pages attachments' permission
165 * Adds 'Delete wiki pages attachments' permission
164 * Show the most recent file when displaying an inline image
166 * Show the most recent file when displaying an inline image
165 * Makes permission screens localized
167 * Makes permission screens localized
166 * AuthSource list: display associated users count and disable 'Delete' buton if any
168 * AuthSource list: display associated users count and disable 'Delete' buton if any
167 * Make the 'duplicates of' relation asymmetric
169 * Make the 'duplicates of' relation asymmetric
168 * Adds username to the password reminder email
170 * Adds username to the password reminder email
169 * Adds links to forum messages using message#id syntax
171 * Adds links to forum messages using message#id syntax
170 * Allow same name for custom fields on different object types
172 * Allow same name for custom fields on different object types
171 * One-click bulk edition using the issue list context menu within the same project
173 * One-click bulk edition using the issue list context menu within the same project
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.
174 * 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.
173 * Adds checkboxes toggle links on permissions report
175 * Adds checkboxes toggle links on permissions report
174 * Adds Trac-Like anchors on wiki headings
176 * Adds Trac-Like anchors on wiki headings
175 * Adds support for wiki links with anchor
177 * Adds support for wiki links with anchor
176 * Adds category to the issue context menu
178 * Adds category to the issue context menu
177 * Adds a workflow overview screen
179 * Adds a workflow overview screen
178 * Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename
180 * Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename
179 * Dots allowed in custom field name
181 * Dots allowed in custom field name
180 * Adds posts quoting functionality
182 * Adds posts quoting functionality
181 * Adds an option to generate sequential project identifiers
183 * Adds an option to generate sequential project identifiers
182 * Adds mailto link on the user administration list
184 * Adds mailto link on the user administration list
183 * Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value
185 * Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value
184 * Gantt chart: display issues that don't have a due date if they are assigned to a version with a date
186 * Gantt chart: display issues that don't have a due date if they are assigned to a version with a date
185 * Change projects homepage limit to 255 chars
187 * Change projects homepage limit to 255 chars
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
188 * 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
187 * Adds "please select" to activity select box if no activity is set as default
189 * Adds "please select" to activity select box if no activity is set as default
188 * Do not silently ignore timelog validation failure on issue edit
190 * Do not silently ignore timelog validation failure on issue edit
189 * Adds a rake task to send reminder emails
191 * Adds a rake task to send reminder emails
190 * Allow empty cells in wiki tables
192 * Allow empty cells in wiki tables
191 * Makes wiki text formatter pluggable
193 * Makes wiki text formatter pluggable
192 * Adds back textile acronyms support
194 * Adds back textile acronyms support
193 * Remove pre tag attributes
195 * Remove pre tag attributes
194 * Plugin hooks
196 * Plugin hooks
195 * Pluggable admin menu
197 * Pluggable admin menu
196 * Plugins can provide activity content
198 * Plugins can provide activity content
197 * Moves plugin list to its own administration menu item
199 * Moves plugin list to its own administration menu item
198 * Adds url and author_url plugin attributes
200 * Adds url and author_url plugin attributes
199 * Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version
201 * Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version
200 * Adds atom feed on time entries details
202 * Adds atom feed on time entries details
201 * Adds project name to issues feed title
203 * Adds project name to issues feed title
202 * Adds a css class on menu items in order to apply item specific styles (eg. icons)
204 * Adds a css class on menu items in order to apply item specific styles (eg. icons)
203 * Adds a Redmine plugin generators
205 * Adds a Redmine plugin generators
204 * Adds timelog link to the issue context menu
206 * Adds timelog link to the issue context menu
205 * Adds links to the user page on various views
207 * Adds links to the user page on various views
206 * Turkish translation by Ismail Sezen
208 * Turkish translation by Ismail Sezen
207 * Catalan translation
209 * Catalan translation
208 * Vietnamese translation
210 * Vietnamese translation
209 * Slovak translation
211 * Slovak translation
210 * Better naming of activity feed if only one kind of event is displayed
212 * Better naming of activity feed if only one kind of event is displayed
211 * Enable syntax highlight on issues, messages and news
213 * Enable syntax highlight on issues, messages and news
212 * Add target version to the issue list context menu
214 * Add target version to the issue list context menu
213 * Hide 'Target version' filter if no version is defined
215 * Hide 'Target version' filter if no version is defined
214 * Add filters on cross-project issue list for custom fields marked as 'For all projects'
216 * Add filters on cross-project issue list for custom fields marked as 'For all projects'
215 * Turn ftp urls into links
217 * Turn ftp urls into links
216 * Hiding the View Differences button when a wiki page's history only has one version
218 * Hiding the View Differences button when a wiki page's history only has one version
217 * Messages on a Board can now be sorted by the number of replies
219 * Messages on a Board can now be sorted by the number of replies
218 * Adds a class ('me') to events of the activity view created by current user
220 * Adds a class ('me') to events of the activity view created by current user
219 * Strip pre/code tags content from activity view events
221 * Strip pre/code tags content from activity view events
220 * Display issue notes in the activity view
222 * Display issue notes in the activity view
221 * Adds links to changesets atom feed on repository browser
223 * Adds links to changesets atom feed on repository browser
222 * Track project and tracker changes in issue history
224 * Track project and tracker changes in issue history
223 * Adds anchor to atom feed messages links
225 * Adds anchor to atom feed messages links
224 * Adds a key in lang files to set the decimal separator (point or comma) in csv exports
226 * Adds a key in lang files to set the decimal separator (point or comma) in csv exports
225 * Makes importer work with Trac 0.8.x
227 * Makes importer work with Trac 0.8.x
226 * Upgraded to Prototype 1.6.0.1
228 * Upgraded to Prototype 1.6.0.1
227 * File viewer for attached text files
229 * File viewer for attached text files
228 * Menu mapper: add support for :before, :after and :last options to #push method and add #delete method
230 * Menu mapper: add support for :before, :after and :last options to #push method and add #delete method
229 * Removed inconsistent revision numbers on diff view
231 * Removed inconsistent revision numbers on diff view
230 * CVS: add support for modules names with spaces
232 * CVS: add support for modules names with spaces
231 * Log the user in after registration if account activation is not needed
233 * Log the user in after registration if account activation is not needed
232 * Mercurial adapter improvements
234 * Mercurial adapter improvements
233 * Trac importer: read session_attribute table to find user's email and real name
235 * Trac importer: read session_attribute table to find user's email and real name
234 * Ability to disable unused SCM adapters in application settings
236 * Ability to disable unused SCM adapters in application settings
235 * Adds Filesystem adapter
237 * Adds Filesystem adapter
236 * Clear changesets and changes with raw sql when deleting a repository for performance
238 * Clear changesets and changes with raw sql when deleting a repository for performance
237 * Redmine.pm now uses the 'commit access' permission defined in Redmine
239 * Redmine.pm now uses the 'commit access' permission defined in Redmine
238 * Reposman can create any type of scm (--scm option)
240 * Reposman can create any type of scm (--scm option)
239 * Reposman creates a repository if the 'repository' module is enabled at project level only
241 * Reposman creates a repository if the 'repository' module is enabled at project level only
240 * Display svn properties in the browser, svn >= 1.5.0 only
242 * Display svn properties in the browser, svn >= 1.5.0 only
241 * Reduces memory usage when importing large git repositories
243 * Reduces memory usage when importing large git repositories
242 * Wider SVG graphs in repository stats
244 * Wider SVG graphs in repository stats
243 * SubversionAdapter#entries performance improvement
245 * SubversionAdapter#entries performance improvement
244 * SCM browser: ability to download raw unified diffs
246 * SCM browser: ability to download raw unified diffs
245 * More detailed error message in log when scm command fails
247 * More detailed error message in log when scm command fails
246 * Adds support for file viewing with Darcs 2.0+
248 * Adds support for file viewing with Darcs 2.0+
247 * Check that git changeset is not in the database before creating it
249 * Check that git changeset is not in the database before creating it
248 * Unified diff viewer for attached files with .patch or .diff extension
250 * Unified diff viewer for attached files with .patch or .diff extension
249 * File size display with Bazaar repositories
251 * File size display with Bazaar repositories
250 * Git adapter: use commit time instead of author time
252 * Git adapter: use commit time instead of author time
251 * Prettier url for changesets
253 * Prettier url for changesets
252 * Makes changes link to entries on the revision view
254 * Makes changes link to entries on the revision view
253 * Adds a field on the repository view to browse at specific revision
255 * Adds a field on the repository view to browse at specific revision
254 * Adds new projects atom feed
256 * Adds new projects atom feed
255 * Added rake tasks to generate rcov code coverage reports
257 * Added rake tasks to generate rcov code coverage reports
256 * Add Redcloth's :block_markdown_rule to allow horizontal rules in wiki
258 * Add Redcloth's :block_markdown_rule to allow horizontal rules in wiki
257 * Show the project hierarchy in the drop down list for new membership on user administration screen
259 * Show the project hierarchy in the drop down list for new membership on user administration screen
258 * Split user edit screen into tabs
260 * Split user edit screen into tabs
259 * Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead
261 * Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead
260 * Fixed: Roadmap crashes when a version has a due date > 2037
262 * Fixed: Roadmap crashes when a version has a due date > 2037
261 * Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen
263 * Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen
262 * Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory
264 * Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory
263 * Fixed: logtime entry duplicated when edited from parent project
265 * Fixed: logtime entry duplicated when edited from parent project
264 * Fixed: wrong digest for text files under Windows
266 * Fixed: wrong digest for text files under Windows
265 * Fixed: associated revisions are displayed in wrong order on issue view
267 * Fixed: associated revisions are displayed in wrong order on issue view
266 * Fixed: Git Adapter date parsing ignores timezone
268 * Fixed: Git Adapter date parsing ignores timezone
267 * Fixed: Printing long roadmap doesn't split across pages
269 * Fixed: Printing long roadmap doesn't split across pages
268 * Fixes custom fields display order at several places
270 * Fixes custom fields display order at several places
269 * Fixed: urls containing @ are parsed as email adress by the wiki formatter
271 * Fixed: urls containing @ are parsed as email adress by the wiki formatter
270 * Fixed date filters accuracy with SQLite
272 * Fixed date filters accuracy with SQLite
271 * Fixed: tokens not escaped in highlight_tokens regexp
273 * Fixed: tokens not escaped in highlight_tokens regexp
272 * Fixed Bazaar shared repository browsing
274 * Fixed Bazaar shared repository browsing
273 * Fixes platform determination under JRuby
275 * Fixes platform determination under JRuby
274 * Fixed: Estimated time in issue's journal should be rounded to two decimals
276 * Fixed: Estimated time in issue's journal should be rounded to two decimals
275 * Fixed: 'search titles only' box ignored after one search is done on titles only
277 * Fixed: 'search titles only' box ignored after one search is done on titles only
276 * Fixed: non-ASCII subversion path can't be displayed
278 * Fixed: non-ASCII subversion path can't be displayed
277 * Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format
279 * Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format
278 * Fixed: document listing shows on "my page" when viewing documents is disabled for the role
280 * Fixed: document listing shows on "my page" when viewing documents is disabled for the role
279 * Fixed: Latest news appear on the homepage for projects with the News module disabled
281 * Fixed: Latest news appear on the homepage for projects with the News module disabled
280 * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled
282 * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled
281 * Fixed: the default status is lost when reordering issue statuses
283 * Fixed: the default status is lost when reordering issue statuses
282 * Fixes error with Postgresql and non-UTF8 commit logs
284 * Fixes error with Postgresql and non-UTF8 commit logs
283 * Fixed: textile footnotes no longer work
285 * Fixed: textile footnotes no longer work
284 * Fixed: http links containing parentheses fail to reder correctly
286 * Fixed: http links containing parentheses fail to reder correctly
285 * Fixed: GitAdapter#get_rev should use current branch instead of hardwiring master
287 * Fixed: GitAdapter#get_rev should use current branch instead of hardwiring master
286
288
287
289
288 == 2008-07-06 v0.7.3
290 == 2008-07-06 v0.7.3
289
291
290 * Allow dot in firstnames and lastnames
292 * Allow dot in firstnames and lastnames
291 * Add project name to cross-project Atom feeds
293 * Add project name to cross-project Atom feeds
292 * Encoding set to utf8 in example database.yml
294 * Encoding set to utf8 in example database.yml
293 * HTML titles on forums related views
295 * HTML titles on forums related views
294 * Fixed: various XSS vulnerabilities
296 * Fixed: various XSS vulnerabilities
295 * Fixed: Entourage (and some old client) fails to correctly render notification styles
297 * Fixed: Entourage (and some old client) fails to correctly render notification styles
296 * Fixed: Fixed: timelog redirects inappropriately when :back_url is blank
298 * Fixed: Fixed: timelog redirects inappropriately when :back_url is blank
297 * Fixed: wrong relative paths to images in wiki_syntax.html
299 * Fixed: wrong relative paths to images in wiki_syntax.html
298
300
299
301
300 == 2008-06-15 v0.7.2
302 == 2008-06-15 v0.7.2
301
303
302 * "New Project" link on Projects page
304 * "New Project" link on Projects page
303 * Links to repository directories on the repo browser
305 * Links to repository directories on the repo browser
304 * Move status to front in Activity View
306 * Move status to front in Activity View
305 * Remove edit step from Status context menu
307 * Remove edit step from Status context menu
306 * Fixed: No way to do textile horizontal rule
308 * Fixed: No way to do textile horizontal rule
307 * Fixed: Repository: View differences doesn't work
309 * Fixed: Repository: View differences doesn't work
308 * Fixed: attachement's name maybe invalid.
310 * Fixed: attachement's name maybe invalid.
309 * Fixed: Error when creating a new issue
311 * Fixed: Error when creating a new issue
310 * Fixed: NoMethodError on @available_filters.has_key?
312 * Fixed: NoMethodError on @available_filters.has_key?
311 * Fixed: Check All / Uncheck All in Email Settings
313 * Fixed: Check All / Uncheck All in Email Settings
312 * Fixed: "View differences" of one file at /repositories/revision/ fails
314 * Fixed: "View differences" of one file at /repositories/revision/ fails
313 * Fixed: Column width in "my page"
315 * Fixed: Column width in "my page"
314 * Fixed: private subprojects are listed on Issues view
316 * Fixed: private subprojects are listed on Issues view
315 * Fixed: Textile: bold, italics, underline, etc... not working after parentheses
317 * Fixed: Textile: bold, italics, underline, etc... not working after parentheses
316 * Fixed: Update issue form: comment field from log time end out of screen
318 * Fixed: Update issue form: comment field from log time end out of screen
317 * Fixed: Editing role: "issue can be assigned to this role" out of box
319 * Fixed: Editing role: "issue can be assigned to this role" out of box
318 * Fixed: Unable use angular braces after include word
320 * Fixed: Unable use angular braces after include word
319 * Fixed: Using '*' as keyword for repository referencing keywords doesn't work
321 * Fixed: Using '*' as keyword for repository referencing keywords doesn't work
320 * Fixed: Subversion repository "View differences" on each file rise ERROR
322 * Fixed: Subversion repository "View differences" on each file rise ERROR
321 * Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root
323 * Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root
322 * Fixed: It is possible to lock out the last admin account
324 * Fixed: It is possible to lock out the last admin account
323 * Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access
325 * Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access
324 * Fixed: Issue number display clipped on 'my issues'
326 * Fixed: Issue number display clipped on 'my issues'
325 * Fixed: Roadmap version list links not carrying state
327 * Fixed: Roadmap version list links not carrying state
326 * Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default
328 * Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default
327 * Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master"
329 * Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master"
328 * Fixed: browser's language subcodes ignored
330 * Fixed: browser's language subcodes ignored
329 * Fixed: Error on project selection with numeric (only) identifier.
331 * Fixed: Error on project selection with numeric (only) identifier.
330 * Fixed: Link to PDF doesn't work after creating new issue
332 * Fixed: Link to PDF doesn't work after creating new issue
331 * Fixed: "Replies" should not be shown on forum threads that are locked
333 * Fixed: "Replies" should not be shown on forum threads that are locked
332 * Fixed: SVN errors lead to svn username/password being displayed to end users (security issue)
334 * Fixed: SVN errors lead to svn username/password being displayed to end users (security issue)
333 * Fixed: http links containing hashes don't display correct
335 * Fixed: http links containing hashes don't display correct
334 * Fixed: Allow ampersands in Enumeration names
336 * Fixed: Allow ampersands in Enumeration names
335 * Fixed: Atom link on saved query does not include query_id
337 * Fixed: Atom link on saved query does not include query_id
336 * Fixed: Logtime info lost when there's an error updating an issue
338 * Fixed: Logtime info lost when there's an error updating an issue
337 * Fixed: TOC does not parse colorization markups
339 * Fixed: TOC does not parse colorization markups
338 * Fixed: CVS: add support for modules names with spaces
340 * Fixed: CVS: add support for modules names with spaces
339 * Fixed: Bad rendering on projects/add
341 * Fixed: Bad rendering on projects/add
340 * Fixed: exception when viewing differences on cvs
342 * Fixed: exception when viewing differences on cvs
341 * Fixed: export issue to pdf will messup when use Chinese language
343 * Fixed: export issue to pdf will messup when use Chinese language
342 * Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant
344 * Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant
343 * Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE
345 * Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE
344 * Fixed: Importing from trac : some wiki links are messed
346 * Fixed: Importing from trac : some wiki links are messed
345 * Fixed: Incorrect weekend definition in Hebrew calendar locale
347 * Fixed: Incorrect weekend definition in Hebrew calendar locale
346 * Fixed: Atom feeds don't provide author section for repository revisions
348 * Fixed: Atom feeds don't provide author section for repository revisions
347 * Fixed: In Activity views, changesets titles can be multiline while they should not
349 * Fixed: In Activity views, changesets titles can be multiline while they should not
348 * Fixed: Ignore unreadable subversion directories (read disabled using authz)
350 * Fixed: Ignore unreadable subversion directories (read disabled using authz)
349 * Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets
351 * Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets
350 * Fixed: Close statement handler in Redmine.pm
352 * Fixed: Close statement handler in Redmine.pm
351
353
352
354
353 == 2008-05-04 v0.7.1
355 == 2008-05-04 v0.7.1
354
356
355 * Thai translation added (Gampol Thitinilnithi)
357 * Thai translation added (Gampol Thitinilnithi)
356 * Translations updates
358 * Translations updates
357 * Escape HTML comment tags
359 * Escape HTML comment tags
358 * Prevent "can't convert nil into String" error when :sort_order param is not present
360 * Prevent "can't convert nil into String" error when :sort_order param is not present
359 * Fixed: Updating tickets add a time log with zero hours
361 * Fixed: Updating tickets add a time log with zero hours
360 * Fixed: private subprojects names are revealed on the project overview
362 * Fixed: private subprojects names are revealed on the project overview
361 * Fixed: Search for target version of "none" fails with postgres 8.3
363 * Fixed: Search for target version of "none" fails with postgres 8.3
362 * Fixed: Home, Logout, Login links shouldn't be absolute links
364 * Fixed: Home, Logout, Login links shouldn't be absolute links
363 * Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects
365 * Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects
364 * Fixed: error when using upcase language name in coderay
366 * Fixed: error when using upcase language name in coderay
365 * Fixed: error on Trac import when :due attribute is nil
367 * Fixed: error on Trac import when :due attribute is nil
366
368
367
369
368 == 2008-04-28 v0.7.0
370 == 2008-04-28 v0.7.0
369
371
370 * Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
372 * Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
371 * Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list.
373 * Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list.
372 * Add predefined date ranges to the time report
374 * Add predefined date ranges to the time report
373 * Time report can be done at issue level
375 * Time report can be done at issue level
374 * Various timelog report enhancements
376 * Various timelog report enhancements
375 * Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30
377 * Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30
376 * Display the context menu above and/or to the left of the click if needed
378 * Display the context menu above and/or to the left of the click if needed
377 * Make the admin project files list sortable
379 * Make the admin project files list sortable
378 * Mercurial: display working directory files sizes unless browsing a specific revision
380 * Mercurial: display working directory files sizes unless browsing a specific revision
379 * Preserve status filter and page number when using lock/unlock/activate links on the users list
381 * Preserve status filter and page number when using lock/unlock/activate links on the users list
380 * Redmine.pm support for LDAP authentication
382 * Redmine.pm support for LDAP authentication
381 * Better error message and AR errors in log for failed LDAP on-the-fly user creation
383 * Better error message and AR errors in log for failed LDAP on-the-fly user creation
382 * Redirected user to where he is coming from after logging hours
384 * Redirected user to where he is coming from after logging hours
383 * Warn user that subprojects are also deleted when deleting a project
385 * Warn user that subprojects are also deleted when deleting a project
384 * Include subprojects versions on calendar and gantt
386 * Include subprojects versions on calendar and gantt
385 * Notify project members when a message is posted if they want to receive notifications
387 * Notify project members when a message is posted if they want to receive notifications
386 * Fixed: Feed content limit setting has no effect
388 * Fixed: Feed content limit setting has no effect
387 * Fixed: Priorities not ordered when displayed as a filter in issue list
389 * Fixed: Priorities not ordered when displayed as a filter in issue list
388 * Fixed: can not display attached images inline in message replies
390 * Fixed: can not display attached images inline in message replies
389 * Fixed: Boards are not deleted when project is deleted
391 * Fixed: Boards are not deleted when project is deleted
390 * Fixed: trying to preview a new issue raises an exception with postgresql
392 * Fixed: trying to preview a new issue raises an exception with postgresql
391 * Fixed: single file 'View difference' links do not work because of duplicate slashes in url
393 * Fixed: single file 'View difference' links do not work because of duplicate slashes in url
392 * Fixed: inline image not displayed when including a wiki page
394 * Fixed: inline image not displayed when including a wiki page
393 * Fixed: CVS duplicate key violation
395 * Fixed: CVS duplicate key violation
394 * Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues
396 * Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues
395 * Fixed: custom field filters behaviour
397 * Fixed: custom field filters behaviour
396 * Fixed: Postgresql 8.3 compatibility
398 * Fixed: Postgresql 8.3 compatibility
397 * Fixed: Links to repository directories don't work
399 * Fixed: Links to repository directories don't work
398
400
399
401
400 == 2008-03-29 v0.7.0-rc1
402 == 2008-03-29 v0.7.0-rc1
401
403
402 * Overall activity view and feed added, link is available on the project list
404 * Overall activity view and feed added, link is available on the project list
403 * Git VCS support
405 * Git VCS support
404 * Rails 2.0 sessions cookie store compatibility
406 * Rails 2.0 sessions cookie store compatibility
405 * Use project identifiers in urls instead of ids
407 * Use project identifiers in urls instead of ids
406 * Default configuration data can now be loaded from the administration screen
408 * Default configuration data can now be loaded from the administration screen
407 * Administration settings screen split to tabs (email notifications options moved to 'Settings')
409 * Administration settings screen split to tabs (email notifications options moved to 'Settings')
408 * Project description is now unlimited and optional
410 * Project description is now unlimited and optional
409 * Wiki annotate view
411 * Wiki annotate view
410 * Escape HTML tag in textile content
412 * Escape HTML tag in textile content
411 * Add Redmine links to documents, versions, attachments and repository files
413 * Add Redmine links to documents, versions, attachments and repository files
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:
414 * 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:
413 * by using checkbox and/or the little pencil that will select/unselect all issues
415 * by using checkbox and/or the little pencil that will select/unselect all issues
414 * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues
416 * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues
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)
417 * 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)
416 * User display format is now configurable in administration settings
418 * User display format is now configurable in administration settings
417 * Issue list now supports bulk edit/move/delete (for a set of issues that belong to the same project)
419 * Issue list now supports bulk edit/move/delete (for a set of issues that belong to the same project)
418 * Merged 'change status', 'edit issue' and 'add note' actions:
420 * Merged 'change status', 'edit issue' and 'add note' actions:
419 * Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status
421 * Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status
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
422 * '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
421 * Details by assignees on issue summary view
423 * Details by assignees on issue summary view
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
424 * '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
423 * Change status select box default to current status
425 * Change status select box default to current status
424 * Preview for issue notes, news and messages
426 * Preview for issue notes, news and messages
425 * Optional description for attachments
427 * Optional description for attachments
426 * 'Fixed version' label changed to 'Target version'
428 * 'Fixed version' label changed to 'Target version'
427 * Let the user choose when deleting issues with reported hours to:
429 * Let the user choose when deleting issues with reported hours to:
428 * delete the hours
430 * delete the hours
429 * assign the hours to the project
431 * assign the hours to the project
430 * reassign the hours to another issue
432 * reassign the hours to another issue
431 * Date range filter and pagination on time entries detail view
433 * Date range filter and pagination on time entries detail view
432 * Propagate time tracking to the parent project
434 * Propagate time tracking to the parent project
433 * Switch added on the project activity view to include subprojects
435 * Switch added on the project activity view to include subprojects
434 * Display total estimated and spent hours on the version detail view
436 * Display total estimated and spent hours on the version detail view
435 * Weekly time tracking block for 'My page'
437 * Weekly time tracking block for 'My page'
436 * Permissions to edit time entries
438 * Permissions to edit time entries
437 * Include subprojects on the issue list, calendar, gantt and timelog by default (can be turned off is administration settings)
439 * Include subprojects on the issue list, calendar, gantt and timelog by default (can be turned off is administration settings)
438 * Roadmap enhancements (separate related issues from wiki contents, leading h1 in version wiki pages is hidden, smaller wiki headings)
440 * Roadmap enhancements (separate related issues from wiki contents, leading h1 in version wiki pages is hidden, smaller wiki headings)
439 * Make versions with same date sorted by name
441 * Make versions with same date sorted by name
440 * Allow issue list to be sorted by target version
442 * Allow issue list to be sorted by target version
441 * Related changesets messages displayed on the issue details view
443 * Related changesets messages displayed on the issue details view
442 * Create a journal and send an email when an issue is closed by commit
444 * Create a journal and send an email when an issue is closed by commit
443 * Add 'Author' to the available columns for the issue list
445 * Add 'Author' to the available columns for the issue list
444 * More appropriate default sort order on sortable columns
446 * More appropriate default sort order on sortable columns
445 * Add issue subject to the time entries view and issue subject, description and tracker to the csv export
447 * Add issue subject to the time entries view and issue subject, description and tracker to the csv export
446 * Permissions to edit issue notes
448 * Permissions to edit issue notes
447 * Display date/time instead of date on files list
449 * Display date/time instead of date on files list
448 * Do not show Roadmap menu item if the project doesn't define any versions
450 * Do not show Roadmap menu item if the project doesn't define any versions
449 * Allow longer version names (60 chars)
451 * Allow longer version names (60 chars)
450 * Ability to copy an existing workflow when creating a new role
452 * Ability to copy an existing workflow when creating a new role
451 * Display custom fields in two columns on the issue form
453 * Display custom fields in two columns on the issue form
452 * Added 'estimated time' in the csv export of the issue list
454 * Added 'estimated time' in the csv export of the issue list
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)
455 * Display the last 30 days on the activity view rather than the current month (number of days can be configured in the application settings)
454 * Setting for whether new projects should be public by default
456 * Setting for whether new projects should be public by default
455 * User preference to choose how comments/replies are displayed: in chronological or reverse chronological order
457 * User preference to choose how comments/replies are displayed: in chronological or reverse chronological order
456 * Added default value for custom fields
458 * Added default value for custom fields
457 * Added tabindex property on wiki toolbar buttons (to easily move from field to field using the tab key)
459 * Added tabindex property on wiki toolbar buttons (to easily move from field to field using the tab key)
458 * Redirect to issue page after creating a new issue
460 * Redirect to issue page after creating a new issue
459 * Wiki toolbar improvements (mainly for Firefox)
461 * Wiki toolbar improvements (mainly for Firefox)
460 * Display wiki syntax quick ref link on all wiki textareas
462 * Display wiki syntax quick ref link on all wiki textareas
461 * Display links to Atom feeds
463 * Display links to Atom feeds
462 * Breadcrumb nav for the forums
464 * Breadcrumb nav for the forums
463 * Show replies when choosing to display messages in the activity
465 * Show replies when choosing to display messages in the activity
464 * Added 'include' macro to include another wiki page
466 * Added 'include' macro to include another wiki page
465 * RedmineWikiFormatting page available as a static HTML file locally
467 * RedmineWikiFormatting page available as a static HTML file locally
466 * Wrap diff content
468 * Wrap diff content
467 * Strip out email address from authors in repository screens
469 * Strip out email address from authors in repository screens
468 * Highlight the current item of the main menu
470 * Highlight the current item of the main menu
469 * Added simple syntax highlighters for php and java languages
471 * Added simple syntax highlighters for php and java languages
470 * Do not show empty diffs
472 * Do not show empty diffs
471 * Show explicit error message when the scm command failed (eg. when svn binary is not available)
473 * Show explicit error message when the scm command failed (eg. when svn binary is not available)
472 * Lithuanian translation added (Sergej Jegorov)
474 * Lithuanian translation added (Sergej Jegorov)
473 * Ukrainan translation added (Natalia Konovka & Mykhaylo Sorochan)
475 * Ukrainan translation added (Natalia Konovka & Mykhaylo Sorochan)
474 * Danish translation added (Mads Vestergaard)
476 * Danish translation added (Mads Vestergaard)
475 * Added i18n support to the jstoolbar and various settings screen
477 * Added i18n support to the jstoolbar and various settings screen
476 * RedCloth's glyphs no longer user
478 * RedCloth's glyphs no longer user
477 * New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/)
479 * New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/)
478 * The following menus can now be extended by plugins: top_menu, account_menu, application_menu
480 * The following menus can now be extended by plugins: top_menu, account_menu, application_menu
479 * Added a simple rake task to fetch changesets from the repositories: rake redmine:fetch_changesets
481 * Added a simple rake task to fetch changesets from the repositories: rake redmine:fetch_changesets
480 * Remove hardcoded "Redmine" strings in account related emails and use application title instead
482 * Remove hardcoded "Redmine" strings in account related emails and use application title instead
481 * Mantis importer preserve bug ids
483 * Mantis importer preserve bug ids
482 * Trac importer: Trac guide wiki pages skipped
484 * Trac importer: Trac guide wiki pages skipped
483 * Trac importer: wiki attachments migration added
485 * Trac importer: wiki attachments migration added
484 * Trac importer: support database schema for Trac migration
486 * Trac importer: support database schema for Trac migration
485 * Trac importer: support CamelCase links
487 * Trac importer: support CamelCase links
486 * Removes the Redmine version from the footer (can be viewed on admin -> info)
488 * Removes the Redmine version from the footer (can be viewed on admin -> info)
487 * Rescue and display an error message when trying to delete a role that is in use
489 * Rescue and display an error message when trying to delete a role that is in use
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
490 * 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
489 * Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs
491 * Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs
490 * Fixed: Gantt and calendar not properly refreshed (fragment caching removed)
492 * Fixed: Gantt and calendar not properly refreshed (fragment caching removed)
491 * Fixed: Textile image with style attribute cause internal server error
493 * Fixed: Textile image with style attribute cause internal server error
492 * Fixed: wiki TOC not rendered properly when used in an issue or document description
494 * Fixed: wiki TOC not rendered properly when used in an issue or document description
493 * Fixed: 'has already been taken' error message on username and email fields if left empty
495 * Fixed: 'has already been taken' error message on username and email fields if left empty
494 * Fixed: non-ascii attachement filename with IE
496 * Fixed: non-ascii attachement filename with IE
495 * Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed
497 * Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed
496 * Fixed: search for all words doesn't work
498 * Fixed: search for all words doesn't work
497 * Fixed: Do not show sticky and locked checkboxes when replying to a message
499 * Fixed: Do not show sticky and locked checkboxes when replying to a message
498 * Fixed: Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank
500 * Fixed: Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank
499 * Fixed: Date custom fields not displayed as specified in application settings
501 * Fixed: Date custom fields not displayed as specified in application settings
500 * Fixed: titles not escaped in the activity view
502 * Fixed: titles not escaped in the activity view
501 * Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context
503 * Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context
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
504 * 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
503 * Fixed: locked users should not receive email notifications
505 * Fixed: locked users should not receive email notifications
504 * Fixed: custom field selection is not saved when unchecking them all on project settings
506 * Fixed: custom field selection is not saved when unchecking them all on project settings
505 * Fixed: can not lock a topic when creating it
507 * Fixed: can not lock a topic when creating it
506 * Fixed: Incorrect filtering for unset values when using 'is not' filter
508 * Fixed: Incorrect filtering for unset values when using 'is not' filter
507 * Fixed: PostgreSQL issues_seq_id not updated when using Trac importer
509 * Fixed: PostgreSQL issues_seq_id not updated when using Trac importer
508 * Fixed: ajax pagination does not scroll up
510 * Fixed: ajax pagination does not scroll up
509 * Fixed: error when uploading a file with no content-type specified by the browser
511 * Fixed: error when uploading a file with no content-type specified by the browser
510 * Fixed: wiki and changeset links not displayed when previewing issue description or notes
512 * Fixed: wiki and changeset links not displayed when previewing issue description or notes
511 * Fixed: 'LdapError: no bind result' error when authenticating
513 * Fixed: 'LdapError: no bind result' error when authenticating
512 * Fixed: 'LdapError: invalid binding information' when no username/password are set on the LDAP account
514 * Fixed: 'LdapError: invalid binding information' when no username/password are set on the LDAP account
513 * Fixed: CVS repository doesn't work if port is used in the url
515 * Fixed: CVS repository doesn't work if port is used in the url
514 * Fixed: Email notifications: host name is missing in generated links
516 * Fixed: Email notifications: host name is missing in generated links
515 * Fixed: Email notifications: referenced changesets, wiki pages, attachments... are not turned into links
517 * Fixed: Email notifications: referenced changesets, wiki pages, attachments... are not turned into links
516 * Fixed: Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed
518 * Fixed: Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed
517 * Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console
519 * Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console
518 * Fixed: Do not send an email with no recipient, cc or bcc
520 * Fixed: Do not send an email with no recipient, cc or bcc
519 * Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues.
521 * Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues.
520 * Fixed: Mercurial browsing under unix-like os and for directory depth > 2
522 * Fixed: Mercurial browsing under unix-like os and for directory depth > 2
521 * Fixed: Wiki links with pipe can not be used in wiki tables
523 * Fixed: Wiki links with pipe can not be used in wiki tables
522 * Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets
524 * Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets
523 * Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql
525 * Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql
524
526
525
527
526 == 2008-03-12 v0.6.4
528 == 2008-03-12 v0.6.4
527
529
528 * Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects
530 * Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects
529 * Fixed: potential LDAP authentication security flaw
531 * Fixed: potential LDAP authentication security flaw
530 * Fixed: context submenus on the issue list don't show up with IE6.
532 * Fixed: context submenus on the issue list don't show up with IE6.
531 * Fixed: Themes are not applied with Rails 2.0
533 * Fixed: Themes are not applied with Rails 2.0
532 * Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil
534 * Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil
533 * Fixed: Mercurial repository browsing
535 * Fixed: Mercurial repository browsing
534 * Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails
536 * Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails
535 * Fixed: not null constraints not removed with Postgresql
537 * Fixed: not null constraints not removed with Postgresql
536 * Doctype set to transitional
538 * Doctype set to transitional
537
539
538
540
539 == 2007-12-18 v0.6.3
541 == 2007-12-18 v0.6.3
540
542
541 * Fixed: upload doesn't work in 'Files' section
543 * Fixed: upload doesn't work in 'Files' section
542
544
543
545
544 == 2007-12-16 v0.6.2
546 == 2007-12-16 v0.6.2
545
547
546 * Search engine: issue custom fields can now be searched
548 * Search engine: issue custom fields can now be searched
547 * News comments are now textilized
549 * News comments are now textilized
548 * Updated Japanese translation (Satoru Kurashiki)
550 * Updated Japanese translation (Satoru Kurashiki)
549 * Updated Chinese translation (Shortie Lo)
551 * Updated Chinese translation (Shortie Lo)
550 * Fixed Rails 2.0 compatibility bugs:
552 * Fixed Rails 2.0 compatibility bugs:
551 * Unable to create a wiki
553 * Unable to create a wiki
552 * Gantt and calendar error
554 * Gantt and calendar error
553 * Trac importer error (readonly? is defined by ActiveRecord)
555 * Trac importer error (readonly? is defined by ActiveRecord)
554 * Fixed: 'assigned to me' filter broken
556 * Fixed: 'assigned to me' filter broken
555 * Fixed: crash when validation fails on issue edition with no custom fields
557 * Fixed: crash when validation fails on issue edition with no custom fields
556 * Fixed: reposman "can't find group" error
558 * Fixed: reposman "can't find group" error
557 * Fixed: 'LDAP account password is too long' error when leaving the field empty on creation
559 * Fixed: 'LDAP account password is too long' error when leaving the field empty on creation
558 * Fixed: empty lines when displaying repository files with Windows style eol
560 * Fixed: empty lines when displaying repository files with Windows style eol
559 * Fixed: missing body closing tag in repository annotate and entry views
561 * Fixed: missing body closing tag in repository annotate and entry views
560
562
561
563
562 == 2007-12-10 v0.6.1
564 == 2007-12-10 v0.6.1
563
565
564 * Rails 2.0 compatibility
566 * Rails 2.0 compatibility
565 * Custom fields can now be displayed as columns on the issue list
567 * Custom fields can now be displayed as columns on the issue list
566 * Added version details view (accessible from the roadmap)
568 * Added version details view (accessible from the roadmap)
567 * Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account)
569 * Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account)
568 * Added per-project tracker selection. Trackers can be selected on project settings
570 * Added per-project tracker selection. Trackers can be selected on project settings
569 * Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums
571 * Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums
570 * Forums: messages can now be edited/deleted (explicit permissions need to be given)
572 * Forums: messages can now be edited/deleted (explicit permissions need to be given)
571 * Forums: topics can be locked so that no reply can be added
573 * Forums: topics can be locked so that no reply can be added
572 * Forums: topics can be marked as sticky so that they always appear at the top of the list
574 * Forums: topics can be marked as sticky so that they always appear at the top of the list
573 * Forums: attachments can now be added to replies
575 * Forums: attachments can now be added to replies
574 * Added time zone support
576 * Added time zone support
575 * Added a setting to choose the account activation strategy (available in application settings)
577 * Added a setting to choose the account activation strategy (available in application settings)
576 * Added 'Classic' theme (inspired from the v0.51 design)
578 * Added 'Classic' theme (inspired from the v0.51 design)
577 * Added an alternate theme which provides issue list colorization based on issues priority
579 * Added an alternate theme which provides issue list colorization based on issues priority
578 * Added Bazaar SCM adapter
580 * Added Bazaar SCM adapter
579 * Added Annotate/Blame view in the repository browser (except for Darcs SCM)
581 * Added Annotate/Blame view in the repository browser (except for Darcs SCM)
580 * Diff style (inline or side by side) automatically saved as a user preference
582 * Diff style (inline or side by side) automatically saved as a user preference
581 * Added issues status changes on the activity view (by Cyril Mougel)
583 * Added issues status changes on the activity view (by Cyril Mougel)
582 * Added forums topics on the activity view (disabled by default)
584 * Added forums topics on the activity view (disabled by default)
583 * Added an option on 'My account' for users who don't want to be notified of changes that they make
585 * Added an option on 'My account' for users who don't want to be notified of changes that they make
584 * Trac importer now supports mysql and postgresql databases
586 * Trac importer now supports mysql and postgresql databases
585 * Trac importer improvements (by Mat Trudel)
587 * Trac importer improvements (by Mat Trudel)
586 * 'fixed version' field can now be displayed on the issue list
588 * 'fixed version' field can now be displayed on the issue list
587 * Added a couple of new formats for the 'date format' setting
589 * Added a couple of new formats for the 'date format' setting
588 * Added Traditional Chinese translation (by Shortie Lo)
590 * Added Traditional Chinese translation (by Shortie Lo)
589 * Added Russian translation (iGor kMeta)
591 * Added Russian translation (iGor kMeta)
590 * Project name format limitation removed (name can now contain any character)
592 * Project name format limitation removed (name can now contain any character)
591 * Project identifier maximum length changed from 12 to 20
593 * Project identifier maximum length changed from 12 to 20
592 * Changed the maximum length of LDAP account to 255 characters
594 * Changed the maximum length of LDAP account to 255 characters
593 * Removed the 12 characters limit on passwords
595 * Removed the 12 characters limit on passwords
594 * Added wiki macros support
596 * Added wiki macros support
595 * Performance improvement on workflow setup screen
597 * Performance improvement on workflow setup screen
596 * More detailed html title on several views
598 * More detailed html title on several views
597 * Custom fields can now be reordered
599 * Custom fields can now be reordered
598 * Search engine: search can be restricted to an exact phrase by using quotation marks
600 * Search engine: search can be restricted to an exact phrase by using quotation marks
599 * Added custom fields marked as 'For all projects' to the csv export of the cross project issue list
601 * Added custom fields marked as 'For all projects' to the csv export of the cross project issue list
600 * Email notifications are now sent as Blind carbon copy by default
602 * Email notifications are now sent as Blind carbon copy by default
601 * Fixed: all members (including non active) should be deleted when deleting a project
603 * Fixed: all members (including non active) should be deleted when deleting a project
602 * Fixed: Error on wiki syntax link (accessible from wiki/edit)
604 * Fixed: Error on wiki syntax link (accessible from wiki/edit)
603 * Fixed: 'quick jump to a revision' form on the revisions list
605 * Fixed: 'quick jump to a revision' form on the revisions list
604 * Fixed: error on admin/info if there's more than 1 plugin installed
606 * Fixed: error on admin/info if there's more than 1 plugin installed
605 * Fixed: svn or ldap password can be found in clear text in the html source in editing mode
607 * Fixed: svn or ldap password can be found in clear text in the html source in editing mode
606 * Fixed: 'Assigned to' drop down list is not sorted
608 * Fixed: 'Assigned to' drop down list is not sorted
607 * Fixed: 'View all issues' link doesn't work on issues/show
609 * Fixed: 'View all issues' link doesn't work on issues/show
608 * Fixed: error on account/register when validation fails
610 * Fixed: error on account/register when validation fails
609 * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter'
611 * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter'
610 * Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt)
612 * Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt)
611 * Fixed: Wrong feed URLs on the home page
613 * Fixed: Wrong feed URLs on the home page
612 * Fixed: Update of time entry fails when the issue has been moved to an other project
614 * Fixed: Update of time entry fails when the issue has been moved to an other project
613 * Fixed: Error when moving an issue without changing its tracker (Postgresql)
615 * Fixed: Error when moving an issue without changing its tracker (Postgresql)
614 * Fixed: Changes not recorded when using :pserver string (CVS adapter)
616 * Fixed: Changes not recorded when using :pserver string (CVS adapter)
615 * Fixed: admin should be able to move issues to any project
617 * Fixed: admin should be able to move issues to any project
616 * Fixed: adding an attachment is not possible when changing the status of an issue
618 * Fixed: adding an attachment is not possible when changing the status of an issue
617 * Fixed: No mime-types in documents/files downloading
619 * Fixed: No mime-types in documents/files downloading
618 * Fixed: error when sorting the messages if there's only one board for the project
620 * Fixed: error when sorting the messages if there's only one board for the project
619 * Fixed: 'me' doesn't appear in the drop down filters on a project issue list.
621 * Fixed: 'me' doesn't appear in the drop down filters on a project issue list.
620
622
621 == 2007-11-04 v0.6.0
623 == 2007-11-04 v0.6.0
622
624
623 * Permission model refactoring.
625 * Permission model refactoring.
624 * Permissions: there are now 2 builtin roles that can be used to specify permissions given to other users than members of projects
626 * Permissions: there are now 2 builtin roles that can be used to specify permissions given to other users than members of projects
625 * Permissions: some permissions (eg. browse the repository) can be removed for certain roles
627 * Permissions: some permissions (eg. browse the repository) can be removed for certain roles
626 * Permissions: modules (eg. issue tracking, news, documents...) can be enabled/disabled at project level
628 * Permissions: modules (eg. issue tracking, news, documents...) can be enabled/disabled at project level
627 * Added Mantis and Trac importers
629 * Added Mantis and Trac importers
628 * New application layout
630 * New application layout
629 * Added "Bulk edit" functionality on the issue list
631 * Added "Bulk edit" functionality on the issue list
630 * More flexible mail notifications settings at user level
632 * More flexible mail notifications settings at user level
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
633 * 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
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
634 * 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
633 * Added the ability to customize issue list columns (at application level or for each saved query)
635 * Added the ability to customize issue list columns (at application level or for each saved query)
634 * Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap
636 * Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap
635 * Added the ability to rename wiki pages (specific permission required)
637 * Added the ability to rename wiki pages (specific permission required)
636 * Search engines now supports pagination. Results are sorted in reverse chronological order
638 * Search engines now supports pagination. Results are sorted in reverse chronological order
637 * Added "Estimated hours" attribute on issues
639 * Added "Estimated hours" attribute on issues
638 * A category with assigned issue can now be deleted. 2 options are proposed: remove assignments or reassign issues to another category
640 * A category with assigned issue can now be deleted. 2 options are proposed: remove assignments or reassign issues to another category
639 * Forum notifications are now also sent to the authors of the thread, even if they donοΏ½t watch the board
641 * Forum notifications are now also sent to the authors of the thread, even if they donοΏ½t watch the board
640 * Added an application setting to specify the application protocol (http or https) used to generate urls in emails
642 * Added an application setting to specify the application protocol (http or https) used to generate urls in emails
641 * Gantt chart: now starts at the current month by default
643 * Gantt chart: now starts at the current month by default
642 * Gantt chart: month count and zoom factor are automatically saved as user preferences
644 * Gantt chart: month count and zoom factor are automatically saved as user preferences
643 * Wiki links can now refer to other project wikis
645 * Wiki links can now refer to other project wikis
644 * Added wiki index by date
646 * Added wiki index by date
645 * Added preview on add/edit issue form
647 * Added preview on add/edit issue form
646 * Emails footer can now be customized from the admin interface (Admin -> Email notifications)
648 * Emails footer can now be customized from the admin interface (Admin -> Email notifications)
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)
649 * 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)
648 * Calendar: first day of week can now be set in lang files
650 * Calendar: first day of week can now be set in lang files
649 * Automatic closing of duplicate issues
651 * Automatic closing of duplicate issues
650 * Added a cross-project issue list
652 * Added a cross-project issue list
651 * AJAXified the SCM browser (tree view)
653 * AJAXified the SCM browser (tree view)
652 * Pretty URL for the repository browser (Cyril Mougel)
654 * Pretty URL for the repository browser (Cyril Mougel)
653 * Search engine: added a checkbox to search titles only
655 * Search engine: added a checkbox to search titles only
654 * Added "% done" in the filter list
656 * Added "% done" in the filter list
655 * Enumerations: values can now be reordered and a default value can be specified (eg. default issue priority)
657 * Enumerations: values can now be reordered and a default value can be specified (eg. default issue priority)
656 * Added some accesskeys
658 * Added some accesskeys
657 * Added "Float" as a custom field format
659 * Added "Float" as a custom field format
658 * Added basic Theme support
660 * Added basic Theme support
659 * Added the ability to set the οΏ½done ratioοΏ½ of issues fixed by commit (Nikolay Solakov)
661 * Added the ability to set the οΏ½done ratioοΏ½ of issues fixed by commit (Nikolay Solakov)
660 * Added custom fields in issue related mail notifications
662 * Added custom fields in issue related mail notifications
661 * Email notifications are now sent in plain text and html
663 * Email notifications are now sent in plain text and html
662 * Gantt chart can now be exported to a graphic file (png). This functionality is only available if RMagick is installed.
664 * Gantt chart can now be exported to a graphic file (png). This functionality is only available if RMagick is installed.
663 * Added syntax highlightment for repository files and wiki
665 * Added syntax highlightment for repository files and wiki
664 * Improved automatic Redmine links
666 * Improved automatic Redmine links
665 * Added automatic table of content support on wiki pages
667 * Added automatic table of content support on wiki pages
666 * Added radio buttons on the documents list to sort documents by category, date, title or author
668 * Added radio buttons on the documents list to sort documents by category, date, title or author
667 * Added basic plugin support, with a sample plugin
669 * Added basic plugin support, with a sample plugin
668 * Added a link to add a new category when creating or editing an issue
670 * Added a link to add a new category when creating or editing an issue
669 * Added a "Assignable" boolean on the Role model. If unchecked, issues can not be assigned to users having this role.
671 * Added a "Assignable" boolean on the Role model. If unchecked, issues can not be assigned to users having this role.
670 * Added an option to be able to relate issues in different projects
672 * Added an option to be able to relate issues in different projects
671 * Added the ability to move issues (to another project) without changing their trackers.
673 * Added the ability to move issues (to another project) without changing their trackers.
672 * Atom feeds added on project activity, news and changesets
674 * Atom feeds added on project activity, news and changesets
673 * Added the ability to reset its own RSS access key
675 * Added the ability to reset its own RSS access key
674 * Main project list now displays root projects with their subprojects
676 * Main project list now displays root projects with their subprojects
675 * Added anchor links to issue notes
677 * Added anchor links to issue notes
676 * Added reposman Ruby version. This script can now register created repositories in Redmine (Nicolas Chuche)
678 * Added reposman Ruby version. This script can now register created repositories in Redmine (Nicolas Chuche)
677 * Issue notes are now included in search
679 * Issue notes are now included in search
678 * Added email sending test functionality
680 * Added email sending test functionality
679 * Added LDAPS support for LDAP authentication
681 * Added LDAPS support for LDAP authentication
680 * Removed hard-coded URLs in mail templates
682 * Removed hard-coded URLs in mail templates
681 * Subprojects are now grouped by projects in the navigation drop-down menu
683 * Subprojects are now grouped by projects in the navigation drop-down menu
682 * Added a new value for date filters: this week
684 * Added a new value for date filters: this week
683 * Added cache for application settings
685 * Added cache for application settings
684 * Added Polish translation (Tomasz Gawryl)
686 * Added Polish translation (Tomasz Gawryl)
685 * Added Czech translation (Jan Kadlecek)
687 * Added Czech translation (Jan Kadlecek)
686 * Added Romanian translation (Csongor Bartus)
688 * Added Romanian translation (Csongor Bartus)
687 * Added Hebrew translation (Bob Builder)
689 * Added Hebrew translation (Bob Builder)
688 * Added Serbian translation (Dragan Matic)
690 * Added Serbian translation (Dragan Matic)
689 * Added Korean translation (Choi Jong Yoon)
691 * Added Korean translation (Choi Jong Yoon)
690 * Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations
692 * Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations
691 * Performance improvement on calendar and gantt
693 * Performance improvement on calendar and gantt
692 * Fixed: wiki preview doesnοΏ½t work on long entries
694 * Fixed: wiki preview doesnοΏ½t work on long entries
693 * Fixed: queries with multiple custom fields return no result
695 * Fixed: queries with multiple custom fields return no result
694 * Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters
696 * Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters
695 * Fixed: URL with ~ broken in wiki formatting
697 * Fixed: URL with ~ broken in wiki formatting
696 * Fixed: some quotation marks are rendered as strange characters in pdf
698 * Fixed: some quotation marks are rendered as strange characters in pdf
697
699
698
700
699 == 2007-07-15 v0.5.1
701 == 2007-07-15 v0.5.1
700
702
701 * per project forums added
703 * per project forums added
702 * added the ability to archive projects
704 * added the ability to archive projects
703 * added οΏ½WatchοΏ½ functionality on issues. It allows users to receive notifications about issue changes
705 * added οΏ½WatchοΏ½ functionality on issues. It allows users to receive notifications about issue changes
704 * custom fields for issues can now be used as filters on issue list
706 * custom fields for issues can now be used as filters on issue list
705 * added per user custom queries
707 * added per user custom queries
706 * commit messages are now scanned for referenced or fixed issue IDs (keywords defined in Admin -> Settings)
708 * commit messages are now scanned for referenced or fixed issue IDs (keywords defined in Admin -> Settings)
707 * projects list now shows the list of public projects and private projects for which the user is a member
709 * projects list now shows the list of public projects and private projects for which the user is a member
708 * versions can now be created with no date
710 * versions can now be created with no date
709 * added issue count details for versions on Reports view
711 * added issue count details for versions on Reports view
710 * added time report, by member/activity/tracker/version and year/month/week for the selected period
712 * added time report, by member/activity/tracker/version and year/month/week for the selected period
711 * each category can now be associated to a user, so that new issues in that category are automatically assigned to that user
713 * each category can now be associated to a user, so that new issues in that category are automatically assigned to that user
712 * added autologin feature (disabled by default)
714 * added autologin feature (disabled by default)
713 * optimistic locking added for wiki edits
715 * optimistic locking added for wiki edits
714 * added wiki diff
716 * added wiki diff
715 * added the ability to destroy wiki pages (requires permission)
717 * added the ability to destroy wiki pages (requires permission)
716 * a wiki page can now be attached to each version, and displayed on the roadmap
718 * a wiki page can now be attached to each version, and displayed on the roadmap
717 * attachments can now be added to wiki pages (original patch by Pavol Murin) and displayed online
719 * attachments can now be added to wiki pages (original patch by Pavol Murin) and displayed online
718 * added an option to see all versions in the roadmap view (including completed ones)
720 * added an option to see all versions in the roadmap view (including completed ones)
719 * added basic issue relations
721 * added basic issue relations
720 * added the ability to log time when changing an issue status
722 * added the ability to log time when changing an issue status
721 * account information can now be sent to the user when creating an account
723 * account information can now be sent to the user when creating an account
722 * author and assignee of an issue always receive notifications (even if they turned of mail notifications)
724 * author and assignee of an issue always receive notifications (even if they turned of mail notifications)
723 * added a quick search form in page header
725 * added a quick search form in page header
724 * added 'me' value for 'assigned to' and 'author' query filters
726 * added 'me' value for 'assigned to' and 'author' query filters
725 * added a link on revision screen to see the entire diff for the revision
727 * added a link on revision screen to see the entire diff for the revision
726 * added last commit message for each entry in repository browser
728 * added last commit message for each entry in repository browser
727 * added the ability to view a file diff with free to/from revision selection.
729 * added the ability to view a file diff with free to/from revision selection.
728 * text files can now be viewed online when browsing the repository
730 * text files can now be viewed online when browsing the repository
729 * added basic support for other SCM: CVS (Ralph Vater), Mercurial and Darcs
731 * added basic support for other SCM: CVS (Ralph Vater), Mercurial and Darcs
730 * added fragment caching for svn diffs
732 * added fragment caching for svn diffs
731 * added fragment caching for calendar and gantt views
733 * added fragment caching for calendar and gantt views
732 * login field automatically focused on login form
734 * login field automatically focused on login form
733 * subproject name displayed on issue list, calendar and gantt
735 * subproject name displayed on issue list, calendar and gantt
734 * added an option to choose the date format: language based or ISO 8601
736 * added an option to choose the date format: language based or ISO 8601
735 * added a simple mail handler. It lets users add notes to an existing issue by replying to the initial notification email.
737 * added a simple mail handler. It lets users add notes to an existing issue by replying to the initial notification email.
736 * a 403 error page is now displayed (instead of a blank page) when trying to access a protected page
738 * a 403 error page is now displayed (instead of a blank page) when trying to access a protected page
737 * added portuguese translation (Joao Carlos Clementoni)
739 * added portuguese translation (Joao Carlos Clementoni)
738 * added partial online help japanese translation (Ken Date)
740 * added partial online help japanese translation (Ken Date)
739 * added bulgarian translation (Nikolay Solakov)
741 * added bulgarian translation (Nikolay Solakov)
740 * added dutch translation (Linda van den Brink)
742 * added dutch translation (Linda van den Brink)
741 * added swedish translation (Thomas Habets)
743 * added swedish translation (Thomas Habets)
742 * italian translation update (Alessio Spadaro)
744 * italian translation update (Alessio Spadaro)
743 * japanese translation update (Satoru Kurashiki)
745 * japanese translation update (Satoru Kurashiki)
744 * fixed: error on history atom feed when thereοΏ½s no notes on an issue change
746 * fixed: error on history atom feed when thereοΏ½s no notes on an issue change
745 * fixed: error in journalizing an issue with longtext custom fields (Postgresql)
747 * fixed: error in journalizing an issue with longtext custom fields (Postgresql)
746 * fixed: creation of Oracle schema
748 * fixed: creation of Oracle schema
747 * fixed: last day of the month not included in project activity
749 * fixed: last day of the month not included in project activity
748 * fixed: files with an apostrophe in their names can't be accessed in SVN repository
750 * fixed: files with an apostrophe in their names can't be accessed in SVN repository
749 * fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000)
751 * fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000)
750 * fixed: open/closed issue counts are always 0 on reports view (postgresql)
752 * fixed: open/closed issue counts are always 0 on reports view (postgresql)
751 * fixed: date query filters (wrong results and sql error with postgresql)
753 * fixed: date query filters (wrong results and sql error with postgresql)
752 * fixed: confidentiality issue on account/show (private project names displayed to anyone)
754 * fixed: confidentiality issue on account/show (private project names displayed to anyone)
753 * fixed: Long text custom fields displayed without line breaks
755 * fixed: Long text custom fields displayed without line breaks
754 * fixed: Error when editing the wokflow after deleting a status
756 * fixed: Error when editing the wokflow after deleting a status
755 * fixed: SVN commit dates are now stored as local time
757 * fixed: SVN commit dates are now stored as local time
756
758
757
759
758 == 2007-04-11 v0.5.0
760 == 2007-04-11 v0.5.0
759
761
760 * added per project Wiki
762 * added per project Wiki
761 * added rss/atom feeds at project level (custom queries can be used as feeds)
763 * added rss/atom feeds at project level (custom queries can be used as feeds)
762 * added search engine (search in issues, news, commits, wiki pages, documents)
764 * added search engine (search in issues, news, commits, wiki pages, documents)
763 * simple time tracking functionality added
765 * simple time tracking functionality added
764 * added version due dates on calendar and gantt
766 * added version due dates on calendar and gantt
765 * added subprojects issue count on project Reports page
767 * added subprojects issue count on project Reports page
766 * added the ability to copy an existing workflow when creating a new tracker
768 * added the ability to copy an existing workflow when creating a new tracker
767 * added the ability to include subprojects on calendar and gantt
769 * added the ability to include subprojects on calendar and gantt
768 * added the ability to select trackers to display on calendar and gantt (Jeffrey Jones)
770 * added the ability to select trackers to display on calendar and gantt (Jeffrey Jones)
769 * added side by side svn diff view (Cyril Mougel)
771 * added side by side svn diff view (Cyril Mougel)
770 * added back subproject filter on issue list
772 * added back subproject filter on issue list
771 * added permissions report in admin area
773 * added permissions report in admin area
772 * added a status filter on users list
774 * added a status filter on users list
773 * support for password-protected SVN repositories
775 * support for password-protected SVN repositories
774 * SVN commits are now stored in the database
776 * SVN commits are now stored in the database
775 * added simple svn statistics SVG graphs
777 * added simple svn statistics SVG graphs
776 * progress bars for roadmap versions (Nick Read)
778 * progress bars for roadmap versions (Nick Read)
777 * issue history now shows file uploads and deletions
779 * issue history now shows file uploads and deletions
778 * #id patterns are turned into links to issues in descriptions and commit messages
780 * #id patterns are turned into links to issues in descriptions and commit messages
779 * japanese translation added (Satoru Kurashiki)
781 * japanese translation added (Satoru Kurashiki)
780 * chinese simplified translation added (Andy Wu)
782 * chinese simplified translation added (Andy Wu)
781 * italian translation added (Alessio Spadaro)
783 * italian translation added (Alessio Spadaro)
782 * added scripts to manage SVN repositories creation and user access control using ssh+svn (Nicolas Chuche)
784 * added scripts to manage SVN repositories creation and user access control using ssh+svn (Nicolas Chuche)
783 * better calendar rendering time
785 * better calendar rendering time
784 * fixed migration scripts to work with mysql 5 running in strict mode
786 * fixed migration scripts to work with mysql 5 running in strict mode
785 * fixed: error when clicking "add" with no block selected on my/page_layout
787 * fixed: error when clicking "add" with no block selected on my/page_layout
786 * fixed: hard coded links in navigation bar
788 * fixed: hard coded links in navigation bar
787 * fixed: table_name pre/suffix support
789 * fixed: table_name pre/suffix support
788
790
789
791
790 == 2007-02-18 v0.4.2
792 == 2007-02-18 v0.4.2
791
793
792 * Rails 1.2 is now required
794 * Rails 1.2 is now required
793 * settings are now stored in the database and editable through the application in: Admin -> Settings (config_custom.rb is no longer used)
795 * settings are now stored in the database and editable through the application in: Admin -> Settings (config_custom.rb is no longer used)
794 * added project roadmap view
796 * added project roadmap view
795 * mail notifications added when a document, a file or an attachment is added
797 * mail notifications added when a document, a file or an attachment is added
796 * tooltips added on Gantt chart and calender to view the details of the issues
798 * tooltips added on Gantt chart and calender to view the details of the issues
797 * ability to set the sort order for roles, trackers, issue statuses
799 * ability to set the sort order for roles, trackers, issue statuses
798 * added missing fields to csv export: priority, start date, due date, done ratio
800 * added missing fields to csv export: priority, start date, due date, done ratio
799 * added total number of issues per tracker on project overview
801 * added total number of issues per tracker on project overview
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-)
802 * 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-)
801 * added back "fixed version" field on issue screen and in filters
803 * added back "fixed version" field on issue screen and in filters
802 * project settings screen split in 4 tabs
804 * project settings screen split in 4 tabs
803 * custom fields screen split in 3 tabs (one for each kind of custom field)
805 * custom fields screen split in 3 tabs (one for each kind of custom field)
804 * multiple issues pdf export now rendered as a table
806 * multiple issues pdf export now rendered as a table
805 * added a button on users/list to manually activate an account
807 * added a button on users/list to manually activate an account
806 * added a setting option to disable "password lost" functionality
808 * added a setting option to disable "password lost" functionality
807 * added a setting option to set max number of issues in csv/pdf exports
809 * added a setting option to set max number of issues in csv/pdf exports
808 * fixed: subprojects count is always 0 on projects list
810 * fixed: subprojects count is always 0 on projects list
809 * fixed: locked users are proposed when adding a member to a project
811 * fixed: locked users are proposed when adding a member to a project
810 * fixed: setting an issue status as default status leads to an sql error with SQLite
812 * fixed: setting an issue status as default status leads to an sql error with SQLite
811 * fixed: unable to delete an issue status even if it's not used yet
813 * fixed: unable to delete an issue status even if it's not used yet
812 * fixed: filters ignored when exporting a predefined query to csv/pdf
814 * fixed: filters ignored when exporting a predefined query to csv/pdf
813 * fixed: crash when french "issue_edit" email notification is sent
815 * fixed: crash when french "issue_edit" email notification is sent
814 * fixed: hide mail preference not saved (my/account)
816 * fixed: hide mail preference not saved (my/account)
815 * fixed: crash when a new user try to edit its "my page" layout
817 * fixed: crash when a new user try to edit its "my page" layout
816
818
817
819
818 == 2007-01-03 v0.4.1
820 == 2007-01-03 v0.4.1
819
821
820 * fixed: emails have no recipient when one of the project members has notifications disabled
822 * fixed: emails have no recipient when one of the project members has notifications disabled
821
823
822
824
823 == 2007-01-02 v0.4.0
825 == 2007-01-02 v0.4.0
824
826
825 * simple SVN browser added (just needs svn binaries in PATH)
827 * simple SVN browser added (just needs svn binaries in PATH)
826 * comments can now be added on news
828 * comments can now be added on news
827 * "my page" is now customizable
829 * "my page" is now customizable
828 * more powerfull and savable filters for issues lists
830 * more powerfull and savable filters for issues lists
829 * improved issues change history
831 * improved issues change history
830 * new functionality: move an issue to another project or tracker
832 * new functionality: move an issue to another project or tracker
831 * new functionality: add a note to an issue
833 * new functionality: add a note to an issue
832 * new report: project activity
834 * new report: project activity
833 * "start date" and "% done" fields added on issues
835 * "start date" and "% done" fields added on issues
834 * project calendar added
836 * project calendar added
835 * gantt chart added (exportable to pdf)
837 * gantt chart added (exportable to pdf)
836 * single/multiple issues pdf export added
838 * single/multiple issues pdf export added
837 * issues reports improvements
839 * issues reports improvements
838 * multiple file upload for issues, documents and files
840 * multiple file upload for issues, documents and files
839 * option to set maximum size of uploaded files
841 * option to set maximum size of uploaded files
840 * textile formating of issue and news descritions (RedCloth required)
842 * textile formating of issue and news descritions (RedCloth required)
841 * integration of DotClear jstoolbar for textile formatting
843 * integration of DotClear jstoolbar for textile formatting
842 * calendar date picker for date fields (LGPL DHTML Calendar http://sourceforge.net/projects/jscalendar)
844 * calendar date picker for date fields (LGPL DHTML Calendar http://sourceforge.net/projects/jscalendar)
843 * new filter in issues list: Author
845 * new filter in issues list: Author
844 * ajaxified paginators
846 * ajaxified paginators
845 * news rss feed added
847 * news rss feed added
846 * option to set number of results per page on issues list
848 * option to set number of results per page on issues list
847 * localized csv separator (comma/semicolon)
849 * localized csv separator (comma/semicolon)
848 * csv output encoded to ISO-8859-1
850 * csv output encoded to ISO-8859-1
849 * user custom field displayed on account/show
851 * user custom field displayed on account/show
850 * default configuration improved (default roles, trackers, status, permissions and workflows)
852 * default configuration improved (default roles, trackers, status, permissions and workflows)
851 * language for default configuration data can now be chosen when running 'load_default_data' task
853 * language for default configuration data can now be chosen when running 'load_default_data' task
852 * javascript added on custom field form to show/hide fields according to the format of custom field
854 * javascript added on custom field form to show/hide fields according to the format of custom field
853 * fixed: custom fields not in csv exports
855 * fixed: custom fields not in csv exports
854 * fixed: project settings now displayed according to user's permissions
856 * fixed: project settings now displayed according to user's permissions
855 * fixed: application error when no version is selected on projects/add_file
857 * fixed: application error when no version is selected on projects/add_file
856 * fixed: public actions not authorized for members of non public projects
858 * fixed: public actions not authorized for members of non public projects
857 * fixed: non public projects were shown on welcome screen even if current user is not a member
859 * fixed: non public projects were shown on welcome screen even if current user is not a member
858
860
859
861
860 == 2006-10-08 v0.3.0
862 == 2006-10-08 v0.3.0
861
863
862 * user authentication against multiple LDAP (optional)
864 * user authentication against multiple LDAP (optional)
863 * token based "lost password" functionality
865 * token based "lost password" functionality
864 * user self-registration functionality (optional)
866 * user self-registration functionality (optional)
865 * custom fields now available for issues, users and projects
867 * custom fields now available for issues, users and projects
866 * new custom field format "text" (displayed as a textarea field)
868 * new custom field format "text" (displayed as a textarea field)
867 * project & administration drop down menus in navigation bar for quicker access
869 * project & administration drop down menus in navigation bar for quicker access
868 * text formatting is preserved for long text fields (issues, projects and news descriptions)
870 * text formatting is preserved for long text fields (issues, projects and news descriptions)
869 * urls and emails are turned into clickable links in long text fields
871 * urls and emails are turned into clickable links in long text fields
870 * "due date" field added on issues
872 * "due date" field added on issues
871 * tracker selection filter added on change log
873 * tracker selection filter added on change log
872 * Localization plugin replaced with GLoc 1.1.0 (iconv required)
874 * Localization plugin replaced with GLoc 1.1.0 (iconv required)
873 * error messages internationalization
875 * error messages internationalization
874 * german translation added (thanks to Karim Trott)
876 * german translation added (thanks to Karim Trott)
875 * data locking for issues to prevent update conflicts (using ActiveRecord builtin optimistic locking)
877 * data locking for issues to prevent update conflicts (using ActiveRecord builtin optimistic locking)
876 * new filter in issues list: "Fixed version"
878 * new filter in issues list: "Fixed version"
877 * active filters are displayed with colored background on issues list
879 * active filters are displayed with colored background on issues list
878 * custom configuration is now defined in config/config_custom.rb
880 * custom configuration is now defined in config/config_custom.rb
879 * user object no more stored in session (only user_id)
881 * user object no more stored in session (only user_id)
880 * news summary field is no longer required
882 * news summary field is no longer required
881 * tables and forms redesign
883 * tables and forms redesign
882 * Fixed: boolean custom field not working
884 * Fixed: boolean custom field not working
883 * Fixed: error messages for custom fields are not displayed
885 * Fixed: error messages for custom fields are not displayed
884 * Fixed: invalid custom fields should have a red border
886 * Fixed: invalid custom fields should have a red border
885 * Fixed: custom fields values are not validated on issue update
887 * Fixed: custom fields values are not validated on issue update
886 * Fixed: unable to choose an empty value for 'List' custom fields
888 * Fixed: unable to choose an empty value for 'List' custom fields
887 * Fixed: no issue categories sorting
889 * Fixed: no issue categories sorting
888 * Fixed: incorrect versions sorting
890 * Fixed: incorrect versions sorting
889
891
890
892
891 == 2006-07-12 - v0.2.2
893 == 2006-07-12 - v0.2.2
892
894
893 * Fixed: bug in "issues list"
895 * Fixed: bug in "issues list"
894
896
895
897
896 == 2006-07-09 - v0.2.1
898 == 2006-07-09 - v0.2.1
897
899
898 * new databases supported: Oracle, PostgreSQL, SQL Server
900 * new databases supported: Oracle, PostgreSQL, SQL Server
899 * projects/subprojects hierarchy (1 level of subprojects only)
901 * projects/subprojects hierarchy (1 level of subprojects only)
900 * environment information display in admin/info
902 * environment information display in admin/info
901 * more filter options in issues list (rev6)
903 * more filter options in issues list (rev6)
902 * default language based on browser settings (Accept-Language HTTP header)
904 * default language based on browser settings (Accept-Language HTTP header)
903 * issues list exportable to CSV (rev6)
905 * issues list exportable to CSV (rev6)
904 * simple_format and auto_link on long text fields
906 * simple_format and auto_link on long text fields
905 * more data validations
907 * more data validations
906 * Fixed: error when all mail notifications are unchecked in admin/mail_options
908 * Fixed: error when all mail notifications are unchecked in admin/mail_options
907 * Fixed: all project news are displayed on project summary
909 * Fixed: all project news are displayed on project summary
908 * Fixed: Can't change user password in users/edit
910 * Fixed: Can't change user password in users/edit
909 * Fixed: Error on tables creation with PostgreSQL (rev5)
911 * Fixed: Error on tables creation with PostgreSQL (rev5)
910 * Fixed: SQL error in "issue reports" view with PostgreSQL (rev5)
912 * Fixed: SQL error in "issue reports" view with PostgreSQL (rev5)
911
913
912
914
913 == 2006-06-25 - v0.1.0
915 == 2006-06-25 - v0.1.0
914
916
915 * multiple users/multiple projects
917 * multiple users/multiple projects
916 * role based access control
918 * role based access control
917 * issue tracking system
919 * issue tracking system
918 * fully customizable workflow
920 * fully customizable workflow
919 * documents/files repository
921 * documents/files repository
920 * email notifications on issue creation and update
922 * email notifications on issue creation and update
921 * multilanguage support (except for error messages):english, french, spanish
923 * multilanguage support (except for error messages):english, french, spanish
922 * online manual in french (unfinished)
924 * online manual in french (unfinished)
@@ -1,112 +1,124
1 ---
1 ---
2 attachments_001:
2 attachments_001:
3 created_on: 2006-07-19 21:07:27 +02:00
3 created_on: 2006-07-19 21:07:27 +02:00
4 downloads: 0
4 downloads: 0
5 content_type: text/plain
5 content_type: text/plain
6 disk_filename: 060719210727_error281.txt
6 disk_filename: 060719210727_error281.txt
7 container_id: 3
7 container_id: 3
8 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
8 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
9 id: 1
9 id: 1
10 container_type: Issue
10 container_type: Issue
11 filesize: 28
11 filesize: 28
12 filename: error281.txt
12 filename: error281.txt
13 author_id: 2
13 author_id: 2
14 attachments_002:
14 attachments_002:
15 created_on: 2006-07-19 21:07:27 +02:00
15 created_on: 2006-07-19 21:07:27 +02:00
16 downloads: 0
16 downloads: 0
17 content_type: text/plain
17 content_type: text/plain
18 disk_filename: 060719210727_document.txt
18 disk_filename: 060719210727_document.txt
19 container_id: 1
19 container_id: 1
20 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
20 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
21 id: 2
21 id: 2
22 container_type: Document
22 container_type: Document
23 filesize: 28
23 filesize: 28
24 filename: document.txt
24 filename: document.txt
25 author_id: 2
25 author_id: 2
26 attachments_003:
26 attachments_003:
27 created_on: 2006-07-19 21:07:27 +02:00
27 created_on: 2006-07-19 21:07:27 +02:00
28 downloads: 0
28 downloads: 0
29 content_type: image/gif
29 content_type: image/gif
30 disk_filename: 060719210727_logo.gif
30 disk_filename: 060719210727_logo.gif
31 container_id: 4
31 container_id: 4
32 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
32 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
33 id: 3
33 id: 3
34 container_type: WikiPage
34 container_type: WikiPage
35 filesize: 280
35 filesize: 280
36 filename: logo.gif
36 filename: logo.gif
37 description: This is a logo
37 description: This is a logo
38 author_id: 2
38 author_id: 2
39 attachments_004:
39 attachments_004:
40 created_on: 2006-07-19 21:07:27 +02:00
40 created_on: 2006-07-19 21:07:27 +02:00
41 container_type: Issue
41 container_type: Issue
42 container_id: 3
42 container_id: 3
43 downloads: 0
43 downloads: 0
44 disk_filename: 060719210727_source.rb
44 disk_filename: 060719210727_source.rb
45 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
45 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
46 id: 4
46 id: 4
47 filesize: 153
47 filesize: 153
48 filename: source.rb
48 filename: source.rb
49 author_id: 2
49 author_id: 2
50 description: This is a Ruby source file
50 description: This is a Ruby source file
51 content_type: application/x-ruby
51 content_type: application/x-ruby
52 attachments_005:
52 attachments_005:
53 created_on: 2006-07-19 21:07:27 +02:00
53 created_on: 2006-07-19 21:07:27 +02:00
54 container_type: Issue
54 container_type: Issue
55 container_id: 3
55 container_id: 3
56 downloads: 0
56 downloads: 0
57 disk_filename: 060719210727_changeset.diff
57 disk_filename: 060719210727_changeset.diff
58 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
58 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
59 id: 5
59 id: 5
60 filesize: 687
60 filesize: 687
61 filename: changeset.diff
61 filename: changeset.diff
62 author_id: 2
62 author_id: 2
63 content_type: text/x-diff
63 content_type: text/x-diff
64 attachments_006:
64 attachments_006:
65 created_on: 2006-07-19 21:07:27 +02:00
65 created_on: 2006-07-19 21:07:27 +02:00
66 container_type: Issue
66 container_type: Issue
67 container_id: 3
67 container_id: 3
68 downloads: 0
68 downloads: 0
69 disk_filename: 060719210727_archive.zip
69 disk_filename: 060719210727_archive.zip
70 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
70 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
71 id: 6
71 id: 6
72 filesize: 157
72 filesize: 157
73 filename: archive.zip
73 filename: archive.zip
74 author_id: 2
74 author_id: 2
75 content_type: application/octet-stream
75 content_type: application/octet-stream
76 attachments_007:
76 attachments_007:
77 created_on: 2006-07-19 21:07:27 +02:00
77 created_on: 2006-07-19 21:07:27 +02:00
78 container_type: Issue
78 container_type: Issue
79 container_id: 4
79 container_id: 4
80 downloads: 0
80 downloads: 0
81 disk_filename: 060719210727_archive.zip
81 disk_filename: 060719210727_archive.zip
82 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
82 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
83 id: 7
83 id: 7
84 filesize: 157
84 filesize: 157
85 filename: archive.zip
85 filename: archive.zip
86 author_id: 1
86 author_id: 1
87 content_type: application/octet-stream
87 content_type: application/octet-stream
88 attachments_008:
88 attachments_008:
89 created_on: 2006-07-19 21:07:27 +02:00
89 created_on: 2006-07-19 21:07:27 +02:00
90 container_type: Project
90 container_type: Project
91 container_id: 1
91 container_id: 1
92 downloads: 0
92 downloads: 0
93 disk_filename: 060719210727_project_file.zip
93 disk_filename: 060719210727_project_file.zip
94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
95 id: 8
95 id: 8
96 filesize: 320
96 filesize: 320
97 filename: project_file.zip
97 filename: project_file.zip
98 author_id: 2
98 author_id: 2
99 content_type: application/octet-stream
99 content_type: application/octet-stream
100 attachments_009:
100 attachments_009:
101 created_on: 2006-07-19 21:07:27 +02:00
101 created_on: 2006-07-19 21:07:27 +02:00
102 container_type: Version
102 container_type: Version
103 container_id: 1
103 container_id: 1
104 downloads: 0
104 downloads: 0
105 disk_filename: 060719210727_version_file.zip
105 disk_filename: 060719210727_version_file.zip
106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
107 id: 9
107 id: 9
108 filesize: 452
108 filesize: 452
109 filename: version_file.zip
109 filename: version_file.zip
110 author_id: 2
110 author_id: 2
111 content_type: application/octet-stream
111 content_type: application/octet-stream
112 attachments_010:
113 created_on: 2006-07-19 21:07:27 +02:00
114 container_type: Issue
115 container_id: 2
116 downloads: 0
117 disk_filename: 060719210727_picture.jpg
118 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
119 id: 10
120 filesize: 452
121 filename: picture.jpg
122 author_id: 2
123 content_type: image/jpeg
112 No newline at end of file
124
@@ -1,16 +1,23
1 ---
1 ---
2 journals_001:
2 journals_001:
3 created_on: <%= 2.days.ago.to_date.to_s(:db) %>
3 created_on: <%= 2.days.ago.to_date.to_s(:db) %>
4 notes: "Journal notes"
4 notes: "Journal notes"
5 id: 1
5 id: 1
6 journalized_type: Issue
6 journalized_type: Issue
7 user_id: 1
7 user_id: 1
8 journalized_id: 1
8 journalized_id: 1
9 journals_002:
9 journals_002:
10 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
10 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
11 notes: "Some notes with Redmine links: #2, r2."
11 notes: "Some notes with Redmine links: #2, r2."
12 id: 2
12 id: 2
13 journalized_type: Issue
13 journalized_type: Issue
14 user_id: 2
14 user_id: 2
15 journalized_id: 1
15 journalized_id: 1
16 journals_003:
17 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
18 notes: "A comment with inline image: !picture.jpg!"
19 id: 3
20 journalized_type: Issue
21 user_id: 2
22 journalized_id: 2
16 No newline at end of file
23
@@ -1,790 +1,798
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < Test::Unit::TestCase
24 class IssuesControllerTest < Test::Unit::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :versions,
31 :versions,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :issue_categories,
34 :issue_categories,
35 :enabled_modules,
35 :enabled_modules,
36 :enumerations,
36 :enumerations,
37 :attachments,
37 :attachments,
38 :workflows,
38 :workflows,
39 :custom_fields,
39 :custom_fields,
40 :custom_values,
40 :custom_values,
41 :custom_fields_trackers,
41 :custom_fields_trackers,
42 :time_entries,
42 :time_entries,
43 :journals,
43 :journals,
44 :journal_details
44 :journal_details
45
45
46 def setup
46 def setup
47 @controller = IssuesController.new
47 @controller = IssuesController.new
48 @request = ActionController::TestRequest.new
48 @request = ActionController::TestRequest.new
49 @response = ActionController::TestResponse.new
49 @response = ActionController::TestResponse.new
50 User.current = nil
50 User.current = nil
51 end
51 end
52
52
53 def test_index
53 def test_index
54 Setting.default_language = 'en'
54 Setting.default_language = 'en'
55
55
56 get :index
56 get :index
57 assert_response :success
57 assert_response :success
58 assert_template 'index.rhtml'
58 assert_template 'index.rhtml'
59 assert_not_nil assigns(:issues)
59 assert_not_nil assigns(:issues)
60 assert_nil assigns(:project)
60 assert_nil assigns(:project)
61 assert_tag :tag => 'a', :content => /Can't print recipes/
61 assert_tag :tag => 'a', :content => /Can't print recipes/
62 assert_tag :tag => 'a', :content => /Subproject issue/
62 assert_tag :tag => 'a', :content => /Subproject issue/
63 # private projects hidden
63 # private projects hidden
64 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
64 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
65 assert_no_tag :tag => 'a', :content => /Issue on project 2/
65 assert_no_tag :tag => 'a', :content => /Issue on project 2/
66 # project column
66 # project column
67 assert_tag :tag => 'th', :content => /Project/
67 assert_tag :tag => 'th', :content => /Project/
68 end
68 end
69
69
70 def test_index_should_not_list_issues_when_module_disabled
70 def test_index_should_not_list_issues_when_module_disabled
71 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
71 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
72 get :index
72 get :index
73 assert_response :success
73 assert_response :success
74 assert_template 'index.rhtml'
74 assert_template 'index.rhtml'
75 assert_not_nil assigns(:issues)
75 assert_not_nil assigns(:issues)
76 assert_nil assigns(:project)
76 assert_nil assigns(:project)
77 assert_no_tag :tag => 'a', :content => /Can't print recipes/
77 assert_no_tag :tag => 'a', :content => /Can't print recipes/
78 assert_tag :tag => 'a', :content => /Subproject issue/
78 assert_tag :tag => 'a', :content => /Subproject issue/
79 end
79 end
80
80
81 def test_index_with_project
81 def test_index_with_project
82 Setting.display_subprojects_issues = 0
82 Setting.display_subprojects_issues = 0
83 get :index, :project_id => 1
83 get :index, :project_id => 1
84 assert_response :success
84 assert_response :success
85 assert_template 'index.rhtml'
85 assert_template 'index.rhtml'
86 assert_not_nil assigns(:issues)
86 assert_not_nil assigns(:issues)
87 assert_tag :tag => 'a', :content => /Can't print recipes/
87 assert_tag :tag => 'a', :content => /Can't print recipes/
88 assert_no_tag :tag => 'a', :content => /Subproject issue/
88 assert_no_tag :tag => 'a', :content => /Subproject issue/
89 end
89 end
90
90
91 def test_index_with_project_and_subprojects
91 def test_index_with_project_and_subprojects
92 Setting.display_subprojects_issues = 1
92 Setting.display_subprojects_issues = 1
93 get :index, :project_id => 1
93 get :index, :project_id => 1
94 assert_response :success
94 assert_response :success
95 assert_template 'index.rhtml'
95 assert_template 'index.rhtml'
96 assert_not_nil assigns(:issues)
96 assert_not_nil assigns(:issues)
97 assert_tag :tag => 'a', :content => /Can't print recipes/
97 assert_tag :tag => 'a', :content => /Can't print recipes/
98 assert_tag :tag => 'a', :content => /Subproject issue/
98 assert_tag :tag => 'a', :content => /Subproject issue/
99 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
99 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
100 end
100 end
101
101
102 def test_index_with_project_and_subprojects_should_show_private_subprojects
102 def test_index_with_project_and_subprojects_should_show_private_subprojects
103 @request.session[:user_id] = 2
103 @request.session[:user_id] = 2
104 Setting.display_subprojects_issues = 1
104 Setting.display_subprojects_issues = 1
105 get :index, :project_id => 1
105 get :index, :project_id => 1
106 assert_response :success
106 assert_response :success
107 assert_template 'index.rhtml'
107 assert_template 'index.rhtml'
108 assert_not_nil assigns(:issues)
108 assert_not_nil assigns(:issues)
109 assert_tag :tag => 'a', :content => /Can't print recipes/
109 assert_tag :tag => 'a', :content => /Can't print recipes/
110 assert_tag :tag => 'a', :content => /Subproject issue/
110 assert_tag :tag => 'a', :content => /Subproject issue/
111 assert_tag :tag => 'a', :content => /Issue of a private subproject/
111 assert_tag :tag => 'a', :content => /Issue of a private subproject/
112 end
112 end
113
113
114 def test_index_with_project_and_filter
114 def test_index_with_project_and_filter
115 get :index, :project_id => 1, :set_filter => 1
115 get :index, :project_id => 1, :set_filter => 1
116 assert_response :success
116 assert_response :success
117 assert_template 'index.rhtml'
117 assert_template 'index.rhtml'
118 assert_not_nil assigns(:issues)
118 assert_not_nil assigns(:issues)
119 end
119 end
120
120
121 def test_index_csv_with_project
121 def test_index_csv_with_project
122 get :index, :format => 'csv'
122 get :index, :format => 'csv'
123 assert_response :success
123 assert_response :success
124 assert_not_nil assigns(:issues)
124 assert_not_nil assigns(:issues)
125 assert_equal 'text/csv', @response.content_type
125 assert_equal 'text/csv', @response.content_type
126
126
127 get :index, :project_id => 1, :format => 'csv'
127 get :index, :project_id => 1, :format => 'csv'
128 assert_response :success
128 assert_response :success
129 assert_not_nil assigns(:issues)
129 assert_not_nil assigns(:issues)
130 assert_equal 'text/csv', @response.content_type
130 assert_equal 'text/csv', @response.content_type
131 end
131 end
132
132
133 def test_index_pdf
133 def test_index_pdf
134 get :index, :format => 'pdf'
134 get :index, :format => 'pdf'
135 assert_response :success
135 assert_response :success
136 assert_not_nil assigns(:issues)
136 assert_not_nil assigns(:issues)
137 assert_equal 'application/pdf', @response.content_type
137 assert_equal 'application/pdf', @response.content_type
138
138
139 get :index, :project_id => 1, :format => 'pdf'
139 get :index, :project_id => 1, :format => 'pdf'
140 assert_response :success
140 assert_response :success
141 assert_not_nil assigns(:issues)
141 assert_not_nil assigns(:issues)
142 assert_equal 'application/pdf', @response.content_type
142 assert_equal 'application/pdf', @response.content_type
143 end
143 end
144
144
145 def test_index_sort
145 def test_index_sort
146 get :index, :sort_key => 'tracker'
146 get :index, :sort_key => 'tracker'
147 assert_response :success
147 assert_response :success
148
148
149 sort_params = @request.session['issuesindex_sort']
149 sort_params = @request.session['issuesindex_sort']
150 assert sort_params.is_a?(Hash)
150 assert sort_params.is_a?(Hash)
151 assert_equal 'tracker', sort_params[:key]
151 assert_equal 'tracker', sort_params[:key]
152 assert_equal 'ASC', sort_params[:order]
152 assert_equal 'ASC', sort_params[:order]
153 end
153 end
154
154
155 def test_gantt
155 def test_gantt
156 get :gantt, :project_id => 1
156 get :gantt, :project_id => 1
157 assert_response :success
157 assert_response :success
158 assert_template 'gantt.rhtml'
158 assert_template 'gantt.rhtml'
159 assert_not_nil assigns(:gantt)
159 assert_not_nil assigns(:gantt)
160 events = assigns(:gantt).events
160 events = assigns(:gantt).events
161 assert_not_nil events
161 assert_not_nil events
162 # Issue with start and due dates
162 # Issue with start and due dates
163 i = Issue.find(1)
163 i = Issue.find(1)
164 assert_not_nil i.due_date
164 assert_not_nil i.due_date
165 assert events.include?(Issue.find(1))
165 assert events.include?(Issue.find(1))
166 # Issue with without due date but targeted to a version with date
166 # Issue with without due date but targeted to a version with date
167 i = Issue.find(2)
167 i = Issue.find(2)
168 assert_nil i.due_date
168 assert_nil i.due_date
169 assert events.include?(i)
169 assert events.include?(i)
170 end
170 end
171
171
172 def test_cross_project_gantt
172 def test_cross_project_gantt
173 get :gantt
173 get :gantt
174 assert_response :success
174 assert_response :success
175 assert_template 'gantt.rhtml'
175 assert_template 'gantt.rhtml'
176 assert_not_nil assigns(:gantt)
176 assert_not_nil assigns(:gantt)
177 events = assigns(:gantt).events
177 events = assigns(:gantt).events
178 assert_not_nil events
178 assert_not_nil events
179 end
179 end
180
180
181 def test_gantt_export_to_pdf
181 def test_gantt_export_to_pdf
182 get :gantt, :project_id => 1, :format => 'pdf'
182 get :gantt, :project_id => 1, :format => 'pdf'
183 assert_response :success
183 assert_response :success
184 assert_equal 'application/pdf', @response.content_type
184 assert_equal 'application/pdf', @response.content_type
185 assert @response.body.starts_with?('%PDF')
185 assert @response.body.starts_with?('%PDF')
186 assert_not_nil assigns(:gantt)
186 assert_not_nil assigns(:gantt)
187 end
187 end
188
188
189 def test_cross_project_gantt_export_to_pdf
189 def test_cross_project_gantt_export_to_pdf
190 get :gantt, :format => 'pdf'
190 get :gantt, :format => 'pdf'
191 assert_response :success
191 assert_response :success
192 assert_equal 'application/pdf', @response.content_type
192 assert_equal 'application/pdf', @response.content_type
193 assert @response.body.starts_with?('%PDF')
193 assert @response.body.starts_with?('%PDF')
194 assert_not_nil assigns(:gantt)
194 assert_not_nil assigns(:gantt)
195 end
195 end
196
196
197 if Object.const_defined?(:Magick)
197 if Object.const_defined?(:Magick)
198 def test_gantt_image
198 def test_gantt_image
199 get :gantt, :project_id => 1, :format => 'png'
199 get :gantt, :project_id => 1, :format => 'png'
200 assert_response :success
200 assert_response :success
201 assert_equal 'image/png', @response.content_type
201 assert_equal 'image/png', @response.content_type
202 end
202 end
203 else
203 else
204 puts "RMagick not installed. Skipping tests !!!"
204 puts "RMagick not installed. Skipping tests !!!"
205 end
205 end
206
206
207 def test_calendar
207 def test_calendar
208 get :calendar, :project_id => 1
208 get :calendar, :project_id => 1
209 assert_response :success
209 assert_response :success
210 assert_template 'calendar'
210 assert_template 'calendar'
211 assert_not_nil assigns(:calendar)
211 assert_not_nil assigns(:calendar)
212 end
212 end
213
213
214 def test_cross_project_calendar
214 def test_cross_project_calendar
215 get :calendar
215 get :calendar
216 assert_response :success
216 assert_response :success
217 assert_template 'calendar'
217 assert_template 'calendar'
218 assert_not_nil assigns(:calendar)
218 assert_not_nil assigns(:calendar)
219 end
219 end
220
220
221 def test_changes
221 def test_changes
222 get :changes, :project_id => 1
222 get :changes, :project_id => 1
223 assert_response :success
223 assert_response :success
224 assert_not_nil assigns(:journals)
224 assert_not_nil assigns(:journals)
225 assert_equal 'application/atom+xml', @response.content_type
225 assert_equal 'application/atom+xml', @response.content_type
226 end
226 end
227
227
228 def test_show_by_anonymous
228 def test_show_by_anonymous
229 get :show, :id => 1
229 get :show, :id => 1
230 assert_response :success
230 assert_response :success
231 assert_template 'show.rhtml'
231 assert_template 'show.rhtml'
232 assert_not_nil assigns(:issue)
232 assert_not_nil assigns(:issue)
233 assert_equal Issue.find(1), assigns(:issue)
233 assert_equal Issue.find(1), assigns(:issue)
234
234
235 # anonymous role is allowed to add a note
235 # anonymous role is allowed to add a note
236 assert_tag :tag => 'form',
236 assert_tag :tag => 'form',
237 :descendant => { :tag => 'fieldset',
237 :descendant => { :tag => 'fieldset',
238 :child => { :tag => 'legend',
238 :child => { :tag => 'legend',
239 :content => /Notes/ } }
239 :content => /Notes/ } }
240 end
240 end
241
241
242 def test_show_by_manager
242 def test_show_by_manager
243 @request.session[:user_id] = 2
243 @request.session[:user_id] = 2
244 get :show, :id => 1
244 get :show, :id => 1
245 assert_response :success
245 assert_response :success
246
246
247 assert_tag :tag => 'form',
247 assert_tag :tag => 'form',
248 :descendant => { :tag => 'fieldset',
248 :descendant => { :tag => 'fieldset',
249 :child => { :tag => 'legend',
249 :child => { :tag => 'legend',
250 :content => /Change properties/ } },
250 :content => /Change properties/ } },
251 :descendant => { :tag => 'fieldset',
251 :descendant => { :tag => 'fieldset',
252 :child => { :tag => 'legend',
252 :child => { :tag => 'legend',
253 :content => /Log time/ } },
253 :content => /Log time/ } },
254 :descendant => { :tag => 'fieldset',
254 :descendant => { :tag => 'fieldset',
255 :child => { :tag => 'legend',
255 :child => { :tag => 'legend',
256 :content => /Notes/ } }
256 :content => /Notes/ } }
257 end
257 end
258
259 def test_show_atom
260 get :show, :id => 2, :format => 'atom'
261 assert_response :success
262 assert_template 'changes'
263 # Inline image
264 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;")
265 end
258
266
259 def test_show_export_to_pdf
267 def test_show_export_to_pdf
260 get :show, :id => 3, :format => 'pdf'
268 get :show, :id => 3, :format => 'pdf'
261 assert_response :success
269 assert_response :success
262 assert_equal 'application/pdf', @response.content_type
270 assert_equal 'application/pdf', @response.content_type
263 assert @response.body.starts_with?('%PDF')
271 assert @response.body.starts_with?('%PDF')
264 assert_not_nil assigns(:issue)
272 assert_not_nil assigns(:issue)
265 end
273 end
266
274
267 def test_get_new
275 def test_get_new
268 @request.session[:user_id] = 2
276 @request.session[:user_id] = 2
269 get :new, :project_id => 1, :tracker_id => 1
277 get :new, :project_id => 1, :tracker_id => 1
270 assert_response :success
278 assert_response :success
271 assert_template 'new'
279 assert_template 'new'
272
280
273 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
281 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
274 :value => 'Default string' }
282 :value => 'Default string' }
275 end
283 end
276
284
277 def test_get_new_without_tracker_id
285 def test_get_new_without_tracker_id
278 @request.session[:user_id] = 2
286 @request.session[:user_id] = 2
279 get :new, :project_id => 1
287 get :new, :project_id => 1
280 assert_response :success
288 assert_response :success
281 assert_template 'new'
289 assert_template 'new'
282
290
283 issue = assigns(:issue)
291 issue = assigns(:issue)
284 assert_not_nil issue
292 assert_not_nil issue
285 assert_equal Project.find(1).trackers.first, issue.tracker
293 assert_equal Project.find(1).trackers.first, issue.tracker
286 end
294 end
287
295
288 def test_update_new_form
296 def test_update_new_form
289 @request.session[:user_id] = 2
297 @request.session[:user_id] = 2
290 xhr :post, :new, :project_id => 1,
298 xhr :post, :new, :project_id => 1,
291 :issue => {:tracker_id => 2,
299 :issue => {:tracker_id => 2,
292 :subject => 'This is the test_new issue',
300 :subject => 'This is the test_new issue',
293 :description => 'This is the description',
301 :description => 'This is the description',
294 :priority_id => 5}
302 :priority_id => 5}
295 assert_response :success
303 assert_response :success
296 assert_template 'new'
304 assert_template 'new'
297 end
305 end
298
306
299 def test_post_new
307 def test_post_new
300 @request.session[:user_id] = 2
308 @request.session[:user_id] = 2
301 post :new, :project_id => 1,
309 post :new, :project_id => 1,
302 :issue => {:tracker_id => 3,
310 :issue => {:tracker_id => 3,
303 :subject => 'This is the test_new issue',
311 :subject => 'This is the test_new issue',
304 :description => 'This is the description',
312 :description => 'This is the description',
305 :priority_id => 5,
313 :priority_id => 5,
306 :estimated_hours => '',
314 :estimated_hours => '',
307 :custom_field_values => {'2' => 'Value for field 2'}}
315 :custom_field_values => {'2' => 'Value for field 2'}}
308 assert_redirected_to 'issues/show'
316 assert_redirected_to 'issues/show'
309
317
310 issue = Issue.find_by_subject('This is the test_new issue')
318 issue = Issue.find_by_subject('This is the test_new issue')
311 assert_not_nil issue
319 assert_not_nil issue
312 assert_equal 2, issue.author_id
320 assert_equal 2, issue.author_id
313 assert_equal 3, issue.tracker_id
321 assert_equal 3, issue.tracker_id
314 assert_nil issue.estimated_hours
322 assert_nil issue.estimated_hours
315 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
323 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
316 assert_not_nil v
324 assert_not_nil v
317 assert_equal 'Value for field 2', v.value
325 assert_equal 'Value for field 2', v.value
318 end
326 end
319
327
320 def test_post_new_and_continue
328 def test_post_new_and_continue
321 @request.session[:user_id] = 2
329 @request.session[:user_id] = 2
322 post :new, :project_id => 1,
330 post :new, :project_id => 1,
323 :issue => {:tracker_id => 3,
331 :issue => {:tracker_id => 3,
324 :subject => 'This is first issue',
332 :subject => 'This is first issue',
325 :priority_id => 5},
333 :priority_id => 5},
326 :continue => ''
334 :continue => ''
327 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
335 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
328 end
336 end
329
337
330 def test_post_new_without_custom_fields_param
338 def test_post_new_without_custom_fields_param
331 @request.session[:user_id] = 2
339 @request.session[:user_id] = 2
332 post :new, :project_id => 1,
340 post :new, :project_id => 1,
333 :issue => {:tracker_id => 1,
341 :issue => {:tracker_id => 1,
334 :subject => 'This is the test_new issue',
342 :subject => 'This is the test_new issue',
335 :description => 'This is the description',
343 :description => 'This is the description',
336 :priority_id => 5}
344 :priority_id => 5}
337 assert_redirected_to 'issues/show'
345 assert_redirected_to 'issues/show'
338 end
346 end
339
347
340 def test_post_new_with_required_custom_field_and_without_custom_fields_param
348 def test_post_new_with_required_custom_field_and_without_custom_fields_param
341 field = IssueCustomField.find_by_name('Database')
349 field = IssueCustomField.find_by_name('Database')
342 field.update_attribute(:is_required, true)
350 field.update_attribute(:is_required, true)
343
351
344 @request.session[:user_id] = 2
352 @request.session[:user_id] = 2
345 post :new, :project_id => 1,
353 post :new, :project_id => 1,
346 :issue => {:tracker_id => 1,
354 :issue => {:tracker_id => 1,
347 :subject => 'This is the test_new issue',
355 :subject => 'This is the test_new issue',
348 :description => 'This is the description',
356 :description => 'This is the description',
349 :priority_id => 5}
357 :priority_id => 5}
350 assert_response :success
358 assert_response :success
351 assert_template 'new'
359 assert_template 'new'
352 issue = assigns(:issue)
360 issue = assigns(:issue)
353 assert_not_nil issue
361 assert_not_nil issue
354 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
362 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
355 end
363 end
356
364
357 def test_post_new_with_watchers
365 def test_post_new_with_watchers
358 @request.session[:user_id] = 2
366 @request.session[:user_id] = 2
359 ActionMailer::Base.deliveries.clear
367 ActionMailer::Base.deliveries.clear
360
368
361 assert_difference 'Watcher.count', 2 do
369 assert_difference 'Watcher.count', 2 do
362 post :new, :project_id => 1,
370 post :new, :project_id => 1,
363 :issue => {:tracker_id => 1,
371 :issue => {:tracker_id => 1,
364 :subject => 'This is a new issue with watchers',
372 :subject => 'This is a new issue with watchers',
365 :description => 'This is the description',
373 :description => 'This is the description',
366 :priority_id => 5,
374 :priority_id => 5,
367 :watcher_user_ids => ['2', '3']}
375 :watcher_user_ids => ['2', '3']}
368 end
376 end
369 assert_redirected_to 'issues/show'
377 assert_redirected_to 'issues/show'
370
378
371 issue = Issue.find_by_subject('This is a new issue with watchers')
379 issue = Issue.find_by_subject('This is a new issue with watchers')
372 # Watchers added
380 # Watchers added
373 assert_equal [2, 3], issue.watcher_user_ids.sort
381 assert_equal [2, 3], issue.watcher_user_ids.sort
374 assert issue.watched_by?(User.find(3))
382 assert issue.watched_by?(User.find(3))
375 # Watchers notified
383 # Watchers notified
376 mail = ActionMailer::Base.deliveries.last
384 mail = ActionMailer::Base.deliveries.last
377 assert_kind_of TMail::Mail, mail
385 assert_kind_of TMail::Mail, mail
378 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
386 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
379 end
387 end
380
388
381 def test_post_should_preserve_fields_values_on_validation_failure
389 def test_post_should_preserve_fields_values_on_validation_failure
382 @request.session[:user_id] = 2
390 @request.session[:user_id] = 2
383 post :new, :project_id => 1,
391 post :new, :project_id => 1,
384 :issue => {:tracker_id => 1,
392 :issue => {:tracker_id => 1,
385 # empty subject
393 # empty subject
386 :subject => '',
394 :subject => '',
387 :description => 'This is a description',
395 :description => 'This is a description',
388 :priority_id => 6,
396 :priority_id => 6,
389 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
397 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
390 assert_response :success
398 assert_response :success
391 assert_template 'new'
399 assert_template 'new'
392
400
393 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
401 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
394 :content => 'This is a description'
402 :content => 'This is a description'
395 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
403 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
396 :child => { :tag => 'option', :attributes => { :selected => 'selected',
404 :child => { :tag => 'option', :attributes => { :selected => 'selected',
397 :value => '6' },
405 :value => '6' },
398 :content => 'High' }
406 :content => 'High' }
399 # Custom fields
407 # Custom fields
400 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
408 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
401 :child => { :tag => 'option', :attributes => { :selected => 'selected',
409 :child => { :tag => 'option', :attributes => { :selected => 'selected',
402 :value => 'Oracle' },
410 :value => 'Oracle' },
403 :content => 'Oracle' }
411 :content => 'Oracle' }
404 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
412 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
405 :value => 'Value for field 2'}
413 :value => 'Value for field 2'}
406 end
414 end
407
415
408 def test_copy_issue
416 def test_copy_issue
409 @request.session[:user_id] = 2
417 @request.session[:user_id] = 2
410 get :new, :project_id => 1, :copy_from => 1
418 get :new, :project_id => 1, :copy_from => 1
411 assert_template 'new'
419 assert_template 'new'
412 assert_not_nil assigns(:issue)
420 assert_not_nil assigns(:issue)
413 orig = Issue.find(1)
421 orig = Issue.find(1)
414 assert_equal orig.subject, assigns(:issue).subject
422 assert_equal orig.subject, assigns(:issue).subject
415 end
423 end
416
424
417 def test_get_edit
425 def test_get_edit
418 @request.session[:user_id] = 2
426 @request.session[:user_id] = 2
419 get :edit, :id => 1
427 get :edit, :id => 1
420 assert_response :success
428 assert_response :success
421 assert_template 'edit'
429 assert_template 'edit'
422 assert_not_nil assigns(:issue)
430 assert_not_nil assigns(:issue)
423 assert_equal Issue.find(1), assigns(:issue)
431 assert_equal Issue.find(1), assigns(:issue)
424 end
432 end
425
433
426 def test_get_edit_with_params
434 def test_get_edit_with_params
427 @request.session[:user_id] = 2
435 @request.session[:user_id] = 2
428 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
436 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
429 assert_response :success
437 assert_response :success
430 assert_template 'edit'
438 assert_template 'edit'
431
439
432 issue = assigns(:issue)
440 issue = assigns(:issue)
433 assert_not_nil issue
441 assert_not_nil issue
434
442
435 assert_equal 5, issue.status_id
443 assert_equal 5, issue.status_id
436 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
444 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
437 :child => { :tag => 'option',
445 :child => { :tag => 'option',
438 :content => 'Closed',
446 :content => 'Closed',
439 :attributes => { :selected => 'selected' } }
447 :attributes => { :selected => 'selected' } }
440
448
441 assert_equal 7, issue.priority_id
449 assert_equal 7, issue.priority_id
442 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
450 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
443 :child => { :tag => 'option',
451 :child => { :tag => 'option',
444 :content => 'Urgent',
452 :content => 'Urgent',
445 :attributes => { :selected => 'selected' } }
453 :attributes => { :selected => 'selected' } }
446 end
454 end
447
455
448 def test_reply_to_issue
456 def test_reply_to_issue
449 @request.session[:user_id] = 2
457 @request.session[:user_id] = 2
450 get :reply, :id => 1
458 get :reply, :id => 1
451 assert_response :success
459 assert_response :success
452 assert_select_rjs :show, "update"
460 assert_select_rjs :show, "update"
453 end
461 end
454
462
455 def test_reply_to_note
463 def test_reply_to_note
456 @request.session[:user_id] = 2
464 @request.session[:user_id] = 2
457 get :reply, :id => 1, :journal_id => 2
465 get :reply, :id => 1, :journal_id => 2
458 assert_response :success
466 assert_response :success
459 assert_select_rjs :show, "update"
467 assert_select_rjs :show, "update"
460 end
468 end
461
469
462 def test_post_edit_without_custom_fields_param
470 def test_post_edit_without_custom_fields_param
463 @request.session[:user_id] = 2
471 @request.session[:user_id] = 2
464 ActionMailer::Base.deliveries.clear
472 ActionMailer::Base.deliveries.clear
465
473
466 issue = Issue.find(1)
474 issue = Issue.find(1)
467 assert_equal '125', issue.custom_value_for(2).value
475 assert_equal '125', issue.custom_value_for(2).value
468 old_subject = issue.subject
476 old_subject = issue.subject
469 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
477 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
470
478
471 assert_difference('Journal.count') do
479 assert_difference('Journal.count') do
472 assert_difference('JournalDetail.count', 2) do
480 assert_difference('JournalDetail.count', 2) do
473 post :edit, :id => 1, :issue => {:subject => new_subject,
481 post :edit, :id => 1, :issue => {:subject => new_subject,
474 :priority_id => '6',
482 :priority_id => '6',
475 :category_id => '1' # no change
483 :category_id => '1' # no change
476 }
484 }
477 end
485 end
478 end
486 end
479 assert_redirected_to 'issues/show/1'
487 assert_redirected_to 'issues/show/1'
480 issue.reload
488 issue.reload
481 assert_equal new_subject, issue.subject
489 assert_equal new_subject, issue.subject
482 # Make sure custom fields were not cleared
490 # Make sure custom fields were not cleared
483 assert_equal '125', issue.custom_value_for(2).value
491 assert_equal '125', issue.custom_value_for(2).value
484
492
485 mail = ActionMailer::Base.deliveries.last
493 mail = ActionMailer::Base.deliveries.last
486 assert_kind_of TMail::Mail, mail
494 assert_kind_of TMail::Mail, mail
487 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
495 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
488 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
496 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
489 end
497 end
490
498
491 def test_post_edit_with_custom_field_change
499 def test_post_edit_with_custom_field_change
492 @request.session[:user_id] = 2
500 @request.session[:user_id] = 2
493 issue = Issue.find(1)
501 issue = Issue.find(1)
494 assert_equal '125', issue.custom_value_for(2).value
502 assert_equal '125', issue.custom_value_for(2).value
495
503
496 assert_difference('Journal.count') do
504 assert_difference('Journal.count') do
497 assert_difference('JournalDetail.count', 3) do
505 assert_difference('JournalDetail.count', 3) do
498 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
506 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
499 :priority_id => '6',
507 :priority_id => '6',
500 :category_id => '1', # no change
508 :category_id => '1', # no change
501 :custom_field_values => { '2' => 'New custom value' }
509 :custom_field_values => { '2' => 'New custom value' }
502 }
510 }
503 end
511 end
504 end
512 end
505 assert_redirected_to 'issues/show/1'
513 assert_redirected_to 'issues/show/1'
506 issue.reload
514 issue.reload
507 assert_equal 'New custom value', issue.custom_value_for(2).value
515 assert_equal 'New custom value', issue.custom_value_for(2).value
508
516
509 mail = ActionMailer::Base.deliveries.last
517 mail = ActionMailer::Base.deliveries.last
510 assert_kind_of TMail::Mail, mail
518 assert_kind_of TMail::Mail, mail
511 assert mail.body.include?("Searchable field changed from 125 to New custom value")
519 assert mail.body.include?("Searchable field changed from 125 to New custom value")
512 end
520 end
513
521
514 def test_post_edit_with_status_and_assignee_change
522 def test_post_edit_with_status_and_assignee_change
515 issue = Issue.find(1)
523 issue = Issue.find(1)
516 assert_equal 1, issue.status_id
524 assert_equal 1, issue.status_id
517 @request.session[:user_id] = 2
525 @request.session[:user_id] = 2
518 assert_difference('TimeEntry.count', 0) do
526 assert_difference('TimeEntry.count', 0) do
519 post :edit,
527 post :edit,
520 :id => 1,
528 :id => 1,
521 :issue => { :status_id => 2, :assigned_to_id => 3 },
529 :issue => { :status_id => 2, :assigned_to_id => 3 },
522 :notes => 'Assigned to dlopper',
530 :notes => 'Assigned to dlopper',
523 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
531 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
524 end
532 end
525 assert_redirected_to 'issues/show/1'
533 assert_redirected_to 'issues/show/1'
526 issue.reload
534 issue.reload
527 assert_equal 2, issue.status_id
535 assert_equal 2, issue.status_id
528 j = issue.journals.find(:first, :order => 'id DESC')
536 j = issue.journals.find(:first, :order => 'id DESC')
529 assert_equal 'Assigned to dlopper', j.notes
537 assert_equal 'Assigned to dlopper', j.notes
530 assert_equal 2, j.details.size
538 assert_equal 2, j.details.size
531
539
532 mail = ActionMailer::Base.deliveries.last
540 mail = ActionMailer::Base.deliveries.last
533 assert mail.body.include?("Status changed from New to Assigned")
541 assert mail.body.include?("Status changed from New to Assigned")
534 end
542 end
535
543
536 def test_post_edit_with_note_only
544 def test_post_edit_with_note_only
537 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
545 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
538 # anonymous user
546 # anonymous user
539 post :edit,
547 post :edit,
540 :id => 1,
548 :id => 1,
541 :notes => notes
549 :notes => notes
542 assert_redirected_to 'issues/show/1'
550 assert_redirected_to 'issues/show/1'
543 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
551 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
544 assert_equal notes, j.notes
552 assert_equal notes, j.notes
545 assert_equal 0, j.details.size
553 assert_equal 0, j.details.size
546 assert_equal User.anonymous, j.user
554 assert_equal User.anonymous, j.user
547
555
548 mail = ActionMailer::Base.deliveries.last
556 mail = ActionMailer::Base.deliveries.last
549 assert mail.body.include?(notes)
557 assert mail.body.include?(notes)
550 end
558 end
551
559
552 def test_post_edit_with_note_and_spent_time
560 def test_post_edit_with_note_and_spent_time
553 @request.session[:user_id] = 2
561 @request.session[:user_id] = 2
554 spent_hours_before = Issue.find(1).spent_hours
562 spent_hours_before = Issue.find(1).spent_hours
555 assert_difference('TimeEntry.count') do
563 assert_difference('TimeEntry.count') do
556 post :edit,
564 post :edit,
557 :id => 1,
565 :id => 1,
558 :notes => '2.5 hours added',
566 :notes => '2.5 hours added',
559 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
567 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
560 end
568 end
561 assert_redirected_to 'issues/show/1'
569 assert_redirected_to 'issues/show/1'
562
570
563 issue = Issue.find(1)
571 issue = Issue.find(1)
564
572
565 j = issue.journals.find(:first, :order => 'id DESC')
573 j = issue.journals.find(:first, :order => 'id DESC')
566 assert_equal '2.5 hours added', j.notes
574 assert_equal '2.5 hours added', j.notes
567 assert_equal 0, j.details.size
575 assert_equal 0, j.details.size
568
576
569 t = issue.time_entries.find(:first, :order => 'id DESC')
577 t = issue.time_entries.find(:first, :order => 'id DESC')
570 assert_not_nil t
578 assert_not_nil t
571 assert_equal 2.5, t.hours
579 assert_equal 2.5, t.hours
572 assert_equal spent_hours_before + 2.5, issue.spent_hours
580 assert_equal spent_hours_before + 2.5, issue.spent_hours
573 end
581 end
574
582
575 def test_post_edit_with_attachment_only
583 def test_post_edit_with_attachment_only
576 set_tmp_attachments_directory
584 set_tmp_attachments_directory
577
585
578 # Delete all fixtured journals, a race condition can occur causing the wrong
586 # Delete all fixtured journals, a race condition can occur causing the wrong
579 # journal to get fetched in the next find.
587 # journal to get fetched in the next find.
580 Journal.delete_all
588 Journal.delete_all
581
589
582 # anonymous user
590 # anonymous user
583 post :edit,
591 post :edit,
584 :id => 1,
592 :id => 1,
585 :notes => '',
593 :notes => '',
586 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
594 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
587 assert_redirected_to 'issues/show/1'
595 assert_redirected_to 'issues/show/1'
588 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
596 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
589 assert j.notes.blank?
597 assert j.notes.blank?
590 assert_equal 1, j.details.size
598 assert_equal 1, j.details.size
591 assert_equal 'testfile.txt', j.details.first.value
599 assert_equal 'testfile.txt', j.details.first.value
592 assert_equal User.anonymous, j.user
600 assert_equal User.anonymous, j.user
593
601
594 mail = ActionMailer::Base.deliveries.last
602 mail = ActionMailer::Base.deliveries.last
595 assert mail.body.include?('testfile.txt')
603 assert mail.body.include?('testfile.txt')
596 end
604 end
597
605
598 def test_post_edit_with_no_change
606 def test_post_edit_with_no_change
599 issue = Issue.find(1)
607 issue = Issue.find(1)
600 issue.journals.clear
608 issue.journals.clear
601 ActionMailer::Base.deliveries.clear
609 ActionMailer::Base.deliveries.clear
602
610
603 post :edit,
611 post :edit,
604 :id => 1,
612 :id => 1,
605 :notes => ''
613 :notes => ''
606 assert_redirected_to 'issues/show/1'
614 assert_redirected_to 'issues/show/1'
607
615
608 issue.reload
616 issue.reload
609 assert issue.journals.empty?
617 assert issue.journals.empty?
610 # No email should be sent
618 # No email should be sent
611 assert ActionMailer::Base.deliveries.empty?
619 assert ActionMailer::Base.deliveries.empty?
612 end
620 end
613
621
614 def test_post_edit_with_invalid_spent_time
622 def test_post_edit_with_invalid_spent_time
615 @request.session[:user_id] = 2
623 @request.session[:user_id] = 2
616 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
624 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
617
625
618 assert_no_difference('Journal.count') do
626 assert_no_difference('Journal.count') do
619 post :edit,
627 post :edit,
620 :id => 1,
628 :id => 1,
621 :notes => notes,
629 :notes => notes,
622 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
630 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
623 end
631 end
624 assert_response :success
632 assert_response :success
625 assert_template 'edit'
633 assert_template 'edit'
626
634
627 assert_tag :textarea, :attributes => { :name => 'notes' },
635 assert_tag :textarea, :attributes => { :name => 'notes' },
628 :content => notes
636 :content => notes
629 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
637 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
630 end
638 end
631
639
632 def test_bulk_edit
640 def test_bulk_edit
633 @request.session[:user_id] = 2
641 @request.session[:user_id] = 2
634 # update issues priority
642 # update issues priority
635 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
643 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
636 assert_response 302
644 assert_response 302
637 # check that the issues were updated
645 # check that the issues were updated
638 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
646 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
639 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
647 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
640 end
648 end
641
649
642 def test_bulk_unassign
650 def test_bulk_unassign
643 assert_not_nil Issue.find(2).assigned_to
651 assert_not_nil Issue.find(2).assigned_to
644 @request.session[:user_id] = 2
652 @request.session[:user_id] = 2
645 # unassign issues
653 # unassign issues
646 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
654 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
647 assert_response 302
655 assert_response 302
648 # check that the issues were updated
656 # check that the issues were updated
649 assert_nil Issue.find(2).assigned_to
657 assert_nil Issue.find(2).assigned_to
650 end
658 end
651
659
652 def test_move_one_issue_to_another_project
660 def test_move_one_issue_to_another_project
653 @request.session[:user_id] = 1
661 @request.session[:user_id] = 1
654 post :move, :id => 1, :new_project_id => 2
662 post :move, :id => 1, :new_project_id => 2
655 assert_redirected_to 'projects/ecookbook/issues'
663 assert_redirected_to 'projects/ecookbook/issues'
656 assert_equal 2, Issue.find(1).project_id
664 assert_equal 2, Issue.find(1).project_id
657 end
665 end
658
666
659 def test_bulk_move_to_another_project
667 def test_bulk_move_to_another_project
660 @request.session[:user_id] = 1
668 @request.session[:user_id] = 1
661 post :move, :ids => [1, 2], :new_project_id => 2
669 post :move, :ids => [1, 2], :new_project_id => 2
662 assert_redirected_to 'projects/ecookbook/issues'
670 assert_redirected_to 'projects/ecookbook/issues'
663 # Issues moved to project 2
671 # Issues moved to project 2
664 assert_equal 2, Issue.find(1).project_id
672 assert_equal 2, Issue.find(1).project_id
665 assert_equal 2, Issue.find(2).project_id
673 assert_equal 2, Issue.find(2).project_id
666 # No tracker change
674 # No tracker change
667 assert_equal 1, Issue.find(1).tracker_id
675 assert_equal 1, Issue.find(1).tracker_id
668 assert_equal 2, Issue.find(2).tracker_id
676 assert_equal 2, Issue.find(2).tracker_id
669 end
677 end
670
678
671 def test_bulk_move_to_another_tracker
679 def test_bulk_move_to_another_tracker
672 @request.session[:user_id] = 1
680 @request.session[:user_id] = 1
673 post :move, :ids => [1, 2], :new_tracker_id => 2
681 post :move, :ids => [1, 2], :new_tracker_id => 2
674 assert_redirected_to 'projects/ecookbook/issues'
682 assert_redirected_to 'projects/ecookbook/issues'
675 assert_equal 2, Issue.find(1).tracker_id
683 assert_equal 2, Issue.find(1).tracker_id
676 assert_equal 2, Issue.find(2).tracker_id
684 assert_equal 2, Issue.find(2).tracker_id
677 end
685 end
678
686
679 def test_context_menu_one_issue
687 def test_context_menu_one_issue
680 @request.session[:user_id] = 2
688 @request.session[:user_id] = 2
681 get :context_menu, :ids => [1]
689 get :context_menu, :ids => [1]
682 assert_response :success
690 assert_response :success
683 assert_template 'context_menu'
691 assert_template 'context_menu'
684 assert_tag :tag => 'a', :content => 'Edit',
692 assert_tag :tag => 'a', :content => 'Edit',
685 :attributes => { :href => '/issues/edit/1',
693 :attributes => { :href => '/issues/edit/1',
686 :class => 'icon-edit' }
694 :class => 'icon-edit' }
687 assert_tag :tag => 'a', :content => 'Closed',
695 assert_tag :tag => 'a', :content => 'Closed',
688 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
696 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
689 :class => '' }
697 :class => '' }
690 assert_tag :tag => 'a', :content => 'Immediate',
698 assert_tag :tag => 'a', :content => 'Immediate',
691 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
699 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
692 :class => '' }
700 :class => '' }
693 assert_tag :tag => 'a', :content => 'Dave Lopper',
701 assert_tag :tag => 'a', :content => 'Dave Lopper',
694 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
702 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
695 :class => '' }
703 :class => '' }
696 assert_tag :tag => 'a', :content => 'Copy',
704 assert_tag :tag => 'a', :content => 'Copy',
697 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
705 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
698 :class => 'icon-copy' }
706 :class => 'icon-copy' }
699 assert_tag :tag => 'a', :content => 'Move',
707 assert_tag :tag => 'a', :content => 'Move',
700 :attributes => { :href => '/issues/move?ids%5B%5D=1',
708 :attributes => { :href => '/issues/move?ids%5B%5D=1',
701 :class => 'icon-move' }
709 :class => 'icon-move' }
702 assert_tag :tag => 'a', :content => 'Delete',
710 assert_tag :tag => 'a', :content => 'Delete',
703 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
711 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
704 :class => 'icon-del' }
712 :class => 'icon-del' }
705 end
713 end
706
714
707 def test_context_menu_one_issue_by_anonymous
715 def test_context_menu_one_issue_by_anonymous
708 get :context_menu, :ids => [1]
716 get :context_menu, :ids => [1]
709 assert_response :success
717 assert_response :success
710 assert_template 'context_menu'
718 assert_template 'context_menu'
711 assert_tag :tag => 'a', :content => 'Delete',
719 assert_tag :tag => 'a', :content => 'Delete',
712 :attributes => { :href => '#',
720 :attributes => { :href => '#',
713 :class => 'icon-del disabled' }
721 :class => 'icon-del disabled' }
714 end
722 end
715
723
716 def test_context_menu_multiple_issues_of_same_project
724 def test_context_menu_multiple_issues_of_same_project
717 @request.session[:user_id] = 2
725 @request.session[:user_id] = 2
718 get :context_menu, :ids => [1, 2]
726 get :context_menu, :ids => [1, 2]
719 assert_response :success
727 assert_response :success
720 assert_template 'context_menu'
728 assert_template 'context_menu'
721 assert_tag :tag => 'a', :content => 'Edit',
729 assert_tag :tag => 'a', :content => 'Edit',
722 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
730 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
723 :class => 'icon-edit' }
731 :class => 'icon-edit' }
724 assert_tag :tag => 'a', :content => 'Immediate',
732 assert_tag :tag => 'a', :content => 'Immediate',
725 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
733 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
726 :class => '' }
734 :class => '' }
727 assert_tag :tag => 'a', :content => 'Dave Lopper',
735 assert_tag :tag => 'a', :content => 'Dave Lopper',
728 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
736 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
729 :class => '' }
737 :class => '' }
730 assert_tag :tag => 'a', :content => 'Move',
738 assert_tag :tag => 'a', :content => 'Move',
731 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
739 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
732 :class => 'icon-move' }
740 :class => 'icon-move' }
733 assert_tag :tag => 'a', :content => 'Delete',
741 assert_tag :tag => 'a', :content => 'Delete',
734 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
742 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
735 :class => 'icon-del' }
743 :class => 'icon-del' }
736 end
744 end
737
745
738 def test_context_menu_multiple_issues_of_different_project
746 def test_context_menu_multiple_issues_of_different_project
739 @request.session[:user_id] = 2
747 @request.session[:user_id] = 2
740 get :context_menu, :ids => [1, 2, 4]
748 get :context_menu, :ids => [1, 2, 4]
741 assert_response :success
749 assert_response :success
742 assert_template 'context_menu'
750 assert_template 'context_menu'
743 assert_tag :tag => 'a', :content => 'Delete',
751 assert_tag :tag => 'a', :content => 'Delete',
744 :attributes => { :href => '#',
752 :attributes => { :href => '#',
745 :class => 'icon-del disabled' }
753 :class => 'icon-del disabled' }
746 end
754 end
747
755
748 def test_destroy_issue_with_no_time_entries
756 def test_destroy_issue_with_no_time_entries
749 assert_nil TimeEntry.find_by_issue_id(2)
757 assert_nil TimeEntry.find_by_issue_id(2)
750 @request.session[:user_id] = 2
758 @request.session[:user_id] = 2
751 post :destroy, :id => 2
759 post :destroy, :id => 2
752 assert_redirected_to 'projects/ecookbook/issues'
760 assert_redirected_to 'projects/ecookbook/issues'
753 assert_nil Issue.find_by_id(2)
761 assert_nil Issue.find_by_id(2)
754 end
762 end
755
763
756 def test_destroy_issues_with_time_entries
764 def test_destroy_issues_with_time_entries
757 @request.session[:user_id] = 2
765 @request.session[:user_id] = 2
758 post :destroy, :ids => [1, 3]
766 post :destroy, :ids => [1, 3]
759 assert_response :success
767 assert_response :success
760 assert_template 'destroy'
768 assert_template 'destroy'
761 assert_not_nil assigns(:hours)
769 assert_not_nil assigns(:hours)
762 assert Issue.find_by_id(1) && Issue.find_by_id(3)
770 assert Issue.find_by_id(1) && Issue.find_by_id(3)
763 end
771 end
764
772
765 def test_destroy_issues_and_destroy_time_entries
773 def test_destroy_issues_and_destroy_time_entries
766 @request.session[:user_id] = 2
774 @request.session[:user_id] = 2
767 post :destroy, :ids => [1, 3], :todo => 'destroy'
775 post :destroy, :ids => [1, 3], :todo => 'destroy'
768 assert_redirected_to 'projects/ecookbook/issues'
776 assert_redirected_to 'projects/ecookbook/issues'
769 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
777 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
770 assert_nil TimeEntry.find_by_id([1, 2])
778 assert_nil TimeEntry.find_by_id([1, 2])
771 end
779 end
772
780
773 def test_destroy_issues_and_assign_time_entries_to_project
781 def test_destroy_issues_and_assign_time_entries_to_project
774 @request.session[:user_id] = 2
782 @request.session[:user_id] = 2
775 post :destroy, :ids => [1, 3], :todo => 'nullify'
783 post :destroy, :ids => [1, 3], :todo => 'nullify'
776 assert_redirected_to 'projects/ecookbook/issues'
784 assert_redirected_to 'projects/ecookbook/issues'
777 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
785 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
778 assert_nil TimeEntry.find(1).issue_id
786 assert_nil TimeEntry.find(1).issue_id
779 assert_nil TimeEntry.find(2).issue_id
787 assert_nil TimeEntry.find(2).issue_id
780 end
788 end
781
789
782 def test_destroy_issues_and_reassign_time_entries_to_another_issue
790 def test_destroy_issues_and_reassign_time_entries_to_another_issue
783 @request.session[:user_id] = 2
791 @request.session[:user_id] = 2
784 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
792 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
785 assert_redirected_to 'projects/ecookbook/issues'
793 assert_redirected_to 'projects/ecookbook/issues'
786 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
794 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
787 assert_equal 2, TimeEntry.find(1).issue_id
795 assert_equal 2, TimeEntry.find(1).issue_id
788 assert_equal 2, TimeEntry.find(2).issue_id
796 assert_equal 2, TimeEntry.find(2).issue_id
789 end
797 end
790 end
798 end
@@ -1,212 +1,214
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class QueryTest < Test::Unit::TestCase
20 class QueryTest < Test::Unit::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def find_issues_with_query(query)
29 def find_issues_with_query(query)
30 Issue.find :all,
30 Issue.find :all,
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
32 :conditions => query.statement
32 :conditions => query.statement
33 end
33 end
34
34
35 def test_query_with_multiple_custom_fields
35 def test_query_with_multiple_custom_fields
36 query = Query.find(1)
36 query = Query.find(1)
37 assert query.valid?
37 assert query.valid?
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
39 issues = find_issues_with_query(query)
39 issues = find_issues_with_query(query)
40 assert_equal 1, issues.length
40 assert_equal 1, issues.length
41 assert_equal Issue.find(3), issues.first
41 assert_equal Issue.find(3), issues.first
42 end
42 end
43
43
44 def test_operator_none
44 def test_operator_none
45 query = Query.new(:project => Project.find(1), :name => '_')
45 query = Query.new(:project => Project.find(1), :name => '_')
46 query.add_filter('fixed_version_id', '!*', [''])
46 query.add_filter('fixed_version_id', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
50 find_issues_with_query(query)
50 find_issues_with_query(query)
51 end
51 end
52
52
53 def test_operator_none_for_integer
53 def test_operator_none_for_integer
54 query = Query.new(:project => Project.find(1), :name => '_')
54 query = Query.new(:project => Project.find(1), :name => '_')
55 query.add_filter('estimated_hours', '!*', [''])
55 query.add_filter('estimated_hours', '!*', [''])
56 issues = find_issues_with_query(query)
56 issues = find_issues_with_query(query)
57 assert !issues.empty?
57 assert !issues.empty?
58 assert issues.all? {|i| !i.estimated_hours}
58 assert issues.all? {|i| !i.estimated_hours}
59 end
59 end
60
60
61 def test_operator_all
61 def test_operator_all
62 query = Query.new(:project => Project.find(1), :name => '_')
62 query = Query.new(:project => Project.find(1), :name => '_')
63 query.add_filter('fixed_version_id', '*', [''])
63 query.add_filter('fixed_version_id', '*', [''])
64 query.add_filter('cf_1', '*', [''])
64 query.add_filter('cf_1', '*', [''])
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
67 find_issues_with_query(query)
67 find_issues_with_query(query)
68 end
68 end
69
69
70 def test_operator_greater_than
70 def test_operator_greater_than
71 query = Query.new(:project => Project.find(1), :name => '_')
71 query = Query.new(:project => Project.find(1), :name => '_')
72 query.add_filter('done_ratio', '>=', ['40'])
72 query.add_filter('done_ratio', '>=', ['40'])
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
74 find_issues_with_query(query)
74 find_issues_with_query(query)
75 end
75 end
76
76
77 def test_operator_in_more_than
77 def test_operator_in_more_than
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('due_date', '>t+', ['15'])
80 query.add_filter('due_date', '>t+', ['15'])
81 issues = find_issues_with_query(query)
81 issues = find_issues_with_query(query)
82 assert !issues.empty?
82 assert !issues.empty?
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
84 end
84 end
85
85
86 def test_operator_in_less_than
86 def test_operator_in_less_than
87 query = Query.new(:project => Project.find(1), :name => '_')
87 query = Query.new(:project => Project.find(1), :name => '_')
88 query.add_filter('due_date', '<t+', ['15'])
88 query.add_filter('due_date', '<t+', ['15'])
89 issues = find_issues_with_query(query)
89 issues = find_issues_with_query(query)
90 assert !issues.empty?
90 assert !issues.empty?
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
92 end
92 end
93
93
94 def test_operator_less_than_ago
94 def test_operator_less_than_ago
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('due_date', '>t-', ['3'])
97 query.add_filter('due_date', '>t-', ['3'])
98 issues = find_issues_with_query(query)
98 issues = find_issues_with_query(query)
99 assert !issues.empty?
99 assert !issues.empty?
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
101 end
101 end
102
102
103 def test_operator_more_than_ago
103 def test_operator_more_than_ago
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('due_date', '<t-', ['10'])
106 query.add_filter('due_date', '<t-', ['10'])
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
108 issues = find_issues_with_query(query)
108 issues = find_issues_with_query(query)
109 assert !issues.empty?
109 assert !issues.empty?
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
111 end
111 end
112
112
113 def test_operator_in
113 def test_operator_in
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
115 query = Query.new(:project => Project.find(1), :name => '_')
115 query = Query.new(:project => Project.find(1), :name => '_')
116 query.add_filter('due_date', 't+', ['2'])
116 query.add_filter('due_date', 't+', ['2'])
117 issues = find_issues_with_query(query)
117 issues = find_issues_with_query(query)
118 assert !issues.empty?
118 assert !issues.empty?
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
120 end
120 end
121
121
122 def test_operator_ago
122 def test_operator_ago
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
124 query = Query.new(:project => Project.find(1), :name => '_')
124 query = Query.new(:project => Project.find(1), :name => '_')
125 query.add_filter('due_date', 't-', ['3'])
125 query.add_filter('due_date', 't-', ['3'])
126 issues = find_issues_with_query(query)
126 issues = find_issues_with_query(query)
127 assert !issues.empty?
127 assert !issues.empty?
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
129 end
129 end
130
130
131 def test_operator_today
131 def test_operator_today
132 query = Query.new(:project => Project.find(1), :name => '_')
132 query = Query.new(:project => Project.find(1), :name => '_')
133 query.add_filter('due_date', 't', [''])
133 query.add_filter('due_date', 't', [''])
134 issues = find_issues_with_query(query)
134 issues = find_issues_with_query(query)
135 assert !issues.empty?
135 assert !issues.empty?
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
137 end
137 end
138
138
139 def test_operator_this_week_on_date
139 def test_operator_this_week_on_date
140 query = Query.new(:project => Project.find(1), :name => '_')
140 query = Query.new(:project => Project.find(1), :name => '_')
141 query.add_filter('due_date', 'w', [''])
141 query.add_filter('due_date', 'w', [''])
142 find_issues_with_query(query)
142 find_issues_with_query(query)
143 end
143 end
144
144
145 def test_operator_this_week_on_datetime
145 def test_operator_this_week_on_datetime
146 query = Query.new(:project => Project.find(1), :name => '_')
146 query = Query.new(:project => Project.find(1), :name => '_')
147 query.add_filter('created_on', 'w', [''])
147 query.add_filter('created_on', 'w', [''])
148 find_issues_with_query(query)
148 find_issues_with_query(query)
149 end
149 end
150
150
151 def test_operator_contains
151 def test_operator_contains
152 query = Query.new(:project => Project.find(1), :name => '_')
152 query = Query.new(:project => Project.find(1), :name => '_')
153 query.add_filter('subject', '~', ['string'])
153 query.add_filter('subject', '~', ['uNable'])
154 assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'")
154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
155 find_issues_with_query(query)
155 result = find_issues_with_query(query)
156 assert result.empty?
157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
156 end
158 end
157
159
158 def test_operator_does_not_contains
160 def test_operator_does_not_contains
159 query = Query.new(:project => Project.find(1), :name => '_')
161 query = Query.new(:project => Project.find(1), :name => '_')
160 query.add_filter('subject', '!~', ['string'])
162 query.add_filter('subject', '!~', ['uNable'])
161 assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'")
163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
162 find_issues_with_query(query)
164 find_issues_with_query(query)
163 end
165 end
164
166
165 def test_default_columns
167 def test_default_columns
166 q = Query.new
168 q = Query.new
167 assert !q.columns.empty?
169 assert !q.columns.empty?
168 end
170 end
169
171
170 def test_set_column_names
172 def test_set_column_names
171 q = Query.new
173 q = Query.new
172 q.column_names = ['tracker', :subject, '', 'unknonw_column']
174 q.column_names = ['tracker', :subject, '', 'unknonw_column']
173 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
175 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
174 c = q.columns.first
176 c = q.columns.first
175 assert q.has_column?(c)
177 assert q.has_column?(c)
176 end
178 end
177
179
178 def test_label_for
180 def test_label_for
179 q = Query.new
181 q = Query.new
180 assert_equal 'assigned_to', q.label_for('assigned_to_id')
182 assert_equal 'assigned_to', q.label_for('assigned_to_id')
181 end
183 end
182
184
183 def test_editable_by
185 def test_editable_by
184 admin = User.find(1)
186 admin = User.find(1)
185 manager = User.find(2)
187 manager = User.find(2)
186 developer = User.find(3)
188 developer = User.find(3)
187
189
188 # Public query on project 1
190 # Public query on project 1
189 q = Query.find(1)
191 q = Query.find(1)
190 assert q.editable_by?(admin)
192 assert q.editable_by?(admin)
191 assert q.editable_by?(manager)
193 assert q.editable_by?(manager)
192 assert !q.editable_by?(developer)
194 assert !q.editable_by?(developer)
193
195
194 # Private query on project 1
196 # Private query on project 1
195 q = Query.find(2)
197 q = Query.find(2)
196 assert q.editable_by?(admin)
198 assert q.editable_by?(admin)
197 assert !q.editable_by?(manager)
199 assert !q.editable_by?(manager)
198 assert q.editable_by?(developer)
200 assert q.editable_by?(developer)
199
201
200 # Private query for all projects
202 # Private query for all projects
201 q = Query.find(3)
203 q = Query.find(3)
202 assert q.editable_by?(admin)
204 assert q.editable_by?(admin)
203 assert !q.editable_by?(manager)
205 assert !q.editable_by?(manager)
204 assert q.editable_by?(developer)
206 assert q.editable_by?(developer)
205
207
206 # Public query for all projects
208 # Public query for all projects
207 q = Query.find(4)
209 q = Query.find(4)
208 assert q.editable_by?(admin)
210 assert q.editable_by?(admin)
209 assert !q.editable_by?(manager)
211 assert !q.editable_by?(manager)
210 assert !q.editable_by?(developer)
212 assert !q.editable_by?(developer)
211 end
213 end
212 end
214 end
General Comments 0
You need to be logged in to leave comments. Login now