@@ -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 |
|
|
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 |
|
|
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 |
* Fixe |
|
|
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?("<img src=\"http://test.host/attachments/download/10\" alt=\"\" />") | |
|
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&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&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&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&ids%5B%5D=2&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&ids%5B%5D=1&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&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&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', '~', [' |
|
|
154 |
assert query.statement.include?("#{Issue.table_name}.subject LIKE '% |
|
|
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', '!~', [' |
|
|
161 |
assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '% |
|
|
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