@@ -0,0 +1,10 | |||
|
1 | <p> | |
|
2 | <% if @repository.supports_cat? %> | |
|
3 | <%= link_to_if action_name != 'entry', l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | | |
|
4 | <% end %> | |
|
5 | <% if @repository.supports_annotate? %> | |
|
6 | <%= link_to_if action_name != 'annotate', l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | | |
|
7 | <% end %> | |
|
8 | <%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %> | |
|
9 | <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %> | |
|
10 | </p> |
@@ -1,495 +1,498 | |||
|
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 IssuesController < ApplicationController |
|
19 | 19 | menu_item :new_issue, :only => :new |
|
20 | 20 | |
|
21 | 21 | before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment] |
|
22 | 22 | before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] |
|
23 | 23 | before_filter :find_project, :only => [:new, :update_form, :preview] |
|
24 | 24 | before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu] |
|
25 | 25 | before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] |
|
26 | 26 | accept_key_auth :index, :changes |
|
27 | 27 | |
|
28 | 28 | helper :journals |
|
29 | 29 | helper :projects |
|
30 | 30 | include ProjectsHelper |
|
31 | 31 | helper :custom_fields |
|
32 | 32 | include CustomFieldsHelper |
|
33 | 33 | helper :ifpdf |
|
34 | 34 | include IfpdfHelper |
|
35 | 35 | helper :issue_relations |
|
36 | 36 | include IssueRelationsHelper |
|
37 | 37 | helper :watchers |
|
38 | 38 | include WatchersHelper |
|
39 | 39 | helper :attachments |
|
40 | 40 | include AttachmentsHelper |
|
41 | 41 | helper :queries |
|
42 | 42 | helper :sort |
|
43 | 43 | include SortHelper |
|
44 | 44 | include IssuesHelper |
|
45 | 45 | helper :timelog |
|
46 | 46 | |
|
47 | 47 | def index |
|
48 | 48 | retrieve_query |
|
49 | 49 | sort_init 'id', 'desc' |
|
50 | 50 | sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) |
|
51 | 51 | |
|
52 | 52 | if @query.valid? |
|
53 | 53 | limit = per_page_option |
|
54 | 54 | respond_to do |format| |
|
55 | 55 | format.html { } |
|
56 | 56 | format.atom { } |
|
57 | 57 | format.csv { limit = Setting.issues_export_limit.to_i } |
|
58 | 58 | format.pdf { limit = Setting.issues_export_limit.to_i } |
|
59 | 59 | end |
|
60 | 60 | @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement) |
|
61 | 61 | @issue_pages = Paginator.new self, @issue_count, limit, params['page'] |
|
62 | 62 | @issues = Issue.find :all, :order => sort_clause, |
|
63 | 63 | :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], |
|
64 | 64 | :conditions => @query.statement, |
|
65 | 65 | :limit => limit, |
|
66 | 66 | :offset => @issue_pages.current.offset |
|
67 | 67 | respond_to do |format| |
|
68 | 68 | format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? } |
|
69 | 69 | format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } |
|
70 | 70 | format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } |
|
71 | 71 | format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') } |
|
72 | 72 | end |
|
73 | 73 | else |
|
74 | 74 | # Send html if the query is not valid |
|
75 | 75 | render(:template => 'issues/index.rhtml', :layout => !request.xhr?) |
|
76 | 76 | end |
|
77 | 77 | rescue ActiveRecord::RecordNotFound |
|
78 | 78 | render_404 |
|
79 | 79 | end |
|
80 | 80 | |
|
81 | 81 | def changes |
|
82 | 82 | retrieve_query |
|
83 | 83 | sort_init 'id', 'desc' |
|
84 | 84 | sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h})) |
|
85 | 85 | |
|
86 | 86 | if @query.valid? |
|
87 | 87 | @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ], |
|
88 | 88 | :conditions => @query.statement, |
|
89 | 89 | :limit => 25, |
|
90 | 90 | :order => "#{Journal.table_name}.created_on DESC" |
|
91 | 91 | end |
|
92 | 92 | @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name) |
|
93 | 93 | render :layout => false, :content_type => 'application/atom+xml' |
|
94 | 94 | rescue ActiveRecord::RecordNotFound |
|
95 | 95 | render_404 |
|
96 | 96 | end |
|
97 | 97 | |
|
98 | 98 | def show |
|
99 | 99 | @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC") |
|
100 | 100 | @journals.each_with_index {|j,i| j.indice = i+1} |
|
101 | 101 | @journals.reverse! if User.current.wants_comments_in_reverse_order? |
|
102 | 102 | @allowed_statuses = @issue.new_statuses_allowed_to(User.current) |
|
103 | 103 | @edit_allowed = User.current.allowed_to?(:edit_issues, @project) |
|
104 | 104 | @priorities = Enumeration::get_values('IPRI') |
|
105 | 105 | @time_entry = TimeEntry.new |
|
106 | 106 | respond_to do |format| |
|
107 | 107 | format.html { render :template => 'issues/show.rhtml' } |
|
108 | 108 | format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' } |
|
109 | 109 | format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") } |
|
110 | 110 | end |
|
111 | 111 | end |
|
112 | 112 | |
|
113 | 113 | # Add a new issue |
|
114 | 114 | # The new issue will be created from an existing one if copy_from parameter is given |
|
115 | 115 | def new |
|
116 | 116 | @issue = Issue.new |
|
117 | 117 | @issue.copy_from(params[:copy_from]) if params[:copy_from] |
|
118 | 118 | @issue.project = @project |
|
119 | 119 | # Tracker must be set before custom field values |
|
120 | 120 | @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first) |
|
121 | 121 | if @issue.tracker.nil? |
|
122 | 122 | flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.' |
|
123 | 123 | render :nothing => true, :layout => true |
|
124 | 124 | return |
|
125 | 125 | end |
|
126 | if params[:issue].is_a?(Hash) | |
|
126 | 127 | @issue.attributes = params[:issue] |
|
128 | @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project) | |
|
129 | end | |
|
127 | 130 | @issue.author = User.current |
|
128 | 131 | |
|
129 | 132 | default_status = IssueStatus.default |
|
130 | 133 | unless default_status |
|
131 | 134 | flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' |
|
132 | 135 | render :nothing => true, :layout => true |
|
133 | 136 | return |
|
134 | 137 | end |
|
135 | 138 | @issue.status = default_status |
|
136 | 139 | @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq |
|
137 | 140 | |
|
138 | 141 | if request.get? || request.xhr? |
|
139 | 142 | @issue.start_date ||= Date.today |
|
140 | 143 | else |
|
141 | 144 | requested_status = IssueStatus.find_by_id(params[:issue][:status_id]) |
|
142 | 145 | # Check that the user is allowed to apply the requested status |
|
143 | 146 | @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status |
|
144 | 147 | if @issue.save |
|
145 | 148 | attach_files(@issue, params[:attachments]) |
|
146 | 149 | flash[:notice] = l(:notice_successful_create) |
|
147 | 150 | Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added') |
|
148 | 151 | redirect_to :controller => 'issues', :action => 'show', :id => @issue |
|
149 | 152 | return |
|
150 | 153 | end |
|
151 | 154 | end |
|
152 | 155 | @priorities = Enumeration::get_values('IPRI') |
|
153 | 156 | render :layout => !request.xhr? |
|
154 | 157 | end |
|
155 | 158 | |
|
156 | 159 | # Attributes that can be updated on workflow transition (without :edit permission) |
|
157 | 160 | # TODO: make it configurable (at least per role) |
|
158 | 161 | UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION) |
|
159 | 162 | |
|
160 | 163 | def edit |
|
161 | 164 | @allowed_statuses = @issue.new_statuses_allowed_to(User.current) |
|
162 | 165 | @priorities = Enumeration::get_values('IPRI') |
|
163 | 166 | @edit_allowed = User.current.allowed_to?(:edit_issues, @project) |
|
164 | 167 | @time_entry = TimeEntry.new |
|
165 | 168 | |
|
166 | 169 | @notes = params[:notes] |
|
167 | 170 | journal = @issue.init_journal(User.current, @notes) |
|
168 | 171 | # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed |
|
169 | 172 | if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue] |
|
170 | 173 | attrs = params[:issue].dup |
|
171 | 174 | attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed |
|
172 | 175 | attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s} |
|
173 | 176 | @issue.attributes = attrs |
|
174 | 177 | end |
|
175 | 178 | |
|
176 | 179 | if request.post? |
|
177 | 180 | @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today) |
|
178 | 181 | @time_entry.attributes = params[:time_entry] |
|
179 | 182 | attachments = attach_files(@issue, params[:attachments]) |
|
180 | 183 | attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)} |
|
181 | 184 | |
|
182 | 185 | call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal}) |
|
183 | 186 | |
|
184 | 187 | if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save |
|
185 | 188 | # Log spend time |
|
186 | 189 | if current_role.allowed_to?(:log_time) |
|
187 | 190 | @time_entry.save |
|
188 | 191 | end |
|
189 | 192 | if !journal.new_record? |
|
190 | 193 | # Only send notification if something was actually changed |
|
191 | 194 | flash[:notice] = l(:notice_successful_update) |
|
192 | 195 | Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated') |
|
193 | 196 | end |
|
194 | 197 | redirect_to(params[:back_to] || {:action => 'show', :id => @issue}) |
|
195 | 198 | end |
|
196 | 199 | end |
|
197 | 200 | rescue ActiveRecord::StaleObjectError |
|
198 | 201 | # Optimistic locking exception |
|
199 | 202 | flash.now[:error] = l(:notice_locking_conflict) |
|
200 | 203 | end |
|
201 | 204 | |
|
202 | 205 | def reply |
|
203 | 206 | journal = Journal.find(params[:journal_id]) if params[:journal_id] |
|
204 | 207 | if journal |
|
205 | 208 | user = journal.user |
|
206 | 209 | text = journal.notes |
|
207 | 210 | else |
|
208 | 211 | user = @issue.author |
|
209 | 212 | text = @issue.description |
|
210 | 213 | end |
|
211 | 214 | content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> " |
|
212 | 215 | content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n" |
|
213 | 216 | render(:update) { |page| |
|
214 | 217 | page.<< "$('notes').value = \"#{content}\";" |
|
215 | 218 | page.show 'update' |
|
216 | 219 | page << "Form.Element.focus('notes');" |
|
217 | 220 | page << "Element.scrollTo('update');" |
|
218 | 221 | page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;" |
|
219 | 222 | } |
|
220 | 223 | end |
|
221 | 224 | |
|
222 | 225 | # Bulk edit a set of issues |
|
223 | 226 | def bulk_edit |
|
224 | 227 | if request.post? |
|
225 | 228 | status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id]) |
|
226 | 229 | priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id]) |
|
227 | 230 | assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id]) |
|
228 | 231 | category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id]) |
|
229 | 232 | fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id]) |
|
230 | 233 | |
|
231 | 234 | unsaved_issue_ids = [] |
|
232 | 235 | @issues.each do |issue| |
|
233 | 236 | journal = issue.init_journal(User.current, params[:notes]) |
|
234 | 237 | issue.priority = priority if priority |
|
235 | 238 | issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none' |
|
236 | 239 | issue.category = category if category || params[:category_id] == 'none' |
|
237 | 240 | issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none' |
|
238 | 241 | issue.start_date = params[:start_date] unless params[:start_date].blank? |
|
239 | 242 | issue.due_date = params[:due_date] unless params[:due_date].blank? |
|
240 | 243 | issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank? |
|
241 | 244 | call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) |
|
242 | 245 | # Don't save any change to the issue if the user is not authorized to apply the requested status |
|
243 | 246 | if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save |
|
244 | 247 | # Send notification for each issue (if changed) |
|
245 | 248 | Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated') |
|
246 | 249 | else |
|
247 | 250 | # Keep unsaved issue ids to display them in flash error |
|
248 | 251 | unsaved_issue_ids << issue.id |
|
249 | 252 | end |
|
250 | 253 | end |
|
251 | 254 | if unsaved_issue_ids.empty? |
|
252 | 255 | flash[:notice] = l(:notice_successful_update) unless @issues.empty? |
|
253 | 256 | else |
|
254 | 257 | flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) |
|
255 | 258 | end |
|
256 | 259 | redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project}) |
|
257 | 260 | return |
|
258 | 261 | end |
|
259 | 262 | # Find potential statuses the user could be allowed to switch issues to |
|
260 | 263 | @available_statuses = Workflow.find(:all, :include => :new_status, |
|
261 | 264 | :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort |
|
262 | 265 | end |
|
263 | 266 | |
|
264 | 267 | def move |
|
265 | 268 | @allowed_projects = [] |
|
266 | 269 | # find projects to which the user is allowed to move the issue |
|
267 | 270 | if User.current.admin? |
|
268 | 271 | # admin is allowed to move issues to any active (visible) project |
|
269 | 272 | @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name') |
|
270 | 273 | else |
|
271 | 274 | User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)} |
|
272 | 275 | end |
|
273 | 276 | @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id] |
|
274 | 277 | @target_project ||= @project |
|
275 | 278 | @trackers = @target_project.trackers |
|
276 | 279 | if request.post? |
|
277 | 280 | new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id]) |
|
278 | 281 | unsaved_issue_ids = [] |
|
279 | 282 | @issues.each do |issue| |
|
280 | 283 | issue.init_journal(User.current) |
|
281 | 284 | unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker) |
|
282 | 285 | end |
|
283 | 286 | if unsaved_issue_ids.empty? |
|
284 | 287 | flash[:notice] = l(:notice_successful_update) unless @issues.empty? |
|
285 | 288 | else |
|
286 | 289 | flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #')) |
|
287 | 290 | end |
|
288 | 291 | redirect_to :controller => 'issues', :action => 'index', :project_id => @project |
|
289 | 292 | return |
|
290 | 293 | end |
|
291 | 294 | render :layout => false if request.xhr? |
|
292 | 295 | end |
|
293 | 296 | |
|
294 | 297 | def destroy |
|
295 | 298 | @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f |
|
296 | 299 | if @hours > 0 |
|
297 | 300 | case params[:todo] |
|
298 | 301 | when 'destroy' |
|
299 | 302 | # nothing to do |
|
300 | 303 | when 'nullify' |
|
301 | 304 | TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues]) |
|
302 | 305 | when 'reassign' |
|
303 | 306 | reassign_to = @project.issues.find_by_id(params[:reassign_to_id]) |
|
304 | 307 | if reassign_to.nil? |
|
305 | 308 | flash.now[:error] = l(:error_issue_not_found_in_project) |
|
306 | 309 | return |
|
307 | 310 | else |
|
308 | 311 | TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues]) |
|
309 | 312 | end |
|
310 | 313 | else |
|
311 | 314 | # display the destroy form |
|
312 | 315 | return |
|
313 | 316 | end |
|
314 | 317 | end |
|
315 | 318 | @issues.each(&:destroy) |
|
316 | 319 | redirect_to :action => 'index', :project_id => @project |
|
317 | 320 | end |
|
318 | 321 | |
|
319 | 322 | def destroy_attachment |
|
320 | 323 | a = @issue.attachments.find(params[:attachment_id]) |
|
321 | 324 | a.destroy |
|
322 | 325 | journal = @issue.init_journal(User.current) |
|
323 | 326 | journal.details << JournalDetail.new(:property => 'attachment', |
|
324 | 327 | :prop_key => a.id, |
|
325 | 328 | :old_value => a.filename) |
|
326 | 329 | journal.save |
|
327 | 330 | redirect_to :action => 'show', :id => @issue |
|
328 | 331 | end |
|
329 | 332 | |
|
330 | 333 | def gantt |
|
331 | 334 | @gantt = Redmine::Helpers::Gantt.new(params) |
|
332 | 335 | retrieve_query |
|
333 | 336 | if @query.valid? |
|
334 | 337 | events = [] |
|
335 | 338 | # Issues that have start and due dates |
|
336 | 339 | events += Issue.find(:all, |
|
337 | 340 | :order => "start_date, due_date", |
|
338 | 341 | :include => [:tracker, :status, :assigned_to, :priority, :project], |
|
339 | 342 | :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] |
|
340 | 343 | ) |
|
341 | 344 | # Issues that don't have a due date but that are assigned to a version with a date |
|
342 | 345 | events += Issue.find(:all, |
|
343 | 346 | :order => "start_date, effective_date", |
|
344 | 347 | :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version], |
|
345 | 348 | :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to] |
|
346 | 349 | ) |
|
347 | 350 | # Versions |
|
348 | 351 | events += Version.find(:all, :include => :project, |
|
349 | 352 | :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to]) |
|
350 | 353 | |
|
351 | 354 | @gantt.events = events |
|
352 | 355 | end |
|
353 | 356 | |
|
354 | 357 | respond_to do |format| |
|
355 | 358 | format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? } |
|
356 | 359 | format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image') |
|
357 | 360 | format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") } |
|
358 | 361 | end |
|
359 | 362 | end |
|
360 | 363 | |
|
361 | 364 | def calendar |
|
362 | 365 | if params[:year] and params[:year].to_i > 1900 |
|
363 | 366 | @year = params[:year].to_i |
|
364 | 367 | if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13 |
|
365 | 368 | @month = params[:month].to_i |
|
366 | 369 | end |
|
367 | 370 | end |
|
368 | 371 | @year ||= Date.today.year |
|
369 | 372 | @month ||= Date.today.month |
|
370 | 373 | |
|
371 | 374 | @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month) |
|
372 | 375 | retrieve_query |
|
373 | 376 | if @query.valid? |
|
374 | 377 | events = [] |
|
375 | 378 | events += Issue.find(:all, |
|
376 | 379 | :include => [:tracker, :status, :assigned_to, :priority, :project], |
|
377 | 380 | :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt] |
|
378 | 381 | ) |
|
379 | 382 | events += Version.find(:all, :include => :project, |
|
380 | 383 | :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt]) |
|
381 | 384 | |
|
382 | 385 | @calendar.events = events |
|
383 | 386 | end |
|
384 | 387 | |
|
385 | 388 | render :layout => false if request.xhr? |
|
386 | 389 | end |
|
387 | 390 | |
|
388 | 391 | def context_menu |
|
389 | 392 | @issues = Issue.find_all_by_id(params[:ids], :include => :project) |
|
390 | 393 | if (@issues.size == 1) |
|
391 | 394 | @issue = @issues.first |
|
392 | 395 | @allowed_statuses = @issue.new_statuses_allowed_to(User.current) |
|
393 | 396 | end |
|
394 | 397 | projects = @issues.collect(&:project).compact.uniq |
|
395 | 398 | @project = projects.first if projects.size == 1 |
|
396 | 399 | |
|
397 | 400 | @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), |
|
398 | 401 | :log_time => (@project && User.current.allowed_to?(:log_time, @project)), |
|
399 | 402 | :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), |
|
400 | 403 | :move => (@project && User.current.allowed_to?(:move_issues, @project)), |
|
401 | 404 | :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), |
|
402 | 405 | :delete => (@project && User.current.allowed_to?(:delete_issues, @project)) |
|
403 | 406 | } |
|
404 | 407 | if @project |
|
405 | 408 | @assignables = @project.assignable_users |
|
406 | 409 | @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) |
|
407 | 410 | end |
|
408 | 411 | |
|
409 | 412 | @priorities = Enumeration.get_values('IPRI').reverse |
|
410 | 413 | @statuses = IssueStatus.find(:all, :order => 'position') |
|
411 | 414 | @back = request.env['HTTP_REFERER'] |
|
412 | 415 | |
|
413 | 416 | render :layout => false |
|
414 | 417 | end |
|
415 | 418 | |
|
416 | 419 | def update_form |
|
417 | 420 | @issue = Issue.new(params[:issue]) |
|
418 | 421 | render :action => :new, :layout => false |
|
419 | 422 | end |
|
420 | 423 | |
|
421 | 424 | def preview |
|
422 | 425 | @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? |
|
423 | 426 | @attachements = @issue.attachments if @issue |
|
424 | 427 | @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil) |
|
425 | 428 | render :partial => 'common/preview' |
|
426 | 429 | end |
|
427 | 430 | |
|
428 | 431 | private |
|
429 | 432 | def find_issue |
|
430 | 433 | @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category]) |
|
431 | 434 | @project = @issue.project |
|
432 | 435 | rescue ActiveRecord::RecordNotFound |
|
433 | 436 | render_404 |
|
434 | 437 | end |
|
435 | 438 | |
|
436 | 439 | # Filter for bulk operations |
|
437 | 440 | def find_issues |
|
438 | 441 | @issues = Issue.find_all_by_id(params[:id] || params[:ids]) |
|
439 | 442 | raise ActiveRecord::RecordNotFound if @issues.empty? |
|
440 | 443 | projects = @issues.collect(&:project).compact.uniq |
|
441 | 444 | if projects.size == 1 |
|
442 | 445 | @project = projects.first |
|
443 | 446 | else |
|
444 | 447 | # TODO: let users bulk edit/move/destroy issues from different projects |
|
445 | 448 | render_error 'Can not bulk edit/move/destroy issues from different projects' and return false |
|
446 | 449 | end |
|
447 | 450 | rescue ActiveRecord::RecordNotFound |
|
448 | 451 | render_404 |
|
449 | 452 | end |
|
450 | 453 | |
|
451 | 454 | def find_project |
|
452 | 455 | @project = Project.find(params[:project_id]) |
|
453 | 456 | rescue ActiveRecord::RecordNotFound |
|
454 | 457 | render_404 |
|
455 | 458 | end |
|
456 | 459 | |
|
457 | 460 | def find_optional_project |
|
458 | 461 | @project = Project.find(params[:project_id]) unless params[:project_id].blank? |
|
459 | 462 | allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true) |
|
460 | 463 | allowed ? true : deny_access |
|
461 | 464 | rescue ActiveRecord::RecordNotFound |
|
462 | 465 | render_404 |
|
463 | 466 | end |
|
464 | 467 | |
|
465 | 468 | # Retrieve query from session or build a new query |
|
466 | 469 | def retrieve_query |
|
467 | 470 | if !params[:query_id].blank? |
|
468 | 471 | cond = "project_id IS NULL" |
|
469 | 472 | cond << " OR project_id = #{@project.id}" if @project |
|
470 | 473 | @query = Query.find(params[:query_id], :conditions => cond) |
|
471 | 474 | @query.project = @project |
|
472 | 475 | session[:query] = {:id => @query.id, :project_id => @query.project_id} |
|
473 | 476 | else |
|
474 | 477 | if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil) |
|
475 | 478 | # Give it a name, required to be valid |
|
476 | 479 | @query = Query.new(:name => "_") |
|
477 | 480 | @query.project = @project |
|
478 | 481 | if params[:fields] and params[:fields].is_a? Array |
|
479 | 482 | params[:fields].each do |field| |
|
480 | 483 | @query.add_filter(field, params[:operators][field], params[:values][field]) |
|
481 | 484 | end |
|
482 | 485 | else |
|
483 | 486 | @query.available_filters.keys.each do |field| |
|
484 | 487 | @query.add_short_filter(field, params[field]) if params[field] |
|
485 | 488 | end |
|
486 | 489 | end |
|
487 | 490 | session[:query] = {:project_id => @query.project_id, :filters => @query.filters} |
|
488 | 491 | else |
|
489 | 492 | @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] |
|
490 | 493 | @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters]) |
|
491 | 494 | @query.project = @project |
|
492 | 495 | end |
|
493 | 496 | end |
|
494 | 497 | end |
|
495 | 498 | end |
@@ -1,325 +1,328 | |||
|
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 'SVG/Graph/Bar' |
|
19 | 19 | require 'SVG/Graph/BarHorizontal' |
|
20 | 20 | require 'digest/sha1' |
|
21 | 21 | |
|
22 | 22 | class ChangesetNotFound < Exception; end |
|
23 | 23 | class InvalidRevisionParam < Exception; end |
|
24 | 24 | |
|
25 | 25 | class RepositoriesController < ApplicationController |
|
26 | 26 | menu_item :repository |
|
27 | 27 | before_filter :find_repository, :except => :edit |
|
28 | 28 | before_filter :find_project, :only => :edit |
|
29 | 29 | before_filter :authorize |
|
30 | 30 | accept_key_auth :revisions |
|
31 | 31 | |
|
32 | 32 | rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed |
|
33 | 33 | |
|
34 | 34 | def edit |
|
35 | 35 | @repository = @project.repository |
|
36 | 36 | if !@repository |
|
37 | 37 | @repository = Repository.factory(params[:repository_scm]) |
|
38 | 38 | @repository.project = @project if @repository |
|
39 | 39 | end |
|
40 | 40 | if request.post? && @repository |
|
41 | 41 | @repository.attributes = params[:repository] |
|
42 | 42 | @repository.save |
|
43 | 43 | end |
|
44 | 44 | render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'} |
|
45 | 45 | end |
|
46 | 46 | |
|
47 | 47 | def committers |
|
48 | 48 | @committers = @repository.committers |
|
49 | 49 | @users = @project.users |
|
50 | 50 | additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id) |
|
51 | 51 | @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty? |
|
52 | 52 | @users.compact! |
|
53 | 53 | @users.sort! |
|
54 | 54 | if request.post? && params[:committers].is_a?(Hash) |
|
55 | 55 | # Build a hash with repository usernames as keys and corresponding user ids as values |
|
56 | 56 | @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h} |
|
57 | 57 | flash[:notice] = l(:notice_successful_update) |
|
58 | 58 | redirect_to :action => 'committers', :id => @project |
|
59 | 59 | end |
|
60 | 60 | end |
|
61 | 61 | |
|
62 | 62 | def destroy |
|
63 | 63 | @repository.destroy |
|
64 | 64 | redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository' |
|
65 | 65 | end |
|
66 | 66 | |
|
67 | 67 | def show |
|
68 | 68 | # check if new revisions have been committed in the repository |
|
69 | 69 | @repository.fetch_changesets if Setting.autofetch_changesets? |
|
70 | 70 | # root entries |
|
71 | 71 | @entries = @repository.entries('', @rev) |
|
72 | 72 | # latest changesets |
|
73 | 73 | @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC") |
|
74 | 74 | show_error_not_found unless @entries || @changesets.any? |
|
75 | 75 | end |
|
76 | 76 | |
|
77 | 77 | def browse |
|
78 | 78 | @entries = @repository.entries(@path, @rev) |
|
79 | 79 | if request.xhr? |
|
80 | 80 | @entries ? render(:partial => 'dir_list_content') : render(:nothing => true) |
|
81 | 81 | else |
|
82 | 82 | show_error_not_found and return unless @entries |
|
83 | 83 | @properties = @repository.properties(@path, @rev) |
|
84 | 84 | render :action => 'browse' |
|
85 | 85 | end |
|
86 | 86 | end |
|
87 | 87 | |
|
88 | 88 | def changes |
|
89 | 89 | @entry = @repository.entry(@path, @rev) |
|
90 | 90 | show_error_not_found and return unless @entry |
|
91 | 91 | @changesets = @repository.changesets_for_path(@path) |
|
92 | 92 | @properties = @repository.properties(@path, @rev) |
|
93 | 93 | end |
|
94 | 94 | |
|
95 | 95 | def revisions |
|
96 | 96 | @changeset_count = @repository.changesets.count |
|
97 | 97 | @changeset_pages = Paginator.new self, @changeset_count, |
|
98 | 98 | per_page_option, |
|
99 | 99 | params['page'] |
|
100 | 100 | @changesets = @repository.changesets.find(:all, |
|
101 | 101 | :limit => @changeset_pages.items_per_page, |
|
102 | 102 | :offset => @changeset_pages.current.offset, |
|
103 | 103 | :include => :user) |
|
104 | 104 | |
|
105 | 105 | respond_to do |format| |
|
106 | 106 | format.html { render :layout => false if request.xhr? } |
|
107 | 107 | format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") } |
|
108 | 108 | end |
|
109 | 109 | end |
|
110 | 110 | |
|
111 | 111 | def entry |
|
112 | 112 | @entry = @repository.entry(@path, @rev) |
|
113 | 113 | show_error_not_found and return unless @entry |
|
114 | 114 | |
|
115 | 115 | # If the entry is a dir, show the browser |
|
116 | 116 | browse and return if @entry.is_dir? |
|
117 | 117 | |
|
118 | 118 | @content = @repository.cat(@path, @rev) |
|
119 | 119 | show_error_not_found and return unless @content |
|
120 | 120 | if 'raw' == params[:format] || @content.is_binary_data? |
|
121 | 121 | # Force the download if it's a binary file |
|
122 | 122 | send_data @content, :filename => @path.split('/').last |
|
123 | 123 | else |
|
124 | 124 | # Prevent empty lines when displaying a file with Windows style eol |
|
125 | 125 | @content.gsub!("\r\n", "\n") |
|
126 | 126 | end |
|
127 | 127 | end |
|
128 | 128 | |
|
129 | 129 | def annotate |
|
130 | @entry = @repository.entry(@path, @rev) | |
|
131 | show_error_not_found and return unless @entry | |
|
132 | ||
|
130 | 133 | @annotate = @repository.scm.annotate(@path, @rev) |
|
131 | 134 | render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty? |
|
132 | 135 | end |
|
133 | 136 | |
|
134 | 137 | def revision |
|
135 | 138 | @changeset = @repository.changesets.find_by_revision(@rev) |
|
136 | 139 | raise ChangesetNotFound unless @changeset |
|
137 | 140 | |
|
138 | 141 | respond_to do |format| |
|
139 | 142 | format.html |
|
140 | 143 | format.js {render :layout => false} |
|
141 | 144 | end |
|
142 | 145 | rescue ChangesetNotFound |
|
143 | 146 | show_error_not_found |
|
144 | 147 | end |
|
145 | 148 | |
|
146 | 149 | def diff |
|
147 | 150 | if params[:format] == 'diff' |
|
148 | 151 | @diff = @repository.diff(@path, @rev, @rev_to) |
|
149 | 152 | show_error_not_found and return unless @diff |
|
150 | 153 | filename = "changeset_r#{@rev}" |
|
151 | 154 | filename << "_r#{@rev_to}" if @rev_to |
|
152 | 155 | send_data @diff.join, :filename => "#{filename}.diff", |
|
153 | 156 | :type => 'text/x-patch', |
|
154 | 157 | :disposition => 'attachment' |
|
155 | 158 | else |
|
156 | 159 | @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline' |
|
157 | 160 | @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type) |
|
158 | 161 | |
|
159 | 162 | # Save diff type as user preference |
|
160 | 163 | if User.current.logged? && @diff_type != User.current.pref[:diff_type] |
|
161 | 164 | User.current.pref[:diff_type] = @diff_type |
|
162 | 165 | User.current.preference.save |
|
163 | 166 | end |
|
164 | 167 | |
|
165 | 168 | @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}") |
|
166 | 169 | unless read_fragment(@cache_key) |
|
167 | 170 | @diff = @repository.diff(@path, @rev, @rev_to) |
|
168 | 171 | show_error_not_found unless @diff |
|
169 | 172 | end |
|
170 | 173 | end |
|
171 | 174 | end |
|
172 | 175 | |
|
173 | 176 | def stats |
|
174 | 177 | end |
|
175 | 178 | |
|
176 | 179 | def graph |
|
177 | 180 | data = nil |
|
178 | 181 | case params[:graph] |
|
179 | 182 | when "commits_per_month" |
|
180 | 183 | data = graph_commits_per_month(@repository) |
|
181 | 184 | when "commits_per_author" |
|
182 | 185 | data = graph_commits_per_author(@repository) |
|
183 | 186 | end |
|
184 | 187 | if data |
|
185 | 188 | headers["Content-Type"] = "image/svg+xml" |
|
186 | 189 | send_data(data, :type => "image/svg+xml", :disposition => "inline") |
|
187 | 190 | else |
|
188 | 191 | render_404 |
|
189 | 192 | end |
|
190 | 193 | end |
|
191 | 194 | |
|
192 | 195 | private |
|
193 | 196 | def find_project |
|
194 | 197 | @project = Project.find(params[:id]) |
|
195 | 198 | rescue ActiveRecord::RecordNotFound |
|
196 | 199 | render_404 |
|
197 | 200 | end |
|
198 | 201 | |
|
199 | 202 | REV_PARAM_RE = %r{^[a-f0-9]*$} |
|
200 | 203 | |
|
201 | 204 | def find_repository |
|
202 | 205 | @project = Project.find(params[:id]) |
|
203 | 206 | @repository = @project.repository |
|
204 | 207 | render_404 and return false unless @repository |
|
205 | 208 | @path = params[:path].join('/') unless params[:path].nil? |
|
206 | 209 | @path ||= '' |
|
207 | 210 | @rev = params[:rev] |
|
208 | 211 | @rev_to = params[:rev_to] |
|
209 | 212 | raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE) |
|
210 | 213 | rescue ActiveRecord::RecordNotFound |
|
211 | 214 | render_404 |
|
212 | 215 | rescue InvalidRevisionParam |
|
213 | 216 | show_error_not_found |
|
214 | 217 | end |
|
215 | 218 | |
|
216 | 219 | def show_error_not_found |
|
217 | 220 | render_error l(:error_scm_not_found) |
|
218 | 221 | end |
|
219 | 222 | |
|
220 | 223 | # Handler for Redmine::Scm::Adapters::CommandFailed exception |
|
221 | 224 | def show_error_command_failed(exception) |
|
222 | 225 | render_error l(:error_scm_command_failed, exception.message) |
|
223 | 226 | end |
|
224 | 227 | |
|
225 | 228 | def graph_commits_per_month(repository) |
|
226 | 229 | @date_to = Date.today |
|
227 | 230 | @date_from = @date_to << 11 |
|
228 | 231 | @date_from = Date.civil(@date_from.year, @date_from.month, 1) |
|
229 | 232 | commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) |
|
230 | 233 | commits_by_month = [0] * 12 |
|
231 | 234 | commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last } |
|
232 | 235 | |
|
233 | 236 | changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to]) |
|
234 | 237 | changes_by_month = [0] * 12 |
|
235 | 238 | changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last } |
|
236 | 239 | |
|
237 | 240 | fields = [] |
|
238 | 241 | month_names = l(:actionview_datehelper_select_month_names_abbr).split(',') |
|
239 | 242 | 12.times {|m| fields << month_names[((Date.today.month - 1 - m) % 12)]} |
|
240 | 243 | |
|
241 | 244 | graph = SVG::Graph::Bar.new( |
|
242 | 245 | :height => 300, |
|
243 | 246 | :width => 800, |
|
244 | 247 | :fields => fields.reverse, |
|
245 | 248 | :stack => :side, |
|
246 | 249 | :scale_integers => true, |
|
247 | 250 | :step_x_labels => 2, |
|
248 | 251 | :show_data_values => false, |
|
249 | 252 | :graph_title => l(:label_commits_per_month), |
|
250 | 253 | :show_graph_title => true |
|
251 | 254 | ) |
|
252 | 255 | |
|
253 | 256 | graph.add_data( |
|
254 | 257 | :data => commits_by_month[0..11].reverse, |
|
255 | 258 | :title => l(:label_revision_plural) |
|
256 | 259 | ) |
|
257 | 260 | |
|
258 | 261 | graph.add_data( |
|
259 | 262 | :data => changes_by_month[0..11].reverse, |
|
260 | 263 | :title => l(:label_change_plural) |
|
261 | 264 | ) |
|
262 | 265 | |
|
263 | 266 | graph.burn |
|
264 | 267 | end |
|
265 | 268 | |
|
266 | 269 | def graph_commits_per_author(repository) |
|
267 | 270 | commits_by_author = repository.changesets.count(:all, :group => :committer) |
|
268 | 271 | commits_by_author.sort! {|x, y| x.last <=> y.last} |
|
269 | 272 | |
|
270 | 273 | changes_by_author = repository.changes.count(:all, :group => :committer) |
|
271 | 274 | h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o} |
|
272 | 275 | |
|
273 | 276 | fields = commits_by_author.collect {|r| r.first} |
|
274 | 277 | commits_data = commits_by_author.collect {|r| r.last} |
|
275 | 278 | changes_data = commits_by_author.collect {|r| h[r.first] || 0} |
|
276 | 279 | |
|
277 | 280 | fields = fields + [""]*(10 - fields.length) if fields.length<10 |
|
278 | 281 | commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10 |
|
279 | 282 | changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10 |
|
280 | 283 | |
|
281 | 284 | # Remove email adress in usernames |
|
282 | 285 | fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') } |
|
283 | 286 | |
|
284 | 287 | graph = SVG::Graph::BarHorizontal.new( |
|
285 | 288 | :height => 400, |
|
286 | 289 | :width => 800, |
|
287 | 290 | :fields => fields, |
|
288 | 291 | :stack => :side, |
|
289 | 292 | :scale_integers => true, |
|
290 | 293 | :show_data_values => false, |
|
291 | 294 | :rotate_y_labels => false, |
|
292 | 295 | :graph_title => l(:label_commits_per_author), |
|
293 | 296 | :show_graph_title => true |
|
294 | 297 | ) |
|
295 | 298 | |
|
296 | 299 | graph.add_data( |
|
297 | 300 | :data => commits_data, |
|
298 | 301 | :title => l(:label_revision_plural) |
|
299 | 302 | ) |
|
300 | 303 | |
|
301 | 304 | graph.add_data( |
|
302 | 305 | :data => changes_data, |
|
303 | 306 | :title => l(:label_change_plural) |
|
304 | 307 | ) |
|
305 | 308 | |
|
306 | 309 | graph.burn |
|
307 | 310 | end |
|
308 | 311 | |
|
309 | 312 | end |
|
310 | 313 | |
|
311 | 314 | class Date |
|
312 | 315 | def months_ago(date = Date.today) |
|
313 | 316 | (date.year - self.year)*12 + (date.month - self.month) |
|
314 | 317 | end |
|
315 | 318 | |
|
316 | 319 | def weeks_ago(date = Date.today) |
|
317 | 320 | (date.year - self.year)*52 + (date.cweek - self.cweek) |
|
318 | 321 | end |
|
319 | 322 | end |
|
320 | 323 | |
|
321 | 324 | class String |
|
322 | 325 | def with_leading_slash |
|
323 | 326 | starts_with?('/') ? self : "/#{self}" |
|
324 | 327 | end |
|
325 | 328 | end |
@@ -1,247 +1,248 | |||
|
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 Mailer < ActionMailer::Base |
|
19 | 19 | helper :application |
|
20 | 20 | helper :issues |
|
21 | 21 | helper :custom_fields |
|
22 | 22 | |
|
23 | 23 | include ActionController::UrlWriter |
|
24 | 24 | |
|
25 | 25 | def issue_add(issue) |
|
26 | 26 | redmine_headers 'Project' => issue.project.identifier, |
|
27 | 27 | 'Issue-Id' => issue.id, |
|
28 | 28 | 'Issue-Author' => issue.author.login |
|
29 | 29 | redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to |
|
30 | 30 | recipients issue.recipients |
|
31 | cc(issue.watcher_recipients - @recipients) | |
|
31 | 32 | subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}" |
|
32 | 33 | body :issue => issue, |
|
33 | 34 | :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) |
|
34 | 35 | end |
|
35 | 36 | |
|
36 | 37 | def issue_edit(journal) |
|
37 | 38 | issue = journal.journalized |
|
38 | 39 | redmine_headers 'Project' => issue.project.identifier, |
|
39 | 40 | 'Issue-Id' => issue.id, |
|
40 | 41 | 'Issue-Author' => issue.author.login |
|
41 | 42 | redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to |
|
42 | 43 | recipients issue.recipients |
|
43 | 44 | # Watchers in cc |
|
44 | 45 | cc(issue.watcher_recipients - @recipients) |
|
45 | 46 | s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] " |
|
46 | 47 | s << "(#{issue.status.name}) " if journal.new_value_for('status_id') |
|
47 | 48 | s << issue.subject |
|
48 | 49 | subject s |
|
49 | 50 | body :issue => issue, |
|
50 | 51 | :journal => journal, |
|
51 | 52 | :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue) |
|
52 | 53 | end |
|
53 | 54 | |
|
54 | 55 | def reminder(user, issues, days) |
|
55 | 56 | set_language_if_valid user.language |
|
56 | 57 | recipients user.mail |
|
57 | 58 | subject l(:mail_subject_reminder, issues.size) |
|
58 | 59 | body :issues => issues, |
|
59 | 60 | :days => days, |
|
60 | 61 | :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc') |
|
61 | 62 | end |
|
62 | 63 | |
|
63 | 64 | def document_added(document) |
|
64 | 65 | redmine_headers 'Project' => document.project.identifier |
|
65 | 66 | recipients document.project.recipients |
|
66 | 67 | subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}" |
|
67 | 68 | body :document => document, |
|
68 | 69 | :document_url => url_for(:controller => 'documents', :action => 'show', :id => document) |
|
69 | 70 | end |
|
70 | 71 | |
|
71 | 72 | def attachments_added(attachments) |
|
72 | 73 | container = attachments.first.container |
|
73 | 74 | added_to = '' |
|
74 | 75 | added_to_url = '' |
|
75 | 76 | case container.class.name |
|
76 | 77 | when 'Version' |
|
77 | 78 | added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id) |
|
78 | 79 | added_to = "#{l(:label_version)}: #{container.name}" |
|
79 | 80 | when 'Document' |
|
80 | 81 | added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id) |
|
81 | 82 | added_to = "#{l(:label_document)}: #{container.title}" |
|
82 | 83 | end |
|
83 | 84 | redmine_headers 'Project' => container.project.identifier |
|
84 | 85 | recipients container.project.recipients |
|
85 | 86 | subject "[#{container.project.name}] #{l(:label_attachment_new)}" |
|
86 | 87 | body :attachments => attachments, |
|
87 | 88 | :added_to => added_to, |
|
88 | 89 | :added_to_url => added_to_url |
|
89 | 90 | end |
|
90 | 91 | |
|
91 | 92 | def news_added(news) |
|
92 | 93 | redmine_headers 'Project' => news.project.identifier |
|
93 | 94 | recipients news.project.recipients |
|
94 | 95 | subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}" |
|
95 | 96 | body :news => news, |
|
96 | 97 | :news_url => url_for(:controller => 'news', :action => 'show', :id => news) |
|
97 | 98 | end |
|
98 | 99 | |
|
99 | 100 | def message_posted(message, recipients) |
|
100 | 101 | redmine_headers 'Project' => message.project.identifier, |
|
101 | 102 | 'Topic-Id' => (message.parent_id || message.id) |
|
102 | 103 | recipients(recipients) |
|
103 | 104 | subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}" |
|
104 | 105 | body :message => message, |
|
105 | 106 | :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root) |
|
106 | 107 | end |
|
107 | 108 | |
|
108 | 109 | def account_information(user, password) |
|
109 | 110 | set_language_if_valid user.language |
|
110 | 111 | recipients user.mail |
|
111 | 112 | subject l(:mail_subject_register, Setting.app_title) |
|
112 | 113 | body :user => user, |
|
113 | 114 | :password => password, |
|
114 | 115 | :login_url => url_for(:controller => 'account', :action => 'login') |
|
115 | 116 | end |
|
116 | 117 | |
|
117 | 118 | def account_activation_request(user) |
|
118 | 119 | # Send the email to all active administrators |
|
119 | 120 | recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact |
|
120 | 121 | subject l(:mail_subject_account_activation_request, Setting.app_title) |
|
121 | 122 | body :user => user, |
|
122 | 123 | :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc') |
|
123 | 124 | end |
|
124 | 125 | |
|
125 | 126 | def lost_password(token) |
|
126 | 127 | set_language_if_valid(token.user.language) |
|
127 | 128 | recipients token.user.mail |
|
128 | 129 | subject l(:mail_subject_lost_password, Setting.app_title) |
|
129 | 130 | body :token => token, |
|
130 | 131 | :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value) |
|
131 | 132 | end |
|
132 | 133 | |
|
133 | 134 | def register(token) |
|
134 | 135 | set_language_if_valid(token.user.language) |
|
135 | 136 | recipients token.user.mail |
|
136 | 137 | subject l(:mail_subject_register, Setting.app_title) |
|
137 | 138 | body :token => token, |
|
138 | 139 | :url => url_for(:controller => 'account', :action => 'activate', :token => token.value) |
|
139 | 140 | end |
|
140 | 141 | |
|
141 | 142 | def test(user) |
|
142 | 143 | set_language_if_valid(user.language) |
|
143 | 144 | recipients user.mail |
|
144 | 145 | subject 'Redmine test' |
|
145 | 146 | body :url => url_for(:controller => 'welcome') |
|
146 | 147 | end |
|
147 | 148 | |
|
148 | 149 | # Overrides default deliver! method to prevent from sending an email |
|
149 | 150 | # with no recipient, cc or bcc |
|
150 | 151 | def deliver!(mail = @mail) |
|
151 | 152 | return false if (recipients.nil? || recipients.empty?) && |
|
152 | 153 | (cc.nil? || cc.empty?) && |
|
153 | 154 | (bcc.nil? || bcc.empty?) |
|
154 | 155 | super |
|
155 | 156 | end |
|
156 | 157 | |
|
157 | 158 | # Sends reminders to issue assignees |
|
158 | 159 | # Available options: |
|
159 | 160 | # * :days => how many days in the future to remind about (defaults to 7) |
|
160 | 161 | # * :tracker => id of tracker for filtering issues (defaults to all trackers) |
|
161 | 162 | # * :project => id or identifier of project to process (defaults to all projects) |
|
162 | 163 | def self.reminders(options={}) |
|
163 | 164 | days = options[:days] || 7 |
|
164 | 165 | project = options[:project] ? Project.find(options[:project]) : nil |
|
165 | 166 | tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil |
|
166 | 167 | |
|
167 | 168 | s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date] |
|
168 | 169 | s << "#{Issue.table_name}.assigned_to_id IS NOT NULL" |
|
169 | 170 | s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}" |
|
170 | 171 | s << "#{Issue.table_name}.project_id = #{project.id}" if project |
|
171 | 172 | s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker |
|
172 | 173 | |
|
173 | 174 | issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker], |
|
174 | 175 | :conditions => s.conditions |
|
175 | 176 | ).group_by(&:assigned_to) |
|
176 | 177 | issues_by_assignee.each do |assignee, issues| |
|
177 | 178 | deliver_reminder(assignee, issues, days) unless assignee.nil? |
|
178 | 179 | end |
|
179 | 180 | end |
|
180 | 181 | |
|
181 | 182 | private |
|
182 | 183 | def initialize_defaults(method_name) |
|
183 | 184 | super |
|
184 | 185 | set_language_if_valid Setting.default_language |
|
185 | 186 | from Setting.mail_from |
|
186 | 187 | |
|
187 | 188 | # URL options |
|
188 | 189 | h = Setting.host_name |
|
189 | 190 | h = h.to_s.gsub(%r{\/.*$}, '') unless ActionController::AbstractRequest.relative_url_root.blank? |
|
190 | 191 | default_url_options[:host] = h |
|
191 | 192 | default_url_options[:protocol] = Setting.protocol |
|
192 | 193 | |
|
193 | 194 | # Common headers |
|
194 | 195 | headers 'X-Mailer' => 'Redmine', |
|
195 | 196 | 'X-Redmine-Host' => Setting.host_name, |
|
196 | 197 | 'X-Redmine-Site' => Setting.app_title |
|
197 | 198 | end |
|
198 | 199 | |
|
199 | 200 | # Appends a Redmine header field (name is prepended with 'X-Redmine-') |
|
200 | 201 | def redmine_headers(h) |
|
201 | 202 | h.each { |k,v| headers["X-Redmine-#{k}"] = v } |
|
202 | 203 | end |
|
203 | 204 | |
|
204 | 205 | # Overrides the create_mail method |
|
205 | 206 | def create_mail |
|
206 | 207 | # Removes the current user from the recipients and cc |
|
207 | 208 | # if he doesn't want to receive notifications about what he does |
|
208 | 209 | if User.current.pref[:no_self_notified] |
|
209 | 210 | recipients.delete(User.current.mail) if recipients |
|
210 | 211 | cc.delete(User.current.mail) if cc |
|
211 | 212 | end |
|
212 | 213 | # Blind carbon copy recipients |
|
213 | 214 | if Setting.bcc_recipients? |
|
214 | 215 | bcc([recipients, cc].flatten.compact.uniq) |
|
215 | 216 | recipients [] |
|
216 | 217 | cc [] |
|
217 | 218 | end |
|
218 | 219 | super |
|
219 | 220 | end |
|
220 | 221 | |
|
221 | 222 | # Renders a message with the corresponding layout |
|
222 | 223 | def render_message(method_name, body) |
|
223 | 224 | layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml' |
|
224 | 225 | body[:content_for_layout] = render(:file => method_name, :body => body) |
|
225 | 226 | ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true) |
|
226 | 227 | end |
|
227 | 228 | |
|
228 | 229 | # for the case of plain text only |
|
229 | 230 | def body(*params) |
|
230 | 231 | value = super(*params) |
|
231 | 232 | if Setting.plain_text_mail? |
|
232 | 233 | templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}") |
|
233 | 234 | unless String === @body or templates.empty? |
|
234 | 235 | template = File.basename(templates.first) |
|
235 | 236 | @body[:content_for_layout] = render(:file => template, :body => @body) |
|
236 | 237 | @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true) |
|
237 | 238 | return @body |
|
238 | 239 | end |
|
239 | 240 | end |
|
240 | 241 | return value |
|
241 | 242 | end |
|
242 | 243 | |
|
243 | 244 | # Makes partial rendering work with Rails 1.2 (retro-compatibility) |
|
244 | 245 | def self.controller_path |
|
245 | 246 | '' |
|
246 | 247 | end unless respond_to?('controller_path') |
|
247 | 248 | end |
@@ -1,53 +1,61 | |||
|
1 | 1 | <% if @issue.new_record? %> |
|
2 | 2 | <p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, :required => true %></p> |
|
3 | 3 | <%= observe_field :issue_tracker_id, :url => { :action => :new }, |
|
4 | 4 | :update => :content, |
|
5 | 5 | :with => "Form.serialize('issue-form')" %> |
|
6 | 6 | <hr /> |
|
7 | 7 | <% end %> |
|
8 | 8 | |
|
9 | 9 | <div id="issue_descr_fields" <%= 'style="display:none"' unless @issue.new_record? || @issue.errors.any? %>> |
|
10 | 10 | <p><%= f.text_field :subject, :size => 80, :required => true %></p> |
|
11 | 11 | <p><%= f.text_area :description, :required => true, |
|
12 | 12 | :cols => 60, |
|
13 | 13 | :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min), |
|
14 | 14 | :accesskey => accesskey(:edit), |
|
15 | 15 | :class => 'wiki-edit' %></p> |
|
16 | 16 | </div> |
|
17 | 17 | |
|
18 | 18 | <div class="splitcontentleft"> |
|
19 | 19 | <% if @issue.new_record? || @allowed_statuses.any? %> |
|
20 | 20 | <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p> |
|
21 | 21 | <% else %> |
|
22 | 22 | <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p> |
|
23 | 23 | <% end %> |
|
24 | 24 | |
|
25 | 25 | <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p> |
|
26 | 26 | <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p> |
|
27 | 27 | <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> |
|
28 | 28 | <%= prompt_to_remote(l(:label_issue_category_new), |
|
29 | 29 | l(:label_issue_category_new), 'category[name]', |
|
30 | 30 | {:controller => 'projects', :action => 'add_issue_category', :id => @project}, |
|
31 | 31 | :class => 'small', :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p> |
|
32 | 32 | <%= content_tag('p', f.select(:fixed_version_id, |
|
33 | 33 | (@project.versions.sort.collect {|v| [v.name, v.id]}), |
|
34 | 34 | { :include_blank => true })) unless @project.versions.empty? %> |
|
35 | 35 | </div> |
|
36 | 36 | |
|
37 | 37 | <div class="splitcontentright"> |
|
38 | 38 | <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p> |
|
39 | 39 | <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p> |
|
40 | 40 | <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p> |
|
41 | 41 | <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p> |
|
42 | 42 | </div> |
|
43 | 43 | |
|
44 | 44 | <div style="clear:both;"> </div> |
|
45 | 45 | <%= render :partial => 'form_custom_fields' %> |
|
46 | 46 | |
|
47 | 47 | <% if @issue.new_record? %> |
|
48 | 48 | <p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> |
|
49 | 49 | <% end %> |
|
50 | 50 | |
|
51 | <% if @issue.new_record? && User.current.allowed_to?(:add_issue_watchers, @project) -%> | |
|
52 | <p><label><%= l(:label_issue_watchers) %></label> | |
|
53 | <% @issue.project.users.sort.each do |user| -%> | |
|
54 | <label class="floating"><%= check_box_tag 'issue[watcher_user_ids][]', user.id, @issue.watcher_user_ids.include?(user.id) %> <%=h user %></label> | |
|
55 | <% end -%> | |
|
56 | </p> | |
|
57 | <% end %> | |
|
58 | ||
|
51 | 59 | <%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %> |
|
52 | 60 | |
|
53 | 61 | <%= wikitoolbar_for 'issue_description' %> |
@@ -1,28 +1,30 | |||
|
1 | 1 | <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2> |
|
2 | 2 | |
|
3 | <p><%= render :partial => 'link_to_functions' %></p> | |
|
4 | ||
|
3 | 5 | <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> |
|
4 | 6 | |
|
5 | 7 | <div class="autoscroll"> |
|
6 | 8 | <table class="filecontent annotate CodeRay"> |
|
7 | 9 | <tbody> |
|
8 | 10 | <% line_num = 1 %> |
|
9 | 11 | <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %> |
|
10 | 12 | <% revision = @annotate.revisions[line_num-1] %> |
|
11 | 13 | <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>"> |
|
12 | 14 | <th class="line-num"><%= line_num %></th> |
|
13 | 15 | <td class="revision"> |
|
14 | 16 | <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td> |
|
15 | 17 | <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td> |
|
16 | 18 | <td class="line-code"><pre><%= line %></pre></td> |
|
17 | 19 | </tr> |
|
18 | 20 | <% line_num += 1 %> |
|
19 | 21 | <% end %> |
|
20 | 22 | </tbody> |
|
21 | 23 | </table> |
|
22 | 24 | </div> |
|
23 | 25 | |
|
24 | 26 | <% html_title(l(:button_annotate)) -%> |
|
25 | 27 | |
|
26 | 28 | <% content_for :header_tags do %> |
|
27 | 29 | <%= stylesheet_link_tag 'scm' %> |
|
28 | 30 | <% end %> |
@@ -1,19 +1,10 | |||
|
1 | 1 | <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2> |
|
2 | 2 | |
|
3 | <p> | |
|
4 | <% if @repository.supports_cat? %> | |
|
5 | <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | | |
|
6 | <% end %> | |
|
7 | <% if @repository.supports_annotate? %> | |
|
8 | <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> | | |
|
9 | <% end %> | |
|
10 | <%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %> | |
|
11 | <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %> | |
|
12 | </p> | |
|
3 | <p><%= render :partial => 'link_to_functions' %></p> | |
|
13 | 4 | |
|
14 | 5 | <%= render_properties(@properties) %> |
|
15 | 6 | |
|
16 | 7 | <%= render(:partial => 'revisions', |
|
17 | 8 | :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %> |
|
18 | 9 | |
|
19 | 10 | <% html_title(l(:label_change_plural)) -%> |
@@ -1,7 +1,9 | |||
|
1 | 1 | <h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2> |
|
2 | 2 | |
|
3 | <p><%= render :partial => 'link_to_functions' %></p> | |
|
4 | ||
|
3 | 5 | <%= render :partial => 'common/file', :locals => {:filename => @path, :content => @content} %> |
|
4 | 6 | |
|
5 | 7 | <% content_for :header_tags do %> |
|
6 | 8 | <%= stylesheet_link_tag "scm" %> |
|
7 | 9 | <% end %> |
@@ -1,832 +1,839 | |||
|
1 | 1 | == Redmine changelog |
|
2 | 2 | |
|
3 | 3 | Redmine - project management software |
|
4 | 4 | Copyright (C) 2006-2008 Jean-Philippe Lang |
|
5 | 5 | http://www.redmine.org/ |
|
6 | 6 | |
|
7 | 7 | |
|
8 | == v0.8.1 | |
|
9 | ||
|
10 | * Select watchers on new issue form | |
|
11 | * Show view/annotate/download links on entry and annotate views | |
|
12 | * Fixed: Deleted files are shown when using Darcs | |
|
13 | ||
|
14 | ||
|
8 | 15 | == 2008-12-30 v0.8.0 |
|
9 | 16 | |
|
10 | 17 | * Setting added in order to limit the number of diff lines that should be displayed |
|
11 | 18 | * Makes logged-in username in topbar linking to |
|
12 | 19 | * Mail handler: strip tags when receiving a html-only email |
|
13 | 20 | * Mail handler: add watchers before sending notification |
|
14 | 21 | * Adds a css class (overdue) to overdue issues on issue lists and detail views |
|
15 | 22 | * Fixed: project activity truncated after viewing user's activity |
|
16 | 23 | * Fixed: email address entered for password recovery shouldn't be case-sensitive |
|
17 | 24 | * Fixed: default flag removed when editing a default enumeration |
|
18 | 25 | * Fixed: default category ignored when adding a document |
|
19 | 26 | * Fixed: error on repository user mapping when a repository username is blank |
|
20 | 27 | * Fixed: Firefox cuts off large diffs |
|
21 | 28 | * Fixed: CVS browser should not show dead revisions (deleted files) |
|
22 | 29 | * Fixed: escape double-quotes in image titles |
|
23 | 30 | * Fixed: escape textarea content when editing a issue note |
|
24 | 31 | * Fixed: JS error on context menu with IE |
|
25 | 32 | * Fixed: bold syntax around single character in series doesn't work |
|
26 | 33 | * Fixed several XSS vulnerabilities |
|
27 | 34 | * Fixed a SQL injection vulnerability |
|
28 | 35 | |
|
29 | 36 | |
|
30 | 37 | == 2008-12-07 v0.8.0-rc1 |
|
31 | 38 | |
|
32 | 39 | * Wiki page protection |
|
33 | 40 | * Wiki page hierarchy. Parent page can be assigned on the Rename screen |
|
34 | 41 | * Adds support for issue creation via email |
|
35 | 42 | * Adds support for free ticket filtering and custom queries on Gantt chart and calendar |
|
36 | 43 | * Cross-project search |
|
37 | 44 | * Ability to search a project and its subprojects |
|
38 | 45 | * Ability to search the projects the user belongs to |
|
39 | 46 | * Adds custom fields on time entries |
|
40 | 47 | * Adds boolean and list custom fields for time entries as criteria on time report |
|
41 | 48 | * Cross-project time reports |
|
42 | 49 | * Display latest user's activity on account/show view |
|
43 | 50 | * Show last connexion time on user's page |
|
44 | 51 | * Obfuscates email address on user's account page using javascript |
|
45 | 52 | * wiki TOC rendered as an unordered list |
|
46 | 53 | * Adds the ability to search for a user on the administration users list |
|
47 | 54 | * Adds the ability to search for a project name or identifier on the administration projects list |
|
48 | 55 | * Redirect user to the previous page after logging in |
|
49 | 56 | * Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users |
|
50 | 57 | * Adds permissions for viewing the watcher list and adding new watchers on the issue detail view |
|
51 | 58 | * Adds permissions to let users edit and/or delete their messages |
|
52 | 59 | * Link to activity view when displaying dates |
|
53 | 60 | * Hide Redmine version in atom feeds and pdf properties |
|
54 | 61 | * 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. |
|
55 | 62 | * Sort users by their display names so that user dropdown lists are sorted alphabetically |
|
56 | 63 | * Adds estimated hours to issue filters |
|
57 | 64 | * Switch order of current and previous revisions in side-by-side diff |
|
58 | 65 | * Render the commit changes list as a tree |
|
59 | 66 | * Adds watch/unwatch functionality at forum topic level |
|
60 | 67 | * When moving an issue to another project, reassign it to the category with same name if any |
|
61 | 68 | * Adds child_pages macro for wiki pages |
|
62 | 69 | * Use GET instead of POST on roadmap (#718), gantt and calendar forms |
|
63 | 70 | * Search engine: display total results count and count by result type |
|
64 | 71 | * Email delivery configuration moved to an unversioned YAML file (config/email.yml, see the sample file) |
|
65 | 72 | * Adds icons on search results |
|
66 | 73 | * Adds 'Edit' link on account/show for admin users |
|
67 | 74 | * Adds Lock/Unlock/Activate link on user edit screen |
|
68 | 75 | * Adds user count in status drop down on admin user list |
|
69 | 76 | * Adds multi-levels blockquotes support by using > at the beginning of lines |
|
70 | 77 | * Adds a Reply link to each issue note |
|
71 | 78 | * Adds plain text only option for mail notifications |
|
72 | 79 | * Gravatar support for issue detail, user grid, and activity stream (disabled by default) |
|
73 | 80 | * Adds 'Delete wiki pages attachments' permission |
|
74 | 81 | * Show the most recent file when displaying an inline image |
|
75 | 82 | * Makes permission screens localized |
|
76 | 83 | * AuthSource list: display associated users count and disable 'Delete' buton if any |
|
77 | 84 | * Make the 'duplicates of' relation asymmetric |
|
78 | 85 | * Adds username to the password reminder email |
|
79 | 86 | * Adds links to forum messages using message#id syntax |
|
80 | 87 | * Allow same name for custom fields on different object types |
|
81 | 88 | * One-click bulk edition using the issue list context menu within the same project |
|
82 | 89 | * 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. |
|
83 | 90 | * Adds checkboxes toggle links on permissions report |
|
84 | 91 | * Adds Trac-Like anchors on wiki headings |
|
85 | 92 | * Adds support for wiki links with anchor |
|
86 | 93 | * Adds category to the issue context menu |
|
87 | 94 | * Adds a workflow overview screen |
|
88 | 95 | * Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename |
|
89 | 96 | * Dots allowed in custom field name |
|
90 | 97 | * Adds posts quoting functionality |
|
91 | 98 | * Adds an option to generate sequential project identifiers |
|
92 | 99 | * Adds mailto link on the user administration list |
|
93 | 100 | * Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value |
|
94 | 101 | * Gantt chart: display issues that don't have a due date if they are assigned to a version with a date |
|
95 | 102 | * Change projects homepage limit to 255 chars |
|
96 | 103 | * 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 |
|
97 | 104 | * Adds "please select" to activity select box if no activity is set as default |
|
98 | 105 | * Do not silently ignore timelog validation failure on issue edit |
|
99 | 106 | * Adds a rake task to send reminder emails |
|
100 | 107 | * Allow empty cells in wiki tables |
|
101 | 108 | * Makes wiki text formatter pluggable |
|
102 | 109 | * Adds back textile acronyms support |
|
103 | 110 | * Remove pre tag attributes |
|
104 | 111 | * Plugin hooks |
|
105 | 112 | * Pluggable admin menu |
|
106 | 113 | * Plugins can provide activity content |
|
107 | 114 | * Moves plugin list to its own administration menu item |
|
108 | 115 | * Adds url and author_url plugin attributes |
|
109 | 116 | * Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version |
|
110 | 117 | * Adds atom feed on time entries details |
|
111 | 118 | * Adds project name to issues feed title |
|
112 | 119 | * Adds a css class on menu items in order to apply item specific styles (eg. icons) |
|
113 | 120 | * Adds a Redmine plugin generators |
|
114 | 121 | * Adds timelog link to the issue context menu |
|
115 | 122 | * Adds links to the user page on various views |
|
116 | 123 | * Turkish translation by Ismail Sezen |
|
117 | 124 | * Catalan translation |
|
118 | 125 | * Vietnamese translation |
|
119 | 126 | * Slovak translation |
|
120 | 127 | * Better naming of activity feed if only one kind of event is displayed |
|
121 | 128 | * Enable syntax highlight on issues, messages and news |
|
122 | 129 | * Add target version to the issue list context menu |
|
123 | 130 | * Hide 'Target version' filter if no version is defined |
|
124 | 131 | * Add filters on cross-project issue list for custom fields marked as 'For all projects' |
|
125 | 132 | * Turn ftp urls into links |
|
126 | 133 | * Hiding the View Differences button when a wiki page's history only has one version |
|
127 | 134 | * Messages on a Board can now be sorted by the number of replies |
|
128 | 135 | * Adds a class ('me') to events of the activity view created by current user |
|
129 | 136 | * Strip pre/code tags content from activity view events |
|
130 | 137 | * Display issue notes in the activity view |
|
131 | 138 | * Adds links to changesets atom feed on repository browser |
|
132 | 139 | * Track project and tracker changes in issue history |
|
133 | 140 | * Adds anchor to atom feed messages links |
|
134 | 141 | * Adds a key in lang files to set the decimal separator (point or comma) in csv exports |
|
135 | 142 | * Makes importer work with Trac 0.8.x |
|
136 | 143 | * Upgraded to Prototype 1.6.0.1 |
|
137 | 144 | * File viewer for attached text files |
|
138 | 145 | * Menu mapper: add support for :before, :after and :last options to #push method and add #delete method |
|
139 | 146 | * Removed inconsistent revision numbers on diff view |
|
140 | 147 | * CVS: add support for modules names with spaces |
|
141 | 148 | * Log the user in after registration if account activation is not needed |
|
142 | 149 | * Mercurial adapter improvements |
|
143 | 150 | * Trac importer: read session_attribute table to find user's email and real name |
|
144 | 151 | * Ability to disable unused SCM adapters in application settings |
|
145 | 152 | * Adds Filesystem adapter |
|
146 | 153 | * Clear changesets and changes with raw sql when deleting a repository for performance |
|
147 | 154 | * Redmine.pm now uses the 'commit access' permission defined in Redmine |
|
148 | 155 | * Reposman can create any type of scm (--scm option) |
|
149 | 156 | * Reposman creates a repository if the 'repository' module is enabled at project level only |
|
150 | 157 | * Display svn properties in the browser, svn >= 1.5.0 only |
|
151 | 158 | * Reduces memory usage when importing large git repositories |
|
152 | 159 | * Wider SVG graphs in repository stats |
|
153 | 160 | * SubversionAdapter#entries performance improvement |
|
154 | 161 | * SCM browser: ability to download raw unified diffs |
|
155 | 162 | * More detailed error message in log when scm command fails |
|
156 | 163 | * Adds support for file viewing with Darcs 2.0+ |
|
157 | 164 | * Check that git changeset is not in the database before creating it |
|
158 | 165 | * Unified diff viewer for attached files with .patch or .diff extension |
|
159 | 166 | * File size display with Bazaar repositories |
|
160 | 167 | * Git adapter: use commit time instead of author time |
|
161 | 168 | * Prettier url for changesets |
|
162 | 169 | * Makes changes link to entries on the revision view |
|
163 | 170 | * Adds a field on the repository view to browse at specific revision |
|
164 | 171 | * Adds new projects atom feed |
|
165 | 172 | * Added rake tasks to generate rcov code coverage reports |
|
166 | 173 | * Add Redcloth's :block_markdown_rule to allow horizontal rules in wiki |
|
167 | 174 | * Show the project hierarchy in the drop down list for new membership on user administration screen |
|
168 | 175 | * Split user edit screen into tabs |
|
169 | 176 | * Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead |
|
170 | 177 | * Fixed: Roadmap crashes when a version has a due date > 2037 |
|
171 | 178 | * Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen |
|
172 | 179 | * Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory |
|
173 | 180 | * Fixed: logtime entry duplicated when edited from parent project |
|
174 | 181 | * Fixed: wrong digest for text files under Windows |
|
175 | 182 | * Fixed: associated revisions are displayed in wrong order on issue view |
|
176 | 183 | * Fixed: Git Adapter date parsing ignores timezone |
|
177 | 184 | * Fixed: Printing long roadmap doesn't split across pages |
|
178 | 185 | * Fixes custom fields display order at several places |
|
179 | 186 | * Fixed: urls containing @ are parsed as email adress by the wiki formatter |
|
180 | 187 | * Fixed date filters accuracy with SQLite |
|
181 | 188 | * Fixed: tokens not escaped in highlight_tokens regexp |
|
182 | 189 | * Fixed Bazaar shared repository browsing |
|
183 | 190 | * Fixes platform determination under JRuby |
|
184 | 191 | * Fixed: Estimated time in issue's journal should be rounded to two decimals |
|
185 | 192 | * Fixed: 'search titles only' box ignored after one search is done on titles only |
|
186 | 193 | * Fixed: non-ASCII subversion path can't be displayed |
|
187 | 194 | * Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format |
|
188 | 195 | * Fixed: document listing shows on "my page" when viewing documents is disabled for the role |
|
189 | 196 | * Fixed: Latest news appear on the homepage for projects with the News module disabled |
|
190 | 197 | * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled |
|
191 | 198 | * Fixed: the default status is lost when reordering issue statuses |
|
192 | 199 | * Fixes error with Postgresql and non-UTF8 commit logs |
|
193 | 200 | * Fixed: textile footnotes no longer work |
|
194 | 201 | * Fixed: http links containing parentheses fail to reder correctly |
|
195 | 202 | * Fixed: GitAdapter#get_rev should use current branch instead of hardwiring master |
|
196 | 203 | |
|
197 | 204 | |
|
198 | 205 | == 2008-07-06 v0.7.3 |
|
199 | 206 | |
|
200 | 207 | * Allow dot in firstnames and lastnames |
|
201 | 208 | * Add project name to cross-project Atom feeds |
|
202 | 209 | * Encoding set to utf8 in example database.yml |
|
203 | 210 | * HTML titles on forums related views |
|
204 | 211 | * Fixed: various XSS vulnerabilities |
|
205 | 212 | * Fixed: Entourage (and some old client) fails to correctly render notification styles |
|
206 | 213 | * Fixed: Fixed: timelog redirects inappropriately when :back_url is blank |
|
207 | 214 | * Fixed: wrong relative paths to images in wiki_syntax.html |
|
208 | 215 | |
|
209 | 216 | |
|
210 | 217 | == 2008-06-15 v0.7.2 |
|
211 | 218 | |
|
212 | 219 | * "New Project" link on Projects page |
|
213 | 220 | * Links to repository directories on the repo browser |
|
214 | 221 | * Move status to front in Activity View |
|
215 | 222 | * Remove edit step from Status context menu |
|
216 | 223 | * Fixed: No way to do textile horizontal rule |
|
217 | 224 | * Fixed: Repository: View differences doesn't work |
|
218 | 225 | * Fixed: attachement's name maybe invalid. |
|
219 | 226 | * Fixed: Error when creating a new issue |
|
220 | 227 | * Fixed: NoMethodError on @available_filters.has_key? |
|
221 | 228 | * Fixed: Check All / Uncheck All in Email Settings |
|
222 | 229 | * Fixed: "View differences" of one file at /repositories/revision/ fails |
|
223 | 230 | * Fixed: Column width in "my page" |
|
224 | 231 | * Fixed: private subprojects are listed on Issues view |
|
225 | 232 | * Fixed: Textile: bold, italics, underline, etc... not working after parentheses |
|
226 | 233 | * Fixed: Update issue form: comment field from log time end out of screen |
|
227 | 234 | * Fixed: Editing role: "issue can be assigned to this role" out of box |
|
228 | 235 | * Fixed: Unable use angular braces after include word |
|
229 | 236 | * Fixed: Using '*' as keyword for repository referencing keywords doesn't work |
|
230 | 237 | * Fixed: Subversion repository "View differences" on each file rise ERROR |
|
231 | 238 | * Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root |
|
232 | 239 | * Fixed: It is possible to lock out the last admin account |
|
233 | 240 | * Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access |
|
234 | 241 | * Fixed: Issue number display clipped on 'my issues' |
|
235 | 242 | * Fixed: Roadmap version list links not carrying state |
|
236 | 243 | * Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default |
|
237 | 244 | * Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master" |
|
238 | 245 | * Fixed: browser's language subcodes ignored |
|
239 | 246 | * Fixed: Error on project selection with numeric (only) identifier. |
|
240 | 247 | * Fixed: Link to PDF doesn't work after creating new issue |
|
241 | 248 | * Fixed: "Replies" should not be shown on forum threads that are locked |
|
242 | 249 | * Fixed: SVN errors lead to svn username/password being displayed to end users (security issue) |
|
243 | 250 | * Fixed: http links containing hashes don't display correct |
|
244 | 251 | * Fixed: Allow ampersands in Enumeration names |
|
245 | 252 | * Fixed: Atom link on saved query does not include query_id |
|
246 | 253 | * Fixed: Logtime info lost when there's an error updating an issue |
|
247 | 254 | * Fixed: TOC does not parse colorization markups |
|
248 | 255 | * Fixed: CVS: add support for modules names with spaces |
|
249 | 256 | * Fixed: Bad rendering on projects/add |
|
250 | 257 | * Fixed: exception when viewing differences on cvs |
|
251 | 258 | * Fixed: export issue to pdf will messup when use Chinese language |
|
252 | 259 | * Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant |
|
253 | 260 | * Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE |
|
254 | 261 | * Fixed: Importing from trac : some wiki links are messed |
|
255 | 262 | * Fixed: Incorrect weekend definition in Hebrew calendar locale |
|
256 | 263 | * Fixed: Atom feeds don't provide author section for repository revisions |
|
257 | 264 | * Fixed: In Activity views, changesets titles can be multiline while they should not |
|
258 | 265 | * Fixed: Ignore unreadable subversion directories (read disabled using authz) |
|
259 | 266 | * Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets |
|
260 | 267 | * Fixed: Close statement handler in Redmine.pm |
|
261 | 268 | |
|
262 | 269 | |
|
263 | 270 | == 2008-05-04 v0.7.1 |
|
264 | 271 | |
|
265 | 272 | * Thai translation added (Gampol Thitinilnithi) |
|
266 | 273 | * Translations updates |
|
267 | 274 | * Escape HTML comment tags |
|
268 | 275 | * Prevent "can't convert nil into String" error when :sort_order param is not present |
|
269 | 276 | * Fixed: Updating tickets add a time log with zero hours |
|
270 | 277 | * Fixed: private subprojects names are revealed on the project overview |
|
271 | 278 | * Fixed: Search for target version of "none" fails with postgres 8.3 |
|
272 | 279 | * Fixed: Home, Logout, Login links shouldn't be absolute links |
|
273 | 280 | * Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects |
|
274 | 281 | * Fixed: error when using upcase language name in coderay |
|
275 | 282 | * Fixed: error on Trac import when :due attribute is nil |
|
276 | 283 | |
|
277 | 284 | |
|
278 | 285 | == 2008-04-28 v0.7.0 |
|
279 | 286 | |
|
280 | 287 | * Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present |
|
281 | 288 | * Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list. |
|
282 | 289 | * Add predefined date ranges to the time report |
|
283 | 290 | * Time report can be done at issue level |
|
284 | 291 | * Various timelog report enhancements |
|
285 | 292 | * Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30 |
|
286 | 293 | * Display the context menu above and/or to the left of the click if needed |
|
287 | 294 | * Make the admin project files list sortable |
|
288 | 295 | * Mercurial: display working directory files sizes unless browsing a specific revision |
|
289 | 296 | * Preserve status filter and page number when using lock/unlock/activate links on the users list |
|
290 | 297 | * Redmine.pm support for LDAP authentication |
|
291 | 298 | * Better error message and AR errors in log for failed LDAP on-the-fly user creation |
|
292 | 299 | * Redirected user to where he is coming from after logging hours |
|
293 | 300 | * Warn user that subprojects are also deleted when deleting a project |
|
294 | 301 | * Include subprojects versions on calendar and gantt |
|
295 | 302 | * Notify project members when a message is posted if they want to receive notifications |
|
296 | 303 | * Fixed: Feed content limit setting has no effect |
|
297 | 304 | * Fixed: Priorities not ordered when displayed as a filter in issue list |
|
298 | 305 | * Fixed: can not display attached images inline in message replies |
|
299 | 306 | * Fixed: Boards are not deleted when project is deleted |
|
300 | 307 | * Fixed: trying to preview a new issue raises an exception with postgresql |
|
301 | 308 | * Fixed: single file 'View difference' links do not work because of duplicate slashes in url |
|
302 | 309 | * Fixed: inline image not displayed when including a wiki page |
|
303 | 310 | * Fixed: CVS duplicate key violation |
|
304 | 311 | * Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues |
|
305 | 312 | * Fixed: custom field filters behaviour |
|
306 | 313 | * Fixed: Postgresql 8.3 compatibility |
|
307 | 314 | * Fixed: Links to repository directories don't work |
|
308 | 315 | |
|
309 | 316 | |
|
310 | 317 | == 2008-03-29 v0.7.0-rc1 |
|
311 | 318 | |
|
312 | 319 | * Overall activity view and feed added, link is available on the project list |
|
313 | 320 | * Git VCS support |
|
314 | 321 | * Rails 2.0 sessions cookie store compatibility |
|
315 | 322 | * Use project identifiers in urls instead of ids |
|
316 | 323 | * Default configuration data can now be loaded from the administration screen |
|
317 | 324 | * Administration settings screen split to tabs (email notifications options moved to 'Settings') |
|
318 | 325 | * Project description is now unlimited and optional |
|
319 | 326 | * Wiki annotate view |
|
320 | 327 | * Escape HTML tag in textile content |
|
321 | 328 | * Add Redmine links to documents, versions, attachments and repository files |
|
322 | 329 | * 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: |
|
323 | 330 | * by using checkbox and/or the little pencil that will select/unselect all issues |
|
324 | 331 | * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues |
|
325 | 332 | * 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) |
|
326 | 333 | * User display format is now configurable in administration settings |
|
327 | 334 | * Issue list now supports bulk edit/move/delete (for a set of issues that belong to the same project) |
|
328 | 335 | * Merged 'change status', 'edit issue' and 'add note' actions: |
|
329 | 336 | * Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status |
|
330 | 337 | * '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 |
|
331 | 338 | * Details by assignees on issue summary view |
|
332 | 339 | * '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 |
|
333 | 340 | * Change status select box default to current status |
|
334 | 341 | * Preview for issue notes, news and messages |
|
335 | 342 | * Optional description for attachments |
|
336 | 343 | * 'Fixed version' label changed to 'Target version' |
|
337 | 344 | * Let the user choose when deleting issues with reported hours to: |
|
338 | 345 | * delete the hours |
|
339 | 346 | * assign the hours to the project |
|
340 | 347 | * reassign the hours to another issue |
|
341 | 348 | * Date range filter and pagination on time entries detail view |
|
342 | 349 | * Propagate time tracking to the parent project |
|
343 | 350 | * Switch added on the project activity view to include subprojects |
|
344 | 351 | * Display total estimated and spent hours on the version detail view |
|
345 | 352 | * Weekly time tracking block for 'My page' |
|
346 | 353 | * Permissions to edit time entries |
|
347 | 354 | * Include subprojects on the issue list, calendar, gantt and timelog by default (can be turned off is administration settings) |
|
348 | 355 | * Roadmap enhancements (separate related issues from wiki contents, leading h1 in version wiki pages is hidden, smaller wiki headings) |
|
349 | 356 | * Make versions with same date sorted by name |
|
350 | 357 | * Allow issue list to be sorted by target version |
|
351 | 358 | * Related changesets messages displayed on the issue details view |
|
352 | 359 | * Create a journal and send an email when an issue is closed by commit |
|
353 | 360 | * Add 'Author' to the available columns for the issue list |
|
354 | 361 | * More appropriate default sort order on sortable columns |
|
355 | 362 | * Add issue subject to the time entries view and issue subject, description and tracker to the csv export |
|
356 | 363 | * Permissions to edit issue notes |
|
357 | 364 | * Display date/time instead of date on files list |
|
358 | 365 | * Do not show Roadmap menu item if the project doesn't define any versions |
|
359 | 366 | * Allow longer version names (60 chars) |
|
360 | 367 | * Ability to copy an existing workflow when creating a new role |
|
361 | 368 | * Display custom fields in two columns on the issue form |
|
362 | 369 | * Added 'estimated time' in the csv export of the issue list |
|
363 | 370 | * Display the last 30 days on the activity view rather than the current month (number of days can be configured in the application settings) |
|
364 | 371 | * Setting for whether new projects should be public by default |
|
365 | 372 | * User preference to choose how comments/replies are displayed: in chronological or reverse chronological order |
|
366 | 373 | * Added default value for custom fields |
|
367 | 374 | * Added tabindex property on wiki toolbar buttons (to easily move from field to field using the tab key) |
|
368 | 375 | * Redirect to issue page after creating a new issue |
|
369 | 376 | * Wiki toolbar improvements (mainly for Firefox) |
|
370 | 377 | * Display wiki syntax quick ref link on all wiki textareas |
|
371 | 378 | * Display links to Atom feeds |
|
372 | 379 | * Breadcrumb nav for the forums |
|
373 | 380 | * Show replies when choosing to display messages in the activity |
|
374 | 381 | * Added 'include' macro to include another wiki page |
|
375 | 382 | * RedmineWikiFormatting page available as a static HTML file locally |
|
376 | 383 | * Wrap diff content |
|
377 | 384 | * Strip out email address from authors in repository screens |
|
378 | 385 | * Highlight the current item of the main menu |
|
379 | 386 | * Added simple syntax highlighters for php and java languages |
|
380 | 387 | * Do not show empty diffs |
|
381 | 388 | * Show explicit error message when the scm command failed (eg. when svn binary is not available) |
|
382 | 389 | * Lithuanian translation added (Sergej Jegorov) |
|
383 | 390 | * Ukrainan translation added (Natalia Konovka & Mykhaylo Sorochan) |
|
384 | 391 | * Danish translation added (Mads Vestergaard) |
|
385 | 392 | * Added i18n support to the jstoolbar and various settings screen |
|
386 | 393 | * RedCloth's glyphs no longer user |
|
387 | 394 | * New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/) |
|
388 | 395 | * The following menus can now be extended by plugins: top_menu, account_menu, application_menu |
|
389 | 396 | * Added a simple rake task to fetch changesets from the repositories: rake redmine:fetch_changesets |
|
390 | 397 | * Remove hardcoded "Redmine" strings in account related emails and use application title instead |
|
391 | 398 | * Mantis importer preserve bug ids |
|
392 | 399 | * Trac importer: Trac guide wiki pages skipped |
|
393 | 400 | * Trac importer: wiki attachments migration added |
|
394 | 401 | * Trac importer: support database schema for Trac migration |
|
395 | 402 | * Trac importer: support CamelCase links |
|
396 | 403 | * Removes the Redmine version from the footer (can be viewed on admin -> info) |
|
397 | 404 | * Rescue and display an error message when trying to delete a role that is in use |
|
398 | 405 | * 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 |
|
399 | 406 | * Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs |
|
400 | 407 | * Fixed: Gantt and calendar not properly refreshed (fragment caching removed) |
|
401 | 408 | * Fixed: Textile image with style attribute cause internal server error |
|
402 | 409 | * Fixed: wiki TOC not rendered properly when used in an issue or document description |
|
403 | 410 | * Fixed: 'has already been taken' error message on username and email fields if left empty |
|
404 | 411 | * Fixed: non-ascii attachement filename with IE |
|
405 | 412 | * Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed |
|
406 | 413 | * Fixed: search for all words doesn't work |
|
407 | 414 | * Fixed: Do not show sticky and locked checkboxes when replying to a message |
|
408 | 415 | * Fixed: Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank |
|
409 | 416 | * Fixed: Date custom fields not displayed as specified in application settings |
|
410 | 417 | * Fixed: titles not escaped in the activity view |
|
411 | 418 | * Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context |
|
412 | 419 | * 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 |
|
413 | 420 | * Fixed: locked users should not receive email notifications |
|
414 | 421 | * Fixed: custom field selection is not saved when unchecking them all on project settings |
|
415 | 422 | * Fixed: can not lock a topic when creating it |
|
416 | 423 | * Fixed: Incorrect filtering for unset values when using 'is not' filter |
|
417 | 424 | * Fixed: PostgreSQL issues_seq_id not updated when using Trac importer |
|
418 | 425 | * Fixed: ajax pagination does not scroll up |
|
419 | 426 | * Fixed: error when uploading a file with no content-type specified by the browser |
|
420 | 427 | * Fixed: wiki and changeset links not displayed when previewing issue description or notes |
|
421 | 428 | * Fixed: 'LdapError: no bind result' error when authenticating |
|
422 | 429 | * Fixed: 'LdapError: invalid binding information' when no username/password are set on the LDAP account |
|
423 | 430 | * Fixed: CVS repository doesn't work if port is used in the url |
|
424 | 431 | * Fixed: Email notifications: host name is missing in generated links |
|
425 | 432 | * Fixed: Email notifications: referenced changesets, wiki pages, attachments... are not turned into links |
|
426 | 433 | * Fixed: Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed |
|
427 | 434 | * Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console |
|
428 | 435 | * Fixed: Do not send an email with no recipient, cc or bcc |
|
429 | 436 | * Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues. |
|
430 | 437 | * Fixed: Mercurial browsing under unix-like os and for directory depth > 2 |
|
431 | 438 | * Fixed: Wiki links with pipe can not be used in wiki tables |
|
432 | 439 | * Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets |
|
433 | 440 | * Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql |
|
434 | 441 | |
|
435 | 442 | |
|
436 | 443 | == 2008-03-12 v0.6.4 |
|
437 | 444 | |
|
438 | 445 | * Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects |
|
439 | 446 | * Fixed: potential LDAP authentication security flaw |
|
440 | 447 | * Fixed: context submenus on the issue list don't show up with IE6. |
|
441 | 448 | * Fixed: Themes are not applied with Rails 2.0 |
|
442 | 449 | * Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil |
|
443 | 450 | * Fixed: Mercurial repository browsing |
|
444 | 451 | * Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails |
|
445 | 452 | * Fixed: not null constraints not removed with Postgresql |
|
446 | 453 | * Doctype set to transitional |
|
447 | 454 | |
|
448 | 455 | |
|
449 | 456 | == 2007-12-18 v0.6.3 |
|
450 | 457 | |
|
451 | 458 | * Fixed: upload doesn't work in 'Files' section |
|
452 | 459 | |
|
453 | 460 | |
|
454 | 461 | == 2007-12-16 v0.6.2 |
|
455 | 462 | |
|
456 | 463 | * Search engine: issue custom fields can now be searched |
|
457 | 464 | * News comments are now textilized |
|
458 | 465 | * Updated Japanese translation (Satoru Kurashiki) |
|
459 | 466 | * Updated Chinese translation (Shortie Lo) |
|
460 | 467 | * Fixed Rails 2.0 compatibility bugs: |
|
461 | 468 | * Unable to create a wiki |
|
462 | 469 | * Gantt and calendar error |
|
463 | 470 | * Trac importer error (readonly? is defined by ActiveRecord) |
|
464 | 471 | * Fixed: 'assigned to me' filter broken |
|
465 | 472 | * Fixed: crash when validation fails on issue edition with no custom fields |
|
466 | 473 | * Fixed: reposman "can't find group" error |
|
467 | 474 | * Fixed: 'LDAP account password is too long' error when leaving the field empty on creation |
|
468 | 475 | * Fixed: empty lines when displaying repository files with Windows style eol |
|
469 | 476 | * Fixed: missing body closing tag in repository annotate and entry views |
|
470 | 477 | |
|
471 | 478 | |
|
472 | 479 | == 2007-12-10 v0.6.1 |
|
473 | 480 | |
|
474 | 481 | * Rails 2.0 compatibility |
|
475 | 482 | * Custom fields can now be displayed as columns on the issue list |
|
476 | 483 | * Added version details view (accessible from the roadmap) |
|
477 | 484 | * Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account) |
|
478 | 485 | * Added per-project tracker selection. Trackers can be selected on project settings |
|
479 | 486 | * Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums |
|
480 | 487 | * Forums: messages can now be edited/deleted (explicit permissions need to be given) |
|
481 | 488 | * Forums: topics can be locked so that no reply can be added |
|
482 | 489 | * Forums: topics can be marked as sticky so that they always appear at the top of the list |
|
483 | 490 | * Forums: attachments can now be added to replies |
|
484 | 491 | * Added time zone support |
|
485 | 492 | * Added a setting to choose the account activation strategy (available in application settings) |
|
486 | 493 | * Added 'Classic' theme (inspired from the v0.51 design) |
|
487 | 494 | * Added an alternate theme which provides issue list colorization based on issues priority |
|
488 | 495 | * Added Bazaar SCM adapter |
|
489 | 496 | * Added Annotate/Blame view in the repository browser (except for Darcs SCM) |
|
490 | 497 | * Diff style (inline or side by side) automatically saved as a user preference |
|
491 | 498 | * Added issues status changes on the activity view (by Cyril Mougel) |
|
492 | 499 | * Added forums topics on the activity view (disabled by default) |
|
493 | 500 | * Added an option on 'My account' for users who don't want to be notified of changes that they make |
|
494 | 501 | * Trac importer now supports mysql and postgresql databases |
|
495 | 502 | * Trac importer improvements (by Mat Trudel) |
|
496 | 503 | * 'fixed version' field can now be displayed on the issue list |
|
497 | 504 | * Added a couple of new formats for the 'date format' setting |
|
498 | 505 | * Added Traditional Chinese translation (by Shortie Lo) |
|
499 | 506 | * Added Russian translation (iGor kMeta) |
|
500 | 507 | * Project name format limitation removed (name can now contain any character) |
|
501 | 508 | * Project identifier maximum length changed from 12 to 20 |
|
502 | 509 | * Changed the maximum length of LDAP account to 255 characters |
|
503 | 510 | * Removed the 12 characters limit on passwords |
|
504 | 511 | * Added wiki macros support |
|
505 | 512 | * Performance improvement on workflow setup screen |
|
506 | 513 | * More detailed html title on several views |
|
507 | 514 | * Custom fields can now be reordered |
|
508 | 515 | * Search engine: search can be restricted to an exact phrase by using quotation marks |
|
509 | 516 | * Added custom fields marked as 'For all projects' to the csv export of the cross project issue list |
|
510 | 517 | * Email notifications are now sent as Blind carbon copy by default |
|
511 | 518 | * Fixed: all members (including non active) should be deleted when deleting a project |
|
512 | 519 | * Fixed: Error on wiki syntax link (accessible from wiki/edit) |
|
513 | 520 | * Fixed: 'quick jump to a revision' form on the revisions list |
|
514 | 521 | * Fixed: error on admin/info if there's more than 1 plugin installed |
|
515 | 522 | * Fixed: svn or ldap password can be found in clear text in the html source in editing mode |
|
516 | 523 | * Fixed: 'Assigned to' drop down list is not sorted |
|
517 | 524 | * Fixed: 'View all issues' link doesn't work on issues/show |
|
518 | 525 | * Fixed: error on account/register when validation fails |
|
519 | 526 | * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter' |
|
520 | 527 | * Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt) |
|
521 | 528 | * Fixed: Wrong feed URLs on the home page |
|
522 | 529 | * Fixed: Update of time entry fails when the issue has been moved to an other project |
|
523 | 530 | * Fixed: Error when moving an issue without changing its tracker (Postgresql) |
|
524 | 531 | * Fixed: Changes not recorded when using :pserver string (CVS adapter) |
|
525 | 532 | * Fixed: admin should be able to move issues to any project |
|
526 | 533 | * Fixed: adding an attachment is not possible when changing the status of an issue |
|
527 | 534 | * Fixed: No mime-types in documents/files downloading |
|
528 | 535 | * Fixed: error when sorting the messages if there's only one board for the project |
|
529 | 536 | * Fixed: 'me' doesn't appear in the drop down filters on a project issue list. |
|
530 | 537 | |
|
531 | 538 | == 2007-11-04 v0.6.0 |
|
532 | 539 | |
|
533 | 540 | * Permission model refactoring. |
|
534 | 541 | * Permissions: there are now 2 builtin roles that can be used to specify permissions given to other users than members of projects |
|
535 | 542 | * Permissions: some permissions (eg. browse the repository) can be removed for certain roles |
|
536 | 543 | * Permissions: modules (eg. issue tracking, news, documents...) can be enabled/disabled at project level |
|
537 | 544 | * Added Mantis and Trac importers |
|
538 | 545 | * New application layout |
|
539 | 546 | * Added "Bulk edit" functionality on the issue list |
|
540 | 547 | * More flexible mail notifications settings at user level |
|
541 | 548 | * 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 |
|
542 | 549 | * 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 |
|
543 | 550 | * Added the ability to customize issue list columns (at application level or for each saved query) |
|
544 | 551 | * Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap |
|
545 | 552 | * Added the ability to rename wiki pages (specific permission required) |
|
546 | 553 | * Search engines now supports pagination. Results are sorted in reverse chronological order |
|
547 | 554 | * Added "Estimated hours" attribute on issues |
|
548 | 555 | * A category with assigned issue can now be deleted. 2 options are proposed: remove assignments or reassign issues to another category |
|
549 | 556 | * Forum notifications are now also sent to the authors of the thread, even if they donοΏ½t watch the board |
|
550 | 557 | * Added an application setting to specify the application protocol (http or https) used to generate urls in emails |
|
551 | 558 | * Gantt chart: now starts at the current month by default |
|
552 | 559 | * Gantt chart: month count and zoom factor are automatically saved as user preferences |
|
553 | 560 | * Wiki links can now refer to other project wikis |
|
554 | 561 | * Added wiki index by date |
|
555 | 562 | * Added preview on add/edit issue form |
|
556 | 563 | * Emails footer can now be customized from the admin interface (Admin -> Email notifications) |
|
557 | 564 | * 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) |
|
558 | 565 | * Calendar: first day of week can now be set in lang files |
|
559 | 566 | * Automatic closing of duplicate issues |
|
560 | 567 | * Added a cross-project issue list |
|
561 | 568 | * AJAXified the SCM browser (tree view) |
|
562 | 569 | * Pretty URL for the repository browser (Cyril Mougel) |
|
563 | 570 | * Search engine: added a checkbox to search titles only |
|
564 | 571 | * Added "% done" in the filter list |
|
565 | 572 | * Enumerations: values can now be reordered and a default value can be specified (eg. default issue priority) |
|
566 | 573 | * Added some accesskeys |
|
567 | 574 | * Added "Float" as a custom field format |
|
568 | 575 | * Added basic Theme support |
|
569 | 576 | * Added the ability to set the οΏ½done ratioοΏ½ of issues fixed by commit (Nikolay Solakov) |
|
570 | 577 | * Added custom fields in issue related mail notifications |
|
571 | 578 | * Email notifications are now sent in plain text and html |
|
572 | 579 | * Gantt chart can now be exported to a graphic file (png). This functionality is only available if RMagick is installed. |
|
573 | 580 | * Added syntax highlightment for repository files and wiki |
|
574 | 581 | * Improved automatic Redmine links |
|
575 | 582 | * Added automatic table of content support on wiki pages |
|
576 | 583 | * Added radio buttons on the documents list to sort documents by category, date, title or author |
|
577 | 584 | * Added basic plugin support, with a sample plugin |
|
578 | 585 | * Added a link to add a new category when creating or editing an issue |
|
579 | 586 | * Added a "Assignable" boolean on the Role model. If unchecked, issues can not be assigned to users having this role. |
|
580 | 587 | * Added an option to be able to relate issues in different projects |
|
581 | 588 | * Added the ability to move issues (to another project) without changing their trackers. |
|
582 | 589 | * Atom feeds added on project activity, news and changesets |
|
583 | 590 | * Added the ability to reset its own RSS access key |
|
584 | 591 | * Main project list now displays root projects with their subprojects |
|
585 | 592 | * Added anchor links to issue notes |
|
586 | 593 | * Added reposman Ruby version. This script can now register created repositories in Redmine (Nicolas Chuche) |
|
587 | 594 | * Issue notes are now included in search |
|
588 | 595 | * Added email sending test functionality |
|
589 | 596 | * Added LDAPS support for LDAP authentication |
|
590 | 597 | * Removed hard-coded URLs in mail templates |
|
591 | 598 | * Subprojects are now grouped by projects in the navigation drop-down menu |
|
592 | 599 | * Added a new value for date filters: this week |
|
593 | 600 | * Added cache for application settings |
|
594 | 601 | * Added Polish translation (Tomasz Gawryl) |
|
595 | 602 | * Added Czech translation (Jan Kadlecek) |
|
596 | 603 | * Added Romanian translation (Csongor Bartus) |
|
597 | 604 | * Added Hebrew translation (Bob Builder) |
|
598 | 605 | * Added Serbian translation (Dragan Matic) |
|
599 | 606 | * Added Korean translation (Choi Jong Yoon) |
|
600 | 607 | * Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations |
|
601 | 608 | * Performance improvement on calendar and gantt |
|
602 | 609 | * Fixed: wiki preview doesnοΏ½t work on long entries |
|
603 | 610 | * Fixed: queries with multiple custom fields return no result |
|
604 | 611 | * Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters |
|
605 | 612 | * Fixed: URL with ~ broken in wiki formatting |
|
606 | 613 | * Fixed: some quotation marks are rendered as strange characters in pdf |
|
607 | 614 | |
|
608 | 615 | |
|
609 | 616 | == 2007-07-15 v0.5.1 |
|
610 | 617 | |
|
611 | 618 | * per project forums added |
|
612 | 619 | * added the ability to archive projects |
|
613 | 620 | * added οΏ½WatchοΏ½ functionality on issues. It allows users to receive notifications about issue changes |
|
614 | 621 | * custom fields for issues can now be used as filters on issue list |
|
615 | 622 | * added per user custom queries |
|
616 | 623 | * commit messages are now scanned for referenced or fixed issue IDs (keywords defined in Admin -> Settings) |
|
617 | 624 | * projects list now shows the list of public projects and private projects for which the user is a member |
|
618 | 625 | * versions can now be created with no date |
|
619 | 626 | * added issue count details for versions on Reports view |
|
620 | 627 | * added time report, by member/activity/tracker/version and year/month/week for the selected period |
|
621 | 628 | * each category can now be associated to a user, so that new issues in that category are automatically assigned to that user |
|
622 | 629 | * added autologin feature (disabled by default) |
|
623 | 630 | * optimistic locking added for wiki edits |
|
624 | 631 | * added wiki diff |
|
625 | 632 | * added the ability to destroy wiki pages (requires permission) |
|
626 | 633 | * a wiki page can now be attached to each version, and displayed on the roadmap |
|
627 | 634 | * attachments can now be added to wiki pages (original patch by Pavol Murin) and displayed online |
|
628 | 635 | * added an option to see all versions in the roadmap view (including completed ones) |
|
629 | 636 | * added basic issue relations |
|
630 | 637 | * added the ability to log time when changing an issue status |
|
631 | 638 | * account information can now be sent to the user when creating an account |
|
632 | 639 | * author and assignee of an issue always receive notifications (even if they turned of mail notifications) |
|
633 | 640 | * added a quick search form in page header |
|
634 | 641 | * added 'me' value for 'assigned to' and 'author' query filters |
|
635 | 642 | * added a link on revision screen to see the entire diff for the revision |
|
636 | 643 | * added last commit message for each entry in repository browser |
|
637 | 644 | * added the ability to view a file diff with free to/from revision selection. |
|
638 | 645 | * text files can now be viewed online when browsing the repository |
|
639 | 646 | * added basic support for other SCM: CVS (Ralph Vater), Mercurial and Darcs |
|
640 | 647 | * added fragment caching for svn diffs |
|
641 | 648 | * added fragment caching for calendar and gantt views |
|
642 | 649 | * login field automatically focused on login form |
|
643 | 650 | * subproject name displayed on issue list, calendar and gantt |
|
644 | 651 | * added an option to choose the date format: language based or ISO 8601 |
|
645 | 652 | * added a simple mail handler. It lets users add notes to an existing issue by replying to the initial notification email. |
|
646 | 653 | * a 403 error page is now displayed (instead of a blank page) when trying to access a protected page |
|
647 | 654 | * added portuguese translation (Joao Carlos Clementoni) |
|
648 | 655 | * added partial online help japanese translation (Ken Date) |
|
649 | 656 | * added bulgarian translation (Nikolay Solakov) |
|
650 | 657 | * added dutch translation (Linda van den Brink) |
|
651 | 658 | * added swedish translation (Thomas Habets) |
|
652 | 659 | * italian translation update (Alessio Spadaro) |
|
653 | 660 | * japanese translation update (Satoru Kurashiki) |
|
654 | 661 | * fixed: error on history atom feed when thereοΏ½s no notes on an issue change |
|
655 | 662 | * fixed: error in journalizing an issue with longtext custom fields (Postgresql) |
|
656 | 663 | * fixed: creation of Oracle schema |
|
657 | 664 | * fixed: last day of the month not included in project activity |
|
658 | 665 | * fixed: files with an apostrophe in their names can't be accessed in SVN repository |
|
659 | 666 | * fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000) |
|
660 | 667 | * fixed: open/closed issue counts are always 0 on reports view (postgresql) |
|
661 | 668 | * fixed: date query filters (wrong results and sql error with postgresql) |
|
662 | 669 | * fixed: confidentiality issue on account/show (private project names displayed to anyone) |
|
663 | 670 | * fixed: Long text custom fields displayed without line breaks |
|
664 | 671 | * fixed: Error when editing the wokflow after deleting a status |
|
665 | 672 | * fixed: SVN commit dates are now stored as local time |
|
666 | 673 | |
|
667 | 674 | |
|
668 | 675 | == 2007-04-11 v0.5.0 |
|
669 | 676 | |
|
670 | 677 | * added per project Wiki |
|
671 | 678 | * added rss/atom feeds at project level (custom queries can be used as feeds) |
|
672 | 679 | * added search engine (search in issues, news, commits, wiki pages, documents) |
|
673 | 680 | * simple time tracking functionality added |
|
674 | 681 | * added version due dates on calendar and gantt |
|
675 | 682 | * added subprojects issue count on project Reports page |
|
676 | 683 | * added the ability to copy an existing workflow when creating a new tracker |
|
677 | 684 | * added the ability to include subprojects on calendar and gantt |
|
678 | 685 | * added the ability to select trackers to display on calendar and gantt (Jeffrey Jones) |
|
679 | 686 | * added side by side svn diff view (Cyril Mougel) |
|
680 | 687 | * added back subproject filter on issue list |
|
681 | 688 | * added permissions report in admin area |
|
682 | 689 | * added a status filter on users list |
|
683 | 690 | * support for password-protected SVN repositories |
|
684 | 691 | * SVN commits are now stored in the database |
|
685 | 692 | * added simple svn statistics SVG graphs |
|
686 | 693 | * progress bars for roadmap versions (Nick Read) |
|
687 | 694 | * issue history now shows file uploads and deletions |
|
688 | 695 | * #id patterns are turned into links to issues in descriptions and commit messages |
|
689 | 696 | * japanese translation added (Satoru Kurashiki) |
|
690 | 697 | * chinese simplified translation added (Andy Wu) |
|
691 | 698 | * italian translation added (Alessio Spadaro) |
|
692 | 699 | * added scripts to manage SVN repositories creation and user access control using ssh+svn (Nicolas Chuche) |
|
693 | 700 | * better calendar rendering time |
|
694 | 701 | * fixed migration scripts to work with mysql 5 running in strict mode |
|
695 | 702 | * fixed: error when clicking "add" with no block selected on my/page_layout |
|
696 | 703 | * fixed: hard coded links in navigation bar |
|
697 | 704 | * fixed: table_name pre/suffix support |
|
698 | 705 | |
|
699 | 706 | |
|
700 | 707 | == 2007-02-18 v0.4.2 |
|
701 | 708 | |
|
702 | 709 | * Rails 1.2 is now required |
|
703 | 710 | * settings are now stored in the database and editable through the application in: Admin -> Settings (config_custom.rb is no longer used) |
|
704 | 711 | * added project roadmap view |
|
705 | 712 | * mail notifications added when a document, a file or an attachment is added |
|
706 | 713 | * tooltips added on Gantt chart and calender to view the details of the issues |
|
707 | 714 | * ability to set the sort order for roles, trackers, issue statuses |
|
708 | 715 | * added missing fields to csv export: priority, start date, due date, done ratio |
|
709 | 716 | * added total number of issues per tracker on project overview |
|
710 | 717 | * 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-) |
|
711 | 718 | * added back "fixed version" field on issue screen and in filters |
|
712 | 719 | * project settings screen split in 4 tabs |
|
713 | 720 | * custom fields screen split in 3 tabs (one for each kind of custom field) |
|
714 | 721 | * multiple issues pdf export now rendered as a table |
|
715 | 722 | * added a button on users/list to manually activate an account |
|
716 | 723 | * added a setting option to disable "password lost" functionality |
|
717 | 724 | * added a setting option to set max number of issues in csv/pdf exports |
|
718 | 725 | * fixed: subprojects count is always 0 on projects list |
|
719 | 726 | * fixed: locked users are proposed when adding a member to a project |
|
720 | 727 | * fixed: setting an issue status as default status leads to an sql error with SQLite |
|
721 | 728 | * fixed: unable to delete an issue status even if it's not used yet |
|
722 | 729 | * fixed: filters ignored when exporting a predefined query to csv/pdf |
|
723 | 730 | * fixed: crash when french "issue_edit" email notification is sent |
|
724 | 731 | * fixed: hide mail preference not saved (my/account) |
|
725 | 732 | * fixed: crash when a new user try to edit its "my page" layout |
|
726 | 733 | |
|
727 | 734 | |
|
728 | 735 | == 2007-01-03 v0.4.1 |
|
729 | 736 | |
|
730 | 737 | * fixed: emails have no recipient when one of the project members has notifications disabled |
|
731 | 738 | |
|
732 | 739 | |
|
733 | 740 | == 2007-01-02 v0.4.0 |
|
734 | 741 | |
|
735 | 742 | * simple SVN browser added (just needs svn binaries in PATH) |
|
736 | 743 | * comments can now be added on news |
|
737 | 744 | * "my page" is now customizable |
|
738 | 745 | * more powerfull and savable filters for issues lists |
|
739 | 746 | * improved issues change history |
|
740 | 747 | * new functionality: move an issue to another project or tracker |
|
741 | 748 | * new functionality: add a note to an issue |
|
742 | 749 | * new report: project activity |
|
743 | 750 | * "start date" and "% done" fields added on issues |
|
744 | 751 | * project calendar added |
|
745 | 752 | * gantt chart added (exportable to pdf) |
|
746 | 753 | * single/multiple issues pdf export added |
|
747 | 754 | * issues reports improvements |
|
748 | 755 | * multiple file upload for issues, documents and files |
|
749 | 756 | * option to set maximum size of uploaded files |
|
750 | 757 | * textile formating of issue and news descritions (RedCloth required) |
|
751 | 758 | * integration of DotClear jstoolbar for textile formatting |
|
752 | 759 | * calendar date picker for date fields (LGPL DHTML Calendar http://sourceforge.net/projects/jscalendar) |
|
753 | 760 | * new filter in issues list: Author |
|
754 | 761 | * ajaxified paginators |
|
755 | 762 | * news rss feed added |
|
756 | 763 | * option to set number of results per page on issues list |
|
757 | 764 | * localized csv separator (comma/semicolon) |
|
758 | 765 | * csv output encoded to ISO-8859-1 |
|
759 | 766 | * user custom field displayed on account/show |
|
760 | 767 | * default configuration improved (default roles, trackers, status, permissions and workflows) |
|
761 | 768 | * language for default configuration data can now be chosen when running 'load_default_data' task |
|
762 | 769 | * javascript added on custom field form to show/hide fields according to the format of custom field |
|
763 | 770 | * fixed: custom fields not in csv exports |
|
764 | 771 | * fixed: project settings now displayed according to user's permissions |
|
765 | 772 | * fixed: application error when no version is selected on projects/add_file |
|
766 | 773 | * fixed: public actions not authorized for members of non public projects |
|
767 | 774 | * fixed: non public projects were shown on welcome screen even if current user is not a member |
|
768 | 775 | |
|
769 | 776 | |
|
770 | 777 | == 2006-10-08 v0.3.0 |
|
771 | 778 | |
|
772 | 779 | * user authentication against multiple LDAP (optional) |
|
773 | 780 | * token based "lost password" functionality |
|
774 | 781 | * user self-registration functionality (optional) |
|
775 | 782 | * custom fields now available for issues, users and projects |
|
776 | 783 | * new custom field format "text" (displayed as a textarea field) |
|
777 | 784 | * project & administration drop down menus in navigation bar for quicker access |
|
778 | 785 | * text formatting is preserved for long text fields (issues, projects and news descriptions) |
|
779 | 786 | * urls and emails are turned into clickable links in long text fields |
|
780 | 787 | * "due date" field added on issues |
|
781 | 788 | * tracker selection filter added on change log |
|
782 | 789 | * Localization plugin replaced with GLoc 1.1.0 (iconv required) |
|
783 | 790 | * error messages internationalization |
|
784 | 791 | * german translation added (thanks to Karim Trott) |
|
785 | 792 | * data locking for issues to prevent update conflicts (using ActiveRecord builtin optimistic locking) |
|
786 | 793 | * new filter in issues list: "Fixed version" |
|
787 | 794 | * active filters are displayed with colored background on issues list |
|
788 | 795 | * custom configuration is now defined in config/config_custom.rb |
|
789 | 796 | * user object no more stored in session (only user_id) |
|
790 | 797 | * news summary field is no longer required |
|
791 | 798 | * tables and forms redesign |
|
792 | 799 | * Fixed: boolean custom field not working |
|
793 | 800 | * Fixed: error messages for custom fields are not displayed |
|
794 | 801 | * Fixed: invalid custom fields should have a red border |
|
795 | 802 | * Fixed: custom fields values are not validated on issue update |
|
796 | 803 | * Fixed: unable to choose an empty value for 'List' custom fields |
|
797 | 804 | * Fixed: no issue categories sorting |
|
798 | 805 | * Fixed: incorrect versions sorting |
|
799 | 806 | |
|
800 | 807 | |
|
801 | 808 | == 2006-07-12 - v0.2.2 |
|
802 | 809 | |
|
803 | 810 | * Fixed: bug in "issues list" |
|
804 | 811 | |
|
805 | 812 | |
|
806 | 813 | == 2006-07-09 - v0.2.1 |
|
807 | 814 | |
|
808 | 815 | * new databases supported: Oracle, PostgreSQL, SQL Server |
|
809 | 816 | * projects/subprojects hierarchy (1 level of subprojects only) |
|
810 | 817 | * environment information display in admin/info |
|
811 | 818 | * more filter options in issues list (rev6) |
|
812 | 819 | * default language based on browser settings (Accept-Language HTTP header) |
|
813 | 820 | * issues list exportable to CSV (rev6) |
|
814 | 821 | * simple_format and auto_link on long text fields |
|
815 | 822 | * more data validations |
|
816 | 823 | * Fixed: error when all mail notifications are unchecked in admin/mail_options |
|
817 | 824 | * Fixed: all project news are displayed on project summary |
|
818 | 825 | * Fixed: Can't change user password in users/edit |
|
819 | 826 | * Fixed: Error on tables creation with PostgreSQL (rev5) |
|
820 | 827 | * Fixed: SQL error in "issue reports" view with PostgreSQL (rev5) |
|
821 | 828 | |
|
822 | 829 | |
|
823 | 830 | == 2006-06-25 - v0.1.0 |
|
824 | 831 | |
|
825 | 832 | * multiple users/multiple projects |
|
826 | 833 | * role based access control |
|
827 | 834 | * issue tracking system |
|
828 | 835 | * fully customizable workflow |
|
829 | 836 | * documents/files repository |
|
830 | 837 | * email notifications on issue creation and update |
|
831 | 838 | * multilanguage support (except for error messages):english, french, spanish |
|
832 | 839 | * online manual in french (unfinished) |
@@ -1,700 +1,700 | |||
|
1 | 1 | _gloc_rule_default: '|n| n==1 ? "" : "_plural" ' |
|
2 | 2 | |
|
3 | 3 | actionview_datehelper_select_day_prefix: |
|
4 | 4 | actionview_datehelper_select_month_names: Janvier,FΓ©vrier,Mars,Avril,Mai,Juin,Juillet,AoΓ»t,Septembre,Octobre,Novembre,DΓ©cembre |
|
5 | 5 | actionview_datehelper_select_month_names_abbr: Jan,FΓ©v,Mars,Avril,Mai,Juin,Juil,AoΓ»t,Sept,Oct,Nov,DΓ©c |
|
6 | 6 | actionview_datehelper_select_month_prefix: |
|
7 | 7 | actionview_datehelper_select_year_prefix: |
|
8 | 8 | actionview_datehelper_time_in_words_day: 1 jour |
|
9 | 9 | actionview_datehelper_time_in_words_day_plural: %d jours |
|
10 | 10 | actionview_datehelper_time_in_words_hour_about: environ une heure |
|
11 | 11 | actionview_datehelper_time_in_words_hour_about_plural: environ %d heures |
|
12 | 12 | actionview_datehelper_time_in_words_hour_about_single: environ une heure |
|
13 | 13 | actionview_datehelper_time_in_words_minute: 1 minute |
|
14 | 14 | actionview_datehelper_time_in_words_minute_half: 30 secondes |
|
15 | 15 | actionview_datehelper_time_in_words_minute_less_than: moins d'une minute |
|
16 | 16 | actionview_datehelper_time_in_words_minute_plural: %d minutes |
|
17 | 17 | actionview_datehelper_time_in_words_minute_single: 1 minute |
|
18 | 18 | actionview_datehelper_time_in_words_second_less_than: moins d'une seconde |
|
19 | 19 | actionview_datehelper_time_in_words_second_less_than_plural: moins de %d secondes |
|
20 | 20 | actionview_instancetag_blank_option: Choisir |
|
21 | 21 | |
|
22 | 22 | activerecord_error_inclusion: n'est pas inclus dans la liste |
|
23 | 23 | activerecord_error_exclusion: est reservΓ© |
|
24 | 24 | activerecord_error_invalid: est invalide |
|
25 | 25 | activerecord_error_confirmation: ne correspond pas Γ la confirmation |
|
26 | 26 | activerecord_error_accepted: doit Γͺtre acceptΓ© |
|
27 | 27 | activerecord_error_empty: doit Γͺtre renseignΓ© |
|
28 | 28 | activerecord_error_blank: doit Γͺtre renseignΓ© |
|
29 | 29 | activerecord_error_too_long: est trop long |
|
30 | 30 | activerecord_error_too_short: est trop court |
|
31 | 31 | activerecord_error_wrong_length: n'est pas de la bonne longueur |
|
32 | 32 | activerecord_error_taken: est dΓ©jΓ utilisΓ© |
|
33 | 33 | activerecord_error_not_a_number: n'est pas un nombre |
|
34 | 34 | activerecord_error_not_a_date: n'est pas une date valide |
|
35 | 35 | activerecord_error_greater_than_start_date: doit Γͺtre postΓ©rieur Γ la date de dΓ©but |
|
36 | 36 | activerecord_error_not_same_project: n'appartient pas au mΓͺme projet |
|
37 | 37 | activerecord_error_circular_dependency: Cette relation crΓ©erait une dΓ©pendance circulaire |
|
38 | 38 | |
|
39 | 39 | general_fmt_age: %d an |
|
40 | 40 | general_fmt_age_plural: %d ans |
|
41 | 41 | general_fmt_date: %%d/%%m/%%Y |
|
42 | 42 | general_fmt_datetime: %%d/%%m/%%Y %%H:%%M |
|
43 | 43 | general_fmt_datetime_short: %%d/%%m %%H:%%M |
|
44 | 44 | general_fmt_time: %%H:%%M |
|
45 | 45 | general_text_No: 'Non' |
|
46 | 46 | general_text_Yes: 'Oui' |
|
47 | 47 | general_text_no: 'non' |
|
48 | 48 | general_text_yes: 'oui' |
|
49 | 49 | general_lang_name: 'FranΓ§ais' |
|
50 | 50 | general_csv_separator: ';' |
|
51 | 51 | general_csv_decimal_separator: ',' |
|
52 | 52 | general_csv_encoding: ISO-8859-1 |
|
53 | 53 | general_pdf_encoding: ISO-8859-1 |
|
54 | 54 | general_day_names: Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche |
|
55 | 55 | general_first_day_of_week: '1' |
|
56 | 56 | |
|
57 | 57 | notice_account_updated: Le compte a été mis à jour avec succès. |
|
58 | 58 | notice_account_invalid_creditentials: Identifiant ou mot de passe invalide. |
|
59 | 59 | notice_account_password_updated: Mot de passe mis à jour avec succès. |
|
60 | 60 | notice_account_wrong_password: Mot de passe incorrect |
|
61 | 61 | notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a Γ©tΓ© envoyΓ©. |
|
62 | 62 | notice_account_unknown_email: Aucun compte ne correspond Γ cette adresse. |
|
63 | 63 | notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe. |
|
64 | 64 | notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a Γ©tΓ© envoyΓ©. |
|
65 | 65 | notice_account_activated: Votre compte a Γ©tΓ© activΓ©. Vous pouvez Γ prΓ©sent vous connecter. |
|
66 | 66 | notice_successful_create: Création effectuée avec succès. |
|
67 | 67 | notice_successful_update: Mise à jour effectuée avec succès. |
|
68 | 68 | notice_successful_delete: Suppression effectuée avec succès. |
|
69 | 69 | notice_successful_connection: Connection rΓ©ussie. |
|
70 | 70 | notice_file_not_found: "La page Γ laquelle vous souhaitez accΓ©der n'existe pas ou a Γ©tΓ© supprimΓ©e." |
|
71 | 71 | notice_locking_conflict: Les donnΓ©es ont Γ©tΓ© mises Γ jour par un autre utilisateur. Mise Γ jour impossible. |
|
72 | 72 | notice_not_authorized: "Vous n'Γͺtes pas autorisΓ©s Γ accΓ©der Γ cette page." |
|
73 | 73 | notice_email_sent: "Un email a Γ©tΓ© envoyΓ© Γ %s" |
|
74 | 74 | notice_email_error: "Erreur lors de l'envoi de l'email (%s)" |
|
75 | 75 | notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée." |
|
76 | 76 | notice_failed_to_save_issues: "%d demande(s) sur les %d sΓ©lectionnΓ©es n'ont pas pu Γͺtre mise(s) Γ jour: %s." |
|
77 | 77 | notice_no_issue_selected: "Aucune demande sΓ©lectionnΓ©e ! Cochez les demandes que vous voulez mettre Γ jour." |
|
78 | 78 | notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur." |
|
79 | 79 | notice_default_data_loaded: Paramétrage par défaut chargé avec succès. |
|
80 | 80 | notice_unable_delete_version: Impossible de supprimer cette version. |
|
81 | 81 | |
|
82 | 82 | error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramΓ©trage: %s" |
|
83 | 83 | error_scm_not_found: "L'entrΓ©e et/ou la rΓ©vision demandΓ©e n'existe pas dans le dΓ©pΓ΄t." |
|
84 | 84 | error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt: %s" |
|
85 | 85 | error_scm_annotate: "L'entrΓ©e n'existe pas ou ne peut pas Γͺtre annotΓ©e." |
|
86 | 86 | error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas Γ ce projet" |
|
87 | 87 | |
|
88 | 88 | mail_subject_lost_password: Votre mot de passe %s |
|
89 | 89 | mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant:' |
|
90 | 90 | mail_subject_register: Activation de votre compte %s |
|
91 | 91 | mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant:' |
|
92 | 92 | mail_body_account_information_external: Vous pouvez utiliser votre compte "%s" pour vous connecter. |
|
93 | 93 | mail_body_account_information: Paramètres de connexion de votre compte |
|
94 | 94 | mail_subject_account_activation_request: "Demande d'activation d'un compte %s" |
|
95 | 95 | mail_body_account_activation_request: "Un nouvel utilisateur (%s) s'est inscrit. Son compte nΓ©cessite votre approbation:" |
|
96 | 96 | mail_subject_reminder: "%d demande(s) arrivent Γ Γ©chΓ©ance" |
|
97 | 97 | mail_body_reminder: "%d demande(s) qui vous sont assignΓ©es arrivent Γ Γ©chΓ©ance dans les %d prochains jours:" |
|
98 | 98 | |
|
99 | 99 | gui_validation_error: 1 erreur |
|
100 | 100 | gui_validation_error_plural: %d erreurs |
|
101 | 101 | |
|
102 | 102 | field_name: Nom |
|
103 | 103 | field_description: Description |
|
104 | 104 | field_summary: RΓ©sumΓ© |
|
105 | 105 | field_is_required: Obligatoire |
|
106 | 106 | field_firstname: PrΓ©nom |
|
107 | 107 | field_lastname: Nom |
|
108 | 108 | field_mail: Email |
|
109 | 109 | field_filename: Fichier |
|
110 | 110 | field_filesize: Taille |
|
111 | 111 | field_downloads: TΓ©lΓ©chargements |
|
112 | 112 | field_author: Auteur |
|
113 | 113 | field_created_on: Créé |
|
114 | 114 | field_updated_on: Mis Γ jour |
|
115 | 115 | field_field_format: Format |
|
116 | 116 | field_is_for_all: Pour tous les projets |
|
117 | 117 | field_possible_values: Valeurs possibles |
|
118 | 118 | field_regexp: Expression régulière |
|
119 | 119 | field_min_length: Longueur minimum |
|
120 | 120 | field_max_length: Longueur maximum |
|
121 | 121 | field_value: Valeur |
|
122 | 122 | field_category: CatΓ©gorie |
|
123 | 123 | field_title: Titre |
|
124 | 124 | field_project: Projet |
|
125 | 125 | field_issue: Demande |
|
126 | 126 | field_status: Statut |
|
127 | 127 | field_notes: Notes |
|
128 | 128 | field_is_closed: Demande fermΓ©e |
|
129 | 129 | field_is_default: Valeur par dΓ©faut |
|
130 | 130 | field_tracker: Tracker |
|
131 | 131 | field_subject: Sujet |
|
132 | 132 | field_due_date: Date d'Γ©chΓ©ance |
|
133 | 133 | field_assigned_to: AssignΓ© Γ |
|
134 | 134 | field_priority: PrioritΓ© |
|
135 | 135 | field_fixed_version: Version cible |
|
136 | 136 | field_user: Utilisateur |
|
137 | 137 | field_role: RΓ΄le |
|
138 | 138 | field_homepage: Site web |
|
139 | 139 | field_is_public: Public |
|
140 | 140 | field_parent: Sous-projet de |
|
141 | 141 | field_is_in_chlog: Demandes affichΓ©es dans l'historique |
|
142 | 142 | field_is_in_roadmap: Demandes affichΓ©es dans la roadmap |
|
143 | 143 | field_login: Identifiant |
|
144 | 144 | field_mail_notification: Notifications par mail |
|
145 | 145 | field_admin: Administrateur |
|
146 | 146 | field_last_login_on: Dernière connexion |
|
147 | 147 | field_language: Langue |
|
148 | 148 | field_effective_date: Date |
|
149 | 149 | field_password: Mot de passe |
|
150 | 150 | field_new_password: Nouveau mot de passe |
|
151 | 151 | field_password_confirmation: Confirmation |
|
152 | 152 | field_version: Version |
|
153 | 153 | field_type: Type |
|
154 | 154 | field_host: HΓ΄te |
|
155 | 155 | field_port: Port |
|
156 | 156 | field_account: Compte |
|
157 | 157 | field_base_dn: Base DN |
|
158 | 158 | field_attr_login: Attribut Identifiant |
|
159 | 159 | field_attr_firstname: Attribut PrΓ©nom |
|
160 | 160 | field_attr_lastname: Attribut Nom |
|
161 | 161 | field_attr_mail: Attribut Email |
|
162 | 162 | field_onthefly: CrΓ©ation des utilisateurs Γ la volΓ©e |
|
163 | 163 | field_start_date: DΓ©but |
|
164 | 164 | field_done_ratio: %% RΓ©alisΓ© |
|
165 | 165 | field_auth_source: Mode d'authentification |
|
166 | 166 | field_hide_mail: Cacher mon adresse mail |
|
167 | 167 | field_comments: Commentaire |
|
168 | 168 | field_url: URL |
|
169 | 169 | field_start_page: Page de dΓ©marrage |
|
170 | 170 | field_subproject: Sous-projet |
|
171 | 171 | field_hours: Heures |
|
172 | 172 | field_activity: ActivitΓ© |
|
173 | 173 | field_spent_on: Date |
|
174 | 174 | field_identifier: Identifiant |
|
175 | 175 | field_is_filter: UtilisΓ© comme filtre |
|
176 | 176 | field_issue_to_id: Demande liΓ©e |
|
177 | 177 | field_delay: Retard |
|
178 | 178 | field_assignable: Demandes assignables Γ ce rΓ΄le |
|
179 | 179 | field_redirect_existing_links: Rediriger les liens existants |
|
180 | 180 | field_estimated_hours: Temps estimΓ© |
|
181 | 181 | field_column_names: Colonnes |
|
182 | 182 | field_time_zone: Fuseau horaire |
|
183 | 183 | field_searchable: UtilisΓ© pour les recherches |
|
184 | 184 | field_default_value: Valeur par dΓ©faut |
|
185 | 185 | field_comments_sorting: Afficher les commentaires |
|
186 | 186 | field_parent_title: Page parent |
|
187 | 187 | |
|
188 | 188 | setting_app_title: Titre de l'application |
|
189 | 189 | setting_app_subtitle: Sous-titre de l'application |
|
190 | 190 | setting_welcome_text: Texte d'accueil |
|
191 | 191 | setting_default_language: Langue par dΓ©faut |
|
192 | 192 | setting_login_required: Authentification obligatoire |
|
193 | 193 | setting_self_registration: Inscription des nouveaux utilisateurs |
|
194 | 194 | setting_attachment_max_size: Taille max des fichiers |
|
195 | 195 | setting_issues_export_limit: Limite export demandes |
|
196 | 196 | setting_mail_from: Adresse d'Γ©mission |
|
197 | 197 | setting_bcc_recipients: Destinataires en copie cachΓ©e (cci) |
|
198 | 198 | setting_plain_text_mail: Mail texte brut (non HTML) |
|
199 | 199 | setting_host_name: Nom d'hΓ΄te et chemin |
|
200 | 200 | setting_text_formatting: Formatage du texte |
|
201 | 201 | setting_wiki_compression: Compression historique wiki |
|
202 | 202 | setting_feeds_limit: Limite du contenu des flux RSS |
|
203 | 203 | setting_default_projects_public: DΓ©finir les nouveaux projects comme publics par dΓ©faut |
|
204 | 204 | setting_autofetch_changesets: RΓ©cupΓ©ration auto. des commits |
|
205 | 205 | setting_sys_api_enabled: Activer les WS pour la gestion des dΓ©pΓ΄ts |
|
206 | 206 | setting_commit_ref_keywords: Mot-clΓ©s de rΓ©fΓ©rencement |
|
207 | 207 | setting_commit_fix_keywords: Mot-clΓ©s de rΓ©solution |
|
208 | 208 | setting_autologin: Autologin |
|
209 | 209 | setting_date_format: Format de date |
|
210 | 210 | setting_time_format: Format d'heure |
|
211 | 211 | setting_cross_project_issue_relations: Autoriser les relations entre demandes de diffΓ©rents projets |
|
212 | 212 | setting_issue_list_default_columns: Colonnes affichΓ©es par dΓ©faut sur la liste des demandes |
|
213 | 213 | setting_repositories_encodings: Encodages des dΓ©pΓ΄ts |
|
214 | 214 | setting_commit_logs_encoding: Encodage des messages de commit |
|
215 | 215 | setting_emails_footer: Pied-de-page des emails |
|
216 | 216 | setting_protocol: Protocole |
|
217 | 217 | setting_per_page_options: Options d'objets affichΓ©s par page |
|
218 | 218 | setting_user_format: Format d'affichage des utilisateurs |
|
219 | 219 | setting_activity_days_default: Nombre de jours affichΓ©s sur l'activitΓ© des projets |
|
220 | 220 | setting_display_subprojects_issues: Afficher par dΓ©faut les demandes des sous-projets sur les projets principaux |
|
221 | 221 | setting_enabled_scm: SCM activΓ©s |
|
222 | 222 | setting_mail_handler_api_enabled: "Activer le WS pour la rΓ©ception d'emails" |
|
223 | 223 | setting_mail_handler_api_key: ClΓ© de protection de l'API |
|
224 | 224 | setting_sequential_project_identifiers: GΓ©nΓ©rer des identifiants de projet sΓ©quentiels |
|
225 | 225 | setting_gravatar_enabled: Afficher les Gravatar des utilisateurs |
|
226 | 226 | setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichΓ©es |
|
227 | 227 | |
|
228 | 228 | permission_edit_project: Modifier le projet |
|
229 | 229 | permission_select_project_modules: Choisir les modules |
|
230 | 230 | permission_manage_members: GΓ©rer les members |
|
231 | 231 | permission_manage_versions: GΓ©rer les versions |
|
232 | 232 | permission_manage_categories: GΓ©rer les catΓ©gories de demandes |
|
233 | 233 | permission_add_issues: CrΓ©er des demandes |
|
234 | 234 | permission_edit_issues: Modifier les demandes |
|
235 | 235 | permission_manage_issue_relations: GΓ©rer les relations |
|
236 | 236 | permission_add_issue_notes: Ajouter des notes |
|
237 | 237 | permission_edit_issue_notes: Modifier les notes |
|
238 | 238 | permission_edit_own_issue_notes: Modifier ses propres notes |
|
239 | 239 | permission_move_issues: DΓ©placer les demandes |
|
240 | 240 | permission_delete_issues: Supprimer les demandes |
|
241 | 241 | permission_manage_public_queries: GΓ©rer les requΓͺtes publiques |
|
242 | 242 | permission_save_queries: Sauvegarder les requΓͺtes |
|
243 | 243 | permission_view_gantt: Voir le gantt |
|
244 | 244 | permission_view_calendar: Voir le calendrier |
|
245 | 245 | permission_view_issue_watchers: Voir la liste des observateurs |
|
246 | 246 | permission_add_issue_watchers: Ajouter des observateurs |
|
247 | 247 | permission_log_time: Saisir le temps passΓ© |
|
248 | 248 | permission_view_time_entries: Voir le temps passΓ© |
|
249 | 249 | permission_edit_time_entries: Modifier les temps passΓ©s |
|
250 | 250 | permission_edit_own_time_entries: Modifier son propre temps passΓ© |
|
251 | 251 | permission_manage_news: GΓ©rer les annonces |
|
252 | 252 | permission_comment_news: Commenter les annonces |
|
253 | 253 | permission_manage_documents: GΓ©rer les documents |
|
254 | 254 | permission_view_documents: Voir les documents |
|
255 | 255 | permission_manage_files: GΓ©rer les fichiers |
|
256 | 256 | permission_view_files: Voir les fichiers |
|
257 | 257 | permission_manage_wiki: GΓ©rer le wiki |
|
258 | 258 | permission_rename_wiki_pages: Renommer les pages |
|
259 | 259 | permission_delete_wiki_pages: Supprimer les pages |
|
260 | 260 | permission_view_wiki_pages: Voir le wiki |
|
261 | 261 | permission_view_wiki_edits: "Voir l'historique des modifications" |
|
262 | 262 | permission_edit_wiki_pages: Modifier les pages |
|
263 | 263 | permission_delete_wiki_pages_attachments: Supprimer les fichiers joints |
|
264 | 264 | permission_protect_wiki_pages: ProtΓ©ger les pages |
|
265 | 265 | permission_manage_repository: GΓ©rer le dΓ©pΓ΄t de sources |
|
266 | 266 | permission_browse_repository: Parcourir les sources |
|
267 | 267 | permission_view_changesets: Voir les rΓ©visions |
|
268 | 268 | permission_commit_access: Droit de commit |
|
269 | 269 | permission_manage_boards: GΓ©rer les forums |
|
270 | 270 | permission_view_messages: Voir les messages |
|
271 | 271 | permission_add_messages: Poster un message |
|
272 | 272 | permission_edit_messages: Modifier les messages |
|
273 | 273 | permission_edit_own_messages: Modifier ses propres messages |
|
274 | 274 | permission_delete_messages: Supprimer les messages |
|
275 | 275 | permission_delete_own_messages: Supprimer ses propres messages |
|
276 | 276 | |
|
277 | 277 | project_module_issue_tracking: Suivi des demandes |
|
278 | 278 | project_module_time_tracking: Suivi du temps passΓ© |
|
279 | 279 | project_module_news: Publication d'annonces |
|
280 | 280 | project_module_documents: Publication de documents |
|
281 | 281 | project_module_files: Publication de fichiers |
|
282 | 282 | project_module_wiki: Wiki |
|
283 | 283 | project_module_repository: DΓ©pΓ΄t de sources |
|
284 | 284 | project_module_boards: Forums de discussion |
|
285 | 285 | |
|
286 | 286 | label_user: Utilisateur |
|
287 | 287 | label_user_plural: Utilisateurs |
|
288 | 288 | label_user_new: Nouvel utilisateur |
|
289 | 289 | label_project: Projet |
|
290 | 290 | label_project_new: Nouveau projet |
|
291 | 291 | label_project_plural: Projets |
|
292 | 292 | label_project_all: Tous les projets |
|
293 | 293 | label_project_latest: Derniers projets |
|
294 | 294 | label_issue: Demande |
|
295 | 295 | label_issue_new: Nouvelle demande |
|
296 | 296 | label_issue_plural: Demandes |
|
297 | 297 | label_issue_view_all: Voir toutes les demandes |
|
298 | 298 | label_issue_added: Demande ajoutΓ©e |
|
299 | 299 | label_issue_updated: Demande mise Γ jour |
|
300 | 300 | label_issues_by: Demandes par %s |
|
301 | 301 | label_document: Document |
|
302 | 302 | label_document_new: Nouveau document |
|
303 | 303 | label_document_plural: Documents |
|
304 | 304 | label_document_added: Document ajoutΓ© |
|
305 | 305 | label_role: RΓ΄le |
|
306 | 306 | label_role_plural: RΓ΄les |
|
307 | 307 | label_role_new: Nouveau rΓ΄le |
|
308 | 308 | label_role_and_permissions: RΓ΄les et permissions |
|
309 | 309 | label_member: Membre |
|
310 | 310 | label_member_new: Nouveau membre |
|
311 | 311 | label_member_plural: Membres |
|
312 | 312 | label_tracker: Tracker |
|
313 | 313 | label_tracker_plural: Trackers |
|
314 | 314 | label_tracker_new: Nouveau tracker |
|
315 | 315 | label_workflow: Workflow |
|
316 | 316 | label_issue_status: Statut de demandes |
|
317 | 317 | label_issue_status_plural: Statuts de demandes |
|
318 | 318 | label_issue_status_new: Nouveau statut |
|
319 | 319 | label_issue_category: CatΓ©gorie de demandes |
|
320 | 320 | label_issue_category_plural: CatΓ©gories de demandes |
|
321 | 321 | label_issue_category_new: Nouvelle catΓ©gorie |
|
322 | 322 | label_custom_field: Champ personnalisΓ© |
|
323 | 323 | label_custom_field_plural: Champs personnalisΓ©s |
|
324 | 324 | label_custom_field_new: Nouveau champ personnalisΓ© |
|
325 | 325 | label_enumerations: Listes de valeurs |
|
326 | 326 | label_enumeration_new: Nouvelle valeur |
|
327 | 327 | label_information: Information |
|
328 | 328 | label_information_plural: Informations |
|
329 | 329 | label_please_login: Identification |
|
330 | 330 | label_register: S'enregistrer |
|
331 | 331 | label_password_lost: Mot de passe perdu |
|
332 | 332 | label_home: Accueil |
|
333 | 333 | label_my_page: Ma page |
|
334 | 334 | label_my_account: Mon compte |
|
335 | 335 | label_my_projects: Mes projets |
|
336 | 336 | label_administration: Administration |
|
337 | 337 | label_login: Connexion |
|
338 | 338 | label_logout: DΓ©connexion |
|
339 | 339 | label_help: Aide |
|
340 | 340 | label_reported_issues: Demandes soumises |
|
341 | 341 | label_assigned_to_me_issues: Demandes qui me sont assignΓ©es |
|
342 | 342 | label_last_login: Dernière connexion |
|
343 | 343 | label_last_updates: Dernière mise à jour |
|
344 | 344 | label_last_updates_plural: %d dernières mises à jour |
|
345 | 345 | label_registered_on: Inscrit le |
|
346 | 346 | label_activity: ActivitΓ© |
|
347 | 347 | label_overall_activity: ActivitΓ© globale |
|
348 | 348 | label_user_activity: "ActivitΓ© de %s" |
|
349 | 349 | label_new: Nouveau |
|
350 | 350 | label_logged_as: ConnectΓ© en tant que |
|
351 | 351 | label_environment: Environnement |
|
352 | 352 | label_authentication: Authentification |
|
353 | 353 | label_auth_source: Mode d'authentification |
|
354 | 354 | label_auth_source_new: Nouveau mode d'authentification |
|
355 | 355 | label_auth_source_plural: Modes d'authentification |
|
356 | 356 | label_subproject_plural: Sous-projets |
|
357 | 357 | label_and_its_subprojects: %s et ses sous-projets |
|
358 | 358 | label_min_max_length: Longueurs mini - maxi |
|
359 | 359 | label_list: Liste |
|
360 | 360 | label_date: Date |
|
361 | 361 | label_integer: Entier |
|
362 | 362 | label_float: Nombre dΓ©cimal |
|
363 | 363 | label_boolean: BoolΓ©en |
|
364 | 364 | label_string: Texte |
|
365 | 365 | label_text: Texte long |
|
366 | 366 | label_attribute: Attribut |
|
367 | 367 | label_attribute_plural: Attributs |
|
368 | 368 | label_download: %d TΓ©lΓ©chargement |
|
369 | 369 | label_download_plural: %d TΓ©lΓ©chargements |
|
370 | 370 | label_no_data: Aucune donnΓ©e Γ afficher |
|
371 | 371 | label_change_status: Changer le statut |
|
372 | 372 | label_history: Historique |
|
373 | 373 | label_attachment: Fichier |
|
374 | 374 | label_attachment_new: Nouveau fichier |
|
375 | 375 | label_attachment_delete: Supprimer le fichier |
|
376 | 376 | label_attachment_plural: Fichiers |
|
377 | 377 | label_file_added: Fichier ajoutΓ© |
|
378 | 378 | label_report: Rapport |
|
379 | 379 | label_report_plural: Rapports |
|
380 | 380 | label_news: Annonce |
|
381 | 381 | label_news_new: Nouvelle annonce |
|
382 | 382 | label_news_plural: Annonces |
|
383 | 383 | label_news_latest: Dernières annonces |
|
384 | 384 | label_news_view_all: Voir toutes les annonces |
|
385 | 385 | label_news_added: Annonce ajoutΓ©e |
|
386 | 386 | label_change_log: Historique |
|
387 | 387 | label_settings: Configuration |
|
388 | 388 | label_overview: AperΓ§u |
|
389 | 389 | label_version: Version |
|
390 | 390 | label_version_new: Nouvelle version |
|
391 | 391 | label_version_plural: Versions |
|
392 | 392 | label_confirmation: Confirmation |
|
393 | 393 | label_export_to: 'Formats disponibles:' |
|
394 | 394 | label_read: Lire... |
|
395 | 395 | label_public_projects: Projets publics |
|
396 | 396 | label_open_issues: ouvert |
|
397 | 397 | label_open_issues_plural: ouverts |
|
398 | 398 | label_closed_issues: fermΓ© |
|
399 | 399 | label_closed_issues_plural: fermΓ©s |
|
400 | 400 | label_total: Total |
|
401 | 401 | label_permissions: Permissions |
|
402 | 402 | label_current_status: Statut actuel |
|
403 | 403 | label_new_statuses_allowed: Nouveaux statuts autorisΓ©s |
|
404 | 404 | label_all: tous |
|
405 | 405 | label_none: aucun |
|
406 | 406 | label_nobody: personne |
|
407 | 407 | label_next: Suivant |
|
408 | 408 | label_previous: PrΓ©cΓ©dent |
|
409 | 409 | label_used_by: UtilisΓ© par |
|
410 | 410 | label_details: DΓ©tails |
|
411 | 411 | label_add_note: Ajouter une note |
|
412 | 412 | label_per_page: Par page |
|
413 | 413 | label_calendar: Calendrier |
|
414 | 414 | label_months_from: mois depuis |
|
415 | 415 | label_gantt: Gantt |
|
416 | 416 | label_internal: Interne |
|
417 | 417 | label_last_changes: %d derniers changements |
|
418 | 418 | label_change_view_all: Voir tous les changements |
|
419 | 419 | label_personalize_page: Personnaliser cette page |
|
420 | 420 | label_comment: Commentaire |
|
421 | 421 | label_comment_plural: Commentaires |
|
422 | 422 | label_comment_add: Ajouter un commentaire |
|
423 | 423 | label_comment_added: Commentaire ajoutΓ© |
|
424 | 424 | label_comment_delete: Supprimer les commentaires |
|
425 | 425 | label_query: Rapport personnalisΓ© |
|
426 | 426 | label_query_plural: Rapports personnalisΓ©s |
|
427 | 427 | label_query_new: Nouveau rapport |
|
428 | 428 | label_filter_add: Ajouter le filtre |
|
429 | 429 | label_filter_plural: Filtres |
|
430 | 430 | label_equals: Γ©gal |
|
431 | 431 | label_not_equals: diffΓ©rent |
|
432 | 432 | label_in_less_than: dans moins de |
|
433 | 433 | label_in_more_than: dans plus de |
|
434 | 434 | label_in: dans |
|
435 | 435 | label_today: aujourd'hui |
|
436 | 436 | label_all_time: toute la pΓ©riode |
|
437 | 437 | label_yesterday: hier |
|
438 | 438 | label_this_week: cette semaine |
|
439 | 439 | label_last_week: la semaine dernière |
|
440 | 440 | label_last_n_days: les %d derniers jours |
|
441 | 441 | label_this_month: ce mois-ci |
|
442 | 442 | label_last_month: le mois dernier |
|
443 | 443 | label_this_year: cette annΓ©e |
|
444 | 444 | label_date_range: PΓ©riode |
|
445 | 445 | label_less_than_ago: il y a moins de |
|
446 | 446 | label_more_than_ago: il y a plus de |
|
447 | 447 | label_ago: il y a |
|
448 | 448 | label_contains: contient |
|
449 | 449 | label_not_contains: ne contient pas |
|
450 | 450 | label_day_plural: jours |
|
451 | 451 | label_repository: DΓ©pΓ΄t |
|
452 | 452 | label_repository_plural: DΓ©pΓ΄ts |
|
453 | 453 | label_browse: Parcourir |
|
454 | 454 | label_modification: %d modification |
|
455 | 455 | label_modification_plural: %d modifications |
|
456 | 456 | label_revision: RΓ©vision |
|
457 | 457 | label_revision_plural: RΓ©visions |
|
458 | 458 | label_associated_revisions: RΓ©visions associΓ©es |
|
459 | 459 | label_added: ajoutΓ© |
|
460 | 460 | label_modified: modifiΓ© |
|
461 | 461 | label_copied: copiΓ© |
|
462 | 462 | label_renamed: renommΓ© |
|
463 | 463 | label_deleted: supprimΓ© |
|
464 | 464 | label_latest_revision: Dernière révision |
|
465 | 465 | label_latest_revision_plural: Dernières révisions |
|
466 | 466 | label_view_revisions: Voir les rΓ©visions |
|
467 | 467 | label_max_size: Taille maximale |
|
468 | 468 | label_on: sur |
|
469 | 469 | label_sort_highest: Remonter en premier |
|
470 | 470 | label_sort_higher: Remonter |
|
471 | 471 | label_sort_lower: Descendre |
|
472 | 472 | label_sort_lowest: Descendre en dernier |
|
473 | 473 | label_roadmap: Roadmap |
|
474 | 474 | label_roadmap_due_in: EchΓ©ance dans %s |
|
475 | 475 | label_roadmap_overdue: En retard de %s |
|
476 | 476 | label_roadmap_no_issues: Aucune demande pour cette version |
|
477 | 477 | label_search: Recherche |
|
478 | 478 | label_result_plural: RΓ©sultats |
|
479 | 479 | label_all_words: Tous les mots |
|
480 | 480 | label_wiki: Wiki |
|
481 | 481 | label_wiki_edit: RΓ©vision wiki |
|
482 | 482 | label_wiki_edit_plural: RΓ©visions wiki |
|
483 | 483 | label_wiki_page: Page wiki |
|
484 | 484 | label_wiki_page_plural: Pages wiki |
|
485 | 485 | label_index_by_title: Index par titre |
|
486 | 486 | label_index_by_date: Index par date |
|
487 | 487 | label_current_version: Version actuelle |
|
488 | 488 | label_preview: PrΓ©visualisation |
|
489 | 489 | label_feed_plural: Flux RSS |
|
490 | 490 | label_changes_details: DΓ©tails de tous les changements |
|
491 | 491 | label_issue_tracking: Suivi des demandes |
|
492 | 492 | label_spent_time: Temps passΓ© |
|
493 | 493 | label_f_hour: %.2f heure |
|
494 | 494 | label_f_hour_plural: %.2f heures |
|
495 | 495 | label_time_tracking: Suivi du temps |
|
496 | 496 | label_change_plural: Changements |
|
497 | 497 | label_statistics: Statistiques |
|
498 | 498 | label_commits_per_month: Commits par mois |
|
499 | 499 | label_commits_per_author: Commits par auteur |
|
500 | 500 | label_view_diff: Voir les diffΓ©rences |
|
501 | 501 | label_diff_inline: en ligne |
|
502 | 502 | label_diff_side_by_side: cΓ΄te Γ cΓ΄te |
|
503 | 503 | label_options: Options |
|
504 | 504 | label_copy_workflow_from: Copier le workflow de |
|
505 | 505 | label_permissions_report: Synthèse des permissions |
|
506 | 506 | label_watched_issues: Demandes surveillΓ©es |
|
507 | 507 | label_related_issues: Demandes liΓ©es |
|
508 | 508 | label_applied_status: Statut appliquΓ© |
|
509 | 509 | label_loading: Chargement... |
|
510 | 510 | label_relation_new: Nouvelle relation |
|
511 | 511 | label_relation_delete: Supprimer la relation |
|
512 | 512 | label_relates_to: liΓ© Γ |
|
513 | 513 | label_duplicates: duplique |
|
514 | 514 | label_duplicated_by: dupliquΓ© par |
|
515 | 515 | label_blocks: bloque |
|
516 | 516 | label_blocked_by: bloquΓ© par |
|
517 | 517 | label_precedes: précède |
|
518 | 518 | label_follows: suit |
|
519 | 519 | label_end_to_start: fin Γ dΓ©but |
|
520 | 520 | label_end_to_end: fin Γ fin |
|
521 | 521 | label_start_to_start: dΓ©but Γ dΓ©but |
|
522 | 522 | label_start_to_end: dΓ©but Γ fin |
|
523 | 523 | label_stay_logged_in: Rester connectΓ© |
|
524 | 524 | label_disabled: dΓ©sactivΓ© |
|
525 | 525 | label_show_completed_versions: Voir les versions passΓ©es |
|
526 | 526 | label_me: moi |
|
527 | 527 | label_board: Forum |
|
528 | 528 | label_board_new: Nouveau forum |
|
529 | 529 | label_board_plural: Forums |
|
530 | 530 | label_topic_plural: Discussions |
|
531 | 531 | label_message_plural: Messages |
|
532 | 532 | label_message_last: Dernier message |
|
533 | 533 | label_message_new: Nouveau message |
|
534 | 534 | label_message_posted: Message ajoutΓ© |
|
535 | 535 | label_reply_plural: RΓ©ponses |
|
536 | 536 | label_send_information: Envoyer les informations Γ l'utilisateur |
|
537 | 537 | label_year: AnnΓ©e |
|
538 | 538 | label_month: Mois |
|
539 | 539 | label_week: Semaine |
|
540 | 540 | label_date_from: Du |
|
541 | 541 | label_date_to: Au |
|
542 | 542 | label_language_based: BasΓ© sur la langue de l'utilisateur |
|
543 | 543 | label_sort_by: Trier par %s |
|
544 | 544 | label_send_test_email: Envoyer un email de test |
|
545 | 545 | label_feeds_access_key_created_on: Clé d'accès RSS créée il y a %s |
|
546 | 546 | label_module_plural: Modules |
|
547 | 547 | label_added_time_by: AjoutΓ© par %s il y a %s |
|
548 | 548 | label_updated_time_by: Mis Γ jour par %s il y a %s |
|
549 | 549 | label_updated_time: Mis Γ jour il y a %s |
|
550 | 550 | label_jump_to_a_project: Aller Γ un projet... |
|
551 | 551 | label_file_plural: Fichiers |
|
552 | 552 | label_changeset_plural: RΓ©visions |
|
553 | 553 | label_default_columns: Colonnes par dΓ©faut |
|
554 | 554 | label_no_change_option: (Pas de changement) |
|
555 | 555 | label_bulk_edit_selected_issues: Modifier les demandes sΓ©lectionnΓ©es |
|
556 | 556 | label_theme: Thème |
|
557 | 557 | label_default: DΓ©faut |
|
558 | 558 | label_search_titles_only: Uniquement dans les titres |
|
559 | 559 | label_user_mail_option_all: "Pour tous les Γ©vΓ©nements de tous mes projets" |
|
560 | 560 | label_user_mail_option_selected: "Pour tous les Γ©vΓ©nements des projets sΓ©lectionnΓ©s..." |
|
561 | 561 | label_user_mail_option_none: "Seulement pour ce que je surveille ou Γ quoi je participe" |
|
562 | 562 | label_user_mail_no_self_notified: "Je ne veux pas Γͺtre notifiΓ© des changements que j'effectue" |
|
563 | 563 | label_registration_activation_by_email: activation du compte par email |
|
564 | 564 | label_registration_manual_activation: activation manuelle du compte |
|
565 | 565 | label_registration_automatic_activation: activation automatique du compte |
|
566 | 566 | label_display_per_page: 'Par page: %s' |
|
567 | 567 | label_age: Age |
|
568 | 568 | label_change_properties: Changer les propriΓ©tΓ©s |
|
569 | 569 | label_general: GΓ©nΓ©ral |
|
570 | 570 | label_more: Plus |
|
571 | 571 | label_scm: SCM |
|
572 | 572 | label_plugins: Plugins |
|
573 | 573 | label_ldap_authentication: Authentification LDAP |
|
574 | 574 | label_downloads_abbr: D/L |
|
575 | 575 | label_optional_description: Description facultative |
|
576 | 576 | label_add_another_file: Ajouter un autre fichier |
|
577 | 577 | label_preferences: PrΓ©fΓ©rences |
|
578 | 578 | label_chronological_order: Dans l'ordre chronologique |
|
579 | 579 | label_reverse_chronological_order: Dans l'ordre chronologique inverse |
|
580 | 580 | label_planning: Planning |
|
581 | 581 | label_incoming_emails: Emails entrants |
|
582 | 582 | label_generate_key: GΓ©nΓ©rer une clΓ© |
|
583 |
label_issue_watchers: |
|
|
583 | label_issue_watchers: Observateurs | |
|
584 | 584 | label_example: Exemple |
|
585 | 585 | |
|
586 | 586 | button_login: Connexion |
|
587 | 587 | button_submit: Soumettre |
|
588 | 588 | button_save: Sauvegarder |
|
589 | 589 | button_check_all: Tout cocher |
|
590 | 590 | button_uncheck_all: Tout dΓ©cocher |
|
591 | 591 | button_delete: Supprimer |
|
592 | 592 | button_create: CrΓ©er |
|
593 | 593 | button_test: Tester |
|
594 | 594 | button_edit: Modifier |
|
595 | 595 | button_add: Ajouter |
|
596 | 596 | button_change: Changer |
|
597 | 597 | button_apply: Appliquer |
|
598 | 598 | button_clear: Effacer |
|
599 | 599 | button_lock: Verrouiller |
|
600 | 600 | button_unlock: DΓ©verrouiller |
|
601 | 601 | button_download: TΓ©lΓ©charger |
|
602 | 602 | button_list: Lister |
|
603 | 603 | button_view: Voir |
|
604 | 604 | button_move: DΓ©placer |
|
605 | 605 | button_back: Retour |
|
606 | 606 | button_cancel: Annuler |
|
607 | 607 | button_activate: Activer |
|
608 | 608 | button_sort: Trier |
|
609 | 609 | button_log_time: Saisir temps |
|
610 | 610 | button_rollback: Revenir Γ cette version |
|
611 | 611 | button_watch: Surveiller |
|
612 | 612 | button_unwatch: Ne plus surveiller |
|
613 | 613 | button_reply: RΓ©pondre |
|
614 | 614 | button_archive: Archiver |
|
615 | 615 | button_unarchive: DΓ©sarchiver |
|
616 | 616 | button_reset: RΓ©initialiser |
|
617 | 617 | button_rename: Renommer |
|
618 | 618 | button_change_password: Changer de mot de passe |
|
619 | 619 | button_copy: Copier |
|
620 | 620 | button_annotate: Annoter |
|
621 | 621 | button_update: Mettre Γ jour |
|
622 | 622 | button_configure: Configurer |
|
623 | 623 | button_quote: Citer |
|
624 | 624 | |
|
625 | 625 | status_active: actif |
|
626 | 626 | status_registered: enregistrΓ© |
|
627 | 627 | status_locked: vΓ©rouillΓ© |
|
628 | 628 | |
|
629 | 629 | text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyΓ©e |
|
630 | 630 | text_regexp_info: ex. ^[A-Z0-9]+$ |
|
631 | 631 | text_min_max_length_info: 0 pour aucune restriction |
|
632 | 632 | text_project_destroy_confirmation: Etes-vous sΓ»r de vouloir supprimer ce projet et toutes ses donnΓ©es ? |
|
633 | 633 | text_subprojects_destroy_warning: 'Ses sous-projets: %s seront Γ©galement supprimΓ©s.' |
|
634 | 634 | text_workflow_edit: SΓ©lectionner un tracker et un rΓ΄le pour Γ©diter le workflow |
|
635 | 635 | text_are_you_sure: Etes-vous sΓ»r ? |
|
636 | 636 | text_journal_changed: changΓ© de %s Γ %s |
|
637 | 637 | text_journal_set_to: mis Γ %s |
|
638 | 638 | text_journal_deleted: supprimΓ© |
|
639 | 639 | text_tip_task_begin_day: tΓ’che commenΓ§ant ce jour |
|
640 | 640 | text_tip_task_end_day: tΓ’che finissant ce jour |
|
641 | 641 | text_tip_task_begin_end_day: tΓ’che commenΓ§ant et finissant ce jour |
|
642 | 642 | text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres et tirets sont autorisΓ©s.<br />Un fois sauvegardΓ©, l''identifiant ne pourra plus Γͺtre modifiΓ©.' |
|
643 | 643 | text_caracters_maximum: %d caractères maximum. |
|
644 | 644 | text_caracters_minimum: %d caractères minimum. |
|
645 | 645 | text_length_between: Longueur comprise entre %d et %d caractères. |
|
646 | 646 | text_tracker_no_workflow: Aucun worflow n'est dΓ©fini pour ce tracker |
|
647 | 647 | text_unallowed_characters: Caractères non autorisés |
|
648 | 648 | text_comma_separated: Plusieurs valeurs possibles (sΓ©parΓ©es par des virgules). |
|
649 | 649 | text_issues_ref_in_commit_messages: RΓ©fΓ©rencement et rΓ©solution des demandes dans les commentaires de commits |
|
650 | 650 | text_issue_added: La demande %s a Γ©tΓ© soumise par %s. |
|
651 | 651 | text_issue_updated: La demande %s a Γ©tΓ© mise Γ jour par %s. |
|
652 | 652 | text_wiki_destroy_confirmation: Etes-vous sΓ»r de vouloir supprimer ce wiki et tout son contenu ? |
|
653 | 653 | text_issue_category_destroy_question: %d demandes sont affectΓ©es Γ cette catΓ©gories. Que voulez-vous faire ? |
|
654 | 654 | text_issue_category_destroy_assignments: N'affecter les demandes Γ aucune autre catΓ©gorie |
|
655 | 655 | text_issue_category_reassign_to: RΓ©affecter les demandes Γ cette catΓ©gorie |
|
656 | 656 | text_user_mail_option: "Pour les projets non sΓ©lectionnΓ©s, vous recevrez seulement des notifications pour ce que vous surveillez ou Γ quoi vous participez (exemple: demandes dont vous Γͺtes l'auteur ou la personne assignΓ©e)." |
|
657 | 657 | text_no_configuration_data: "Les rΓ΄les, trackers, statuts et le workflow ne sont pas encore paramΓ©trΓ©s.\nIl est vivement recommandΓ© de charger le paramΓ©trage par defaut. Vous pourrez le modifier une fois chargΓ©." |
|
658 | 658 | text_load_default_configuration: Charger le paramΓ©trage par dΓ©faut |
|
659 | 659 | text_status_changed_by_changeset: AppliquΓ© par commit %s. |
|
660 | 660 | text_issues_destroy_confirmation: 'Etes-vous sΓ»r de vouloir supprimer le(s) demandes(s) selectionnΓ©e(s) ?' |
|
661 | 661 | text_select_project_modules: 'Selectionner les modules Γ activer pour ce project:' |
|
662 | 662 | text_default_administrator_account_changed: Compte administrateur par dΓ©faut changΓ© |
|
663 | 663 | text_file_repository_writable: RΓ©pertoire de stockage des fichiers accessible en Γ©criture |
|
664 | 664 | text_rmagick_available: Bibliothèque RMagick présente (optionnelle) |
|
665 | 665 | text_destroy_time_entries_question: %.02f heures ont Γ©tΓ© enregistrΓ©es sur les demandes Γ supprimer. Que voulez-vous faire ? |
|
666 | 666 | text_destroy_time_entries: Supprimer les heures |
|
667 | 667 | text_assign_time_entries_to_project: Reporter les heures sur le projet |
|
668 | 668 | text_reassign_time_entries: 'Reporter les heures sur cette demande:' |
|
669 | 669 | text_user_wrote: '%s a Γ©crit:' |
|
670 | 670 | text_enumeration_destroy_question: 'Cette valeur est affectΓ©e Γ %d objets.' |
|
671 | 671 | text_enumeration_category_reassign_to: 'RΓ©affecter les objets Γ cette valeur:' |
|
672 | 672 | text_email_delivery_not_configured: "L'envoi de mail n'est pas configurΓ©, les notifications sont dΓ©sactivΓ©es.\nConfigurez votre serveur SMTP dans config/email.yml et redΓ©marrez l'application pour les activer." |
|
673 | 673 | text_repository_usernames_mapping: "Vous pouvez sΓ©lectionner ou modifier l'utilisateur Redmine associΓ© Γ chaque nom d'utilisateur figurant dans l'historique du dΓ©pΓ΄t.\nLes utilisateurs avec le mΓͺme identifiant ou la mΓͺme adresse mail seront automatiquement associΓ©s." |
|
674 | 674 | text_diff_truncated: '... Ce diffΓ©rentiel a Γ©tΓ© tronquΓ© car il excΓ¨de la taille maximale pouvant Γͺtre affichΓ©e.' |
|
675 | 675 | |
|
676 | 676 | default_role_manager: Manager |
|
677 | 677 | default_role_developper: DΓ©veloppeur |
|
678 | 678 | default_role_reporter: Rapporteur |
|
679 | 679 | default_tracker_bug: Anomalie |
|
680 | 680 | default_tracker_feature: Evolution |
|
681 | 681 | default_tracker_support: Assistance |
|
682 | 682 | default_issue_status_new: Nouveau |
|
683 | 683 | default_issue_status_assigned: AssignΓ© |
|
684 | 684 | default_issue_status_resolved: RΓ©solu |
|
685 | 685 | default_issue_status_feedback: Commentaire |
|
686 | 686 | default_issue_status_closed: FermΓ© |
|
687 | 687 | default_issue_status_rejected: RejetΓ© |
|
688 | 688 | default_doc_category_user: Documentation utilisateur |
|
689 | 689 | default_doc_category_tech: Documentation technique |
|
690 | 690 | default_priority_low: Bas |
|
691 | 691 | default_priority_normal: Normal |
|
692 | 692 | default_priority_high: Haut |
|
693 | 693 | default_priority_urgent: Urgent |
|
694 | 694 | default_priority_immediate: ImmΓ©diat |
|
695 | 695 | default_activity_design: Conception |
|
696 | 696 | default_activity_development: DΓ©veloppement |
|
697 | 697 | |
|
698 | 698 | enumeration_issue_priorities: PrioritΓ©s des demandes |
|
699 | 699 | enumeration_doc_categories: CatΓ©gories des documents |
|
700 | 700 | enumeration_activities: ActivitΓ©s (suivi du temps) |
@@ -1,189 +1,196 | |||
|
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 'redmine/scm/adapters/abstract_adapter' |
|
19 | 19 | require 'rexml/document' |
|
20 | 20 | |
|
21 | 21 | module Redmine |
|
22 | 22 | module Scm |
|
23 | 23 | module Adapters |
|
24 | 24 | class DarcsAdapter < AbstractAdapter |
|
25 | 25 | # Darcs executable name |
|
26 | 26 | DARCS_BIN = "darcs" |
|
27 | 27 | |
|
28 | 28 | class << self |
|
29 | 29 | def client_version |
|
30 | 30 | @@client_version ||= (darcs_binary_version || []) |
|
31 | 31 | end |
|
32 | 32 | |
|
33 | 33 | def darcs_binary_version |
|
34 | 34 | cmd = "#{DARCS_BIN} --version" |
|
35 | 35 | version = nil |
|
36 | 36 | shellout(cmd) do |io| |
|
37 | 37 | # Read darcs version in first returned line |
|
38 | 38 | if m = io.gets.match(%r{((\d+\.)+\d+)}) |
|
39 | 39 | version = m[0].scan(%r{\d+}).collect(&:to_i) |
|
40 | 40 | end |
|
41 | 41 | end |
|
42 | 42 | return nil if $? && $?.exitstatus != 0 |
|
43 | 43 | version |
|
44 | 44 | end |
|
45 | 45 | end |
|
46 | 46 | |
|
47 | 47 | def initialize(url, root_url=nil, login=nil, password=nil) |
|
48 | 48 | @url = url |
|
49 | 49 | @root_url = url |
|
50 | 50 | end |
|
51 | 51 | |
|
52 | 52 | def supports_cat? |
|
53 | 53 | # cat supported in darcs 2.0.0 and higher |
|
54 | 54 | self.class.client_version_above?([2, 0, 0]) |
|
55 | 55 | end |
|
56 | 56 | |
|
57 | 57 | # Get info about the darcs repository |
|
58 | 58 | def info |
|
59 | 59 | rev = revisions(nil,nil,nil,{:limit => 1}) |
|
60 | 60 | rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil |
|
61 | 61 | end |
|
62 | 62 | |
|
63 | 63 | # Returns an Entries collection |
|
64 | 64 | # or nil if the given path doesn't exist in the repository |
|
65 | 65 | def entries(path=nil, identifier=nil) |
|
66 | 66 | path_prefix = (path.blank? ? '' : "#{path}/") |
|
67 | 67 | path = '.' if path.blank? |
|
68 | 68 | entries = Entries.new |
|
69 | 69 | cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output" |
|
70 | 70 | cmd << " --match \"hash #{identifier}\"" if identifier |
|
71 | 71 | cmd << " #{path}" |
|
72 | 72 | shellout(cmd) do |io| |
|
73 | 73 | begin |
|
74 | 74 | doc = REXML::Document.new(io) |
|
75 | 75 | if doc.root.name == 'directory' |
|
76 | 76 | doc.elements.each('directory/*') do |element| |
|
77 | 77 | next unless ['file', 'directory'].include? element.name |
|
78 | 78 | entries << entry_from_xml(element, path_prefix) |
|
79 | 79 | end |
|
80 | 80 | elsif doc.root.name == 'file' |
|
81 | 81 | entries << entry_from_xml(doc.root, path_prefix) |
|
82 | 82 | end |
|
83 | 83 | rescue |
|
84 | 84 | end |
|
85 | 85 | end |
|
86 | 86 | return nil if $? && $?.exitstatus != 0 |
|
87 | entries.sort_by_name | |
|
87 | entries.compact.sort_by_name | |
|
88 | 88 | end |
|
89 | 89 | |
|
90 | 90 | def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) |
|
91 | 91 | path = '.' if path.blank? |
|
92 | 92 | revisions = Revisions.new |
|
93 | 93 | cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output" |
|
94 | 94 | cmd << " --from-match \"hash #{identifier_from}\"" if identifier_from |
|
95 | 95 | cmd << " --last #{options[:limit].to_i}" if options[:limit] |
|
96 | 96 | shellout(cmd) do |io| |
|
97 | 97 | begin |
|
98 | 98 | doc = REXML::Document.new(io) |
|
99 | 99 | doc.elements.each("changelog/patch") do |patch| |
|
100 | 100 | message = patch.elements['name'].text |
|
101 | 101 | message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment'] |
|
102 | 102 | revisions << Revision.new({:identifier => nil, |
|
103 | 103 | :author => patch.attributes['author'], |
|
104 | 104 | :scmid => patch.attributes['hash'], |
|
105 | 105 | :time => Time.parse(patch.attributes['local_date']), |
|
106 | 106 | :message => message, |
|
107 | 107 | :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil) |
|
108 | 108 | }) |
|
109 | 109 | end |
|
110 | 110 | rescue |
|
111 | 111 | end |
|
112 | 112 | end |
|
113 | 113 | return nil if $? && $?.exitstatus != 0 |
|
114 | 114 | revisions |
|
115 | 115 | end |
|
116 | 116 | |
|
117 | 117 | def diff(path, identifier_from, identifier_to=nil) |
|
118 | 118 | path = '*' if path.blank? |
|
119 | 119 | cmd = "#{DARCS_BIN} diff --repodir #{@url}" |
|
120 | 120 | if identifier_to.nil? |
|
121 | 121 | cmd << " --match \"hash #{identifier_from}\"" |
|
122 | 122 | else |
|
123 | 123 | cmd << " --to-match \"hash #{identifier_from}\"" |
|
124 | 124 | cmd << " --from-match \"hash #{identifier_to}\"" |
|
125 | 125 | end |
|
126 | 126 | cmd << " -u #{path}" |
|
127 | 127 | diff = [] |
|
128 | 128 | shellout(cmd) do |io| |
|
129 | 129 | io.each_line do |line| |
|
130 | 130 | diff << line |
|
131 | 131 | end |
|
132 | 132 | end |
|
133 | 133 | return nil if $? && $?.exitstatus != 0 |
|
134 | 134 | diff |
|
135 | 135 | end |
|
136 | 136 | |
|
137 | 137 | def cat(path, identifier=nil) |
|
138 | 138 | cmd = "#{DARCS_BIN} show content --repodir #{@url}" |
|
139 | 139 | cmd << " --match \"hash #{identifier}\"" if identifier |
|
140 | 140 | cmd << " #{shell_quote path}" |
|
141 | 141 | cat = nil |
|
142 | 142 | shellout(cmd) do |io| |
|
143 | 143 | io.binmode |
|
144 | 144 | cat = io.read |
|
145 | 145 | end |
|
146 | 146 | return nil if $? && $?.exitstatus != 0 |
|
147 | 147 | cat |
|
148 | 148 | end |
|
149 | 149 | |
|
150 | 150 | private |
|
151 | 151 |
|
|
152 | # Returns an Entry from the given XML element | |
|
153 | # or nil if the entry was deleted | |
|
152 | 154 | def entry_from_xml(element, path_prefix) |
|
155 | modified_element = element.elements['modified'] | |
|
156 | if modified_element.elements['modified_how'].text.match(/removed/) | |
|
157 | return nil | |
|
158 | end | |
|
159 | ||
|
153 | 160 | Entry.new({:name => element.attributes['name'], |
|
154 | 161 | :path => path_prefix + element.attributes['name'], |
|
155 | 162 | :kind => element.name == 'file' ? 'file' : 'dir', |
|
156 | 163 | :size => nil, |
|
157 | 164 | :lastrev => Revision.new({ |
|
158 | 165 | :identifier => nil, |
|
159 |
:scmid => |
|
|
166 | :scmid => modified_element.elements['patch'].attributes['hash'] | |
|
160 | 167 | }) |
|
161 | 168 | }) |
|
162 | 169 | end |
|
163 | 170 | |
|
164 | 171 | # Retrieve changed paths for a single patch |
|
165 | 172 | def get_paths_for_patch(hash) |
|
166 | 173 | cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output" |
|
167 | 174 | cmd << " --match \"hash #{hash}\" " |
|
168 | 175 | paths = [] |
|
169 | 176 | shellout(cmd) do |io| |
|
170 | 177 | begin |
|
171 | 178 | # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7) |
|
172 | 179 | # A root element is added so that REXML doesn't raise an error |
|
173 | 180 | doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>") |
|
174 | 181 | doc.elements.each('fake_root/summary/*') do |modif| |
|
175 | 182 | paths << {:action => modif.name[0,1].upcase, |
|
176 | 183 | :path => "/" + modif.text.chomp.gsub(/^\s*/, '') |
|
177 | 184 | } |
|
178 | 185 | end |
|
179 | 186 | rescue |
|
180 | 187 | end |
|
181 | 188 | end |
|
182 | 189 | paths |
|
183 | 190 | rescue CommandFailed |
|
184 | 191 | paths |
|
185 | 192 | end |
|
186 | 193 | end |
|
187 | 194 | end |
|
188 | 195 | end |
|
189 | 196 | end |
@@ -1,739 +1,763 | |||
|
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 | get :index |
|
55 | 55 | assert_response :success |
|
56 | 56 | assert_template 'index.rhtml' |
|
57 | 57 | assert_not_nil assigns(:issues) |
|
58 | 58 | assert_nil assigns(:project) |
|
59 | 59 | assert_tag :tag => 'a', :content => /Can't print recipes/ |
|
60 | 60 | assert_tag :tag => 'a', :content => /Subproject issue/ |
|
61 | 61 | # private projects hidden |
|
62 | 62 | assert_no_tag :tag => 'a', :content => /Issue of a private subproject/ |
|
63 | 63 | assert_no_tag :tag => 'a', :content => /Issue on project 2/ |
|
64 | 64 | end |
|
65 | 65 | |
|
66 | 66 | def test_index_should_not_list_issues_when_module_disabled |
|
67 | 67 | EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1") |
|
68 | 68 | get :index |
|
69 | 69 | assert_response :success |
|
70 | 70 | assert_template 'index.rhtml' |
|
71 | 71 | assert_not_nil assigns(:issues) |
|
72 | 72 | assert_nil assigns(:project) |
|
73 | 73 | assert_no_tag :tag => 'a', :content => /Can't print recipes/ |
|
74 | 74 | assert_tag :tag => 'a', :content => /Subproject issue/ |
|
75 | 75 | end |
|
76 | 76 | |
|
77 | 77 | def test_index_with_project |
|
78 | 78 | Setting.display_subprojects_issues = 0 |
|
79 | 79 | get :index, :project_id => 1 |
|
80 | 80 | assert_response :success |
|
81 | 81 | assert_template 'index.rhtml' |
|
82 | 82 | assert_not_nil assigns(:issues) |
|
83 | 83 | assert_tag :tag => 'a', :content => /Can't print recipes/ |
|
84 | 84 | assert_no_tag :tag => 'a', :content => /Subproject issue/ |
|
85 | 85 | end |
|
86 | 86 | |
|
87 | 87 | def test_index_with_project_and_subprojects |
|
88 | 88 | Setting.display_subprojects_issues = 1 |
|
89 | 89 | get :index, :project_id => 1 |
|
90 | 90 | assert_response :success |
|
91 | 91 | assert_template 'index.rhtml' |
|
92 | 92 | assert_not_nil assigns(:issues) |
|
93 | 93 | assert_tag :tag => 'a', :content => /Can't print recipes/ |
|
94 | 94 | assert_tag :tag => 'a', :content => /Subproject issue/ |
|
95 | 95 | assert_no_tag :tag => 'a', :content => /Issue of a private subproject/ |
|
96 | 96 | end |
|
97 | 97 | |
|
98 | 98 | def test_index_with_project_and_subprojects_should_show_private_subprojects |
|
99 | 99 | @request.session[:user_id] = 2 |
|
100 | 100 | Setting.display_subprojects_issues = 1 |
|
101 | 101 | get :index, :project_id => 1 |
|
102 | 102 | assert_response :success |
|
103 | 103 | assert_template 'index.rhtml' |
|
104 | 104 | assert_not_nil assigns(:issues) |
|
105 | 105 | assert_tag :tag => 'a', :content => /Can't print recipes/ |
|
106 | 106 | assert_tag :tag => 'a', :content => /Subproject issue/ |
|
107 | 107 | assert_tag :tag => 'a', :content => /Issue of a private subproject/ |
|
108 | 108 | end |
|
109 | 109 | |
|
110 | 110 | def test_index_with_project_and_filter |
|
111 | 111 | get :index, :project_id => 1, :set_filter => 1 |
|
112 | 112 | assert_response :success |
|
113 | 113 | assert_template 'index.rhtml' |
|
114 | 114 | assert_not_nil assigns(:issues) |
|
115 | 115 | end |
|
116 | 116 | |
|
117 | 117 | def test_index_csv_with_project |
|
118 | 118 | get :index, :format => 'csv' |
|
119 | 119 | assert_response :success |
|
120 | 120 | assert_not_nil assigns(:issues) |
|
121 | 121 | assert_equal 'text/csv', @response.content_type |
|
122 | 122 | |
|
123 | 123 | get :index, :project_id => 1, :format => 'csv' |
|
124 | 124 | assert_response :success |
|
125 | 125 | assert_not_nil assigns(:issues) |
|
126 | 126 | assert_equal 'text/csv', @response.content_type |
|
127 | 127 | end |
|
128 | 128 | |
|
129 | 129 | def test_index_pdf |
|
130 | 130 | get :index, :format => 'pdf' |
|
131 | 131 | assert_response :success |
|
132 | 132 | assert_not_nil assigns(:issues) |
|
133 | 133 | assert_equal 'application/pdf', @response.content_type |
|
134 | 134 | |
|
135 | 135 | get :index, :project_id => 1, :format => 'pdf' |
|
136 | 136 | assert_response :success |
|
137 | 137 | assert_not_nil assigns(:issues) |
|
138 | 138 | assert_equal 'application/pdf', @response.content_type |
|
139 | 139 | end |
|
140 | 140 | |
|
141 | 141 | def test_index_sort |
|
142 | 142 | get :index, :sort_key => 'tracker' |
|
143 | 143 | assert_response :success |
|
144 | 144 | |
|
145 | 145 | sort_params = @request.session['issuesindex_sort'] |
|
146 | 146 | assert sort_params.is_a?(Hash) |
|
147 | 147 | assert_equal 'tracker', sort_params[:key] |
|
148 | 148 | assert_equal 'ASC', sort_params[:order] |
|
149 | 149 | end |
|
150 | 150 | |
|
151 | 151 | def test_gantt |
|
152 | 152 | get :gantt, :project_id => 1 |
|
153 | 153 | assert_response :success |
|
154 | 154 | assert_template 'gantt.rhtml' |
|
155 | 155 | assert_not_nil assigns(:gantt) |
|
156 | 156 | events = assigns(:gantt).events |
|
157 | 157 | assert_not_nil events |
|
158 | 158 | # Issue with start and due dates |
|
159 | 159 | i = Issue.find(1) |
|
160 | 160 | assert_not_nil i.due_date |
|
161 | 161 | assert events.include?(Issue.find(1)) |
|
162 | 162 | # Issue with without due date but targeted to a version with date |
|
163 | 163 | i = Issue.find(2) |
|
164 | 164 | assert_nil i.due_date |
|
165 | 165 | assert events.include?(i) |
|
166 | 166 | end |
|
167 | 167 | |
|
168 | 168 | def test_cross_project_gantt |
|
169 | 169 | get :gantt |
|
170 | 170 | assert_response :success |
|
171 | 171 | assert_template 'gantt.rhtml' |
|
172 | 172 | assert_not_nil assigns(:gantt) |
|
173 | 173 | events = assigns(:gantt).events |
|
174 | 174 | assert_not_nil events |
|
175 | 175 | end |
|
176 | 176 | |
|
177 | 177 | def test_gantt_export_to_pdf |
|
178 | 178 | get :gantt, :project_id => 1, :format => 'pdf' |
|
179 | 179 | assert_response :success |
|
180 | 180 | assert_template 'gantt.rfpdf' |
|
181 | 181 | assert_equal 'application/pdf', @response.content_type |
|
182 | 182 | assert_not_nil assigns(:gantt) |
|
183 | 183 | end |
|
184 | 184 | |
|
185 | 185 | def test_cross_project_gantt_export_to_pdf |
|
186 | 186 | get :gantt, :format => 'pdf' |
|
187 | 187 | assert_response :success |
|
188 | 188 | assert_template 'gantt.rfpdf' |
|
189 | 189 | assert_equal 'application/pdf', @response.content_type |
|
190 | 190 | assert_not_nil assigns(:gantt) |
|
191 | 191 | end |
|
192 | 192 | |
|
193 | 193 | if Object.const_defined?(:Magick) |
|
194 | 194 | def test_gantt_image |
|
195 | 195 | get :gantt, :project_id => 1, :format => 'png' |
|
196 | 196 | assert_response :success |
|
197 | 197 | assert_equal 'image/png', @response.content_type |
|
198 | 198 | end |
|
199 | 199 | else |
|
200 | 200 | puts "RMagick not installed. Skipping tests !!!" |
|
201 | 201 | end |
|
202 | 202 | |
|
203 | 203 | def test_calendar |
|
204 | 204 | get :calendar, :project_id => 1 |
|
205 | 205 | assert_response :success |
|
206 | 206 | assert_template 'calendar' |
|
207 | 207 | assert_not_nil assigns(:calendar) |
|
208 | 208 | end |
|
209 | 209 | |
|
210 | 210 | def test_cross_project_calendar |
|
211 | 211 | get :calendar |
|
212 | 212 | assert_response :success |
|
213 | 213 | assert_template 'calendar' |
|
214 | 214 | assert_not_nil assigns(:calendar) |
|
215 | 215 | end |
|
216 | 216 | |
|
217 | 217 | def test_changes |
|
218 | 218 | get :changes, :project_id => 1 |
|
219 | 219 | assert_response :success |
|
220 | 220 | assert_not_nil assigns(:journals) |
|
221 | 221 | assert_equal 'application/atom+xml', @response.content_type |
|
222 | 222 | end |
|
223 | 223 | |
|
224 | 224 | def test_show_by_anonymous |
|
225 | 225 | get :show, :id => 1 |
|
226 | 226 | assert_response :success |
|
227 | 227 | assert_template 'show.rhtml' |
|
228 | 228 | assert_not_nil assigns(:issue) |
|
229 | 229 | assert_equal Issue.find(1), assigns(:issue) |
|
230 | 230 | |
|
231 | 231 | # anonymous role is allowed to add a note |
|
232 | 232 | assert_tag :tag => 'form', |
|
233 | 233 | :descendant => { :tag => 'fieldset', |
|
234 | 234 | :child => { :tag => 'legend', |
|
235 | 235 | :content => /Notes/ } } |
|
236 | 236 | end |
|
237 | 237 | |
|
238 | 238 | def test_show_by_manager |
|
239 | 239 | @request.session[:user_id] = 2 |
|
240 | 240 | get :show, :id => 1 |
|
241 | 241 | assert_response :success |
|
242 | 242 | |
|
243 | 243 | assert_tag :tag => 'form', |
|
244 | 244 | :descendant => { :tag => 'fieldset', |
|
245 | 245 | :child => { :tag => 'legend', |
|
246 | 246 | :content => /Change properties/ } }, |
|
247 | 247 | :descendant => { :tag => 'fieldset', |
|
248 | 248 | :child => { :tag => 'legend', |
|
249 | 249 | :content => /Log time/ } }, |
|
250 | 250 | :descendant => { :tag => 'fieldset', |
|
251 | 251 | :child => { :tag => 'legend', |
|
252 | 252 | :content => /Notes/ } } |
|
253 | 253 | end |
|
254 | 254 | |
|
255 | 255 | def test_get_new |
|
256 | 256 | @request.session[:user_id] = 2 |
|
257 | 257 | get :new, :project_id => 1, :tracker_id => 1 |
|
258 | 258 | assert_response :success |
|
259 | 259 | assert_template 'new' |
|
260 | 260 | |
|
261 | 261 | assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]', |
|
262 | 262 | :value => 'Default string' } |
|
263 | 263 | end |
|
264 | 264 | |
|
265 | 265 | def test_get_new_without_tracker_id |
|
266 | 266 | @request.session[:user_id] = 2 |
|
267 | 267 | get :new, :project_id => 1 |
|
268 | 268 | assert_response :success |
|
269 | 269 | assert_template 'new' |
|
270 | 270 | |
|
271 | 271 | issue = assigns(:issue) |
|
272 | 272 | assert_not_nil issue |
|
273 | 273 | assert_equal Project.find(1).trackers.first, issue.tracker |
|
274 | 274 | end |
|
275 | 275 | |
|
276 | 276 | def test_update_new_form |
|
277 | 277 | @request.session[:user_id] = 2 |
|
278 | 278 | xhr :post, :new, :project_id => 1, |
|
279 | 279 | :issue => {:tracker_id => 2, |
|
280 | 280 | :subject => 'This is the test_new issue', |
|
281 | 281 | :description => 'This is the description', |
|
282 | 282 | :priority_id => 5} |
|
283 | 283 | assert_response :success |
|
284 | 284 | assert_template 'new' |
|
285 | 285 | end |
|
286 | 286 | |
|
287 | 287 | def test_post_new |
|
288 | 288 | @request.session[:user_id] = 2 |
|
289 | 289 | post :new, :project_id => 1, |
|
290 | 290 | :issue => {:tracker_id => 3, |
|
291 | 291 | :subject => 'This is the test_new issue', |
|
292 | 292 | :description => 'This is the description', |
|
293 | 293 | :priority_id => 5, |
|
294 | 294 | :estimated_hours => '', |
|
295 | 295 | :custom_field_values => {'2' => 'Value for field 2'}} |
|
296 | 296 | assert_redirected_to 'issues/show' |
|
297 | 297 | |
|
298 | 298 | issue = Issue.find_by_subject('This is the test_new issue') |
|
299 | 299 | assert_not_nil issue |
|
300 | 300 | assert_equal 2, issue.author_id |
|
301 | 301 | assert_equal 3, issue.tracker_id |
|
302 | 302 | assert_nil issue.estimated_hours |
|
303 | 303 | v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2}) |
|
304 | 304 | assert_not_nil v |
|
305 | 305 | assert_equal 'Value for field 2', v.value |
|
306 | 306 | end |
|
307 | 307 | |
|
308 | 308 | def test_post_new_without_custom_fields_param |
|
309 | 309 | @request.session[:user_id] = 2 |
|
310 | 310 | post :new, :project_id => 1, |
|
311 | 311 | :issue => {:tracker_id => 1, |
|
312 | 312 | :subject => 'This is the test_new issue', |
|
313 | 313 | :description => 'This is the description', |
|
314 | 314 | :priority_id => 5} |
|
315 | 315 | assert_redirected_to 'issues/show' |
|
316 | 316 | end |
|
317 | 317 | |
|
318 | 318 | def test_post_new_with_required_custom_field_and_without_custom_fields_param |
|
319 | 319 | field = IssueCustomField.find_by_name('Database') |
|
320 | 320 | field.update_attribute(:is_required, true) |
|
321 | 321 | |
|
322 | 322 | @request.session[:user_id] = 2 |
|
323 | 323 | post :new, :project_id => 1, |
|
324 | 324 | :issue => {:tracker_id => 1, |
|
325 | 325 | :subject => 'This is the test_new issue', |
|
326 | 326 | :description => 'This is the description', |
|
327 | 327 | :priority_id => 5} |
|
328 | 328 | assert_response :success |
|
329 | 329 | assert_template 'new' |
|
330 | 330 | issue = assigns(:issue) |
|
331 | 331 | assert_not_nil issue |
|
332 | 332 | assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values) |
|
333 | 333 | end |
|
334 | 334 | |
|
335 | def test_post_new_with_watchers | |
|
336 | @request.session[:user_id] = 2 | |
|
337 | ActionMailer::Base.deliveries.clear | |
|
338 | ||
|
339 | assert_difference 'Watcher.count', 2 do | |
|
340 | post :new, :project_id => 1, | |
|
341 | :issue => {:tracker_id => 1, | |
|
342 | :subject => 'This is a new issue with watchers', | |
|
343 | :description => 'This is the description', | |
|
344 | :priority_id => 5, | |
|
345 | :watcher_user_ids => ['2', '3']} | |
|
346 | end | |
|
347 | assert_redirected_to 'issues/show' | |
|
348 | ||
|
349 | issue = Issue.find_by_subject('This is a new issue with watchers') | |
|
350 | # Watchers added | |
|
351 | assert_equal [2, 3], issue.watcher_user_ids.sort | |
|
352 | assert issue.watched_by?(User.find(3)) | |
|
353 | # Watchers notified | |
|
354 | mail = ActionMailer::Base.deliveries.last | |
|
355 | assert_kind_of TMail::Mail, mail | |
|
356 | assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail) | |
|
357 | end | |
|
358 | ||
|
335 | 359 | def test_post_should_preserve_fields_values_on_validation_failure |
|
336 | 360 | @request.session[:user_id] = 2 |
|
337 | 361 | post :new, :project_id => 1, |
|
338 | 362 | :issue => {:tracker_id => 1, |
|
339 | 363 | :subject => 'This is the test_new issue', |
|
340 | 364 | # empty description |
|
341 | 365 | :description => '', |
|
342 | 366 | :priority_id => 6, |
|
343 | 367 | :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}} |
|
344 | 368 | assert_response :success |
|
345 | 369 | assert_template 'new' |
|
346 | 370 | |
|
347 | 371 | assert_tag :input, :attributes => { :name => 'issue[subject]', |
|
348 | 372 | :value => 'This is the test_new issue' } |
|
349 | 373 | assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, |
|
350 | 374 | :child => { :tag => 'option', :attributes => { :selected => 'selected', |
|
351 | 375 | :value => '6' }, |
|
352 | 376 | :content => 'High' } |
|
353 | 377 | # Custom fields |
|
354 | 378 | assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' }, |
|
355 | 379 | :child => { :tag => 'option', :attributes => { :selected => 'selected', |
|
356 | 380 | :value => 'Oracle' }, |
|
357 | 381 | :content => 'Oracle' } |
|
358 | 382 | assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]', |
|
359 | 383 | :value => 'Value for field 2'} |
|
360 | 384 | end |
|
361 | 385 | |
|
362 | 386 | def test_copy_issue |
|
363 | 387 | @request.session[:user_id] = 2 |
|
364 | 388 | get :new, :project_id => 1, :copy_from => 1 |
|
365 | 389 | assert_template 'new' |
|
366 | 390 | assert_not_nil assigns(:issue) |
|
367 | 391 | orig = Issue.find(1) |
|
368 | 392 | assert_equal orig.subject, assigns(:issue).subject |
|
369 | 393 | end |
|
370 | 394 | |
|
371 | 395 | def test_get_edit |
|
372 | 396 | @request.session[:user_id] = 2 |
|
373 | 397 | get :edit, :id => 1 |
|
374 | 398 | assert_response :success |
|
375 | 399 | assert_template 'edit' |
|
376 | 400 | assert_not_nil assigns(:issue) |
|
377 | 401 | assert_equal Issue.find(1), assigns(:issue) |
|
378 | 402 | end |
|
379 | 403 | |
|
380 | 404 | def test_get_edit_with_params |
|
381 | 405 | @request.session[:user_id] = 2 |
|
382 | 406 | get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 } |
|
383 | 407 | assert_response :success |
|
384 | 408 | assert_template 'edit' |
|
385 | 409 | |
|
386 | 410 | issue = assigns(:issue) |
|
387 | 411 | assert_not_nil issue |
|
388 | 412 | |
|
389 | 413 | assert_equal 5, issue.status_id |
|
390 | 414 | assert_tag :select, :attributes => { :name => 'issue[status_id]' }, |
|
391 | 415 | :child => { :tag => 'option', |
|
392 | 416 | :content => 'Closed', |
|
393 | 417 | :attributes => { :selected => 'selected' } } |
|
394 | 418 | |
|
395 | 419 | assert_equal 7, issue.priority_id |
|
396 | 420 | assert_tag :select, :attributes => { :name => 'issue[priority_id]' }, |
|
397 | 421 | :child => { :tag => 'option', |
|
398 | 422 | :content => 'Urgent', |
|
399 | 423 | :attributes => { :selected => 'selected' } } |
|
400 | 424 | end |
|
401 | 425 | |
|
402 | 426 | def test_reply_to_issue |
|
403 | 427 | @request.session[:user_id] = 2 |
|
404 | 428 | get :reply, :id => 1 |
|
405 | 429 | assert_response :success |
|
406 | 430 | assert_select_rjs :show, "update" |
|
407 | 431 | end |
|
408 | 432 | |
|
409 | 433 | def test_reply_to_note |
|
410 | 434 | @request.session[:user_id] = 2 |
|
411 | 435 | get :reply, :id => 1, :journal_id => 2 |
|
412 | 436 | assert_response :success |
|
413 | 437 | assert_select_rjs :show, "update" |
|
414 | 438 | end |
|
415 | 439 | |
|
416 | 440 | def test_post_edit_without_custom_fields_param |
|
417 | 441 | @request.session[:user_id] = 2 |
|
418 | 442 | ActionMailer::Base.deliveries.clear |
|
419 | 443 | |
|
420 | 444 | issue = Issue.find(1) |
|
421 | 445 | assert_equal '125', issue.custom_value_for(2).value |
|
422 | 446 | old_subject = issue.subject |
|
423 | 447 | new_subject = 'Subject modified by IssuesControllerTest#test_post_edit' |
|
424 | 448 | |
|
425 | 449 | assert_difference('Journal.count') do |
|
426 | 450 | assert_difference('JournalDetail.count', 2) do |
|
427 | 451 | post :edit, :id => 1, :issue => {:subject => new_subject, |
|
428 | 452 | :priority_id => '6', |
|
429 | 453 | :category_id => '1' # no change |
|
430 | 454 | } |
|
431 | 455 | end |
|
432 | 456 | end |
|
433 | 457 | assert_redirected_to 'issues/show/1' |
|
434 | 458 | issue.reload |
|
435 | 459 | assert_equal new_subject, issue.subject |
|
436 | 460 | # Make sure custom fields were not cleared |
|
437 | 461 | assert_equal '125', issue.custom_value_for(2).value |
|
438 | 462 | |
|
439 | 463 | mail = ActionMailer::Base.deliveries.last |
|
440 | 464 | assert_kind_of TMail::Mail, mail |
|
441 | 465 | assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]") |
|
442 | 466 | assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}") |
|
443 | 467 | end |
|
444 | 468 | |
|
445 | 469 | def test_post_edit_with_custom_field_change |
|
446 | 470 | @request.session[:user_id] = 2 |
|
447 | 471 | issue = Issue.find(1) |
|
448 | 472 | assert_equal '125', issue.custom_value_for(2).value |
|
449 | 473 | |
|
450 | 474 | assert_difference('Journal.count') do |
|
451 | 475 | assert_difference('JournalDetail.count', 3) do |
|
452 | 476 | post :edit, :id => 1, :issue => {:subject => 'Custom field change', |
|
453 | 477 | :priority_id => '6', |
|
454 | 478 | :category_id => '1', # no change |
|
455 | 479 | :custom_field_values => { '2' => 'New custom value' } |
|
456 | 480 | } |
|
457 | 481 | end |
|
458 | 482 | end |
|
459 | 483 | assert_redirected_to 'issues/show/1' |
|
460 | 484 | issue.reload |
|
461 | 485 | assert_equal 'New custom value', issue.custom_value_for(2).value |
|
462 | 486 | |
|
463 | 487 | mail = ActionMailer::Base.deliveries.last |
|
464 | 488 | assert_kind_of TMail::Mail, mail |
|
465 | 489 | assert mail.body.include?("Searchable field changed from 125 to New custom value") |
|
466 | 490 | end |
|
467 | 491 | |
|
468 | 492 | def test_post_edit_with_status_and_assignee_change |
|
469 | 493 | issue = Issue.find(1) |
|
470 | 494 | assert_equal 1, issue.status_id |
|
471 | 495 | @request.session[:user_id] = 2 |
|
472 | 496 | assert_difference('TimeEntry.count', 0) do |
|
473 | 497 | post :edit, |
|
474 | 498 | :id => 1, |
|
475 | 499 | :issue => { :status_id => 2, :assigned_to_id => 3 }, |
|
476 | 500 | :notes => 'Assigned to dlopper', |
|
477 | 501 | :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first } |
|
478 | 502 | end |
|
479 | 503 | assert_redirected_to 'issues/show/1' |
|
480 | 504 | issue.reload |
|
481 | 505 | assert_equal 2, issue.status_id |
|
482 | 506 | j = issue.journals.find(:first, :order => 'id DESC') |
|
483 | 507 | assert_equal 'Assigned to dlopper', j.notes |
|
484 | 508 | assert_equal 2, j.details.size |
|
485 | 509 | |
|
486 | 510 | mail = ActionMailer::Base.deliveries.last |
|
487 | 511 | assert mail.body.include?("Status changed from New to Assigned") |
|
488 | 512 | end |
|
489 | 513 | |
|
490 | 514 | def test_post_edit_with_note_only |
|
491 | 515 | notes = 'Note added by IssuesControllerTest#test_update_with_note_only' |
|
492 | 516 | # anonymous user |
|
493 | 517 | post :edit, |
|
494 | 518 | :id => 1, |
|
495 | 519 | :notes => notes |
|
496 | 520 | assert_redirected_to 'issues/show/1' |
|
497 | 521 | j = Issue.find(1).journals.find(:first, :order => 'id DESC') |
|
498 | 522 | assert_equal notes, j.notes |
|
499 | 523 | assert_equal 0, j.details.size |
|
500 | 524 | assert_equal User.anonymous, j.user |
|
501 | 525 | |
|
502 | 526 | mail = ActionMailer::Base.deliveries.last |
|
503 | 527 | assert mail.body.include?(notes) |
|
504 | 528 | end |
|
505 | 529 | |
|
506 | 530 | def test_post_edit_with_note_and_spent_time |
|
507 | 531 | @request.session[:user_id] = 2 |
|
508 | 532 | spent_hours_before = Issue.find(1).spent_hours |
|
509 | 533 | assert_difference('TimeEntry.count') do |
|
510 | 534 | post :edit, |
|
511 | 535 | :id => 1, |
|
512 | 536 | :notes => '2.5 hours added', |
|
513 | 537 | :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first } |
|
514 | 538 | end |
|
515 | 539 | assert_redirected_to 'issues/show/1' |
|
516 | 540 | |
|
517 | 541 | issue = Issue.find(1) |
|
518 | 542 | |
|
519 | 543 | j = issue.journals.find(:first, :order => 'id DESC') |
|
520 | 544 | assert_equal '2.5 hours added', j.notes |
|
521 | 545 | assert_equal 0, j.details.size |
|
522 | 546 | |
|
523 | 547 | t = issue.time_entries.find(:first, :order => 'id DESC') |
|
524 | 548 | assert_not_nil t |
|
525 | 549 | assert_equal 2.5, t.hours |
|
526 | 550 | assert_equal spent_hours_before + 2.5, issue.spent_hours |
|
527 | 551 | end |
|
528 | 552 | |
|
529 | 553 | def test_post_edit_with_attachment_only |
|
530 | 554 | set_tmp_attachments_directory |
|
531 | 555 | |
|
532 | 556 | # Delete all fixtured journals, a race condition can occur causing the wrong |
|
533 | 557 | # journal to get fetched in the next find. |
|
534 | 558 | Journal.delete_all |
|
535 | 559 | |
|
536 | 560 | # anonymous user |
|
537 | 561 | post :edit, |
|
538 | 562 | :id => 1, |
|
539 | 563 | :notes => '', |
|
540 | 564 | :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}} |
|
541 | 565 | assert_redirected_to 'issues/show/1' |
|
542 | 566 | j = Issue.find(1).journals.find(:first, :order => 'id DESC') |
|
543 | 567 | assert j.notes.blank? |
|
544 | 568 | assert_equal 1, j.details.size |
|
545 | 569 | assert_equal 'testfile.txt', j.details.first.value |
|
546 | 570 | assert_equal User.anonymous, j.user |
|
547 | 571 | |
|
548 | 572 | mail = ActionMailer::Base.deliveries.last |
|
549 | 573 | assert mail.body.include?('testfile.txt') |
|
550 | 574 | end |
|
551 | 575 | |
|
552 | 576 | def test_post_edit_with_no_change |
|
553 | 577 | issue = Issue.find(1) |
|
554 | 578 | issue.journals.clear |
|
555 | 579 | ActionMailer::Base.deliveries.clear |
|
556 | 580 | |
|
557 | 581 | post :edit, |
|
558 | 582 | :id => 1, |
|
559 | 583 | :notes => '' |
|
560 | 584 | assert_redirected_to 'issues/show/1' |
|
561 | 585 | |
|
562 | 586 | issue.reload |
|
563 | 587 | assert issue.journals.empty? |
|
564 | 588 | # No email should be sent |
|
565 | 589 | assert ActionMailer::Base.deliveries.empty? |
|
566 | 590 | end |
|
567 | 591 | |
|
568 | 592 | def test_bulk_edit |
|
569 | 593 | @request.session[:user_id] = 2 |
|
570 | 594 | # update issues priority |
|
571 | 595 | post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => '' |
|
572 | 596 | assert_response 302 |
|
573 | 597 | # check that the issues were updated |
|
574 | 598 | assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id} |
|
575 | 599 | assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes |
|
576 | 600 | end |
|
577 | 601 | |
|
578 | 602 | def test_bulk_unassign |
|
579 | 603 | assert_not_nil Issue.find(2).assigned_to |
|
580 | 604 | @request.session[:user_id] = 2 |
|
581 | 605 | # unassign issues |
|
582 | 606 | post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none' |
|
583 | 607 | assert_response 302 |
|
584 | 608 | # check that the issues were updated |
|
585 | 609 | assert_nil Issue.find(2).assigned_to |
|
586 | 610 | end |
|
587 | 611 | |
|
588 | 612 | def test_move_one_issue_to_another_project |
|
589 | 613 | @request.session[:user_id] = 1 |
|
590 | 614 | post :move, :id => 1, :new_project_id => 2 |
|
591 | 615 | assert_redirected_to 'projects/ecookbook/issues' |
|
592 | 616 | assert_equal 2, Issue.find(1).project_id |
|
593 | 617 | end |
|
594 | 618 | |
|
595 | 619 | def test_bulk_move_to_another_project |
|
596 | 620 | @request.session[:user_id] = 1 |
|
597 | 621 | post :move, :ids => [1, 2], :new_project_id => 2 |
|
598 | 622 | assert_redirected_to 'projects/ecookbook/issues' |
|
599 | 623 | # Issues moved to project 2 |
|
600 | 624 | assert_equal 2, Issue.find(1).project_id |
|
601 | 625 | assert_equal 2, Issue.find(2).project_id |
|
602 | 626 | # No tracker change |
|
603 | 627 | assert_equal 1, Issue.find(1).tracker_id |
|
604 | 628 | assert_equal 2, Issue.find(2).tracker_id |
|
605 | 629 | end |
|
606 | 630 | |
|
607 | 631 | def test_bulk_move_to_another_tracker |
|
608 | 632 | @request.session[:user_id] = 1 |
|
609 | 633 | post :move, :ids => [1, 2], :new_tracker_id => 2 |
|
610 | 634 | assert_redirected_to 'projects/ecookbook/issues' |
|
611 | 635 | assert_equal 2, Issue.find(1).tracker_id |
|
612 | 636 | assert_equal 2, Issue.find(2).tracker_id |
|
613 | 637 | end |
|
614 | 638 | |
|
615 | 639 | def test_context_menu_one_issue |
|
616 | 640 | @request.session[:user_id] = 2 |
|
617 | 641 | get :context_menu, :ids => [1] |
|
618 | 642 | assert_response :success |
|
619 | 643 | assert_template 'context_menu' |
|
620 | 644 | assert_tag :tag => 'a', :content => 'Edit', |
|
621 | 645 | :attributes => { :href => '/issues/edit/1', |
|
622 | 646 | :class => 'icon-edit' } |
|
623 | 647 | assert_tag :tag => 'a', :content => 'Closed', |
|
624 | 648 | :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5', |
|
625 | 649 | :class => '' } |
|
626 | 650 | assert_tag :tag => 'a', :content => 'Immediate', |
|
627 | 651 | :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&priority_id=8', |
|
628 | 652 | :class => '' } |
|
629 | 653 | assert_tag :tag => 'a', :content => 'Dave Lopper', |
|
630 | 654 | :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&ids%5B%5D=1', |
|
631 | 655 | :class => '' } |
|
632 | 656 | assert_tag :tag => 'a', :content => 'Copy', |
|
633 | 657 | :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1', |
|
634 | 658 | :class => 'icon-copy' } |
|
635 | 659 | assert_tag :tag => 'a', :content => 'Move', |
|
636 | 660 | :attributes => { :href => '/issues/move?ids%5B%5D=1', |
|
637 | 661 | :class => 'icon-move' } |
|
638 | 662 | assert_tag :tag => 'a', :content => 'Delete', |
|
639 | 663 | :attributes => { :href => '/issues/destroy?ids%5B%5D=1', |
|
640 | 664 | :class => 'icon-del' } |
|
641 | 665 | end |
|
642 | 666 | |
|
643 | 667 | def test_context_menu_one_issue_by_anonymous |
|
644 | 668 | get :context_menu, :ids => [1] |
|
645 | 669 | assert_response :success |
|
646 | 670 | assert_template 'context_menu' |
|
647 | 671 | assert_tag :tag => 'a', :content => 'Delete', |
|
648 | 672 | :attributes => { :href => '#', |
|
649 | 673 | :class => 'icon-del disabled' } |
|
650 | 674 | end |
|
651 | 675 | |
|
652 | 676 | def test_context_menu_multiple_issues_of_same_project |
|
653 | 677 | @request.session[:user_id] = 2 |
|
654 | 678 | get :context_menu, :ids => [1, 2] |
|
655 | 679 | assert_response :success |
|
656 | 680 | assert_template 'context_menu' |
|
657 | 681 | assert_tag :tag => 'a', :content => 'Edit', |
|
658 | 682 | :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2', |
|
659 | 683 | :class => 'icon-edit' } |
|
660 | 684 | assert_tag :tag => 'a', :content => 'Immediate', |
|
661 | 685 | :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&ids%5B%5D=2&priority_id=8', |
|
662 | 686 | :class => '' } |
|
663 | 687 | assert_tag :tag => 'a', :content => 'Dave Lopper', |
|
664 | 688 | :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&ids%5B%5D=1&ids%5B%5D=2', |
|
665 | 689 | :class => '' } |
|
666 | 690 | assert_tag :tag => 'a', :content => 'Move', |
|
667 | 691 | :attributes => { :href => '/issues/move?ids%5B%5D=1&ids%5B%5D=2', |
|
668 | 692 | :class => 'icon-move' } |
|
669 | 693 | assert_tag :tag => 'a', :content => 'Delete', |
|
670 | 694 | :attributes => { :href => '/issues/destroy?ids%5B%5D=1&ids%5B%5D=2', |
|
671 | 695 | :class => 'icon-del' } |
|
672 | 696 | end |
|
673 | 697 | |
|
674 | 698 | def test_context_menu_multiple_issues_of_different_project |
|
675 | 699 | @request.session[:user_id] = 2 |
|
676 | 700 | get :context_menu, :ids => [1, 2, 4] |
|
677 | 701 | assert_response :success |
|
678 | 702 | assert_template 'context_menu' |
|
679 | 703 | assert_tag :tag => 'a', :content => 'Delete', |
|
680 | 704 | :attributes => { :href => '#', |
|
681 | 705 | :class => 'icon-del disabled' } |
|
682 | 706 | end |
|
683 | 707 | |
|
684 | 708 | def test_destroy_issue_with_no_time_entries |
|
685 | 709 | assert_nil TimeEntry.find_by_issue_id(2) |
|
686 | 710 | @request.session[:user_id] = 2 |
|
687 | 711 | post :destroy, :id => 2 |
|
688 | 712 | assert_redirected_to 'projects/ecookbook/issues' |
|
689 | 713 | assert_nil Issue.find_by_id(2) |
|
690 | 714 | end |
|
691 | 715 | |
|
692 | 716 | def test_destroy_issues_with_time_entries |
|
693 | 717 | @request.session[:user_id] = 2 |
|
694 | 718 | post :destroy, :ids => [1, 3] |
|
695 | 719 | assert_response :success |
|
696 | 720 | assert_template 'destroy' |
|
697 | 721 | assert_not_nil assigns(:hours) |
|
698 | 722 | assert Issue.find_by_id(1) && Issue.find_by_id(3) |
|
699 | 723 | end |
|
700 | 724 | |
|
701 | 725 | def test_destroy_issues_and_destroy_time_entries |
|
702 | 726 | @request.session[:user_id] = 2 |
|
703 | 727 | post :destroy, :ids => [1, 3], :todo => 'destroy' |
|
704 | 728 | assert_redirected_to 'projects/ecookbook/issues' |
|
705 | 729 | assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) |
|
706 | 730 | assert_nil TimeEntry.find_by_id([1, 2]) |
|
707 | 731 | end |
|
708 | 732 | |
|
709 | 733 | def test_destroy_issues_and_assign_time_entries_to_project |
|
710 | 734 | @request.session[:user_id] = 2 |
|
711 | 735 | post :destroy, :ids => [1, 3], :todo => 'nullify' |
|
712 | 736 | assert_redirected_to 'projects/ecookbook/issues' |
|
713 | 737 | assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) |
|
714 | 738 | assert_nil TimeEntry.find(1).issue_id |
|
715 | 739 | assert_nil TimeEntry.find(2).issue_id |
|
716 | 740 | end |
|
717 | 741 | |
|
718 | 742 | def test_destroy_issues_and_reassign_time_entries_to_another_issue |
|
719 | 743 | @request.session[:user_id] = 2 |
|
720 | 744 | post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2 |
|
721 | 745 | assert_redirected_to 'projects/ecookbook/issues' |
|
722 | 746 | assert !(Issue.find_by_id(1) || Issue.find_by_id(3)) |
|
723 | 747 | assert_equal 2, TimeEntry.find(1).issue_id |
|
724 | 748 | assert_equal 2, TimeEntry.find(2).issue_id |
|
725 | 749 | end |
|
726 | 750 | |
|
727 | 751 | def test_destroy_attachment |
|
728 | 752 | issue = Issue.find(3) |
|
729 | 753 | a = issue.attachments.size |
|
730 | 754 | @request.session[:user_id] = 2 |
|
731 | 755 | post :destroy_attachment, :id => 3, :attachment_id => 1 |
|
732 | 756 | assert_redirected_to 'issues/show/3' |
|
733 | 757 | assert_nil Attachment.find_by_id(1) |
|
734 | 758 | issue.reload |
|
735 | 759 | assert_equal((a-1), issue.attachments.size) |
|
736 | 760 | j = issue.journals.find(:first, :order => 'created_on DESC') |
|
737 | 761 | assert_equal 'attachment', j.details.first.property |
|
738 | 762 | end |
|
739 | 763 | end |
@@ -1,62 +1,68 | |||
|
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 RepositoryDarcsTest < Test::Unit::TestCase |
|
21 | 21 | fixtures :projects |
|
22 | 22 | |
|
23 | 23 | # No '..' in the repository path |
|
24 | 24 | REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository' |
|
25 | 25 | |
|
26 | 26 | def setup |
|
27 | 27 | @project = Project.find(1) |
|
28 | 28 | assert @repository = Repository::Darcs.create(:project => @project, :url => REPOSITORY_PATH) |
|
29 | 29 | end |
|
30 | 30 | |
|
31 | 31 | if File.directory?(REPOSITORY_PATH) |
|
32 | 32 | def test_fetch_changesets_from_scratch |
|
33 | 33 | @repository.fetch_changesets |
|
34 | 34 | @repository.reload |
|
35 | 35 | |
|
36 | 36 | assert_equal 6, @repository.changesets.count |
|
37 | 37 | assert_equal 13, @repository.changes.count |
|
38 | 38 | assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments |
|
39 | 39 | end |
|
40 | 40 | |
|
41 | 41 | def test_fetch_changesets_incremental |
|
42 | 42 | @repository.fetch_changesets |
|
43 | 43 | # Remove changesets with revision > 3 |
|
44 | 44 | @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3} |
|
45 | 45 | @repository.reload |
|
46 | 46 | assert_equal 3, @repository.changesets.count |
|
47 | 47 | |
|
48 | 48 | @repository.fetch_changesets |
|
49 | 49 | assert_equal 6, @repository.changesets.count |
|
50 | 50 | end |
|
51 | 51 | |
|
52 | def test_deleted_files_should_not_be_listed | |
|
53 | entries = @repository.entries('sources') | |
|
54 | assert entries.detect {|e| e.name == 'watchers_controller.rb'} | |
|
55 | assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'} | |
|
56 | end | |
|
57 | ||
|
52 | 58 | def test_cat |
|
53 | 59 | @repository.fetch_changesets |
|
54 | 60 | cat = @repository.cat("sources/welcome_controller.rb", 2) |
|
55 | 61 | assert_not_nil cat |
|
56 | 62 | assert cat.include?('class WelcomeController < ApplicationController') |
|
57 | 63 | end |
|
58 | 64 | else |
|
59 | 65 | puts "Darcs test repository NOT FOUND. Skipping unit tests !!!" |
|
60 | 66 | def test_fake; assert true end |
|
61 | 67 | end |
|
62 | 68 | end |
@@ -1,69 +1,71 | |||
|
1 | 1 | # ActsAsWatchable |
|
2 | 2 | module Redmine |
|
3 | 3 | module Acts |
|
4 | 4 | module Watchable |
|
5 | 5 | def self.included(base) |
|
6 | 6 | base.extend ClassMethods |
|
7 | 7 | end |
|
8 | 8 | |
|
9 | 9 | module ClassMethods |
|
10 | 10 | def acts_as_watchable(options = {}) |
|
11 | 11 | return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods) |
|
12 | 12 | send :include, Redmine::Acts::Watchable::InstanceMethods |
|
13 | 13 | |
|
14 | 14 | class_eval do |
|
15 | 15 | has_many :watchers, :as => :watchable, :dependent => :delete_all |
|
16 | 16 | has_many :watcher_users, :through => :watchers, :source => :user |
|
17 | ||
|
18 | attr_protected :watcher_ids, :watcher_user_ids | |
|
17 | 19 | end |
|
18 | 20 | end |
|
19 | 21 | end |
|
20 | 22 | |
|
21 | 23 | module InstanceMethods |
|
22 | 24 | def self.included(base) |
|
23 | 25 | base.extend ClassMethods |
|
24 | 26 | end |
|
25 | 27 | |
|
26 | 28 | # Returns an array of users that are proposed as watchers |
|
27 | 29 | def addable_watcher_users |
|
28 | 30 | self.project.users.sort - self.watcher_users |
|
29 | 31 | end |
|
30 | 32 | |
|
31 | 33 | # Adds user as a watcher |
|
32 | 34 | def add_watcher(user) |
|
33 | 35 | self.watchers << Watcher.new(:user => user) |
|
34 | 36 | end |
|
35 | 37 | |
|
36 | 38 | # Removes user from the watchers list |
|
37 | 39 | def remove_watcher(user) |
|
38 | 40 | return nil unless user && user.is_a?(User) |
|
39 | 41 | Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}" |
|
40 | 42 | end |
|
41 | 43 | |
|
42 | 44 | # Adds/removes watcher |
|
43 | 45 | def set_watcher(user, watching=true) |
|
44 | 46 | watching ? add_watcher(user) : remove_watcher(user) |
|
45 | 47 | end |
|
46 | 48 | |
|
47 | 49 | # Returns if object is watched by user |
|
48 | 50 | def watched_by?(user) |
|
49 | 51 | !self.watchers.find(:first, |
|
50 | 52 | :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil? |
|
51 | 53 | end |
|
52 | 54 | |
|
53 | 55 | # Returns an array of watchers' email addresses |
|
54 | 56 | def watcher_recipients |
|
55 | 57 | self.watchers.collect { |w| w.user.mail if w.user.active? }.compact |
|
56 | 58 | end |
|
57 | 59 | |
|
58 | 60 | module ClassMethods |
|
59 | 61 | # Returns the objects that are watched by user |
|
60 | 62 | def watched_by(user) |
|
61 | 63 | find(:all, |
|
62 | 64 | :include => :watchers, |
|
63 | 65 | :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]) |
|
64 | 66 | end |
|
65 | 67 | end |
|
66 | 68 | end |
|
67 | 69 | end |
|
68 | 70 | end |
|
69 | 71 | end |
General Comments 0
You need to be logged in to leave comments.
Login now