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