##// END OF EJS Templates
Version sharing (#465) + optional inclusion of subprojects in the roadmap view (#2666)....
Jean-Philippe Lang -
r3009:5f8e9d711820
parent child
Show More
@@ -0,0 +1,10
1 class AddVersionsSharing < ActiveRecord::Migration
2 def self.up
3 add_column :versions, :sharing, :string, :default => 'none', :null => false
4 add_index :versions, :sharing
5 end
6
7 def self.down
8 remove_column :versions, :sharing
9 end
10 end
@@ -0,0 +1,63
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../../test_helper'
19
20 class ProjectsHelperTest < HelperTestCase
21 include ApplicationHelper
22 include ProjectsHelper
23
24 fixtures :all
25
26 def setup
27 super
28 set_language_if_valid('en')
29 User.current = nil
30 end
31
32 def test_link_to_version_within_project
33 @project = Project.find(2)
34 User.current = User.find(1)
35 assert_equal '<a href="/versions/show/5">Alpha</a>', link_to_version(Version.find(5))
36 end
37
38 def test_link_to_version
39 User.current = User.find(1)
40 assert_equal '<a href="/versions/show/5">OnlineStore - Alpha</a>', link_to_version(Version.find(5))
41 end
42
43 def test_link_to_private_version
44 assert_equal 'OnlineStore - Alpha', link_to_version(Version.find(5))
45 end
46
47 def test_link_to_version_invalid_version
48 assert_equal '', link_to_version(Object)
49 end
50
51 def test_format_version_name_within_project
52 @project = Project.find(1)
53 assert_equal "0.1", format_version_name(Version.find(1))
54 end
55
56 def test_format_version_name
57 assert_equal "eCookbook - 0.1", format_version_name(Version.find(1))
58 end
59
60 def test_format_version_name_for_system_version
61 assert_equal "OnlineStore - Systemwide visible version", format_version_name(Version.find(7))
62 end
63 end
@@ -1,537 +1,537
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20 default_search_scope :issues
20 default_search_scope :issues
21
21
22 before_filter :find_issue, :only => [:show, :edit, :reply]
22 before_filter :find_issue, :only => [:show, :edit, :reply]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :find_project, :only => [:new, :update_form, :preview]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
25 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
27 accept_key_auth :index, :show, :changes
27 accept_key_auth :index, :show, :changes
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 include ProjectsHelper
33 include ProjectsHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :issue_relations
36 helper :issue_relations
37 include IssueRelationsHelper
37 include IssueRelationsHelper
38 helper :watchers
38 helper :watchers
39 include WatchersHelper
39 include WatchersHelper
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :queries
42 helper :queries
43 helper :sort
43 helper :sort
44 include SortHelper
44 include SortHelper
45 include IssuesHelper
45 include IssuesHelper
46 helper :timelog
46 helper :timelog
47 include Redmine::Export::PDF
47 include Redmine::Export::PDF
48
48
49 verify :method => :post,
49 verify :method => :post,
50 :only => :destroy,
50 :only => :destroy,
51 :render => { :nothing => true, :status => :method_not_allowed }
51 :render => { :nothing => true, :status => :method_not_allowed }
52
52
53 def index
53 def index
54 retrieve_query
54 retrieve_query
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
55 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
56 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
56 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
57
57
58 if @query.valid?
58 if @query.valid?
59 limit = per_page_option
59 limit = per_page_option
60 respond_to do |format|
60 respond_to do |format|
61 format.html { }
61 format.html { }
62 format.atom { limit = Setting.feeds_limit.to_i }
62 format.atom { limit = Setting.feeds_limit.to_i }
63 format.csv { limit = Setting.issues_export_limit.to_i }
63 format.csv { limit = Setting.issues_export_limit.to_i }
64 format.pdf { limit = Setting.issues_export_limit.to_i }
64 format.pdf { limit = Setting.issues_export_limit.to_i }
65 end
65 end
66
66
67 @issue_count = @query.issue_count
67 @issue_count = @query.issue_count
68 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
68 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 :order => sort_clause,
70 :order => sort_clause,
71 :offset => @issue_pages.current.offset,
71 :offset => @issue_pages.current.offset,
72 :limit => limit)
72 :limit => limit)
73 @issue_count_by_group = @query.issue_count_by_group
73 @issue_count_by_group = @query.issue_count_by_group
74
74
75 respond_to do |format|
75 respond_to do |format|
76 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
76 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
77 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
77 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
78 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
78 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
79 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
79 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
80 end
80 end
81 else
81 else
82 # Send html if the query is not valid
82 # Send html if the query is not valid
83 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
83 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
84 end
84 end
85 rescue ActiveRecord::RecordNotFound
85 rescue ActiveRecord::RecordNotFound
86 render_404
86 render_404
87 end
87 end
88
88
89 def changes
89 def changes
90 retrieve_query
90 retrieve_query
91 sort_init 'id', 'desc'
91 sort_init 'id', 'desc'
92 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
92 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.available_columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
93
93
94 if @query.valid?
94 if @query.valid?
95 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
95 @journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
96 :limit => 25)
96 :limit => 25)
97 end
97 end
98 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
98 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
99 render :layout => false, :content_type => 'application/atom+xml'
99 render :layout => false, :content_type => 'application/atom+xml'
100 rescue ActiveRecord::RecordNotFound
100 rescue ActiveRecord::RecordNotFound
101 render_404
101 render_404
102 end
102 end
103
103
104 def show
104 def show
105 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
105 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
106 @journals.each_with_index {|j,i| j.indice = i+1}
106 @journals.each_with_index {|j,i| j.indice = i+1}
107 @journals.reverse! if User.current.wants_comments_in_reverse_order?
107 @journals.reverse! if User.current.wants_comments_in_reverse_order?
108 @changesets = @issue.changesets
108 @changesets = @issue.changesets
109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
109 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
111 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
112 @priorities = IssuePriority.all
112 @priorities = IssuePriority.all
113 @time_entry = TimeEntry.new
113 @time_entry = TimeEntry.new
114 respond_to do |format|
114 respond_to do |format|
115 format.html { render :template => 'issues/show.rhtml' }
115 format.html { render :template => 'issues/show.rhtml' }
116 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
116 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
117 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
118 end
118 end
119 end
119 end
120
120
121 # Add a new issue
121 # Add a new issue
122 # The new issue will be created from an existing one if copy_from parameter is given
122 # The new issue will be created from an existing one if copy_from parameter is given
123 def new
123 def new
124 @issue = Issue.new
124 @issue = Issue.new
125 @issue.copy_from(params[:copy_from]) if params[:copy_from]
125 @issue.copy_from(params[:copy_from]) if params[:copy_from]
126 @issue.project = @project
126 @issue.project = @project
127 # Tracker must be set before custom field values
127 # Tracker must be set before custom field values
128 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
128 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
129 if @issue.tracker.nil?
129 if @issue.tracker.nil?
130 render_error l(:error_no_tracker_in_project)
130 render_error l(:error_no_tracker_in_project)
131 return
131 return
132 end
132 end
133 if params[:issue].is_a?(Hash)
133 if params[:issue].is_a?(Hash)
134 @issue.attributes = params[:issue]
134 @issue.attributes = params[:issue]
135 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
135 @issue.watcher_user_ids = params[:issue]['watcher_user_ids'] if User.current.allowed_to?(:add_issue_watchers, @project)
136 end
136 end
137 @issue.author = User.current
137 @issue.author = User.current
138
138
139 default_status = IssueStatus.default
139 default_status = IssueStatus.default
140 unless default_status
140 unless default_status
141 render_error l(:error_no_default_issue_status)
141 render_error l(:error_no_default_issue_status)
142 return
142 return
143 end
143 end
144 @issue.status = default_status
144 @issue.status = default_status
145 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
145 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
146
146
147 if request.get? || request.xhr?
147 if request.get? || request.xhr?
148 @issue.start_date ||= Date.today
148 @issue.start_date ||= Date.today
149 else
149 else
150 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
150 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
151 # Check that the user is allowed to apply the requested status
151 # Check that the user is allowed to apply the requested status
152 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
152 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
153 if @issue.save
153 if @issue.save
154 attach_files(@issue, params[:attachments])
154 attach_files(@issue, params[:attachments])
155 flash[:notice] = l(:notice_successful_create)
155 flash[:notice] = l(:notice_successful_create)
156 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
156 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
157 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
157 redirect_to(params[:continue] ? { :action => 'new', :tracker_id => @issue.tracker } :
158 { :action => 'show', :id => @issue })
158 { :action => 'show', :id => @issue })
159 return
159 return
160 end
160 end
161 end
161 end
162 @priorities = IssuePriority.all
162 @priorities = IssuePriority.all
163 render :layout => !request.xhr?
163 render :layout => !request.xhr?
164 end
164 end
165
165
166 # Attributes that can be updated on workflow transition (without :edit permission)
166 # Attributes that can be updated on workflow transition (without :edit permission)
167 # TODO: make it configurable (at least per role)
167 # TODO: make it configurable (at least per role)
168 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
168 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
169
169
170 def edit
170 def edit
171 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
171 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
172 @priorities = IssuePriority.all
172 @priorities = IssuePriority.all
173 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
173 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
174 @time_entry = TimeEntry.new
174 @time_entry = TimeEntry.new
175
175
176 @notes = params[:notes]
176 @notes = params[:notes]
177 journal = @issue.init_journal(User.current, @notes)
177 journal = @issue.init_journal(User.current, @notes)
178 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
178 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
179 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
179 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
180 attrs = params[:issue].dup
180 attrs = params[:issue].dup
181 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
181 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
182 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
182 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
183 @issue.attributes = attrs
183 @issue.attributes = attrs
184 end
184 end
185
185
186 if request.post?
186 if request.post?
187 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
187 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
188 @time_entry.attributes = params[:time_entry]
188 @time_entry.attributes = params[:time_entry]
189 attachments = attach_files(@issue, params[:attachments])
189 attachments = attach_files(@issue, params[:attachments])
190 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
190 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
191
191
192 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
192 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
193
193
194 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
194 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
195 # Log spend time
195 # Log spend time
196 if User.current.allowed_to?(:log_time, @project)
196 if User.current.allowed_to?(:log_time, @project)
197 @time_entry.save
197 @time_entry.save
198 end
198 end
199 if !journal.new_record?
199 if !journal.new_record?
200 # Only send notification if something was actually changed
200 # Only send notification if something was actually changed
201 flash[:notice] = l(:notice_successful_update)
201 flash[:notice] = l(:notice_successful_update)
202 end
202 end
203 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
203 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
204 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
204 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
205 end
205 end
206 end
206 end
207 rescue ActiveRecord::StaleObjectError
207 rescue ActiveRecord::StaleObjectError
208 # Optimistic locking exception
208 # Optimistic locking exception
209 flash.now[:error] = l(:notice_locking_conflict)
209 flash.now[:error] = l(:notice_locking_conflict)
210 # Remove the previously added attachments if issue was not updated
210 # Remove the previously added attachments if issue was not updated
211 attachments.each(&:destroy)
211 attachments.each(&:destroy)
212 end
212 end
213
213
214 def reply
214 def reply
215 journal = Journal.find(params[:journal_id]) if params[:journal_id]
215 journal = Journal.find(params[:journal_id]) if params[:journal_id]
216 if journal
216 if journal
217 user = journal.user
217 user = journal.user
218 text = journal.notes
218 text = journal.notes
219 else
219 else
220 user = @issue.author
220 user = @issue.author
221 text = @issue.description
221 text = @issue.description
222 end
222 end
223 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
223 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
224 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
224 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
225 render(:update) { |page|
225 render(:update) { |page|
226 page.<< "$('notes').value = \"#{content}\";"
226 page.<< "$('notes').value = \"#{content}\";"
227 page.show 'update'
227 page.show 'update'
228 page << "Form.Element.focus('notes');"
228 page << "Form.Element.focus('notes');"
229 page << "Element.scrollTo('update');"
229 page << "Element.scrollTo('update');"
230 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
230 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
231 }
231 }
232 end
232 end
233
233
234 # Bulk edit a set of issues
234 # Bulk edit a set of issues
235 def bulk_edit
235 def bulk_edit
236 if request.post?
236 if request.post?
237 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
237 tracker = params[:tracker_id].blank? ? nil : @project.trackers.find_by_id(params[:tracker_id])
238 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
238 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
239 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
239 priority = params[:priority_id].blank? ? nil : IssuePriority.find_by_id(params[:priority_id])
240 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
240 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
241 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
241 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
242 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
242 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.shared_versions.find_by_id(params[:fixed_version_id])
243 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
243 custom_field_values = params[:custom_field_values] ? params[:custom_field_values].reject {|k,v| v.blank?} : nil
244
244
245 unsaved_issue_ids = []
245 unsaved_issue_ids = []
246 @issues.each do |issue|
246 @issues.each do |issue|
247 journal = issue.init_journal(User.current, params[:notes])
247 journal = issue.init_journal(User.current, params[:notes])
248 issue.tracker = tracker if tracker
248 issue.tracker = tracker if tracker
249 issue.priority = priority if priority
249 issue.priority = priority if priority
250 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
250 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
251 issue.category = category if category || params[:category_id] == 'none'
251 issue.category = category if category || params[:category_id] == 'none'
252 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
252 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
253 issue.start_date = params[:start_date] unless params[:start_date].blank?
253 issue.start_date = params[:start_date] unless params[:start_date].blank?
254 issue.due_date = params[:due_date] unless params[:due_date].blank?
254 issue.due_date = params[:due_date] unless params[:due_date].blank?
255 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
255 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
256 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
256 issue.custom_field_values = custom_field_values if custom_field_values && !custom_field_values.empty?
257 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
257 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
258 # Don't save any change to the issue if the user is not authorized to apply the requested status
258 # Don't save any change to the issue if the user is not authorized to apply the requested status
259 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
259 unless (status.nil? || (issue.new_statuses_allowed_to(User.current).include?(status) && issue.status = status)) && issue.save
260 # Keep unsaved issue ids to display them in flash error
260 # Keep unsaved issue ids to display them in flash error
261 unsaved_issue_ids << issue.id
261 unsaved_issue_ids << issue.id
262 end
262 end
263 end
263 end
264 if unsaved_issue_ids.empty?
264 if unsaved_issue_ids.empty?
265 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
265 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
266 else
266 else
267 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
267 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
268 :total => @issues.size,
268 :total => @issues.size,
269 :ids => '#' + unsaved_issue_ids.join(', #'))
269 :ids => '#' + unsaved_issue_ids.join(', #'))
270 end
270 end
271 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
271 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
272 return
272 return
273 end
273 end
274 @available_statuses = Workflow.available_statuses(@project)
274 @available_statuses = Workflow.available_statuses(@project)
275 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
275 @custom_fields = @project.issue_custom_fields.select {|f| f.field_format == 'list'}
276 end
276 end
277
277
278 def move
278 def move
279 @copy = params[:copy_options] && params[:copy_options][:copy]
279 @copy = params[:copy_options] && params[:copy_options][:copy]
280 @allowed_projects = []
280 @allowed_projects = []
281 # find projects to which the user is allowed to move the issue
281 # find projects to which the user is allowed to move the issue
282 if User.current.admin?
282 if User.current.admin?
283 # admin is allowed to move issues to any active (visible) project
283 # admin is allowed to move issues to any active (visible) project
284 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
284 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current))
285 else
285 else
286 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
286 User.current.memberships.each {|m| @allowed_projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
287 end
287 end
288 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
288 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
289 @target_project ||= @project
289 @target_project ||= @project
290 @trackers = @target_project.trackers
290 @trackers = @target_project.trackers
291 @available_statuses = Workflow.available_statuses(@project)
291 @available_statuses = Workflow.available_statuses(@project)
292 if request.post?
292 if request.post?
293 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
293 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
294 unsaved_issue_ids = []
294 unsaved_issue_ids = []
295 moved_issues = []
295 moved_issues = []
296 @issues.each do |issue|
296 @issues.each do |issue|
297 changed_attributes = {}
297 changed_attributes = {}
298 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
298 [:assigned_to_id, :status_id, :start_date, :due_date].each do |valid_attribute|
299 changed_attributes[valid_attribute] = params[valid_attribute] if params[valid_attribute]
299 changed_attributes[valid_attribute] = params[valid_attribute] if params[valid_attribute]
300 end
300 end
301 issue.init_journal(User.current)
301 issue.init_journal(User.current)
302 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
302 if r = issue.move_to(@target_project, new_tracker, {:copy => @copy, :attributes => changed_attributes})
303 moved_issues << r
303 moved_issues << r
304 else
304 else
305 unsaved_issue_ids << issue.id
305 unsaved_issue_ids << issue.id
306 end
306 end
307 end
307 end
308 if unsaved_issue_ids.empty?
308 if unsaved_issue_ids.empty?
309 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
309 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
310 else
310 else
311 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
311 flash[:error] = l(:notice_failed_to_save_issues, :count => unsaved_issue_ids.size,
312 :total => @issues.size,
312 :total => @issues.size,
313 :ids => '#' + unsaved_issue_ids.join(', #'))
313 :ids => '#' + unsaved_issue_ids.join(', #'))
314 end
314 end
315 if params[:follow]
315 if params[:follow]
316 if @issues.size == 1 && moved_issues.size == 1
316 if @issues.size == 1 && moved_issues.size == 1
317 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
317 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
318 else
318 else
319 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
319 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
320 end
320 end
321 else
321 else
322 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
322 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
323 end
323 end
324 return
324 return
325 end
325 end
326 render :layout => false if request.xhr?
326 render :layout => false if request.xhr?
327 end
327 end
328
328
329 def destroy
329 def destroy
330 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
330 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
331 if @hours > 0
331 if @hours > 0
332 case params[:todo]
332 case params[:todo]
333 when 'destroy'
333 when 'destroy'
334 # nothing to do
334 # nothing to do
335 when 'nullify'
335 when 'nullify'
336 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
336 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
337 when 'reassign'
337 when 'reassign'
338 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
338 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
339 if reassign_to.nil?
339 if reassign_to.nil?
340 flash.now[:error] = l(:error_issue_not_found_in_project)
340 flash.now[:error] = l(:error_issue_not_found_in_project)
341 return
341 return
342 else
342 else
343 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
343 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
344 end
344 end
345 else
345 else
346 # display the destroy form
346 # display the destroy form
347 return
347 return
348 end
348 end
349 end
349 end
350 @issues.each(&:destroy)
350 @issues.each(&:destroy)
351 redirect_to :action => 'index', :project_id => @project
351 redirect_to :action => 'index', :project_id => @project
352 end
352 end
353
353
354 def gantt
354 def gantt
355 @gantt = Redmine::Helpers::Gantt.new(params)
355 @gantt = Redmine::Helpers::Gantt.new(params)
356 retrieve_query
356 retrieve_query
357 if @query.valid?
357 if @query.valid?
358 events = []
358 events = []
359 # Issues that have start and due dates
359 # Issues that have start and due dates
360 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
360 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
361 :order => "start_date, due_date",
361 :order => "start_date, due_date",
362 :conditions => ["(((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]
362 :conditions => ["(((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]
363 )
363 )
364 # Issues that don't have a due date but that are assigned to a version with a date
364 # Issues that don't have a due date but that are assigned to a version with a date
365 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
365 events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
366 :order => "start_date, effective_date",
366 :order => "start_date, effective_date",
367 :conditions => ["(((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]
367 :conditions => ["(((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]
368 )
368 )
369 # Versions
369 # Versions
370 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
370 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
371
371
372 @gantt.events = events
372 @gantt.events = events
373 end
373 end
374
374
375 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
375 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
376
376
377 respond_to do |format|
377 respond_to do |format|
378 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
378 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
379 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
379 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
380 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
380 format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
381 end
381 end
382 end
382 end
383
383
384 def calendar
384 def calendar
385 if params[:year] and params[:year].to_i > 1900
385 if params[:year] and params[:year].to_i > 1900
386 @year = params[:year].to_i
386 @year = params[:year].to_i
387 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
387 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
388 @month = params[:month].to_i
388 @month = params[:month].to_i
389 end
389 end
390 end
390 end
391 @year ||= Date.today.year
391 @year ||= Date.today.year
392 @month ||= Date.today.month
392 @month ||= Date.today.month
393
393
394 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
394 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
395 retrieve_query
395 retrieve_query
396 if @query.valid?
396 if @query.valid?
397 events = []
397 events = []
398 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
398 events += @query.issues(:include => [:tracker, :assigned_to, :priority],
399 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
399 :conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
400 )
400 )
401 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
401 events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
402
402
403 @calendar.events = events
403 @calendar.events = events
404 end
404 end
405
405
406 render :layout => false if request.xhr?
406 render :layout => false if request.xhr?
407 end
407 end
408
408
409 def context_menu
409 def context_menu
410 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
410 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
411 if (@issues.size == 1)
411 if (@issues.size == 1)
412 @issue = @issues.first
412 @issue = @issues.first
413 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
413 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
414 end
414 end
415 projects = @issues.collect(&:project).compact.uniq
415 projects = @issues.collect(&:project).compact.uniq
416 @project = projects.first if projects.size == 1
416 @project = projects.first if projects.size == 1
417
417
418 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
418 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
419 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
419 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
420 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
420 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
421 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
421 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
422 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
422 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
423 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
423 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
424 }
424 }
425 if @project
425 if @project
426 @assignables = @project.assignable_users
426 @assignables = @project.assignable_users
427 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
427 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
428 @trackers = @project.trackers
428 @trackers = @project.trackers
429 end
429 end
430
430
431 @priorities = IssuePriority.all.reverse
431 @priorities = IssuePriority.all.reverse
432 @statuses = IssueStatus.find(:all, :order => 'position')
432 @statuses = IssueStatus.find(:all, :order => 'position')
433 @back = params[:back_url] || request.env['HTTP_REFERER']
433 @back = params[:back_url] || request.env['HTTP_REFERER']
434
434
435 render :layout => false
435 render :layout => false
436 end
436 end
437
437
438 def update_form
438 def update_form
439 if params[:id]
439 if params[:id]
440 @issue = @project.issues.visible.find(params[:id])
440 @issue = @project.issues.visible.find(params[:id])
441 else
441 else
442 @issue = Issue.new
442 @issue = Issue.new
443 @issue.project = @project
443 @issue.project = @project
444 end
444 end
445 @issue.attributes = params[:issue]
445 @issue.attributes = params[:issue]
446 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
446 @allowed_statuses = ([@issue.status] + @issue.status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
447 @priorities = IssuePriority.all
447 @priorities = IssuePriority.all
448
448
449 render :partial => 'attributes'
449 render :partial => 'attributes'
450 end
450 end
451
451
452 def preview
452 def preview
453 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
453 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
454 @attachements = @issue.attachments if @issue
454 @attachements = @issue.attachments if @issue
455 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
455 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
456 render :partial => 'common/preview'
456 render :partial => 'common/preview'
457 end
457 end
458
458
459 private
459 private
460 def find_issue
460 def find_issue
461 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
461 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
462 @project = @issue.project
462 @project = @issue.project
463 rescue ActiveRecord::RecordNotFound
463 rescue ActiveRecord::RecordNotFound
464 render_404
464 render_404
465 end
465 end
466
466
467 # Filter for bulk operations
467 # Filter for bulk operations
468 def find_issues
468 def find_issues
469 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
469 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
470 raise ActiveRecord::RecordNotFound if @issues.empty?
470 raise ActiveRecord::RecordNotFound if @issues.empty?
471 projects = @issues.collect(&:project).compact.uniq
471 projects = @issues.collect(&:project).compact.uniq
472 if projects.size == 1
472 if projects.size == 1
473 @project = projects.first
473 @project = projects.first
474 else
474 else
475 # TODO: let users bulk edit/move/destroy issues from different projects
475 # TODO: let users bulk edit/move/destroy issues from different projects
476 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
476 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
477 end
477 end
478 rescue ActiveRecord::RecordNotFound
478 rescue ActiveRecord::RecordNotFound
479 render_404
479 render_404
480 end
480 end
481
481
482 def find_project
482 def find_project
483 @project = Project.find(params[:project_id])
483 @project = Project.find(params[:project_id])
484 rescue ActiveRecord::RecordNotFound
484 rescue ActiveRecord::RecordNotFound
485 render_404
485 render_404
486 end
486 end
487
487
488 def find_optional_project
488 def find_optional_project
489 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
489 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
490 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
490 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
491 allowed ? true : deny_access
491 allowed ? true : deny_access
492 rescue ActiveRecord::RecordNotFound
492 rescue ActiveRecord::RecordNotFound
493 render_404
493 render_404
494 end
494 end
495
495
496 # Retrieve query from session or build a new query
496 # Retrieve query from session or build a new query
497 def retrieve_query
497 def retrieve_query
498 if !params[:query_id].blank?
498 if !params[:query_id].blank?
499 cond = "project_id IS NULL"
499 cond = "project_id IS NULL"
500 cond << " OR project_id = #{@project.id}" if @project
500 cond << " OR project_id = #{@project.id}" if @project
501 @query = Query.find(params[:query_id], :conditions => cond)
501 @query = Query.find(params[:query_id], :conditions => cond)
502 @query.project = @project
502 @query.project = @project
503 session[:query] = {:id => @query.id, :project_id => @query.project_id}
503 session[:query] = {:id => @query.id, :project_id => @query.project_id}
504 sort_clear
504 sort_clear
505 else
505 else
506 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
506 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
507 # Give it a name, required to be valid
507 # Give it a name, required to be valid
508 @query = Query.new(:name => "_")
508 @query = Query.new(:name => "_")
509 @query.project = @project
509 @query.project = @project
510 if params[:fields] and params[:fields].is_a? Array
510 if params[:fields] and params[:fields].is_a? Array
511 params[:fields].each do |field|
511 params[:fields].each do |field|
512 @query.add_filter(field, params[:operators][field], params[:values][field])
512 @query.add_filter(field, params[:operators][field], params[:values][field])
513 end
513 end
514 else
514 else
515 @query.available_filters.keys.each do |field|
515 @query.available_filters.keys.each do |field|
516 @query.add_short_filter(field, params[field]) if params[field]
516 @query.add_short_filter(field, params[field]) if params[field]
517 end
517 end
518 end
518 end
519 @query.group_by = params[:group_by]
519 @query.group_by = params[:group_by]
520 @query.column_names = params[:query] && params[:query][:column_names]
520 @query.column_names = params[:query] && params[:query][:column_names]
521 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
521 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
522 else
522 else
523 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
523 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
524 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
524 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
525 @query.project = @project
525 @query.project = @project
526 end
526 end
527 end
527 end
528 end
528 end
529
529
530 # Rescues an invalid query statement. Just in case...
530 # Rescues an invalid query statement. Just in case...
531 def query_statement_invalid(exception)
531 def query_statement_invalid(exception)
532 logger.error "Query::StatementInvalid: #{exception.message}" if logger
532 logger.error "Query::StatementInvalid: #{exception.message}" if logger
533 session.delete(:query)
533 session.delete(:query)
534 sort_clear
534 sort_clear
535 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
535 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
536 end
536 end
537 end
537 end
@@ -1,359 +1,406
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24 menu_item :issues, :only => [:changelog]
24 menu_item :issues, :only => [:changelog]
25
25
26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
26 before_filter :find_project, :except => [ :index, :list, :add, :copy, :activity ]
27 before_filter :find_optional_project, :only => :activity
27 before_filter :find_optional_project, :only => :activity
28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize, :except => [ :index, :list, :add, :copy, :archive, :unarchive, :destroy, :activity ]
29 before_filter :authorize_global, :only => :add
29 before_filter :authorize_global, :only => :add
30 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
30 before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
31 accept_key_auth :activity
31 accept_key_auth :activity
32
32
33 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
33 after_filter :only => [:add, :edit, :archive, :unarchive, :destroy] do |controller|
34 if controller.request.post?
34 if controller.request.post?
35 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
35 controller.send :expire_action, :controller => 'welcome', :action => 'robots.txt'
36 end
36 end
37 end
37 end
38
38
39 helper :sort
39 helper :sort
40 include SortHelper
40 include SortHelper
41 helper :custom_fields
41 helper :custom_fields
42 include CustomFieldsHelper
42 include CustomFieldsHelper
43 helper :issues
43 helper :issues
44 helper IssuesHelper
44 helper IssuesHelper
45 helper :queries
45 helper :queries
46 include QueriesHelper
46 include QueriesHelper
47 helper :repositories
47 helper :repositories
48 include RepositoriesHelper
48 include RepositoriesHelper
49 include ProjectsHelper
49 include ProjectsHelper
50
50
51 # Lists visible projects
51 # Lists visible projects
52 def index
52 def index
53 respond_to do |format|
53 respond_to do |format|
54 format.html {
54 format.html {
55 @projects = Project.visible.find(:all, :order => 'lft')
55 @projects = Project.visible.find(:all, :order => 'lft')
56 }
56 }
57 format.atom {
57 format.atom {
58 projects = Project.visible.find(:all, :order => 'created_on DESC',
58 projects = Project.visible.find(:all, :order => 'created_on DESC',
59 :limit => Setting.feeds_limit.to_i)
59 :limit => Setting.feeds_limit.to_i)
60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
60 render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
61 }
61 }
62 end
62 end
63 end
63 end
64
64
65 # Add a new project
65 # Add a new project
66 def add
66 def add
67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
67 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
68 @trackers = Tracker.all
68 @trackers = Tracker.all
69 @project = Project.new(params[:project])
69 @project = Project.new(params[:project])
70 if request.get?
70 if request.get?
71 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
71 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
72 @project.trackers = Tracker.all
72 @project.trackers = Tracker.all
73 @project.is_public = Setting.default_projects_public?
73 @project.is_public = Setting.default_projects_public?
74 @project.enabled_module_names = Setting.default_projects_modules
74 @project.enabled_module_names = Setting.default_projects_modules
75 else
75 else
76 @project.enabled_module_names = params[:enabled_modules]
76 @project.enabled_module_names = params[:enabled_modules]
77 if @project.save
77 if @project.save
78 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
78 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
79 # Add current user as a project member if he is not admin
79 # Add current user as a project member if he is not admin
80 unless User.current.admin?
80 unless User.current.admin?
81 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
81 r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
82 m = Member.new(:user => User.current, :roles => [r])
82 m = Member.new(:user => User.current, :roles => [r])
83 @project.members << m
83 @project.members << m
84 end
84 end
85 flash[:notice] = l(:notice_successful_create)
85 flash[:notice] = l(:notice_successful_create)
86 redirect_to :controller => 'projects', :action => 'settings', :id => @project
86 redirect_to :controller => 'projects', :action => 'settings', :id => @project
87 end
87 end
88 end
88 end
89 end
89 end
90
90
91 def copy
91 def copy
92 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
92 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
93 @trackers = Tracker.all
93 @trackers = Tracker.all
94 @root_projects = Project.find(:all,
94 @root_projects = Project.find(:all,
95 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
95 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
96 :order => 'name')
96 :order => 'name')
97 @source_project = Project.find(params[:id])
97 @source_project = Project.find(params[:id])
98 if request.get?
98 if request.get?
99 @project = Project.copy_from(@source_project)
99 @project = Project.copy_from(@source_project)
100 if @project
100 if @project
101 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
101 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
102 else
102 else
103 redirect_to :controller => 'admin', :action => 'projects'
103 redirect_to :controller => 'admin', :action => 'projects'
104 end
104 end
105 else
105 else
106 @project = Project.new(params[:project])
106 @project = Project.new(params[:project])
107 @project.enabled_module_names = params[:enabled_modules]
107 @project.enabled_module_names = params[:enabled_modules]
108 if @project.copy(@source_project, :only => params[:only])
108 if @project.copy(@source_project, :only => params[:only])
109 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
109 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
110 flash[:notice] = l(:notice_successful_create)
110 flash[:notice] = l(:notice_successful_create)
111 redirect_to :controller => 'admin', :action => 'projects'
111 redirect_to :controller => 'admin', :action => 'projects'
112 end
112 end
113 end
113 end
114 rescue ActiveRecord::RecordNotFound
114 rescue ActiveRecord::RecordNotFound
115 redirect_to :controller => 'admin', :action => 'projects'
115 redirect_to :controller => 'admin', :action => 'projects'
116 end
116 end
117
117
118 # Show @project
118 # Show @project
119 def show
119 def show
120 if params[:jump]
120 if params[:jump]
121 # try to redirect to the requested menu item
121 # try to redirect to the requested menu item
122 redirect_to_project_menu_item(@project, params[:jump]) && return
122 redirect_to_project_menu_item(@project, params[:jump]) && return
123 end
123 end
124
124
125 @users_by_role = @project.users_by_role
125 @users_by_role = @project.users_by_role
126 @subprojects = @project.children.visible
126 @subprojects = @project.children.visible
127 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
127 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
128 @trackers = @project.rolled_up_trackers
128 @trackers = @project.rolled_up_trackers
129
129
130 cond = @project.project_condition(Setting.display_subprojects_issues?)
130 cond = @project.project_condition(Setting.display_subprojects_issues?)
131
131
132 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
132 @open_issues_by_tracker = Issue.visible.count(:group => :tracker,
133 :include => [:project, :status, :tracker],
133 :include => [:project, :status, :tracker],
134 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
134 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
135 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
135 @total_issues_by_tracker = Issue.visible.count(:group => :tracker,
136 :include => [:project, :status, :tracker],
136 :include => [:project, :status, :tracker],
137 :conditions => cond)
137 :conditions => cond)
138
138
139 TimeEntry.visible_by(User.current) do
139 TimeEntry.visible_by(User.current) do
140 @total_hours = TimeEntry.sum(:hours,
140 @total_hours = TimeEntry.sum(:hours,
141 :include => :project,
141 :include => :project,
142 :conditions => cond).to_f
142 :conditions => cond).to_f
143 end
143 end
144 @key = User.current.rss_key
144 @key = User.current.rss_key
145 end
145 end
146
146
147 def settings
147 def settings
148 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
148 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
149 @issue_category ||= IssueCategory.new
149 @issue_category ||= IssueCategory.new
150 @member ||= @project.members.new
150 @member ||= @project.members.new
151 @trackers = Tracker.all
151 @trackers = Tracker.all
152 @repository ||= @project.repository
152 @repository ||= @project.repository
153 @wiki ||= @project.wiki
153 @wiki ||= @project.wiki
154 end
154 end
155
155
156 # Edit @project
156 # Edit @project
157 def edit
157 def edit
158 if request.post?
158 if request.post?
159 @project.attributes = params[:project]
159 @project.attributes = params[:project]
160 if @project.save
160 if @project.save
161 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
161 @project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
162 flash[:notice] = l(:notice_successful_update)
162 flash[:notice] = l(:notice_successful_update)
163 redirect_to :action => 'settings', :id => @project
163 redirect_to :action => 'settings', :id => @project
164 else
164 else
165 settings
165 settings
166 render :action => 'settings'
166 render :action => 'settings'
167 end
167 end
168 end
168 end
169 end
169 end
170
170
171 def modules
171 def modules
172 @project.enabled_module_names = params[:enabled_modules]
172 @project.enabled_module_names = params[:enabled_modules]
173 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
173 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
174 end
174 end
175
175
176 def archive
176 def archive
177 @project.archive if request.post? && @project.active?
177 if request.post?
178 unless @project.archive
179 flash[:error] = l(:error_can_not_archive_project)
180 end
181 end
178 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
182 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
179 end
183 end
180
184
181 def unarchive
185 def unarchive
182 @project.unarchive if request.post? && !@project.active?
186 @project.unarchive if request.post? && !@project.active?
183 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
187 redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
184 end
188 end
185
189
186 # Delete @project
190 # Delete @project
187 def destroy
191 def destroy
188 @project_to_destroy = @project
192 @project_to_destroy = @project
189 if request.post? and params[:confirm]
193 if request.post? and params[:confirm]
190 @project_to_destroy.destroy
194 @project_to_destroy.destroy
191 redirect_to :controller => 'admin', :action => 'projects'
195 redirect_to :controller => 'admin', :action => 'projects'
192 end
196 end
193 # hide project in layout
197 # hide project in layout
194 @project = nil
198 @project = nil
195 end
199 end
196
200
197 # Add a new issue category to @project
201 # Add a new issue category to @project
198 def add_issue_category
202 def add_issue_category
199 @category = @project.issue_categories.build(params[:category])
203 @category = @project.issue_categories.build(params[:category])
200 if request.post?
204 if request.post?
201 if @category.save
205 if @category.save
202 respond_to do |format|
206 respond_to do |format|
203 format.html do
207 format.html do
204 flash[:notice] = l(:notice_successful_create)
208 flash[:notice] = l(:notice_successful_create)
205 redirect_to :action => 'settings', :tab => 'categories', :id => @project
209 redirect_to :action => 'settings', :tab => 'categories', :id => @project
206 end
210 end
207 format.js do
211 format.js do
208 # IE doesn't support the replace_html rjs method for select box options
212 # IE doesn't support the replace_html rjs method for select box options
209 render(:update) {|page| page.replace "issue_category_id",
213 render(:update) {|page| page.replace "issue_category_id",
210 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
214 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
211 }
215 }
212 end
216 end
213 end
217 end
214 else
218 else
215 respond_to do |format|
219 respond_to do |format|
216 format.html
220 format.html
217 format.js do
221 format.js do
218 render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
222 render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
219 end
223 end
220 end
224 end
221 end
225 end
222 end
226 end
223 end
227 end
224
228
225 # Add a new version to @project
229 # Add a new version to @project
226 def add_version
230 def add_version
227 @version = @project.versions.build(params[:version])
231 @version = @project.versions.build
232 if params[:version]
233 attributes = params[:version].dup
234 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
235 @version.attributes = attributes
236 end
228 if request.post? and @version.save
237 if request.post? and @version.save
229 flash[:notice] = l(:notice_successful_create)
238 flash[:notice] = l(:notice_successful_create)
230 redirect_to :action => 'settings', :tab => 'versions', :id => @project
239 redirect_to :action => 'settings', :tab => 'versions', :id => @project
231 end
240 end
232 end
241 end
233
242
234 def add_file
243 def add_file
235 if request.post?
244 if request.post?
236 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
245 container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
237 attachments = attach_files(container, params[:attachments])
246 attachments = attach_files(container, params[:attachments])
238 if !attachments.empty? && Setting.notified_events.include?('file_added')
247 if !attachments.empty? && Setting.notified_events.include?('file_added')
239 Mailer.deliver_attachments_added(attachments)
248 Mailer.deliver_attachments_added(attachments)
240 end
249 end
241 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
250 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
242 return
251 return
243 end
252 end
244 @versions = @project.versions.sort
253 @versions = @project.versions.sort
245 end
254 end
246
255
247 def save_activities
256 def save_activities
248 if request.post? && params[:enumerations]
257 if request.post? && params[:enumerations]
249 Project.transaction do
258 Project.transaction do
250 params[:enumerations].each do |id, activity|
259 params[:enumerations].each do |id, activity|
251 @project.update_or_create_time_entry_activity(id, activity)
260 @project.update_or_create_time_entry_activity(id, activity)
252 end
261 end
253 end
262 end
254 end
263 end
255
264
256 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
265 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
257 end
266 end
258
267
259 def reset_activities
268 def reset_activities
260 @project.time_entry_activities.each do |time_entry_activity|
269 @project.time_entry_activities.each do |time_entry_activity|
261 time_entry_activity.destroy(time_entry_activity.parent)
270 time_entry_activity.destroy(time_entry_activity.parent)
262 end
271 end
263 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
272 redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
264 end
273 end
265
274
266 def list_files
275 def list_files
267 sort_init 'filename', 'asc'
276 sort_init 'filename', 'asc'
268 sort_update 'filename' => "#{Attachment.table_name}.filename",
277 sort_update 'filename' => "#{Attachment.table_name}.filename",
269 'created_on' => "#{Attachment.table_name}.created_on",
278 'created_on' => "#{Attachment.table_name}.created_on",
270 'size' => "#{Attachment.table_name}.filesize",
279 'size' => "#{Attachment.table_name}.filesize",
271 'downloads' => "#{Attachment.table_name}.downloads"
280 'downloads' => "#{Attachment.table_name}.downloads"
272
281
273 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
282 @containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
274 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
283 @containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
275 render :layout => !request.xhr?
284 render :layout => !request.xhr?
276 end
285 end
277
286
278 # Show changelog for @project
287 # Show changelog for @project
279 def changelog
288 def changelog
280 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
289 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
281 retrieve_selected_tracker_ids(@trackers)
290 retrieve_selected_tracker_ids(@trackers)
282 @versions = @project.versions.sort
291 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
292 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
293
294 @versions = @project.shared_versions.sort
295
296 @issues_by_version = {}
297 unless @selected_tracker_ids.empty?
298 @versions.each do |version|
299 conditions = {:tracker_id => @selected_tracker_ids, "#{IssueStatus.table_name}.is_closed" => true}
300 if !@project.versions.include?(version)
301 conditions.merge!(:project_id => project_ids)
302 end
303 issues = version.fixed_issues.visible.find(:all,
304 :include => [:status, :tracker, :priority],
305 :conditions => conditions,
306 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
307 @issues_by_version[version] = issues
308 end
309 end
310 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
283 end
311 end
284
312
285 def roadmap
313 def roadmap
286 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
314 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true], :order => 'position')
287 retrieve_selected_tracker_ids(@trackers)
315 retrieve_selected_tracker_ids(@trackers)
288 @versions = @project.versions.sort
316 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
289 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
317 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
318
319 @versions = @project.shared_versions.sort
320 @versions.reject! {|version| version.closed? || version.completed? } unless params[:completed]
321
322 @issues_by_version = {}
323 unless @selected_tracker_ids.empty?
324 @versions.each do |version|
325 conditions = {:tracker_id => @selected_tracker_ids}
326 if !@project.versions.include?(version)
327 conditions.merge!(:project_id => project_ids)
328 end
329 issues = version.fixed_issues.visible.find(:all,
330 :include => [:status, :tracker, :priority],
331 :conditions => conditions,
332 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
333 @issues_by_version[version] = issues
334 end
335 end
336 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].empty?}
290 end
337 end
291
338
292 def activity
339 def activity
293 @days = Setting.activity_days_default.to_i
340 @days = Setting.activity_days_default.to_i
294
341
295 if params[:from]
342 if params[:from]
296 begin; @date_to = params[:from].to_date + 1; rescue; end
343 begin; @date_to = params[:from].to_date + 1; rescue; end
297 end
344 end
298
345
299 @date_to ||= Date.today + 1
346 @date_to ||= Date.today + 1
300 @date_from = @date_to - @days
347 @date_from = @date_to - @days
301 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
348 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
302 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
349 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
303
350
304 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
351 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
305 :with_subprojects => @with_subprojects,
352 :with_subprojects => @with_subprojects,
306 :author => @author)
353 :author => @author)
307 @activity.scope_select {|t| !params["show_#{t}"].nil?}
354 @activity.scope_select {|t| !params["show_#{t}"].nil?}
308 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
355 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
309
356
310 events = @activity.events(@date_from, @date_to)
357 events = @activity.events(@date_from, @date_to)
311
358
312 if events.empty? || stale?(:etag => [events.first, User.current])
359 if events.empty? || stale?(:etag => [events.first, User.current])
313 respond_to do |format|
360 respond_to do |format|
314 format.html {
361 format.html {
315 @events_by_day = events.group_by(&:event_date)
362 @events_by_day = events.group_by(&:event_date)
316 render :layout => false if request.xhr?
363 render :layout => false if request.xhr?
317 }
364 }
318 format.atom {
365 format.atom {
319 title = l(:label_activity)
366 title = l(:label_activity)
320 if @author
367 if @author
321 title = @author.name
368 title = @author.name
322 elsif @activity.scope.size == 1
369 elsif @activity.scope.size == 1
323 title = l("label_#{@activity.scope.first.singularize}_plural")
370 title = l("label_#{@activity.scope.first.singularize}_plural")
324 end
371 end
325 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
372 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
326 }
373 }
327 end
374 end
328 end
375 end
329
376
330 rescue ActiveRecord::RecordNotFound
377 rescue ActiveRecord::RecordNotFound
331 render_404
378 render_404
332 end
379 end
333
380
334 private
381 private
335 # Find project of id params[:id]
382 # Find project of id params[:id]
336 # if not found, redirect to project list
383 # if not found, redirect to project list
337 # Used as a before_filter
384 # Used as a before_filter
338 def find_project
385 def find_project
339 @project = Project.find(params[:id])
386 @project = Project.find(params[:id])
340 rescue ActiveRecord::RecordNotFound
387 rescue ActiveRecord::RecordNotFound
341 render_404
388 render_404
342 end
389 end
343
390
344 def find_optional_project
391 def find_optional_project
345 return true unless params[:id]
392 return true unless params[:id]
346 @project = Project.find(params[:id])
393 @project = Project.find(params[:id])
347 authorize
394 authorize
348 rescue ActiveRecord::RecordNotFound
395 rescue ActiveRecord::RecordNotFound
349 render_404
396 render_404
350 end
397 end
351
398
352 def retrieve_selected_tracker_ids(selectable_trackers)
399 def retrieve_selected_tracker_ids(selectable_trackers)
353 if ids = params[:tracker_ids]
400 if ids = params[:tracker_ids]
354 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
401 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
355 else
402 else
356 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
403 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
357 end
404 end
358 end
405 end
359 end
406 end
@@ -1,71 +1,76
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class VersionsController < ApplicationController
18 class VersionsController < ApplicationController
19 menu_item :roadmap
19 menu_item :roadmap
20 before_filter :find_version, :except => :close_completed
20 before_filter :find_version, :except => :close_completed
21 before_filter :find_project, :only => :close_completed
21 before_filter :find_project, :only => :close_completed
22 before_filter :authorize
22 before_filter :authorize
23
23
24 helper :custom_fields
24 helper :custom_fields
25 helper :projects
25
26
26 def show
27 def show
27 end
28 end
28
29
29 def edit
30 def edit
30 if request.post? and @version.update_attributes(params[:version])
31 if request.post? && params[:version]
31 flash[:notice] = l(:notice_successful_update)
32 attributes = params[:version].dup
32 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
33 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
34 if @version.update_attributes(attributes)
35 flash[:notice] = l(:notice_successful_update)
36 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
37 end
33 end
38 end
34 end
39 end
35
40
36 def close_completed
41 def close_completed
37 if request.post?
42 if request.post?
38 @project.close_completed_versions
43 @project.close_completed_versions
39 end
44 end
40 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
45 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
41 end
46 end
42
47
43 def destroy
48 def destroy
44 @version.destroy
49 @version.destroy
45 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
50 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
46 rescue
51 rescue
47 flash[:error] = l(:notice_unable_delete_version)
52 flash[:error] = l(:notice_unable_delete_version)
48 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
53 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
49 end
54 end
50
55
51 def status_by
56 def status_by
52 respond_to do |format|
57 respond_to do |format|
53 format.html { render :action => 'show' }
58 format.html { render :action => 'show' }
54 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
59 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
55 end
60 end
56 end
61 end
57
62
58 private
63 private
59 def find_version
64 def find_version
60 @version = Version.find(params[:id])
65 @version = Version.find(params[:id])
61 @project = @version.project
66 @project = @version.project
62 rescue ActiveRecord::RecordNotFound
67 rescue ActiveRecord::RecordNotFound
63 render_404
68 render_404
64 end
69 end
65
70
66 def find_project
71 def find_project
67 @project = Project.find(params[:project_id])
72 @project = Project.find(params[:project_id])
68 rescue ActiveRecord::RecordNotFound
73 rescue ActiveRecord::RecordNotFound
69 render_404
74 render_404
70 end
75 end
71 end
76 end
@@ -1,700 +1,708
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'coderay'
18 require 'coderay'
19 require 'coderay/helpers/file_type'
19 require 'coderay/helpers/file_type'
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
39 end
39 end
40
40
41 # Display a link to remote if user is authorized
41 # Display a link to remote if user is authorized
42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
43 url = options[:url] || {}
43 url = options[:url] || {}
44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active?
51 if user.active?
52 link_to name, :controller => 'users', :action => 'show', :id => user
52 link_to name, :controller => 'users', :action => 'show', :id => user
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 #
67 #
68 def link_to_issue(issue, options={})
68 def link_to_issue(issue, options={})
69 title = nil
69 title = nil
70 subject = nil
70 subject = nil
71 if options[:subject] == false
71 if options[:subject] == false
72 title = truncate(issue.subject, :length => 60)
72 title = truncate(issue.subject, :length => 60)
73 else
73 else
74 subject = issue.subject
74 subject = issue.subject
75 if options[:truncate]
75 if options[:truncate]
76 subject = truncate(subject, :length => options[:truncate])
76 subject = truncate(subject, :length => options[:truncate])
77 end
77 end
78 end
78 end
79 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
79 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
80 :class => issue.css_classes,
80 :class => issue.css_classes,
81 :title => title
81 :title => title
82 s << ": #{h subject}" if subject
82 s << ": #{h subject}" if subject
83 s
83 s
84 end
84 end
85
85
86 # Generates a link to an attachment.
86 # Generates a link to an attachment.
87 # Options:
87 # Options:
88 # * :text - Link text (default to attachment filename)
88 # * :text - Link text (default to attachment filename)
89 # * :download - Force download (default: false)
89 # * :download - Force download (default: false)
90 def link_to_attachment(attachment, options={})
90 def link_to_attachment(attachment, options={})
91 text = options.delete(:text) || attachment.filename
91 text = options.delete(:text) || attachment.filename
92 action = options.delete(:download) ? 'download' : 'show'
92 action = options.delete(:download) ? 'download' : 'show'
93
93
94 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
94 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
95 end
95 end
96
96
97 def toggle_link(name, id, options={})
97 def toggle_link(name, id, options={})
98 onclick = "Element.toggle('#{id}'); "
98 onclick = "Element.toggle('#{id}'); "
99 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
99 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
100 onclick << "return false;"
100 onclick << "return false;"
101 link_to(name, "#", :onclick => onclick)
101 link_to(name, "#", :onclick => onclick)
102 end
102 end
103
103
104 def image_to_function(name, function, html_options = {})
104 def image_to_function(name, function, html_options = {})
105 html_options.symbolize_keys!
105 html_options.symbolize_keys!
106 tag(:input, html_options.merge({
106 tag(:input, html_options.merge({
107 :type => "image", :src => image_path(name),
107 :type => "image", :src => image_path(name),
108 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
108 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
109 }))
109 }))
110 end
110 end
111
111
112 def prompt_to_remote(name, text, param, url, html_options = {})
112 def prompt_to_remote(name, text, param, url, html_options = {})
113 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
113 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
114 link_to name, {}, html_options
114 link_to name, {}, html_options
115 end
115 end
116
116
117 def format_activity_title(text)
117 def format_activity_title(text)
118 h(truncate_single_line(text, :length => 100))
118 h(truncate_single_line(text, :length => 100))
119 end
119 end
120
120
121 def format_activity_day(date)
121 def format_activity_day(date)
122 date == Date.today ? l(:label_today).titleize : format_date(date)
122 date == Date.today ? l(:label_today).titleize : format_date(date)
123 end
123 end
124
124
125 def format_activity_description(text)
125 def format_activity_description(text)
126 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
126 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
127 end
127 end
128
128
129 def format_version_name(version)
130 if version.project == @project
131 h(version)
132 else
133 h("#{version.project} - #{version}")
134 end
135 end
136
129 def due_date_distance_in_words(date)
137 def due_date_distance_in_words(date)
130 if date
138 if date
131 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
139 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
132 end
140 end
133 end
141 end
134
142
135 def render_page_hierarchy(pages, node=nil)
143 def render_page_hierarchy(pages, node=nil)
136 content = ''
144 content = ''
137 if pages[node]
145 if pages[node]
138 content << "<ul class=\"pages-hierarchy\">\n"
146 content << "<ul class=\"pages-hierarchy\">\n"
139 pages[node].each do |page|
147 pages[node].each do |page|
140 content << "<li>"
148 content << "<li>"
141 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
149 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
142 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
150 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
143 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
151 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
144 content << "</li>\n"
152 content << "</li>\n"
145 end
153 end
146 content << "</ul>\n"
154 content << "</ul>\n"
147 end
155 end
148 content
156 content
149 end
157 end
150
158
151 # Renders flash messages
159 # Renders flash messages
152 def render_flash_messages
160 def render_flash_messages
153 s = ''
161 s = ''
154 flash.each do |k,v|
162 flash.each do |k,v|
155 s << content_tag('div', v, :class => "flash #{k}")
163 s << content_tag('div', v, :class => "flash #{k}")
156 end
164 end
157 s
165 s
158 end
166 end
159
167
160 # Renders tabs and their content
168 # Renders tabs and their content
161 def render_tabs(tabs)
169 def render_tabs(tabs)
162 if tabs.any?
170 if tabs.any?
163 render :partial => 'common/tabs', :locals => {:tabs => tabs}
171 render :partial => 'common/tabs', :locals => {:tabs => tabs}
164 else
172 else
165 content_tag 'p', l(:label_no_data), :class => "nodata"
173 content_tag 'p', l(:label_no_data), :class => "nodata"
166 end
174 end
167 end
175 end
168
176
169 # Renders the project quick-jump box
177 # Renders the project quick-jump box
170 def render_project_jump_box
178 def render_project_jump_box
171 # Retrieve them now to avoid a COUNT query
179 # Retrieve them now to avoid a COUNT query
172 projects = User.current.projects.all
180 projects = User.current.projects.all
173 if projects.any?
181 if projects.any?
174 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
182 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
175 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
183 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
176 '<option value="" disabled="disabled">---</option>'
184 '<option value="" disabled="disabled">---</option>'
177 s << project_tree_options_for_select(projects, :selected => @project) do |p|
185 s << project_tree_options_for_select(projects, :selected => @project) do |p|
178 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
186 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
179 end
187 end
180 s << '</select>'
188 s << '</select>'
181 s
189 s
182 end
190 end
183 end
191 end
184
192
185 def project_tree_options_for_select(projects, options = {})
193 def project_tree_options_for_select(projects, options = {})
186 s = ''
194 s = ''
187 project_tree(projects) do |project, level|
195 project_tree(projects) do |project, level|
188 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
196 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
189 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
197 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
190 tag_options.merge!(yield(project)) if block_given?
198 tag_options.merge!(yield(project)) if block_given?
191 s << content_tag('option', name_prefix + h(project), tag_options)
199 s << content_tag('option', name_prefix + h(project), tag_options)
192 end
200 end
193 s
201 s
194 end
202 end
195
203
196 # Yields the given block for each project with its level in the tree
204 # Yields the given block for each project with its level in the tree
197 def project_tree(projects, &block)
205 def project_tree(projects, &block)
198 ancestors = []
206 ancestors = []
199 projects.sort_by(&:lft).each do |project|
207 projects.sort_by(&:lft).each do |project|
200 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
208 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
201 ancestors.pop
209 ancestors.pop
202 end
210 end
203 yield project, ancestors.size
211 yield project, ancestors.size
204 ancestors << project
212 ancestors << project
205 end
213 end
206 end
214 end
207
215
208 def project_nested_ul(projects, &block)
216 def project_nested_ul(projects, &block)
209 s = ''
217 s = ''
210 if projects.any?
218 if projects.any?
211 ancestors = []
219 ancestors = []
212 projects.sort_by(&:lft).each do |project|
220 projects.sort_by(&:lft).each do |project|
213 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
221 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
214 s << "<ul>\n"
222 s << "<ul>\n"
215 else
223 else
216 ancestors.pop
224 ancestors.pop
217 s << "</li>"
225 s << "</li>"
218 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
226 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
219 ancestors.pop
227 ancestors.pop
220 s << "</ul></li>\n"
228 s << "</ul></li>\n"
221 end
229 end
222 end
230 end
223 s << "<li>"
231 s << "<li>"
224 s << yield(project).to_s
232 s << yield(project).to_s
225 ancestors << project
233 ancestors << project
226 end
234 end
227 s << ("</li></ul>\n" * ancestors.size)
235 s << ("</li></ul>\n" * ancestors.size)
228 end
236 end
229 s
237 s
230 end
238 end
231
239
232 def principals_check_box_tags(name, principals)
240 def principals_check_box_tags(name, principals)
233 s = ''
241 s = ''
234 principals.sort.each do |principal|
242 principals.sort.each do |principal|
235 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
243 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
236 end
244 end
237 s
245 s
238 end
246 end
239
247
240 # Truncates and returns the string as a single line
248 # Truncates and returns the string as a single line
241 def truncate_single_line(string, *args)
249 def truncate_single_line(string, *args)
242 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
250 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
243 end
251 end
244
252
245 def html_hours(text)
253 def html_hours(text)
246 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
254 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
247 end
255 end
248
256
249 def authoring(created, author, options={})
257 def authoring(created, author, options={})
250 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
258 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
251 end
259 end
252
260
253 def time_tag(time)
261 def time_tag(time)
254 text = distance_of_time_in_words(Time.now, time)
262 text = distance_of_time_in_words(Time.now, time)
255 if @project
263 if @project
256 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
264 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
257 else
265 else
258 content_tag('acronym', text, :title => format_time(time))
266 content_tag('acronym', text, :title => format_time(time))
259 end
267 end
260 end
268 end
261
269
262 def syntax_highlight(name, content)
270 def syntax_highlight(name, content)
263 type = CodeRay::FileType[name]
271 type = CodeRay::FileType[name]
264 type ? CodeRay.scan(content, type).html : h(content)
272 type ? CodeRay.scan(content, type).html : h(content)
265 end
273 end
266
274
267 def to_path_param(path)
275 def to_path_param(path)
268 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
276 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
269 end
277 end
270
278
271 def pagination_links_full(paginator, count=nil, options={})
279 def pagination_links_full(paginator, count=nil, options={})
272 page_param = options.delete(:page_param) || :page
280 page_param = options.delete(:page_param) || :page
273 url_param = params.dup
281 url_param = params.dup
274 # don't reuse query params if filters are present
282 # don't reuse query params if filters are present
275 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
283 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
276
284
277 html = ''
285 html = ''
278 if paginator.current.previous
286 if paginator.current.previous
279 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
287 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
280 end
288 end
281
289
282 html << (pagination_links_each(paginator, options) do |n|
290 html << (pagination_links_each(paginator, options) do |n|
283 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
291 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
284 end || '')
292 end || '')
285
293
286 if paginator.current.next
294 if paginator.current.next
287 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
295 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
288 end
296 end
289
297
290 unless count.nil?
298 unless count.nil?
291 html << [
299 html << [
292 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
300 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
293 per_page_links(paginator.items_per_page)
301 per_page_links(paginator.items_per_page)
294 ].compact.join(' | ')
302 ].compact.join(' | ')
295 end
303 end
296
304
297 html
305 html
298 end
306 end
299
307
300 def per_page_links(selected=nil)
308 def per_page_links(selected=nil)
301 url_param = params.dup
309 url_param = params.dup
302 url_param.clear if url_param.has_key?(:set_filter)
310 url_param.clear if url_param.has_key?(:set_filter)
303
311
304 links = Setting.per_page_options_array.collect do |n|
312 links = Setting.per_page_options_array.collect do |n|
305 n == selected ? n : link_to_remote(n, {:update => "content",
313 n == selected ? n : link_to_remote(n, {:update => "content",
306 :url => params.dup.merge(:per_page => n),
314 :url => params.dup.merge(:per_page => n),
307 :method => :get},
315 :method => :get},
308 {:href => url_for(url_param.merge(:per_page => n))})
316 {:href => url_for(url_param.merge(:per_page => n))})
309 end
317 end
310 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
318 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
311 end
319 end
312
320
313 def reorder_links(name, url)
321 def reorder_links(name, url)
314 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
322 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
315 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
323 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
316 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
324 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
317 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
325 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
318 end
326 end
319
327
320 def breadcrumb(*args)
328 def breadcrumb(*args)
321 elements = args.flatten
329 elements = args.flatten
322 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
330 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
323 end
331 end
324
332
325 def other_formats_links(&block)
333 def other_formats_links(&block)
326 concat('<p class="other-formats">' + l(:label_export_to))
334 concat('<p class="other-formats">' + l(:label_export_to))
327 yield Redmine::Views::OtherFormatsBuilder.new(self)
335 yield Redmine::Views::OtherFormatsBuilder.new(self)
328 concat('</p>')
336 concat('</p>')
329 end
337 end
330
338
331 def page_header_title
339 def page_header_title
332 if @project.nil? || @project.new_record?
340 if @project.nil? || @project.new_record?
333 h(Setting.app_title)
341 h(Setting.app_title)
334 else
342 else
335 b = []
343 b = []
336 ancestors = (@project.root? ? [] : @project.ancestors.visible)
344 ancestors = (@project.root? ? [] : @project.ancestors.visible)
337 if ancestors.any?
345 if ancestors.any?
338 root = ancestors.shift
346 root = ancestors.shift
339 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
347 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
340 if ancestors.size > 2
348 if ancestors.size > 2
341 b << '&#8230;'
349 b << '&#8230;'
342 ancestors = ancestors[-2, 2]
350 ancestors = ancestors[-2, 2]
343 end
351 end
344 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
352 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
345 end
353 end
346 b << h(@project)
354 b << h(@project)
347 b.join(' &#187; ')
355 b.join(' &#187; ')
348 end
356 end
349 end
357 end
350
358
351 def html_title(*args)
359 def html_title(*args)
352 if args.empty?
360 if args.empty?
353 title = []
361 title = []
354 title << @project.name if @project
362 title << @project.name if @project
355 title += @html_title if @html_title
363 title += @html_title if @html_title
356 title << Setting.app_title
364 title << Setting.app_title
357 title.select {|t| !t.blank? }.join(' - ')
365 title.select {|t| !t.blank? }.join(' - ')
358 else
366 else
359 @html_title ||= []
367 @html_title ||= []
360 @html_title += args
368 @html_title += args
361 end
369 end
362 end
370 end
363
371
364 def accesskey(s)
372 def accesskey(s)
365 Redmine::AccessKeys.key_for s
373 Redmine::AccessKeys.key_for s
366 end
374 end
367
375
368 # Formats text according to system settings.
376 # Formats text according to system settings.
369 # 2 ways to call this method:
377 # 2 ways to call this method:
370 # * with a String: textilizable(text, options)
378 # * with a String: textilizable(text, options)
371 # * with an object and one of its attribute: textilizable(issue, :description, options)
379 # * with an object and one of its attribute: textilizable(issue, :description, options)
372 def textilizable(*args)
380 def textilizable(*args)
373 options = args.last.is_a?(Hash) ? args.pop : {}
381 options = args.last.is_a?(Hash) ? args.pop : {}
374 case args.size
382 case args.size
375 when 1
383 when 1
376 obj = options[:object]
384 obj = options[:object]
377 text = args.shift
385 text = args.shift
378 when 2
386 when 2
379 obj = args.shift
387 obj = args.shift
380 text = obj.send(args.shift).to_s
388 text = obj.send(args.shift).to_s
381 else
389 else
382 raise ArgumentError, 'invalid arguments to textilizable'
390 raise ArgumentError, 'invalid arguments to textilizable'
383 end
391 end
384 return '' if text.blank?
392 return '' if text.blank?
385
393
386 only_path = options.delete(:only_path) == false ? false : true
394 only_path = options.delete(:only_path) == false ? false : true
387
395
388 # when using an image link, try to use an attachment, if possible
396 # when using an image link, try to use an attachment, if possible
389 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
397 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
390
398
391 if attachments
399 if attachments
392 attachments = attachments.sort_by(&:created_on).reverse
400 attachments = attachments.sort_by(&:created_on).reverse
393 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
401 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
394 style = $1
402 style = $1
395 filename = $6.downcase
403 filename = $6.downcase
396 # search for the picture in attachments
404 # search for the picture in attachments
397 if found = attachments.detect { |att| att.filename.downcase == filename }
405 if found = attachments.detect { |att| att.filename.downcase == filename }
398 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
406 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
399 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
407 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
400 alt = desc.blank? ? nil : "(#{desc})"
408 alt = desc.blank? ? nil : "(#{desc})"
401 "!#{style}#{image_url}#{alt}!"
409 "!#{style}#{image_url}#{alt}!"
402 else
410 else
403 m
411 m
404 end
412 end
405 end
413 end
406 end
414 end
407
415
408 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
416 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
409
417
410 # different methods for formatting wiki links
418 # different methods for formatting wiki links
411 case options[:wiki_links]
419 case options[:wiki_links]
412 when :local
420 when :local
413 # used for local links to html files
421 # used for local links to html files
414 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
422 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
415 when :anchor
423 when :anchor
416 # used for single-file wiki export
424 # used for single-file wiki export
417 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
425 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
418 else
426 else
419 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
427 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
420 end
428 end
421
429
422 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
430 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
423
431
424 # Wiki links
432 # Wiki links
425 #
433 #
426 # Examples:
434 # Examples:
427 # [[mypage]]
435 # [[mypage]]
428 # [[mypage|mytext]]
436 # [[mypage|mytext]]
429 # wiki links can refer other project wikis, using project name or identifier:
437 # wiki links can refer other project wikis, using project name or identifier:
430 # [[project:]] -> wiki starting page
438 # [[project:]] -> wiki starting page
431 # [[project:|mytext]]
439 # [[project:|mytext]]
432 # [[project:mypage]]
440 # [[project:mypage]]
433 # [[project:mypage|mytext]]
441 # [[project:mypage|mytext]]
434 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
442 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
435 link_project = project
443 link_project = project
436 esc, all, page, title = $1, $2, $3, $5
444 esc, all, page, title = $1, $2, $3, $5
437 if esc.nil?
445 if esc.nil?
438 if page =~ /^([^\:]+)\:(.*)$/
446 if page =~ /^([^\:]+)\:(.*)$/
439 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
447 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
440 page = $2
448 page = $2
441 title ||= $1 if page.blank?
449 title ||= $1 if page.blank?
442 end
450 end
443
451
444 if link_project && link_project.wiki
452 if link_project && link_project.wiki
445 # extract anchor
453 # extract anchor
446 anchor = nil
454 anchor = nil
447 if page =~ /^(.+?)\#(.+)$/
455 if page =~ /^(.+?)\#(.+)$/
448 page, anchor = $1, $2
456 page, anchor = $1, $2
449 end
457 end
450 # check if page exists
458 # check if page exists
451 wiki_page = link_project.wiki.find_page(page)
459 wiki_page = link_project.wiki.find_page(page)
452 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
460 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
453 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
461 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
454 else
462 else
455 # project or wiki doesn't exist
463 # project or wiki doesn't exist
456 all
464 all
457 end
465 end
458 else
466 else
459 all
467 all
460 end
468 end
461 end
469 end
462
470
463 # Redmine links
471 # Redmine links
464 #
472 #
465 # Examples:
473 # Examples:
466 # Issues:
474 # Issues:
467 # #52 -> Link to issue #52
475 # #52 -> Link to issue #52
468 # Changesets:
476 # Changesets:
469 # r52 -> Link to revision 52
477 # r52 -> Link to revision 52
470 # commit:a85130f -> Link to scmid starting with a85130f
478 # commit:a85130f -> Link to scmid starting with a85130f
471 # Documents:
479 # Documents:
472 # document#17 -> Link to document with id 17
480 # document#17 -> Link to document with id 17
473 # document:Greetings -> Link to the document with title "Greetings"
481 # document:Greetings -> Link to the document with title "Greetings"
474 # document:"Some document" -> Link to the document with title "Some document"
482 # document:"Some document" -> Link to the document with title "Some document"
475 # Versions:
483 # Versions:
476 # version#3 -> Link to version with id 3
484 # version#3 -> Link to version with id 3
477 # version:1.0.0 -> Link to version named "1.0.0"
485 # version:1.0.0 -> Link to version named "1.0.0"
478 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
486 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
479 # Attachments:
487 # Attachments:
480 # attachment:file.zip -> Link to the attachment of the current object named file.zip
488 # attachment:file.zip -> Link to the attachment of the current object named file.zip
481 # Source files:
489 # Source files:
482 # source:some/file -> Link to the file located at /some/file in the project's repository
490 # source:some/file -> Link to the file located at /some/file in the project's repository
483 # source:some/file@52 -> Link to the file's revision 52
491 # source:some/file@52 -> Link to the file's revision 52
484 # source:some/file#L120 -> Link to line 120 of the file
492 # source:some/file#L120 -> Link to line 120 of the file
485 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
493 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
486 # export:some/file -> Force the download of the file
494 # export:some/file -> Force the download of the file
487 # Forum messages:
495 # Forum messages:
488 # message#1218 -> Link to message with id 1218
496 # message#1218 -> Link to message with id 1218
489 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
497 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
490 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
498 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
491 link = nil
499 link = nil
492 if esc.nil?
500 if esc.nil?
493 if prefix.nil? && sep == 'r'
501 if prefix.nil? && sep == 'r'
494 if project && (changeset = project.changesets.find_by_revision(oid))
502 if project && (changeset = project.changesets.find_by_revision(oid))
495 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
503 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
496 :class => 'changeset',
504 :class => 'changeset',
497 :title => truncate_single_line(changeset.comments, :length => 100))
505 :title => truncate_single_line(changeset.comments, :length => 100))
498 end
506 end
499 elsif sep == '#'
507 elsif sep == '#'
500 oid = oid.to_i
508 oid = oid.to_i
501 case prefix
509 case prefix
502 when nil
510 when nil
503 if issue = Issue.visible.find_by_id(oid, :include => :status)
511 if issue = Issue.visible.find_by_id(oid, :include => :status)
504 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
512 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
505 :class => issue.css_classes,
513 :class => issue.css_classes,
506 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
514 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
507 end
515 end
508 when 'document'
516 when 'document'
509 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
517 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
510 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
518 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
511 :class => 'document'
519 :class => 'document'
512 end
520 end
513 when 'version'
521 when 'version'
514 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
522 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
515 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
523 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
516 :class => 'version'
524 :class => 'version'
517 end
525 end
518 when 'message'
526 when 'message'
519 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
527 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
520 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
528 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
521 :controller => 'messages',
529 :controller => 'messages',
522 :action => 'show',
530 :action => 'show',
523 :board_id => message.board,
531 :board_id => message.board,
524 :id => message.root,
532 :id => message.root,
525 :anchor => (message.parent ? "message-#{message.id}" : nil)},
533 :anchor => (message.parent ? "message-#{message.id}" : nil)},
526 :class => 'message'
534 :class => 'message'
527 end
535 end
528 end
536 end
529 elsif sep == ':'
537 elsif sep == ':'
530 # removes the double quotes if any
538 # removes the double quotes if any
531 name = oid.gsub(%r{^"(.*)"$}, "\\1")
539 name = oid.gsub(%r{^"(.*)"$}, "\\1")
532 case prefix
540 case prefix
533 when 'document'
541 when 'document'
534 if project && document = project.documents.find_by_title(name)
542 if project && document = project.documents.find_by_title(name)
535 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
543 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
536 :class => 'document'
544 :class => 'document'
537 end
545 end
538 when 'version'
546 when 'version'
539 if project && version = project.versions.find_by_name(name)
547 if project && version = project.versions.find_by_name(name)
540 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
548 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
541 :class => 'version'
549 :class => 'version'
542 end
550 end
543 when 'commit'
551 when 'commit'
544 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
552 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
545 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
553 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
546 :class => 'changeset',
554 :class => 'changeset',
547 :title => truncate_single_line(changeset.comments, :length => 100)
555 :title => truncate_single_line(changeset.comments, :length => 100)
548 end
556 end
549 when 'source', 'export'
557 when 'source', 'export'
550 if project && project.repository
558 if project && project.repository
551 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
559 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
552 path, rev, anchor = $1, $3, $5
560 path, rev, anchor = $1, $3, $5
553 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
561 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
554 :path => to_path_param(path),
562 :path => to_path_param(path),
555 :rev => rev,
563 :rev => rev,
556 :anchor => anchor,
564 :anchor => anchor,
557 :format => (prefix == 'export' ? 'raw' : nil)},
565 :format => (prefix == 'export' ? 'raw' : nil)},
558 :class => (prefix == 'export' ? 'source download' : 'source')
566 :class => (prefix == 'export' ? 'source download' : 'source')
559 end
567 end
560 when 'attachment'
568 when 'attachment'
561 if attachments && attachment = attachments.detect {|a| a.filename == name }
569 if attachments && attachment = attachments.detect {|a| a.filename == name }
562 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
570 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
563 :class => 'attachment'
571 :class => 'attachment'
564 end
572 end
565 end
573 end
566 end
574 end
567 end
575 end
568 leading + (link || "#{prefix}#{sep}#{oid}")
576 leading + (link || "#{prefix}#{sep}#{oid}")
569 end
577 end
570
578
571 text
579 text
572 end
580 end
573
581
574 # Same as Rails' simple_format helper without using paragraphs
582 # Same as Rails' simple_format helper without using paragraphs
575 def simple_format_without_paragraph(text)
583 def simple_format_without_paragraph(text)
576 text.to_s.
584 text.to_s.
577 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
585 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
578 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
586 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
579 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
587 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
580 end
588 end
581
589
582 def lang_options_for_select(blank=true)
590 def lang_options_for_select(blank=true)
583 (blank ? [["(auto)", ""]] : []) +
591 (blank ? [["(auto)", ""]] : []) +
584 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
592 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
585 end
593 end
586
594
587 def label_tag_for(name, option_tags = nil, options = {})
595 def label_tag_for(name, option_tags = nil, options = {})
588 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
596 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
589 content_tag("label", label_text)
597 content_tag("label", label_text)
590 end
598 end
591
599
592 def labelled_tabular_form_for(name, object, options, &proc)
600 def labelled_tabular_form_for(name, object, options, &proc)
593 options[:html] ||= {}
601 options[:html] ||= {}
594 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
602 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
595 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
603 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
596 end
604 end
597
605
598 def back_url_hidden_field_tag
606 def back_url_hidden_field_tag
599 back_url = params[:back_url] || request.env['HTTP_REFERER']
607 back_url = params[:back_url] || request.env['HTTP_REFERER']
600 back_url = CGI.unescape(back_url.to_s)
608 back_url = CGI.unescape(back_url.to_s)
601 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
609 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
602 end
610 end
603
611
604 def check_all_links(form_name)
612 def check_all_links(form_name)
605 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
613 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
606 " | " +
614 " | " +
607 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
615 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
608 end
616 end
609
617
610 def progress_bar(pcts, options={})
618 def progress_bar(pcts, options={})
611 pcts = [pcts, pcts] unless pcts.is_a?(Array)
619 pcts = [pcts, pcts] unless pcts.is_a?(Array)
612 pcts = pcts.collect(&:round)
620 pcts = pcts.collect(&:round)
613 pcts[1] = pcts[1] - pcts[0]
621 pcts[1] = pcts[1] - pcts[0]
614 pcts << (100 - pcts[1] - pcts[0])
622 pcts << (100 - pcts[1] - pcts[0])
615 width = options[:width] || '100px;'
623 width = options[:width] || '100px;'
616 legend = options[:legend] || ''
624 legend = options[:legend] || ''
617 content_tag('table',
625 content_tag('table',
618 content_tag('tr',
626 content_tag('tr',
619 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
627 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
620 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
628 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
621 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
629 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
622 ), :class => 'progress', :style => "width: #{width};") +
630 ), :class => 'progress', :style => "width: #{width};") +
623 content_tag('p', legend, :class => 'pourcent')
631 content_tag('p', legend, :class => 'pourcent')
624 end
632 end
625
633
626 def context_menu_link(name, url, options={})
634 def context_menu_link(name, url, options={})
627 options[:class] ||= ''
635 options[:class] ||= ''
628 if options.delete(:selected)
636 if options.delete(:selected)
629 options[:class] << ' icon-checked disabled'
637 options[:class] << ' icon-checked disabled'
630 options[:disabled] = true
638 options[:disabled] = true
631 end
639 end
632 if options.delete(:disabled)
640 if options.delete(:disabled)
633 options.delete(:method)
641 options.delete(:method)
634 options.delete(:confirm)
642 options.delete(:confirm)
635 options.delete(:onclick)
643 options.delete(:onclick)
636 options[:class] << ' disabled'
644 options[:class] << ' disabled'
637 url = '#'
645 url = '#'
638 end
646 end
639 link_to name, url, options
647 link_to name, url, options
640 end
648 end
641
649
642 def calendar_for(field_id)
650 def calendar_for(field_id)
643 include_calendar_headers_tags
651 include_calendar_headers_tags
644 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
652 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
645 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
653 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
646 end
654 end
647
655
648 def include_calendar_headers_tags
656 def include_calendar_headers_tags
649 unless @calendar_headers_tags_included
657 unless @calendar_headers_tags_included
650 @calendar_headers_tags_included = true
658 @calendar_headers_tags_included = true
651 content_for :header_tags do
659 content_for :header_tags do
652 javascript_include_tag('calendar/calendar') +
660 javascript_include_tag('calendar/calendar') +
653 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
661 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
654 javascript_include_tag('calendar/calendar-setup') +
662 javascript_include_tag('calendar/calendar-setup') +
655 stylesheet_link_tag('calendar')
663 stylesheet_link_tag('calendar')
656 end
664 end
657 end
665 end
658 end
666 end
659
667
660 def content_for(name, content = nil, &block)
668 def content_for(name, content = nil, &block)
661 @has_content ||= {}
669 @has_content ||= {}
662 @has_content[name] = true
670 @has_content[name] = true
663 super(name, content, &block)
671 super(name, content, &block)
664 end
672 end
665
673
666 def has_content?(name)
674 def has_content?(name)
667 (@has_content && @has_content[name]) || false
675 (@has_content && @has_content[name]) || false
668 end
676 end
669
677
670 # Returns the avatar image tag for the given +user+ if avatars are enabled
678 # Returns the avatar image tag for the given +user+ if avatars are enabled
671 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
679 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
672 def avatar(user, options = { })
680 def avatar(user, options = { })
673 if Setting.gravatar_enabled?
681 if Setting.gravatar_enabled?
674 options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
682 options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
675 email = nil
683 email = nil
676 if user.respond_to?(:mail)
684 if user.respond_to?(:mail)
677 email = user.mail
685 email = user.mail
678 elsif user.to_s =~ %r{<(.+?)>}
686 elsif user.to_s =~ %r{<(.+?)>}
679 email = $1
687 email = $1
680 end
688 end
681 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
689 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
682 end
690 end
683 end
691 end
684
692
685 private
693 private
686
694
687 def wiki_helper
695 def wiki_helper
688 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
696 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
689 extend helper
697 extend helper
690 return self
698 return self
691 end
699 end
692
700
693 def link_to_remote_content_update(text, url_params)
701 def link_to_remote_content_update(text, url_params)
694 link_to_remote(text,
702 link_to_remote(text,
695 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
703 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
696 {:href => url_for(:params => url_params)}
704 {:href => url_for(:params => url_params)}
697 )
705 )
698 end
706 end
699
707
700 end
708 end
@@ -1,199 +1,199
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module IssuesHelper
18 module IssuesHelper
19 include ApplicationHelper
19 include ApplicationHelper
20
20
21 def render_issue_tooltip(issue)
21 def render_issue_tooltip(issue)
22 @cached_label_start_date ||= l(:field_start_date)
22 @cached_label_start_date ||= l(:field_start_date)
23 @cached_label_due_date ||= l(:field_due_date)
23 @cached_label_due_date ||= l(:field_due_date)
24 @cached_label_assigned_to ||= l(:field_assigned_to)
24 @cached_label_assigned_to ||= l(:field_assigned_to)
25 @cached_label_priority ||= l(:field_priority)
25 @cached_label_priority ||= l(:field_priority)
26
26
27 link_to_issue(issue) + "<br /><br />" +
27 link_to_issue(issue) + "<br /><br />" +
28 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
28 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
29 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
29 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
30 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
30 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
31 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
31 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
32 end
32 end
33
33
34 def render_custom_fields_rows(issue)
34 def render_custom_fields_rows(issue)
35 return if issue.custom_field_values.empty?
35 return if issue.custom_field_values.empty?
36 ordered_values = []
36 ordered_values = []
37 half = (issue.custom_field_values.size / 2.0).ceil
37 half = (issue.custom_field_values.size / 2.0).ceil
38 half.times do |i|
38 half.times do |i|
39 ordered_values << issue.custom_field_values[i]
39 ordered_values << issue.custom_field_values[i]
40 ordered_values << issue.custom_field_values[i + half]
40 ordered_values << issue.custom_field_values[i + half]
41 end
41 end
42 s = "<tr>\n"
42 s = "<tr>\n"
43 n = 0
43 n = 0
44 ordered_values.compact.each do |value|
44 ordered_values.compact.each do |value|
45 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
45 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
46 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
46 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
47 n += 1
47 n += 1
48 end
48 end
49 s << "</tr>\n"
49 s << "</tr>\n"
50 s
50 s
51 end
51 end
52
52
53 def sidebar_queries
53 def sidebar_queries
54 unless @sidebar_queries
54 unless @sidebar_queries
55 # User can see public queries and his own queries
55 # User can see public queries and his own queries
56 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
56 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
57 # Project specific queries and global queries
57 # Project specific queries and global queries
58 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
58 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
59 @sidebar_queries = Query.find(:all,
59 @sidebar_queries = Query.find(:all,
60 :select => 'id, name',
60 :select => 'id, name',
61 :order => "name ASC",
61 :order => "name ASC",
62 :conditions => visible.conditions)
62 :conditions => visible.conditions)
63 end
63 end
64 @sidebar_queries
64 @sidebar_queries
65 end
65 end
66
66
67 def show_detail(detail, no_html=false)
67 def show_detail(detail, no_html=false)
68 case detail.property
68 case detail.property
69 when 'attr'
69 when 'attr'
70 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
70 label = l(("field_" + detail.prop_key.to_s.gsub(/\_id$/, "")).to_sym)
71 case detail.prop_key
71 case detail.prop_key
72 when 'due_date', 'start_date'
72 when 'due_date', 'start_date'
73 value = format_date(detail.value.to_date) if detail.value
73 value = format_date(detail.value.to_date) if detail.value
74 old_value = format_date(detail.old_value.to_date) if detail.old_value
74 old_value = format_date(detail.old_value.to_date) if detail.old_value
75 when 'project_id'
75 when 'project_id'
76 p = Project.find_by_id(detail.value) and value = p.name if detail.value
76 p = Project.find_by_id(detail.value) and value = p.name if detail.value
77 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
77 p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
78 when 'status_id'
78 when 'status_id'
79 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
79 s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
80 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
80 s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
81 when 'tracker_id'
81 when 'tracker_id'
82 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
82 t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
83 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
83 t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
84 when 'assigned_to_id'
84 when 'assigned_to_id'
85 u = User.find_by_id(detail.value) and value = u.name if detail.value
85 u = User.find_by_id(detail.value) and value = u.name if detail.value
86 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
86 u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
87 when 'priority_id'
87 when 'priority_id'
88 e = IssuePriority.find_by_id(detail.value) and value = e.name if detail.value
88 e = IssuePriority.find_by_id(detail.value) and value = e.name if detail.value
89 e = IssuePriority.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
89 e = IssuePriority.find_by_id(detail.old_value) and old_value = e.name if detail.old_value
90 when 'category_id'
90 when 'category_id'
91 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
91 c = IssueCategory.find_by_id(detail.value) and value = c.name if detail.value
92 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
92 c = IssueCategory.find_by_id(detail.old_value) and old_value = c.name if detail.old_value
93 when 'fixed_version_id'
93 when 'fixed_version_id'
94 v = Version.find_by_id(detail.value) and value = v.name if detail.value
94 v = Version.find_by_id(detail.value) and value = format_version_name(v) if detail.value
95 v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
95 v = Version.find_by_id(detail.old_value) and old_value = format_version_name(v) if detail.old_value
96 when 'estimated_hours'
96 when 'estimated_hours'
97 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
97 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
98 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
98 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
99 end
99 end
100 when 'cf'
100 when 'cf'
101 custom_field = CustomField.find_by_id(detail.prop_key)
101 custom_field = CustomField.find_by_id(detail.prop_key)
102 if custom_field
102 if custom_field
103 label = custom_field.name
103 label = custom_field.name
104 value = format_value(detail.value, custom_field.field_format) if detail.value
104 value = format_value(detail.value, custom_field.field_format) if detail.value
105 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
105 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
106 end
106 end
107 when 'attachment'
107 when 'attachment'
108 label = l(:label_attachment)
108 label = l(:label_attachment)
109 end
109 end
110 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
110 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
111
111
112 label ||= detail.prop_key
112 label ||= detail.prop_key
113 value ||= detail.value
113 value ||= detail.value
114 old_value ||= detail.old_value
114 old_value ||= detail.old_value
115
115
116 unless no_html
116 unless no_html
117 label = content_tag('strong', label)
117 label = content_tag('strong', label)
118 old_value = content_tag("i", h(old_value)) if detail.old_value
118 old_value = content_tag("i", h(old_value)) if detail.old_value
119 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
119 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
120 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
120 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
121 # Link to the attachment if it has not been removed
121 # Link to the attachment if it has not been removed
122 value = link_to_attachment(a)
122 value = link_to_attachment(a)
123 else
123 else
124 value = content_tag("i", h(value)) if value
124 value = content_tag("i", h(value)) if value
125 end
125 end
126 end
126 end
127
127
128 if !detail.value.blank?
128 if !detail.value.blank?
129 case detail.property
129 case detail.property
130 when 'attr', 'cf'
130 when 'attr', 'cf'
131 if !detail.old_value.blank?
131 if !detail.old_value.blank?
132 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
132 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
133 else
133 else
134 l(:text_journal_set_to, :label => label, :value => value)
134 l(:text_journal_set_to, :label => label, :value => value)
135 end
135 end
136 when 'attachment'
136 when 'attachment'
137 l(:text_journal_added, :label => label, :value => value)
137 l(:text_journal_added, :label => label, :value => value)
138 end
138 end
139 else
139 else
140 l(:text_journal_deleted, :label => label, :old => old_value)
140 l(:text_journal_deleted, :label => label, :old => old_value)
141 end
141 end
142 end
142 end
143
143
144 def issues_to_csv(issues, project = nil)
144 def issues_to_csv(issues, project = nil)
145 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
145 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
146 decimal_separator = l(:general_csv_decimal_separator)
146 decimal_separator = l(:general_csv_decimal_separator)
147 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
147 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
148 # csv header fields
148 # csv header fields
149 headers = [ "#",
149 headers = [ "#",
150 l(:field_status),
150 l(:field_status),
151 l(:field_project),
151 l(:field_project),
152 l(:field_tracker),
152 l(:field_tracker),
153 l(:field_priority),
153 l(:field_priority),
154 l(:field_subject),
154 l(:field_subject),
155 l(:field_assigned_to),
155 l(:field_assigned_to),
156 l(:field_category),
156 l(:field_category),
157 l(:field_fixed_version),
157 l(:field_fixed_version),
158 l(:field_author),
158 l(:field_author),
159 l(:field_start_date),
159 l(:field_start_date),
160 l(:field_due_date),
160 l(:field_due_date),
161 l(:field_done_ratio),
161 l(:field_done_ratio),
162 l(:field_estimated_hours),
162 l(:field_estimated_hours),
163 l(:field_created_on),
163 l(:field_created_on),
164 l(:field_updated_on)
164 l(:field_updated_on)
165 ]
165 ]
166 # Export project custom fields if project is given
166 # Export project custom fields if project is given
167 # otherwise export custom fields marked as "For all projects"
167 # otherwise export custom fields marked as "For all projects"
168 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
168 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
169 custom_fields.each {|f| headers << f.name}
169 custom_fields.each {|f| headers << f.name}
170 # Description in the last column
170 # Description in the last column
171 headers << l(:field_description)
171 headers << l(:field_description)
172 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
172 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
173 # csv lines
173 # csv lines
174 issues.each do |issue|
174 issues.each do |issue|
175 fields = [issue.id,
175 fields = [issue.id,
176 issue.status.name,
176 issue.status.name,
177 issue.project.name,
177 issue.project.name,
178 issue.tracker.name,
178 issue.tracker.name,
179 issue.priority.name,
179 issue.priority.name,
180 issue.subject,
180 issue.subject,
181 issue.assigned_to,
181 issue.assigned_to,
182 issue.category,
182 issue.category,
183 issue.fixed_version,
183 issue.fixed_version,
184 issue.author.name,
184 issue.author.name,
185 format_date(issue.start_date),
185 format_date(issue.start_date),
186 format_date(issue.due_date),
186 format_date(issue.due_date),
187 issue.done_ratio,
187 issue.done_ratio,
188 issue.estimated_hours.to_s.gsub('.', decimal_separator),
188 issue.estimated_hours.to_s.gsub('.', decimal_separator),
189 format_time(issue.created_on),
189 format_time(issue.created_on),
190 format_time(issue.updated_on)
190 format_time(issue.updated_on)
191 ]
191 ]
192 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
192 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
193 fields << issue.description
193 fields << issue.description
194 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
194 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
195 end
195 end
196 end
196 end
197 export
197 export
198 end
198 end
199 end
199 end
@@ -1,72 +1,95
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module ProjectsHelper
18 module ProjectsHelper
19 def link_to_version(version, options = {})
19 def link_to_version(version, options = {})
20 return '' unless version && version.is_a?(Version)
20 return '' unless version && version.is_a?(Version)
21 link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
21 link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
22 end
22 end
23
23
24 def project_settings_tabs
24 def project_settings_tabs
25 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
25 tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
26 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
26 {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
27 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
27 {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
28 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
28 {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
29 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
29 {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
30 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
30 {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
31 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
31 {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
32 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
32 {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
33 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
33 {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
34 ]
34 ]
35 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
35 tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
36 end
36 end
37
37
38 def parent_project_select_tag(project)
38 def parent_project_select_tag(project)
39 options = '<option></option>' + project_tree_options_for_select(project.allowed_parents, :selected => project.parent)
39 options = '<option></option>' + project_tree_options_for_select(project.allowed_parents, :selected => project.parent)
40 content_tag('select', options, :name => 'project[parent_id]')
40 content_tag('select', options, :name => 'project[parent_id]')
41 end
41 end
42
42
43 # Renders a tree of projects as a nested set of unordered lists
43 # Renders a tree of projects as a nested set of unordered lists
44 # The given collection may be a subset of the whole project tree
44 # The given collection may be a subset of the whole project tree
45 # (eg. some intermediate nodes are private and can not be seen)
45 # (eg. some intermediate nodes are private and can not be seen)
46 def render_project_hierarchy(projects)
46 def render_project_hierarchy(projects)
47 s = ''
47 s = ''
48 if projects.any?
48 if projects.any?
49 ancestors = []
49 ancestors = []
50 projects.each do |project|
50 projects.each do |project|
51 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
51 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
52 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
52 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
53 else
53 else
54 ancestors.pop
54 ancestors.pop
55 s << "</li>"
55 s << "</li>"
56 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
56 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
57 ancestors.pop
57 ancestors.pop
58 s << "</ul></li>\n"
58 s << "</ul></li>\n"
59 end
59 end
60 end
60 end
61 classes = (ancestors.empty? ? 'root' : 'child')
61 classes = (ancestors.empty? ? 'root' : 'child')
62 s << "<li class='#{classes}'><div class='#{classes}'>" +
62 s << "<li class='#{classes}'><div class='#{classes}'>" +
63 link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
63 link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
64 s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
64 s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
65 s << "</div>\n"
65 s << "</div>\n"
66 ancestors << project
66 ancestors << project
67 end
67 end
68 s << ("</li></ul>\n" * ancestors.size)
68 s << ("</li></ul>\n" * ancestors.size)
69 end
69 end
70 s
70 s
71 end
71 end
72
73 # Returns a set of options for a select field, grouped by project.
74 def version_options_for_select(versions, selected=nil)
75 grouped = Hash.new {|h,k| h[k] = []}
76 versions.each do |version|
77 grouped[version.project.name] << [h(version.name), version.id]
78 end
79 # Add in the selected
80 if selected && !versions.include?(selected)
81 grouped[selected.project.name] << [h(selected.name), selected.id]
82 end
83
84 if grouped.keys.size > 1
85 grouped_options_for_select(grouped, selected && selected.id)
86 else
87 options_for_select(grouped.values.first, selected && selected.id)
88 end
89 end
90
91 def format_version_sharing(sharing)
92 sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
93 l("label_version_sharing_#{sharing}")
94 end
72 end
95 end
@@ -1,374 +1,394
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 validates_length_of :subject, :maximum => 255
50 validates_length_of :subject, :maximum => 255
51 validates_inclusion_of :done_ratio, :in => 0..100
51 validates_inclusion_of :done_ratio, :in => 0..100
52 validates_numericality_of :estimated_hours, :allow_nil => true
52 validates_numericality_of :estimated_hours, :allow_nil => true
53
53
54 named_scope :visible, lambda {|*args| { :include => :project,
54 named_scope :visible, lambda {|*args| { :include => :project,
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56
56
57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
58
58
59 after_save :create_journal
59 after_save :create_journal
60
60
61 # Returns true if usr or current user is allowed to view the issue
61 # Returns true if usr or current user is allowed to view the issue
62 def visible?(usr=nil)
62 def visible?(usr=nil)
63 (usr || User.current).allowed_to?(:view_issues, self.project)
63 (usr || User.current).allowed_to?(:view_issues, self.project)
64 end
64 end
65
65
66 def after_initialize
66 def after_initialize
67 if new_record?
67 if new_record?
68 # set default values for new records only
68 # set default values for new records only
69 self.status ||= IssueStatus.default
69 self.status ||= IssueStatus.default
70 self.priority ||= IssuePriority.default
70 self.priority ||= IssuePriority.default
71 end
71 end
72 end
72 end
73
73
74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
75 def available_custom_fields
75 def available_custom_fields
76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
77 end
77 end
78
78
79 def copy_from(arg)
79 def copy_from(arg)
80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
81 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
81 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
82 self.custom_values = issue.custom_values.collect {|v| v.clone}
82 self.custom_values = issue.custom_values.collect {|v| v.clone}
83 self.status = issue.status
83 self.status = issue.status
84 self
84 self
85 end
85 end
86
86
87 # Moves/copies an issue to a new project and tracker
87 # Moves/copies an issue to a new project and tracker
88 # Returns the moved/copied issue on success, false on failure
88 # Returns the moved/copied issue on success, false on failure
89 def move_to(new_project, new_tracker = nil, options = {})
89 def move_to(new_project, new_tracker = nil, options = {})
90 options ||= {}
90 options ||= {}
91 issue = options[:copy] ? self.clone : self
91 issue = options[:copy] ? self.clone : self
92 transaction do
92 transaction do
93 if new_project && issue.project_id != new_project.id
93 if new_project && issue.project_id != new_project.id
94 # delete issue relations
94 # delete issue relations
95 unless Setting.cross_project_issue_relations?
95 unless Setting.cross_project_issue_relations?
96 issue.relations_from.clear
96 issue.relations_from.clear
97 issue.relations_to.clear
97 issue.relations_to.clear
98 end
98 end
99 # issue is moved to another project
99 # issue is moved to another project
100 # reassign to the category with same name if any
100 # reassign to the category with same name if any
101 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
101 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
102 issue.category = new_category
102 issue.category = new_category
103 issue.fixed_version = nil
103 # Keep the fixed_version if it's still valid in the new_project
104 unless new_project.shared_versions.include?(issue.fixed_version)
105 issue.fixed_version = nil
106 end
104 issue.project = new_project
107 issue.project = new_project
105 end
108 end
106 if new_tracker
109 if new_tracker
107 issue.tracker = new_tracker
110 issue.tracker = new_tracker
108 end
111 end
109 if options[:copy]
112 if options[:copy]
110 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
113 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
111 issue.status = if options[:attributes] && options[:attributes][:status_id]
114 issue.status = if options[:attributes] && options[:attributes][:status_id]
112 IssueStatus.find_by_id(options[:attributes][:status_id])
115 IssueStatus.find_by_id(options[:attributes][:status_id])
113 else
116 else
114 self.status
117 self.status
115 end
118 end
116 end
119 end
117 # Allow bulk setting of attributes on the issue
120 # Allow bulk setting of attributes on the issue
118 if options[:attributes]
121 if options[:attributes]
119 issue.attributes = options[:attributes]
122 issue.attributes = options[:attributes]
120 end
123 end
121 if issue.save
124 if issue.save
122 unless options[:copy]
125 unless options[:copy]
123 # Manually update project_id on related time entries
126 # Manually update project_id on related time entries
124 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
127 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
125 end
128 end
126 else
129 else
127 Issue.connection.rollback_db_transaction
130 Issue.connection.rollback_db_transaction
128 return false
131 return false
129 end
132 end
130 end
133 end
131 return issue
134 return issue
132 end
135 end
133
136
134 def priority_id=(pid)
137 def priority_id=(pid)
135 self.priority = nil
138 self.priority = nil
136 write_attribute(:priority_id, pid)
139 write_attribute(:priority_id, pid)
137 end
140 end
138
141
139 def tracker_id=(tid)
142 def tracker_id=(tid)
140 self.tracker = nil
143 self.tracker = nil
141 write_attribute(:tracker_id, tid)
144 write_attribute(:tracker_id, tid)
142 end
145 end
143
146
144 def estimated_hours=(h)
147 def estimated_hours=(h)
145 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
148 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
146 end
149 end
147
150
148 def validate
151 def validate
149 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
152 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
150 errors.add :due_date, :not_a_date
153 errors.add :due_date, :not_a_date
151 end
154 end
152
155
153 if self.due_date and self.start_date and self.due_date < self.start_date
156 if self.due_date and self.start_date and self.due_date < self.start_date
154 errors.add :due_date, :greater_than_start_date
157 errors.add :due_date, :greater_than_start_date
155 end
158 end
156
159
157 if start_date && soonest_start && start_date < soonest_start
160 if start_date && soonest_start && start_date < soonest_start
158 errors.add :start_date, :invalid
161 errors.add :start_date, :invalid
159 end
162 end
160
163
161 if fixed_version
164 if fixed_version
162 if !assignable_versions.include?(fixed_version)
165 if !assignable_versions.include?(fixed_version)
163 errors.add :fixed_version_id, :inclusion
166 errors.add :fixed_version_id, :inclusion
164 elsif reopened? && fixed_version.closed?
167 elsif reopened? && fixed_version.closed?
165 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
168 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
166 end
169 end
167 end
170 end
168
171
169 # Checks that the issue can not be added/moved to a disabled tracker
172 # Checks that the issue can not be added/moved to a disabled tracker
170 if project && (tracker_id_changed? || project_id_changed?)
173 if project && (tracker_id_changed? || project_id_changed?)
171 unless project.trackers.include?(tracker)
174 unless project.trackers.include?(tracker)
172 errors.add :tracker_id, :inclusion
175 errors.add :tracker_id, :inclusion
173 end
176 end
174 end
177 end
175 end
178 end
176
179
177 def before_create
180 def before_create
178 # default assignment based on category
181 # default assignment based on category
179 if assigned_to.nil? && category && category.assigned_to
182 if assigned_to.nil? && category && category.assigned_to
180 self.assigned_to = category.assigned_to
183 self.assigned_to = category.assigned_to
181 end
184 end
182 end
185 end
183
186
184 def after_save
187 def after_save
185 # Reload is needed in order to get the right status
188 # Reload is needed in order to get the right status
186 reload
189 reload
187
190
188 # Update start/due dates of following issues
191 # Update start/due dates of following issues
189 relations_from.each(&:set_issue_to_dates)
192 relations_from.each(&:set_issue_to_dates)
190
193
191 # Close duplicates if the issue was closed
194 # Close duplicates if the issue was closed
192 if @issue_before_change && !@issue_before_change.closed? && self.closed?
195 if @issue_before_change && !@issue_before_change.closed? && self.closed?
193 duplicates.each do |duplicate|
196 duplicates.each do |duplicate|
194 # Reload is need in case the duplicate was updated by a previous duplicate
197 # Reload is need in case the duplicate was updated by a previous duplicate
195 duplicate.reload
198 duplicate.reload
196 # Don't re-close it if it's already closed
199 # Don't re-close it if it's already closed
197 next if duplicate.closed?
200 next if duplicate.closed?
198 # Same user and notes
201 # Same user and notes
199 duplicate.init_journal(@current_journal.user, @current_journal.notes)
202 duplicate.init_journal(@current_journal.user, @current_journal.notes)
200 duplicate.update_attribute :status, self.status
203 duplicate.update_attribute :status, self.status
201 end
204 end
202 end
205 end
203 end
206 end
204
207
205 def init_journal(user, notes = "")
208 def init_journal(user, notes = "")
206 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
209 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
207 @issue_before_change = self.clone
210 @issue_before_change = self.clone
208 @issue_before_change.status = self.status
211 @issue_before_change.status = self.status
209 @custom_values_before_change = {}
212 @custom_values_before_change = {}
210 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
213 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
211 # Make sure updated_on is updated when adding a note.
214 # Make sure updated_on is updated when adding a note.
212 updated_on_will_change!
215 updated_on_will_change!
213 @current_journal
216 @current_journal
214 end
217 end
215
218
216 # Return true if the issue is closed, otherwise false
219 # Return true if the issue is closed, otherwise false
217 def closed?
220 def closed?
218 self.status.is_closed?
221 self.status.is_closed?
219 end
222 end
220
223
221 # Return true if the issue is being reopened
224 # Return true if the issue is being reopened
222 def reopened?
225 def reopened?
223 if !new_record? && status_id_changed?
226 if !new_record? && status_id_changed?
224 status_was = IssueStatus.find_by_id(status_id_was)
227 status_was = IssueStatus.find_by_id(status_id_was)
225 status_new = IssueStatus.find_by_id(status_id)
228 status_new = IssueStatus.find_by_id(status_id)
226 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
229 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
227 return true
230 return true
228 end
231 end
229 end
232 end
230 false
233 false
231 end
234 end
232
235
233 # Returns true if the issue is overdue
236 # Returns true if the issue is overdue
234 def overdue?
237 def overdue?
235 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
238 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
236 end
239 end
237
240
238 # Users the issue can be assigned to
241 # Users the issue can be assigned to
239 def assignable_users
242 def assignable_users
240 project.assignable_users
243 project.assignable_users
241 end
244 end
242
245
243 # Versions that the issue can be assigned to
246 # Versions that the issue can be assigned to
244 def assignable_versions
247 def assignable_versions
245 @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
248 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
246 end
249 end
247
250
248 # Returns true if this issue is blocked by another issue that is still open
251 # Returns true if this issue is blocked by another issue that is still open
249 def blocked?
252 def blocked?
250 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
253 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
251 end
254 end
252
255
253 # Returns an array of status that user is able to apply
256 # Returns an array of status that user is able to apply
254 def new_statuses_allowed_to(user)
257 def new_statuses_allowed_to(user)
255 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
258 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
256 statuses << status unless statuses.empty?
259 statuses << status unless statuses.empty?
257 statuses = statuses.uniq.sort
260 statuses = statuses.uniq.sort
258 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
261 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
259 end
262 end
260
263
261 # Returns the mail adresses of users that should be notified
264 # Returns the mail adresses of users that should be notified
262 def recipients
265 def recipients
263 notified = project.notified_users
266 notified = project.notified_users
264 # Author and assignee are always notified unless they have been locked
267 # Author and assignee are always notified unless they have been locked
265 notified << author if author && author.active?
268 notified << author if author && author.active?
266 notified << assigned_to if assigned_to && assigned_to.active?
269 notified << assigned_to if assigned_to && assigned_to.active?
267 notified.uniq!
270 notified.uniq!
268 # Remove users that can not view the issue
271 # Remove users that can not view the issue
269 notified.reject! {|user| !visible?(user)}
272 notified.reject! {|user| !visible?(user)}
270 notified.collect(&:mail)
273 notified.collect(&:mail)
271 end
274 end
272
275
273 # Returns the mail adresses of watchers that should be notified
276 # Returns the mail adresses of watchers that should be notified
274 def watcher_recipients
277 def watcher_recipients
275 notified = watcher_users
278 notified = watcher_users
276 notified.reject! {|user| !user.active? || !visible?(user)}
279 notified.reject! {|user| !user.active? || !visible?(user)}
277 notified.collect(&:mail)
280 notified.collect(&:mail)
278 end
281 end
279
282
280 # Returns the total number of hours spent on this issue.
283 # Returns the total number of hours spent on this issue.
281 #
284 #
282 # Example:
285 # Example:
283 # spent_hours => 0
286 # spent_hours => 0
284 # spent_hours => 50
287 # spent_hours => 50
285 def spent_hours
288 def spent_hours
286 @spent_hours ||= time_entries.sum(:hours) || 0
289 @spent_hours ||= time_entries.sum(:hours) || 0
287 end
290 end
288
291
289 def relations
292 def relations
290 (relations_from + relations_to).sort
293 (relations_from + relations_to).sort
291 end
294 end
292
295
293 def all_dependent_issues
296 def all_dependent_issues
294 dependencies = []
297 dependencies = []
295 relations_from.each do |relation|
298 relations_from.each do |relation|
296 dependencies << relation.issue_to
299 dependencies << relation.issue_to
297 dependencies += relation.issue_to.all_dependent_issues
300 dependencies += relation.issue_to.all_dependent_issues
298 end
301 end
299 dependencies
302 dependencies
300 end
303 end
301
304
302 # Returns an array of issues that duplicate this one
305 # Returns an array of issues that duplicate this one
303 def duplicates
306 def duplicates
304 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
307 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
305 end
308 end
306
309
307 # Returns the due date or the target due date if any
310 # Returns the due date or the target due date if any
308 # Used on gantt chart
311 # Used on gantt chart
309 def due_before
312 def due_before
310 due_date || (fixed_version ? fixed_version.effective_date : nil)
313 due_date || (fixed_version ? fixed_version.effective_date : nil)
311 end
314 end
312
315
313 # Returns the time scheduled for this issue.
316 # Returns the time scheduled for this issue.
314 #
317 #
315 # Example:
318 # Example:
316 # Start Date: 2/26/09, End Date: 3/04/09
319 # Start Date: 2/26/09, End Date: 3/04/09
317 # duration => 6
320 # duration => 6
318 def duration
321 def duration
319 (start_date && due_date) ? due_date - start_date : 0
322 (start_date && due_date) ? due_date - start_date : 0
320 end
323 end
321
324
322 def soonest_start
325 def soonest_start
323 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
326 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
324 end
327 end
325
328
326 def to_s
329 def to_s
327 "#{tracker} ##{id}: #{subject}"
330 "#{tracker} ##{id}: #{subject}"
328 end
331 end
329
332
330 # Returns a string of css classes that apply to the issue
333 # Returns a string of css classes that apply to the issue
331 def css_classes
334 def css_classes
332 s = "issue status-#{status.position} priority-#{priority.position}"
335 s = "issue status-#{status.position} priority-#{priority.position}"
333 s << ' closed' if closed?
336 s << ' closed' if closed?
334 s << ' overdue' if overdue?
337 s << ' overdue' if overdue?
335 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
338 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
336 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
339 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
337 s
340 s
338 end
341 end
342
343 # Update all issues so their versions are not pointing to a
344 # fixed_version that is outside of the issue's project hierarchy.
345 #
346 # OPTIMIZE: does a full table scan of Issues with a fixed_version.
347 def self.update_fixed_versions_from_project_hierarchy_change
348 Issue.all(:conditions => ['fixed_version_id IS NOT NULL'],
349 :include => [:project, :fixed_version]
350 ).each do |issue|
351 next if issue.project.nil? || issue.fixed_version.nil?
352 unless issue.project.shared_versions.include?(issue.fixed_version)
353 issue.init_journal(User.current)
354 issue.fixed_version = nil
355 issue.save
356 end
357 end
358 end
339
359
340 private
360 private
341
361
342 # Callback on attachment deletion
362 # Callback on attachment deletion
343 def attachment_removed(obj)
363 def attachment_removed(obj)
344 journal = init_journal(User.current)
364 journal = init_journal(User.current)
345 journal.details << JournalDetail.new(:property => 'attachment',
365 journal.details << JournalDetail.new(:property => 'attachment',
346 :prop_key => obj.id,
366 :prop_key => obj.id,
347 :old_value => obj.filename)
367 :old_value => obj.filename)
348 journal.save
368 journal.save
349 end
369 end
350
370
351 # Saves the changes in a Journal
371 # Saves the changes in a Journal
352 # Called after_save
372 # Called after_save
353 def create_journal
373 def create_journal
354 if @current_journal
374 if @current_journal
355 # attributes changes
375 # attributes changes
356 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
376 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
357 @current_journal.details << JournalDetail.new(:property => 'attr',
377 @current_journal.details << JournalDetail.new(:property => 'attr',
358 :prop_key => c,
378 :prop_key => c,
359 :old_value => @issue_before_change.send(c),
379 :old_value => @issue_before_change.send(c),
360 :value => send(c)) unless send(c)==@issue_before_change.send(c)
380 :value => send(c)) unless send(c)==@issue_before_change.send(c)
361 }
381 }
362 # custom fields changes
382 # custom fields changes
363 custom_values.each {|c|
383 custom_values.each {|c|
364 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
384 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
365 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
385 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
366 @current_journal.details << JournalDetail.new(:property => 'cf',
386 @current_journal.details << JournalDetail.new(:property => 'cf',
367 :prop_key => c.custom_field_id,
387 :prop_key => c.custom_field_id,
368 :old_value => @custom_values_before_change[c.custom_field_id],
388 :old_value => @custom_values_before_change[c.custom_field_id],
369 :value => c.value)
389 :value => c.value)
370 }
390 }
371 @current_journal.save
391 @current_journal.save
372 end
392 end
373 end
393 end
374 end
394 end
@@ -1,608 +1,637
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities
24 has_many :time_entry_activities
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 has_many :member_principals, :class_name => 'Member',
26 has_many :member_principals, :class_name => 'Member',
27 :include => :principal,
27 :include => :principal,
28 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
28 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 has_many :users, :through => :members
29 has_many :users, :through => :members
30 has_many :principals, :through => :member_principals, :source => :principal
30 has_many :principals, :through => :member_principals, :source => :principal
31
31
32 has_many :enabled_modules, :dependent => :delete_all
32 has_many :enabled_modules, :dependent => :delete_all
33 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
33 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
34 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 has_many :issue_changes, :through => :issues, :source => :journals
35 has_many :issue_changes, :through => :issues, :source => :journals
36 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
36 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 has_many :time_entries, :dependent => :delete_all
37 has_many :time_entries, :dependent => :delete_all
38 has_many :queries, :dependent => :delete_all
38 has_many :queries, :dependent => :delete_all
39 has_many :documents, :dependent => :destroy
39 has_many :documents, :dependent => :destroy
40 has_many :news, :dependent => :delete_all, :include => :author
40 has_many :news, :dependent => :delete_all, :include => :author
41 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
41 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 has_many :boards, :dependent => :destroy, :order => "position ASC"
42 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 has_one :repository, :dependent => :destroy
43 has_one :repository, :dependent => :destroy
44 has_many :changesets, :through => :repository
44 has_many :changesets, :through => :repository
45 has_one :wiki, :dependent => :destroy
45 has_one :wiki, :dependent => :destroy
46 # Custom field for the project issues
46 # Custom field for the project issues
47 has_and_belongs_to_many :issue_custom_fields,
47 has_and_belongs_to_many :issue_custom_fields,
48 :class_name => 'IssueCustomField',
48 :class_name => 'IssueCustomField',
49 :order => "#{CustomField.table_name}.position",
49 :order => "#{CustomField.table_name}.position",
50 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
50 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 :association_foreign_key => 'custom_field_id'
51 :association_foreign_key => 'custom_field_id'
52
52
53 acts_as_nested_set :order => 'name', :dependent => :destroy
53 acts_as_nested_set :order => 'name', :dependent => :destroy
54 acts_as_attachable :view_permission => :view_files,
54 acts_as_attachable :view_permission => :view_files,
55 :delete_permission => :manage_files
55 :delete_permission => :manage_files
56
56
57 acts_as_customizable
57 acts_as_customizable
58 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
58 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
59 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
59 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
60 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
61 :author => nil
61 :author => nil
62
62
63 attr_protected :status, :enabled_module_names
63 attr_protected :status, :enabled_module_names
64
64
65 validates_presence_of :name, :identifier
65 validates_presence_of :name, :identifier
66 validates_uniqueness_of :name, :identifier
66 validates_uniqueness_of :name, :identifier
67 validates_associated :repository, :wiki
67 validates_associated :repository, :wiki
68 validates_length_of :name, :maximum => 30
68 validates_length_of :name, :maximum => 30
69 validates_length_of :homepage, :maximum => 255
69 validates_length_of :homepage, :maximum => 255
70 validates_length_of :identifier, :in => 1..20
70 validates_length_of :identifier, :in => 1..20
71 # donwcase letters, digits, dashes but not digits only
71 # donwcase letters, digits, dashes but not digits only
72 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
72 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 # reserved words
73 # reserved words
74 validates_exclusion_of :identifier, :in => %w( new )
74 validates_exclusion_of :identifier, :in => %w( new )
75
75
76 before_destroy :delete_all_members
76 before_destroy :delete_all_members
77
77
78 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
78 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
79 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
79 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 named_scope :all_public, { :conditions => { :is_public => true } }
80 named_scope :all_public, { :conditions => { :is_public => true } }
81 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
81 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82
82
83 def identifier=(identifier)
83 def identifier=(identifier)
84 super unless identifier_frozen?
84 super unless identifier_frozen?
85 end
85 end
86
86
87 def identifier_frozen?
87 def identifier_frozen?
88 errors[:identifier].nil? && !(new_record? || identifier.blank?)
88 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 end
89 end
90
90
91 # returns latest created projects
91 # returns latest created projects
92 # non public projects will be returned only if user is a member of those
92 # non public projects will be returned only if user is a member of those
93 def self.latest(user=nil, count=5)
93 def self.latest(user=nil, count=5)
94 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
94 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 end
95 end
96
96
97 # Returns a SQL :conditions string used to find all active projects for the specified user.
97 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 #
98 #
99 # Examples:
99 # Examples:
100 # Projects.visible_by(admin) => "projects.status = 1"
100 # Projects.visible_by(admin) => "projects.status = 1"
101 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
101 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 def self.visible_by(user=nil)
102 def self.visible_by(user=nil)
103 user ||= User.current
103 user ||= User.current
104 if user && user.admin?
104 if user && user.admin?
105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 elsif user && user.memberships.any?
106 elsif user && user.memberships.any?
107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
108 else
108 else
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 end
110 end
111 end
111 end
112
112
113 def self.allowed_to_condition(user, permission, options={})
113 def self.allowed_to_condition(user, permission, options={})
114 statements = []
114 statements = []
115 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
115 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 if perm = Redmine::AccessControl.permission(permission)
116 if perm = Redmine::AccessControl.permission(permission)
117 unless perm.project_module.nil?
117 unless perm.project_module.nil?
118 # If the permission belongs to a project module, make sure the module is enabled
118 # If the permission belongs to a project module, make sure the module is enabled
119 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
119 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 end
120 end
121 end
121 end
122 if options[:project]
122 if options[:project]
123 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
123 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
124 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 base_statement = "(#{project_statement}) AND (#{base_statement})"
125 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 end
126 end
127 if user.admin?
127 if user.admin?
128 # no restriction
128 # no restriction
129 else
129 else
130 statements << "1=0"
130 statements << "1=0"
131 if user.logged?
131 if user.logged?
132 if Role.non_member.allowed_to?(permission) && !options[:member]
132 if Role.non_member.allowed_to?(permission) && !options[:member]
133 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
133 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 end
134 end
135 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
135 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
136 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 else
137 else
138 if Role.anonymous.allowed_to?(permission) && !options[:member]
138 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 # anonymous user allowed on public project
139 # anonymous user allowed on public project
140 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
140 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 end
141 end
142 end
142 end
143 end
143 end
144 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
144 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 end
145 end
146
146
147 # Returns the Systemwide and project specific activities
147 # Returns the Systemwide and project specific activities
148 def activities(include_inactive=false)
148 def activities(include_inactive=false)
149 if include_inactive
149 if include_inactive
150 return all_activities
150 return all_activities
151 else
151 else
152 return active_activities
152 return active_activities
153 end
153 end
154 end
154 end
155
155
156 # Will create a new Project specific Activity or update an existing one
156 # Will create a new Project specific Activity or update an existing one
157 #
157 #
158 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
158 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 # does not successfully save.
159 # does not successfully save.
160 def update_or_create_time_entry_activity(id, activity_hash)
160 def update_or_create_time_entry_activity(id, activity_hash)
161 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
161 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 self.create_time_entry_activity_if_needed(activity_hash)
162 self.create_time_entry_activity_if_needed(activity_hash)
163 else
163 else
164 activity = project.time_entry_activities.find_by_id(id.to_i)
164 activity = project.time_entry_activities.find_by_id(id.to_i)
165 activity.update_attributes(activity_hash) if activity
165 activity.update_attributes(activity_hash) if activity
166 end
166 end
167 end
167 end
168
168
169 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
169 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 #
170 #
171 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
171 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 # does not successfully save.
172 # does not successfully save.
173 def create_time_entry_activity_if_needed(activity)
173 def create_time_entry_activity_if_needed(activity)
174 if activity['parent_id']
174 if activity['parent_id']
175
175
176 parent_activity = TimeEntryActivity.find(activity['parent_id'])
176 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 activity['name'] = parent_activity.name
177 activity['name'] = parent_activity.name
178 activity['position'] = parent_activity.position
178 activity['position'] = parent_activity.position
179
179
180 if Enumeration.overridding_change?(activity, parent_activity)
180 if Enumeration.overridding_change?(activity, parent_activity)
181 project_activity = self.time_entry_activities.create(activity)
181 project_activity = self.time_entry_activities.create(activity)
182
182
183 if project_activity.new_record?
183 if project_activity.new_record?
184 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
184 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 else
185 else
186 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
186 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 end
187 end
188 end
188 end
189 end
189 end
190 end
190 end
191
191
192 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
192 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 #
193 #
194 # Examples:
194 # Examples:
195 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
195 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 # project.project_condition(false) => "projects.id = 1"
196 # project.project_condition(false) => "projects.id = 1"
197 def project_condition(with_subprojects)
197 def project_condition(with_subprojects)
198 cond = "#{Project.table_name}.id = #{id}"
198 cond = "#{Project.table_name}.id = #{id}"
199 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
199 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 cond
200 cond
201 end
201 end
202
202
203 def self.find(*args)
203 def self.find(*args)
204 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
204 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 project = find_by_identifier(*args)
205 project = find_by_identifier(*args)
206 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
206 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 project
207 project
208 else
208 else
209 super
209 super
210 end
210 end
211 end
211 end
212
212
213 def to_param
213 def to_param
214 # id is used for projects with a numeric identifier (compatibility)
214 # id is used for projects with a numeric identifier (compatibility)
215 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
215 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 end
216 end
217
217
218 def active?
218 def active?
219 self.status == STATUS_ACTIVE
219 self.status == STATUS_ACTIVE
220 end
220 end
221
221
222 # Archives the project and its descendants recursively
222 # Archives the project and its descendants
223 def archive
223 def archive
224 # Archive subprojects if any
224 # Check that there is no issue of a non descendant project that is assigned
225 children.each do |subproject|
225 # to one of the project or descendant versions
226 subproject.archive
226 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
227 if v_ids.any? && Issue.find(:first, :include => :project,
228 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
229 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230 return false
227 end
231 end
228 update_attribute :status, STATUS_ARCHIVED
232 Project.transaction do
233 archive!
234 end
235 true
229 end
236 end
230
237
231 # Unarchives the project
238 # Unarchives the project
232 # All its ancestors must be active
239 # All its ancestors must be active
233 def unarchive
240 def unarchive
234 return false if ancestors.detect {|a| !a.active?}
241 return false if ancestors.detect {|a| !a.active?}
235 update_attribute :status, STATUS_ACTIVE
242 update_attribute :status, STATUS_ACTIVE
236 end
243 end
237
244
238 # Returns an array of projects the project can be moved to
245 # Returns an array of projects the project can be moved to
239 # by the current user
246 # by the current user
240 def allowed_parents
247 def allowed_parents
241 return @allowed_parents if @allowed_parents
248 return @allowed_parents if @allowed_parents
242 @allowed_parents = (Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_project, :member => true)) - self_and_descendants)
249 @allowed_parents = (Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_project, :member => true)) - self_and_descendants)
243 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
250 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
244 @allowed_parents << parent
251 @allowed_parents << parent
245 end
252 end
246 @allowed_parents
253 @allowed_parents
247 end
254 end
248
255
249 # Sets the parent of the project with authorization check
256 # Sets the parent of the project with authorization check
250 def set_allowed_parent!(p)
257 def set_allowed_parent!(p)
251 unless p.nil? || p.is_a?(Project)
258 unless p.nil? || p.is_a?(Project)
252 if p.to_s.blank?
259 if p.to_s.blank?
253 p = nil
260 p = nil
254 else
261 else
255 p = Project.find_by_id(p)
262 p = Project.find_by_id(p)
256 return false unless p
263 return false unless p
257 end
264 end
258 end
265 end
259 if p.nil?
266 if p.nil?
260 if !new_record? && allowed_parents.empty?
267 if !new_record? && allowed_parents.empty?
261 return false
268 return false
262 end
269 end
263 elsif !allowed_parents.include?(p)
270 elsif !allowed_parents.include?(p)
264 return false
271 return false
265 end
272 end
266 set_parent!(p)
273 set_parent!(p)
267 end
274 end
268
275
269 # Sets the parent of the project
276 # Sets the parent of the project
270 # Argument can be either a Project, a String, a Fixnum or nil
277 # Argument can be either a Project, a String, a Fixnum or nil
271 def set_parent!(p)
278 def set_parent!(p)
272 unless p.nil? || p.is_a?(Project)
279 unless p.nil? || p.is_a?(Project)
273 if p.to_s.blank?
280 if p.to_s.blank?
274 p = nil
281 p = nil
275 else
282 else
276 p = Project.find_by_id(p)
283 p = Project.find_by_id(p)
277 return false unless p
284 return false unless p
278 end
285 end
279 end
286 end
280 if p == parent && !p.nil?
287 if p == parent && !p.nil?
281 # Nothing to do
288 # Nothing to do
282 true
289 true
283 elsif p.nil? || (p.active? && move_possible?(p))
290 elsif p.nil? || (p.active? && move_possible?(p))
284 # Insert the project so that target's children or root projects stay alphabetically sorted
291 # Insert the project so that target's children or root projects stay alphabetically sorted
285 sibs = (p.nil? ? self.class.roots : p.children)
292 sibs = (p.nil? ? self.class.roots : p.children)
286 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
293 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
287 if to_be_inserted_before
294 if to_be_inserted_before
288 move_to_left_of(to_be_inserted_before)
295 move_to_left_of(to_be_inserted_before)
289 elsif p.nil?
296 elsif p.nil?
290 if sibs.empty?
297 if sibs.empty?
291 # move_to_root adds the project in first (ie. left) position
298 # move_to_root adds the project in first (ie. left) position
292 move_to_root
299 move_to_root
293 else
300 else
294 move_to_right_of(sibs.last) unless self == sibs.last
301 move_to_right_of(sibs.last) unless self == sibs.last
295 end
302 end
296 else
303 else
297 # move_to_child_of adds the project in last (ie.right) position
304 # move_to_child_of adds the project in last (ie.right) position
298 move_to_child_of(p)
305 move_to_child_of(p)
299 end
306 end
307 Issue.update_fixed_versions_from_project_hierarchy_change
300 true
308 true
301 else
309 else
302 # Can not move to the given target
310 # Can not move to the given target
303 false
311 false
304 end
312 end
305 end
313 end
306
314
307 # Returns an array of the trackers used by the project and its active sub projects
315 # Returns an array of the trackers used by the project and its active sub projects
308 def rolled_up_trackers
316 def rolled_up_trackers
309 @rolled_up_trackers ||=
317 @rolled_up_trackers ||=
310 Tracker.find(:all, :include => :projects,
318 Tracker.find(:all, :include => :projects,
311 :select => "DISTINCT #{Tracker.table_name}.*",
319 :select => "DISTINCT #{Tracker.table_name}.*",
312 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
320 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
313 :order => "#{Tracker.table_name}.position")
321 :order => "#{Tracker.table_name}.position")
314 end
322 end
315
323
316 # Closes open and locked project versions that are completed
324 # Closes open and locked project versions that are completed
317 def close_completed_versions
325 def close_completed_versions
318 Version.transaction do
326 Version.transaction do
319 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
327 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
320 if version.completed?
328 if version.completed?
321 version.update_attribute(:status, 'closed')
329 version.update_attribute(:status, 'closed')
322 end
330 end
323 end
331 end
324 end
332 end
325 end
333 end
326
334
335 # Returns a scope of the Versions used by the project
336 def shared_versions
337 @shared_versions ||=
338 Version.scoped(:include => :project,
339 :conditions => "#{Project.table_name}.id = #{id}" +
340 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
341 " #{Version.table_name}.sharing = 'system'" +
342 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
343 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
344 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
345 "))")
346 end
347
327 # Returns a hash of project users grouped by role
348 # Returns a hash of project users grouped by role
328 def users_by_role
349 def users_by_role
329 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
350 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
330 m.roles.each do |r|
351 m.roles.each do |r|
331 h[r] ||= []
352 h[r] ||= []
332 h[r] << m.user
353 h[r] << m.user
333 end
354 end
334 h
355 h
335 end
356 end
336 end
357 end
337
358
338 # Deletes all project's members
359 # Deletes all project's members
339 def delete_all_members
360 def delete_all_members
340 me, mr = Member.table_name, MemberRole.table_name
361 me, mr = Member.table_name, MemberRole.table_name
341 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
362 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
342 Member.delete_all(['project_id = ?', id])
363 Member.delete_all(['project_id = ?', id])
343 end
364 end
344
365
345 # Users issues can be assigned to
366 # Users issues can be assigned to
346 def assignable_users
367 def assignable_users
347 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
368 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
348 end
369 end
349
370
350 # Returns the mail adresses of users that should be always notified on project events
371 # Returns the mail adresses of users that should be always notified on project events
351 def recipients
372 def recipients
352 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
373 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
353 end
374 end
354
375
355 # Returns the users that should be notified on project events
376 # Returns the users that should be notified on project events
356 def notified_users
377 def notified_users
357 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
378 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
358 end
379 end
359
380
360 # Returns an array of all custom fields enabled for project issues
381 # Returns an array of all custom fields enabled for project issues
361 # (explictly associated custom fields and custom fields enabled for all projects)
382 # (explictly associated custom fields and custom fields enabled for all projects)
362 def all_issue_custom_fields
383 def all_issue_custom_fields
363 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
384 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
364 end
385 end
365
386
366 def project
387 def project
367 self
388 self
368 end
389 end
369
390
370 def <=>(project)
391 def <=>(project)
371 name.downcase <=> project.name.downcase
392 name.downcase <=> project.name.downcase
372 end
393 end
373
394
374 def to_s
395 def to_s
375 name
396 name
376 end
397 end
377
398
378 # Returns a short description of the projects (first lines)
399 # Returns a short description of the projects (first lines)
379 def short_description(length = 255)
400 def short_description(length = 255)
380 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
401 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
381 end
402 end
382
403
383 # Return true if this project is allowed to do the specified action.
404 # Return true if this project is allowed to do the specified action.
384 # action can be:
405 # action can be:
385 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
406 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
386 # * a permission Symbol (eg. :edit_project)
407 # * a permission Symbol (eg. :edit_project)
387 def allows_to?(action)
408 def allows_to?(action)
388 if action.is_a? Hash
409 if action.is_a? Hash
389 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
410 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
390 else
411 else
391 allowed_permissions.include? action
412 allowed_permissions.include? action
392 end
413 end
393 end
414 end
394
415
395 def module_enabled?(module_name)
416 def module_enabled?(module_name)
396 module_name = module_name.to_s
417 module_name = module_name.to_s
397 enabled_modules.detect {|m| m.name == module_name}
418 enabled_modules.detect {|m| m.name == module_name}
398 end
419 end
399
420
400 def enabled_module_names=(module_names)
421 def enabled_module_names=(module_names)
401 if module_names && module_names.is_a?(Array)
422 if module_names && module_names.is_a?(Array)
402 module_names = module_names.collect(&:to_s)
423 module_names = module_names.collect(&:to_s)
403 # remove disabled modules
424 # remove disabled modules
404 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
425 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
405 # add new modules
426 # add new modules
406 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
427 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
407 else
428 else
408 enabled_modules.clear
429 enabled_modules.clear
409 end
430 end
410 end
431 end
411
432
412 # Returns an auto-generated project identifier based on the last identifier used
433 # Returns an auto-generated project identifier based on the last identifier used
413 def self.next_identifier
434 def self.next_identifier
414 p = Project.find(:first, :order => 'created_on DESC')
435 p = Project.find(:first, :order => 'created_on DESC')
415 p.nil? ? nil : p.identifier.to_s.succ
436 p.nil? ? nil : p.identifier.to_s.succ
416 end
437 end
417
438
418 # Copies and saves the Project instance based on the +project+.
439 # Copies and saves the Project instance based on the +project+.
419 # Duplicates the source project's:
440 # Duplicates the source project's:
420 # * Wiki
441 # * Wiki
421 # * Versions
442 # * Versions
422 # * Categories
443 # * Categories
423 # * Issues
444 # * Issues
424 # * Members
445 # * Members
425 # * Queries
446 # * Queries
426 #
447 #
427 # Accepts an +options+ argument to specify what to copy
448 # Accepts an +options+ argument to specify what to copy
428 #
449 #
429 # Examples:
450 # Examples:
430 # project.copy(1) # => copies everything
451 # project.copy(1) # => copies everything
431 # project.copy(1, :only => 'members') # => copies members only
452 # project.copy(1, :only => 'members') # => copies members only
432 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
453 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
433 def copy(project, options={})
454 def copy(project, options={})
434 project = project.is_a?(Project) ? project : Project.find(project)
455 project = project.is_a?(Project) ? project : Project.find(project)
435
456
436 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
457 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
437 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
458 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
438
459
439 Project.transaction do
460 Project.transaction do
440 if save
461 if save
441 reload
462 reload
442 to_be_copied.each do |name|
463 to_be_copied.each do |name|
443 send "copy_#{name}", project
464 send "copy_#{name}", project
444 end
465 end
445 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
466 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
446 save
467 save
447 end
468 end
448 end
469 end
449 end
470 end
450
471
451
472
452 # Copies +project+ and returns the new instance. This will not save
473 # Copies +project+ and returns the new instance. This will not save
453 # the copy
474 # the copy
454 def self.copy_from(project)
475 def self.copy_from(project)
455 begin
476 begin
456 project = project.is_a?(Project) ? project : Project.find(project)
477 project = project.is_a?(Project) ? project : Project.find(project)
457 if project
478 if project
458 # clear unique attributes
479 # clear unique attributes
459 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
480 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
460 copy = Project.new(attributes)
481 copy = Project.new(attributes)
461 copy.enabled_modules = project.enabled_modules
482 copy.enabled_modules = project.enabled_modules
462 copy.trackers = project.trackers
483 copy.trackers = project.trackers
463 copy.custom_values = project.custom_values.collect {|v| v.clone}
484 copy.custom_values = project.custom_values.collect {|v| v.clone}
464 copy.issue_custom_fields = project.issue_custom_fields
485 copy.issue_custom_fields = project.issue_custom_fields
465 return copy
486 return copy
466 else
487 else
467 return nil
488 return nil
468 end
489 end
469 rescue ActiveRecord::RecordNotFound
490 rescue ActiveRecord::RecordNotFound
470 return nil
491 return nil
471 end
492 end
472 end
493 end
473
494
474 private
495 private
475
496
476 # Copies wiki from +project+
497 # Copies wiki from +project+
477 def copy_wiki(project)
498 def copy_wiki(project)
478 # Check that the source project has a wiki first
499 # Check that the source project has a wiki first
479 unless project.wiki.nil?
500 unless project.wiki.nil?
480 self.wiki ||= Wiki.new
501 self.wiki ||= Wiki.new
481 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
502 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
482 project.wiki.pages.each do |page|
503 project.wiki.pages.each do |page|
483 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
504 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
484 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
505 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
485 new_wiki_page.content = new_wiki_content
506 new_wiki_page.content = new_wiki_content
486 wiki.pages << new_wiki_page
507 wiki.pages << new_wiki_page
487 end
508 end
488 end
509 end
489 end
510 end
490
511
491 # Copies versions from +project+
512 # Copies versions from +project+
492 def copy_versions(project)
513 def copy_versions(project)
493 project.versions.each do |version|
514 project.versions.each do |version|
494 new_version = Version.new
515 new_version = Version.new
495 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
516 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
496 self.versions << new_version
517 self.versions << new_version
497 end
518 end
498 end
519 end
499
520
500 # Copies issue categories from +project+
521 # Copies issue categories from +project+
501 def copy_issue_categories(project)
522 def copy_issue_categories(project)
502 project.issue_categories.each do |issue_category|
523 project.issue_categories.each do |issue_category|
503 new_issue_category = IssueCategory.new
524 new_issue_category = IssueCategory.new
504 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
525 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
505 self.issue_categories << new_issue_category
526 self.issue_categories << new_issue_category
506 end
527 end
507 end
528 end
508
529
509 # Copies issues from +project+
530 # Copies issues from +project+
510 def copy_issues(project)
531 def copy_issues(project)
511 project.issues.each do |issue|
532 project.issues.each do |issue|
512 new_issue = Issue.new
533 new_issue = Issue.new
513 new_issue.copy_from(issue)
534 new_issue.copy_from(issue)
514 # Reassign fixed_versions by name, since names are unique per
535 # Reassign fixed_versions by name, since names are unique per
515 # project and the versions for self are not yet saved
536 # project and the versions for self are not yet saved
516 if issue.fixed_version
537 if issue.fixed_version
517 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
538 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
518 end
539 end
519 # Reassign the category by name, since names are unique per
540 # Reassign the category by name, since names are unique per
520 # project and the categories for self are not yet saved
541 # project and the categories for self are not yet saved
521 if issue.category
542 if issue.category
522 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
543 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
523 end
544 end
524 self.issues << new_issue
545 self.issues << new_issue
525 end
546 end
526 end
547 end
527
548
528 # Copies members from +project+
549 # Copies members from +project+
529 def copy_members(project)
550 def copy_members(project)
530 project.members.each do |member|
551 project.members.each do |member|
531 new_member = Member.new
552 new_member = Member.new
532 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
553 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
533 new_member.role_ids = member.role_ids.dup
554 new_member.role_ids = member.role_ids.dup
534 new_member.project = self
555 new_member.project = self
535 self.members << new_member
556 self.members << new_member
536 end
557 end
537 end
558 end
538
559
539 # Copies queries from +project+
560 # Copies queries from +project+
540 def copy_queries(project)
561 def copy_queries(project)
541 project.queries.each do |query|
562 project.queries.each do |query|
542 new_query = Query.new
563 new_query = Query.new
543 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
564 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
544 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
565 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
545 new_query.project = self
566 new_query.project = self
546 self.queries << new_query
567 self.queries << new_query
547 end
568 end
548 end
569 end
549
570
550 # Copies boards from +project+
571 # Copies boards from +project+
551 def copy_boards(project)
572 def copy_boards(project)
552 project.boards.each do |board|
573 project.boards.each do |board|
553 new_board = Board.new
574 new_board = Board.new
554 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
575 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
555 new_board.project = self
576 new_board.project = self
556 self.boards << new_board
577 self.boards << new_board
557 end
578 end
558 end
579 end
559
580
560 def allowed_permissions
581 def allowed_permissions
561 @allowed_permissions ||= begin
582 @allowed_permissions ||= begin
562 module_names = enabled_modules.collect {|m| m.name}
583 module_names = enabled_modules.collect {|m| m.name}
563 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
584 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
564 end
585 end
565 end
586 end
566
587
567 def allowed_actions
588 def allowed_actions
568 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
589 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
569 end
590 end
570
591
571 # Returns all the active Systemwide and project specific activities
592 # Returns all the active Systemwide and project specific activities
572 def active_activities
593 def active_activities
573 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
594 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
574
595
575 if overridden_activity_ids.empty?
596 if overridden_activity_ids.empty?
576 return TimeEntryActivity.shared.active
597 return TimeEntryActivity.shared.active
577 else
598 else
578 return system_activities_and_project_overrides
599 return system_activities_and_project_overrides
579 end
600 end
580 end
601 end
581
602
582 # Returns all the Systemwide and project specific activities
603 # Returns all the Systemwide and project specific activities
583 # (inactive and active)
604 # (inactive and active)
584 def all_activities
605 def all_activities
585 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
606 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
586
607
587 if overridden_activity_ids.empty?
608 if overridden_activity_ids.empty?
588 return TimeEntryActivity.shared
609 return TimeEntryActivity.shared
589 else
610 else
590 return system_activities_and_project_overrides(true)
611 return system_activities_and_project_overrides(true)
591 end
612 end
592 end
613 end
593
614
594 # Returns the systemwide active activities merged with the project specific overrides
615 # Returns the systemwide active activities merged with the project specific overrides
595 def system_activities_and_project_overrides(include_inactive=false)
616 def system_activities_and_project_overrides(include_inactive=false)
596 if include_inactive
617 if include_inactive
597 return TimeEntryActivity.shared.
618 return TimeEntryActivity.shared.
598 find(:all,
619 find(:all,
599 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
620 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
600 self.time_entry_activities
621 self.time_entry_activities
601 else
622 else
602 return TimeEntryActivity.shared.active.
623 return TimeEntryActivity.shared.active.
603 find(:all,
624 find(:all,
604 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
625 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
605 self.time_entry_activities.active
626 self.time_entry_activities.active
606 end
627 end
607 end
628 end
629
630 # Archives subprojects recursively
631 def archive!
632 children.each do |subproject|
633 subproject.send :archive!
634 end
635 update_attribute :status, STATUS_ARCHIVED
636 end
608 end
637 end
@@ -1,558 +1,558
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 end
30 end
31
31
32 def caption
32 def caption
33 l("field_#{name}")
33 l("field_#{name}")
34 end
34 end
35
35
36 # Returns true if the column is sortable, otherwise false
36 # Returns true if the column is sortable, otherwise false
37 def sortable?
37 def sortable?
38 !sortable.nil?
38 !sortable.nil?
39 end
39 end
40
40
41 def value(issue)
41 def value(issue)
42 issue.send name
42 issue.send name
43 end
43 end
44 end
44 end
45
45
46 class QueryCustomFieldColumn < QueryColumn
46 class QueryCustomFieldColumn < QueryColumn
47
47
48 def initialize(custom_field)
48 def initialize(custom_field)
49 self.name = "cf_#{custom_field.id}".to_sym
49 self.name = "cf_#{custom_field.id}".to_sym
50 self.sortable = custom_field.order_statement || false
50 self.sortable = custom_field.order_statement || false
51 if %w(list date bool int).include?(custom_field.field_format)
51 if %w(list date bool int).include?(custom_field.field_format)
52 self.groupable = custom_field.order_statement
52 self.groupable = custom_field.order_statement
53 end
53 end
54 self.groupable ||= false
54 self.groupable ||= false
55 @cf = custom_field
55 @cf = custom_field
56 end
56 end
57
57
58 def caption
58 def caption
59 @cf.name
59 @cf.name
60 end
60 end
61
61
62 def custom_field
62 def custom_field
63 @cf
63 @cf
64 end
64 end
65
65
66 def value(issue)
66 def value(issue)
67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv && @cf.cast_value(cv.value)
68 cv && @cf.cast_value(cv.value)
69 end
69 end
70 end
70 end
71
71
72 class Query < ActiveRecord::Base
72 class Query < ActiveRecord::Base
73 class StatementInvalid < ::ActiveRecord::StatementInvalid
73 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 end
74 end
75
75
76 belongs_to :project
76 belongs_to :project
77 belongs_to :user
77 belongs_to :user
78 serialize :filters
78 serialize :filters
79 serialize :column_names
79 serialize :column_names
80 serialize :sort_criteria, Array
80 serialize :sort_criteria, Array
81
81
82 attr_protected :project_id, :user_id
82 attr_protected :project_id, :user_id
83
83
84 validates_presence_of :name, :on => :save
84 validates_presence_of :name, :on => :save
85 validates_length_of :name, :maximum => 255
85 validates_length_of :name, :maximum => 255
86
86
87 @@operators = { "=" => :label_equals,
87 @@operators = { "=" => :label_equals,
88 "!" => :label_not_equals,
88 "!" => :label_not_equals,
89 "o" => :label_open_issues,
89 "o" => :label_open_issues,
90 "c" => :label_closed_issues,
90 "c" => :label_closed_issues,
91 "!*" => :label_none,
91 "!*" => :label_none,
92 "*" => :label_all,
92 "*" => :label_all,
93 ">=" => :label_greater_or_equal,
93 ">=" => :label_greater_or_equal,
94 "<=" => :label_less_or_equal,
94 "<=" => :label_less_or_equal,
95 "<t+" => :label_in_less_than,
95 "<t+" => :label_in_less_than,
96 ">t+" => :label_in_more_than,
96 ">t+" => :label_in_more_than,
97 "t+" => :label_in,
97 "t+" => :label_in,
98 "t" => :label_today,
98 "t" => :label_today,
99 "w" => :label_this_week,
99 "w" => :label_this_week,
100 ">t-" => :label_less_than_ago,
100 ">t-" => :label_less_than_ago,
101 "<t-" => :label_more_than_ago,
101 "<t-" => :label_more_than_ago,
102 "t-" => :label_ago,
102 "t-" => :label_ago,
103 "~" => :label_contains,
103 "~" => :label_contains,
104 "!~" => :label_not_contains }
104 "!~" => :label_not_contains }
105
105
106 cattr_reader :operators
106 cattr_reader :operators
107
107
108 @@operators_by_filter_type = { :list => [ "=", "!" ],
108 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 :list_status => [ "o", "=", "!", "c", "*" ],
109 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_optional => [ "=", "!", "!*", "*" ],
110 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_subprojects => [ "*", "!*", "=" ],
111 :list_subprojects => [ "*", "!*", "=" ],
112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :string => [ "=", "~", "!", "!~" ],
114 :string => [ "=", "~", "!", "!~" ],
115 :text => [ "~", "!~" ],
115 :text => [ "~", "!~" ],
116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117
117
118 cattr_reader :operators_by_filter_type
118 cattr_reader :operators_by_filter_type
119
119
120 @@available_columns = [
120 @@available_columns = [
121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
126 QueryColumn.new(:author),
126 QueryColumn.new(:author),
127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
136 ]
136 ]
137 cattr_reader :available_columns
137 cattr_reader :available_columns
138
138
139 def initialize(attributes = nil)
139 def initialize(attributes = nil)
140 super attributes
140 super attributes
141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
142 end
142 end
143
143
144 def after_initialize
144 def after_initialize
145 # Store the fact that project is nil (used in #editable_by?)
145 # Store the fact that project is nil (used in #editable_by?)
146 @is_for_all = project.nil?
146 @is_for_all = project.nil?
147 end
147 end
148
148
149 def validate
149 def validate
150 filters.each_key do |field|
150 filters.each_key do |field|
151 errors.add label_for(field), :blank unless
151 errors.add label_for(field), :blank unless
152 # filter requires one or more values
152 # filter requires one or more values
153 (values_for(field) and !values_for(field).first.blank?) or
153 (values_for(field) and !values_for(field).first.blank?) or
154 # filter doesn't require any value
154 # filter doesn't require any value
155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
156 end if filters
156 end if filters
157 end
157 end
158
158
159 def editable_by?(user)
159 def editable_by?(user)
160 return false unless user
160 return false unless user
161 # Admin can edit them all and regular users can edit their private queries
161 # Admin can edit them all and regular users can edit their private queries
162 return true if user.admin? || (!is_public && self.user_id == user.id)
162 return true if user.admin? || (!is_public && self.user_id == user.id)
163 # Members can not edit public queries that are for all project (only admin is allowed to)
163 # Members can not edit public queries that are for all project (only admin is allowed to)
164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
165 end
165 end
166
166
167 def available_filters
167 def available_filters
168 return @available_filters if @available_filters
168 return @available_filters if @available_filters
169
169
170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
171
171
172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
175 "subject" => { :type => :text, :order => 8 },
175 "subject" => { :type => :text, :order => 8 },
176 "created_on" => { :type => :date_past, :order => 9 },
176 "created_on" => { :type => :date_past, :order => 9 },
177 "updated_on" => { :type => :date_past, :order => 10 },
177 "updated_on" => { :type => :date_past, :order => 10 },
178 "start_date" => { :type => :date, :order => 11 },
178 "start_date" => { :type => :date, :order => 11 },
179 "due_date" => { :type => :date, :order => 12 },
179 "due_date" => { :type => :date, :order => 12 },
180 "estimated_hours" => { :type => :integer, :order => 13 },
180 "estimated_hours" => { :type => :integer, :order => 13 },
181 "done_ratio" => { :type => :integer, :order => 14 }}
181 "done_ratio" => { :type => :integer, :order => 14 }}
182
182
183 user_values = []
183 user_values = []
184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
185 if project
185 if project
186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
187 else
187 else
188 # members of the user's projects
188 # members of the user's projects
189 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
189 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
190 end
190 end
191 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
191 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
192 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
192 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
193
193
194 if User.current.logged?
194 if User.current.logged?
195 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
195 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
196 end
196 end
197
197
198 if project
198 if project
199 # project specific filters
199 # project specific filters
200 unless @project.issue_categories.empty?
200 unless @project.issue_categories.empty?
201 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
201 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
202 end
202 end
203 unless @project.versions.empty?
203 unless @project.shared_versions.empty?
204 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
204 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
205 end
205 end
206 unless @project.descendants.active.empty?
206 unless @project.descendants.active.empty?
207 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
207 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
208 end
208 end
209 add_custom_fields_filters(@project.all_issue_custom_fields)
209 add_custom_fields_filters(@project.all_issue_custom_fields)
210 else
210 else
211 # global filters for cross project issue list
211 # global filters for cross project issue list
212 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
212 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
213 end
213 end
214 @available_filters
214 @available_filters
215 end
215 end
216
216
217 def add_filter(field, operator, values)
217 def add_filter(field, operator, values)
218 # values must be an array
218 # values must be an array
219 return unless values and values.is_a? Array # and !values.first.empty?
219 return unless values and values.is_a? Array # and !values.first.empty?
220 # check if field is defined as an available filter
220 # check if field is defined as an available filter
221 if available_filters.has_key? field
221 if available_filters.has_key? field
222 filter_options = available_filters[field]
222 filter_options = available_filters[field]
223 # check if operator is allowed for that filter
223 # check if operator is allowed for that filter
224 #if @@operators_by_filter_type[filter_options[:type]].include? operator
224 #if @@operators_by_filter_type[filter_options[:type]].include? operator
225 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
225 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
226 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
226 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
227 #end
227 #end
228 filters[field] = {:operator => operator, :values => values }
228 filters[field] = {:operator => operator, :values => values }
229 end
229 end
230 end
230 end
231
231
232 def add_short_filter(field, expression)
232 def add_short_filter(field, expression)
233 return unless expression
233 return unless expression
234 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
234 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
235 add_filter field, (parms[0] || "="), [parms[1] || ""]
235 add_filter field, (parms[0] || "="), [parms[1] || ""]
236 end
236 end
237
237
238 def has_filter?(field)
238 def has_filter?(field)
239 filters and filters[field]
239 filters and filters[field]
240 end
240 end
241
241
242 def operator_for(field)
242 def operator_for(field)
243 has_filter?(field) ? filters[field][:operator] : nil
243 has_filter?(field) ? filters[field][:operator] : nil
244 end
244 end
245
245
246 def values_for(field)
246 def values_for(field)
247 has_filter?(field) ? filters[field][:values] : nil
247 has_filter?(field) ? filters[field][:values] : nil
248 end
248 end
249
249
250 def label_for(field)
250 def label_for(field)
251 label = available_filters[field][:name] if available_filters.has_key?(field)
251 label = available_filters[field][:name] if available_filters.has_key?(field)
252 label ||= field.gsub(/\_id$/, "")
252 label ||= field.gsub(/\_id$/, "")
253 end
253 end
254
254
255 def available_columns
255 def available_columns
256 return @available_columns if @available_columns
256 return @available_columns if @available_columns
257 @available_columns = Query.available_columns
257 @available_columns = Query.available_columns
258 @available_columns += (project ?
258 @available_columns += (project ?
259 project.all_issue_custom_fields :
259 project.all_issue_custom_fields :
260 IssueCustomField.find(:all)
260 IssueCustomField.find(:all)
261 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
261 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
262 end
262 end
263
263
264 # Returns an array of columns that can be used to group the results
264 # Returns an array of columns that can be used to group the results
265 def groupable_columns
265 def groupable_columns
266 available_columns.select {|c| c.groupable}
266 available_columns.select {|c| c.groupable}
267 end
267 end
268
268
269 def columns
269 def columns
270 if has_default_columns?
270 if has_default_columns?
271 available_columns.select do |c|
271 available_columns.select do |c|
272 # Adds the project column by default for cross-project lists
272 # Adds the project column by default for cross-project lists
273 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
273 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
274 end
274 end
275 else
275 else
276 # preserve the column_names order
276 # preserve the column_names order
277 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
277 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
278 end
278 end
279 end
279 end
280
280
281 def column_names=(names)
281 def column_names=(names)
282 if names
282 if names
283 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
283 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
284 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
284 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
285 # Set column_names to nil if default columns
285 # Set column_names to nil if default columns
286 if names.map(&:to_s) == Setting.issue_list_default_columns
286 if names.map(&:to_s) == Setting.issue_list_default_columns
287 names = nil
287 names = nil
288 end
288 end
289 end
289 end
290 write_attribute(:column_names, names)
290 write_attribute(:column_names, names)
291 end
291 end
292
292
293 def has_column?(column)
293 def has_column?(column)
294 column_names && column_names.include?(column.name)
294 column_names && column_names.include?(column.name)
295 end
295 end
296
296
297 def has_default_columns?
297 def has_default_columns?
298 column_names.nil? || column_names.empty?
298 column_names.nil? || column_names.empty?
299 end
299 end
300
300
301 def sort_criteria=(arg)
301 def sort_criteria=(arg)
302 c = []
302 c = []
303 if arg.is_a?(Hash)
303 if arg.is_a?(Hash)
304 arg = arg.keys.sort.collect {|k| arg[k]}
304 arg = arg.keys.sort.collect {|k| arg[k]}
305 end
305 end
306 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
306 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
307 write_attribute(:sort_criteria, c)
307 write_attribute(:sort_criteria, c)
308 end
308 end
309
309
310 def sort_criteria
310 def sort_criteria
311 read_attribute(:sort_criteria) || []
311 read_attribute(:sort_criteria) || []
312 end
312 end
313
313
314 def sort_criteria_key(arg)
314 def sort_criteria_key(arg)
315 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
315 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
316 end
316 end
317
317
318 def sort_criteria_order(arg)
318 def sort_criteria_order(arg)
319 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
319 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
320 end
320 end
321
321
322 # Returns the SQL sort order that should be prepended for grouping
322 # Returns the SQL sort order that should be prepended for grouping
323 def group_by_sort_order
323 def group_by_sort_order
324 if grouped? && (column = group_by_column)
324 if grouped? && (column = group_by_column)
325 column.sortable.is_a?(Array) ?
325 column.sortable.is_a?(Array) ?
326 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
326 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
327 "#{column.sortable} #{column.default_order}"
327 "#{column.sortable} #{column.default_order}"
328 end
328 end
329 end
329 end
330
330
331 # Returns true if the query is a grouped query
331 # Returns true if the query is a grouped query
332 def grouped?
332 def grouped?
333 !group_by.blank?
333 !group_by.blank?
334 end
334 end
335
335
336 def group_by_column
336 def group_by_column
337 groupable_columns.detect {|c| c.name.to_s == group_by}
337 groupable_columns.detect {|c| c.name.to_s == group_by}
338 end
338 end
339
339
340 def group_by_statement
340 def group_by_statement
341 group_by_column.groupable
341 group_by_column.groupable
342 end
342 end
343
343
344 def project_statement
344 def project_statement
345 project_clauses = []
345 project_clauses = []
346 if project && !@project.descendants.active.empty?
346 if project && !@project.descendants.active.empty?
347 ids = [project.id]
347 ids = [project.id]
348 if has_filter?("subproject_id")
348 if has_filter?("subproject_id")
349 case operator_for("subproject_id")
349 case operator_for("subproject_id")
350 when '='
350 when '='
351 # include the selected subprojects
351 # include the selected subprojects
352 ids += values_for("subproject_id").each(&:to_i)
352 ids += values_for("subproject_id").each(&:to_i)
353 when '!*'
353 when '!*'
354 # main project only
354 # main project only
355 else
355 else
356 # all subprojects
356 # all subprojects
357 ids += project.descendants.collect(&:id)
357 ids += project.descendants.collect(&:id)
358 end
358 end
359 elsif Setting.display_subprojects_issues?
359 elsif Setting.display_subprojects_issues?
360 ids += project.descendants.collect(&:id)
360 ids += project.descendants.collect(&:id)
361 end
361 end
362 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
362 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
363 elsif project
363 elsif project
364 project_clauses << "#{Project.table_name}.id = %d" % project.id
364 project_clauses << "#{Project.table_name}.id = %d" % project.id
365 end
365 end
366 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
366 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
367 project_clauses.join(' AND ')
367 project_clauses.join(' AND ')
368 end
368 end
369
369
370 def statement
370 def statement
371 # filters clauses
371 # filters clauses
372 filters_clauses = []
372 filters_clauses = []
373 filters.each_key do |field|
373 filters.each_key do |field|
374 next if field == "subproject_id"
374 next if field == "subproject_id"
375 v = values_for(field).clone
375 v = values_for(field).clone
376 next unless v and !v.empty?
376 next unless v and !v.empty?
377 operator = operator_for(field)
377 operator = operator_for(field)
378
378
379 # "me" value subsitution
379 # "me" value subsitution
380 if %w(assigned_to_id author_id watcher_id).include?(field)
380 if %w(assigned_to_id author_id watcher_id).include?(field)
381 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
381 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
382 end
382 end
383
383
384 sql = ''
384 sql = ''
385 if field =~ /^cf_(\d+)$/
385 if field =~ /^cf_(\d+)$/
386 # custom field
386 # custom field
387 db_table = CustomValue.table_name
387 db_table = CustomValue.table_name
388 db_field = 'value'
388 db_field = 'value'
389 is_custom_filter = true
389 is_custom_filter = true
390 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
390 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
391 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
391 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
392 elsif field == 'watcher_id'
392 elsif field == 'watcher_id'
393 db_table = Watcher.table_name
393 db_table = Watcher.table_name
394 db_field = 'user_id'
394 db_field = 'user_id'
395 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
395 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
396 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
396 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
397 else
397 else
398 # regular field
398 # regular field
399 db_table = Issue.table_name
399 db_table = Issue.table_name
400 db_field = field
400 db_field = field
401 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
401 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
402 end
402 end
403 filters_clauses << sql
403 filters_clauses << sql
404
404
405 end if filters and valid?
405 end if filters and valid?
406
406
407 (filters_clauses << project_statement).join(' AND ')
407 (filters_clauses << project_statement).join(' AND ')
408 end
408 end
409
409
410 # Returns the issue count
410 # Returns the issue count
411 def issue_count
411 def issue_count
412 Issue.count(:include => [:status, :project], :conditions => statement)
412 Issue.count(:include => [:status, :project], :conditions => statement)
413 rescue ::ActiveRecord::StatementInvalid => e
413 rescue ::ActiveRecord::StatementInvalid => e
414 raise StatementInvalid.new(e.message)
414 raise StatementInvalid.new(e.message)
415 end
415 end
416
416
417 # Returns the issue count by group or nil if query is not grouped
417 # Returns the issue count by group or nil if query is not grouped
418 def issue_count_by_group
418 def issue_count_by_group
419 r = nil
419 r = nil
420 if grouped?
420 if grouped?
421 begin
421 begin
422 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
422 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
423 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
423 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
424 rescue ActiveRecord::RecordNotFound
424 rescue ActiveRecord::RecordNotFound
425 r = {nil => issue_count}
425 r = {nil => issue_count}
426 end
426 end
427 c = group_by_column
427 c = group_by_column
428 if c.is_a?(QueryCustomFieldColumn)
428 if c.is_a?(QueryCustomFieldColumn)
429 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
429 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
430 end
430 end
431 end
431 end
432 r
432 r
433 rescue ::ActiveRecord::StatementInvalid => e
433 rescue ::ActiveRecord::StatementInvalid => e
434 raise StatementInvalid.new(e.message)
434 raise StatementInvalid.new(e.message)
435 end
435 end
436
436
437 # Returns the issues
437 # Returns the issues
438 # Valid options are :order, :offset, :limit, :include, :conditions
438 # Valid options are :order, :offset, :limit, :include, :conditions
439 def issues(options={})
439 def issues(options={})
440 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
440 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
441 order_option = nil if order_option.blank?
441 order_option = nil if order_option.blank?
442
442
443 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
443 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
444 :conditions => Query.merge_conditions(statement, options[:conditions]),
444 :conditions => Query.merge_conditions(statement, options[:conditions]),
445 :order => order_option,
445 :order => order_option,
446 :limit => options[:limit],
446 :limit => options[:limit],
447 :offset => options[:offset]
447 :offset => options[:offset]
448 rescue ::ActiveRecord::StatementInvalid => e
448 rescue ::ActiveRecord::StatementInvalid => e
449 raise StatementInvalid.new(e.message)
449 raise StatementInvalid.new(e.message)
450 end
450 end
451
451
452 # Returns the journals
452 # Returns the journals
453 # Valid options are :order, :offset, :limit
453 # Valid options are :order, :offset, :limit
454 def journals(options={})
454 def journals(options={})
455 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
455 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
456 :conditions => statement,
456 :conditions => statement,
457 :order => options[:order],
457 :order => options[:order],
458 :limit => options[:limit],
458 :limit => options[:limit],
459 :offset => options[:offset]
459 :offset => options[:offset]
460 rescue ::ActiveRecord::StatementInvalid => e
460 rescue ::ActiveRecord::StatementInvalid => e
461 raise StatementInvalid.new(e.message)
461 raise StatementInvalid.new(e.message)
462 end
462 end
463
463
464 # Returns the versions
464 # Returns the versions
465 # Valid options are :conditions
465 # Valid options are :conditions
466 def versions(options={})
466 def versions(options={})
467 Version.find :all, :include => :project,
467 Version.find :all, :include => :project,
468 :conditions => Query.merge_conditions(project_statement, options[:conditions])
468 :conditions => Query.merge_conditions(project_statement, options[:conditions])
469 rescue ::ActiveRecord::StatementInvalid => e
469 rescue ::ActiveRecord::StatementInvalid => e
470 raise StatementInvalid.new(e.message)
470 raise StatementInvalid.new(e.message)
471 end
471 end
472
472
473 private
473 private
474
474
475 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
475 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
476 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
476 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
477 sql = ''
477 sql = ''
478 case operator
478 case operator
479 when "="
479 when "="
480 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
480 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
481 when "!"
481 when "!"
482 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
482 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
483 when "!*"
483 when "!*"
484 sql = "#{db_table}.#{db_field} IS NULL"
484 sql = "#{db_table}.#{db_field} IS NULL"
485 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
485 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
486 when "*"
486 when "*"
487 sql = "#{db_table}.#{db_field} IS NOT NULL"
487 sql = "#{db_table}.#{db_field} IS NOT NULL"
488 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
488 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
489 when ">="
489 when ">="
490 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
490 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
491 when "<="
491 when "<="
492 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
492 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
493 when "o"
493 when "o"
494 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
494 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
495 when "c"
495 when "c"
496 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
496 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
497 when ">t-"
497 when ">t-"
498 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
498 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
499 when "<t-"
499 when "<t-"
500 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
500 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
501 when "t-"
501 when "t-"
502 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
502 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
503 when ">t+"
503 when ">t+"
504 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
504 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
505 when "<t+"
505 when "<t+"
506 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
506 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
507 when "t+"
507 when "t+"
508 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
508 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
509 when "t"
509 when "t"
510 sql = date_range_clause(db_table, db_field, 0, 0)
510 sql = date_range_clause(db_table, db_field, 0, 0)
511 when "w"
511 when "w"
512 from = l(:general_first_day_of_week) == '7' ?
512 from = l(:general_first_day_of_week) == '7' ?
513 # week starts on sunday
513 # week starts on sunday
514 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
514 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
515 # week starts on monday (Rails default)
515 # week starts on monday (Rails default)
516 Time.now.at_beginning_of_week
516 Time.now.at_beginning_of_week
517 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
517 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
518 when "~"
518 when "~"
519 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
519 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
520 when "!~"
520 when "!~"
521 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
521 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
522 end
522 end
523
523
524 return sql
524 return sql
525 end
525 end
526
526
527 def add_custom_fields_filters(custom_fields)
527 def add_custom_fields_filters(custom_fields)
528 @available_filters ||= {}
528 @available_filters ||= {}
529
529
530 custom_fields.select(&:is_filter?).each do |field|
530 custom_fields.select(&:is_filter?).each do |field|
531 case field.field_format
531 case field.field_format
532 when "text"
532 when "text"
533 options = { :type => :text, :order => 20 }
533 options = { :type => :text, :order => 20 }
534 when "list"
534 when "list"
535 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
535 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
536 when "date"
536 when "date"
537 options = { :type => :date, :order => 20 }
537 options = { :type => :date, :order => 20 }
538 when "bool"
538 when "bool"
539 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
539 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
540 else
540 else
541 options = { :type => :string, :order => 20 }
541 options = { :type => :string, :order => 20 }
542 end
542 end
543 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
543 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
544 end
544 end
545 end
545 end
546
546
547 # Returns a SQL clause for a date or datetime field.
547 # Returns a SQL clause for a date or datetime field.
548 def date_range_clause(table, field, from, to)
548 def date_range_clause(table, field, from, to)
549 s = []
549 s = []
550 if from
550 if from
551 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
551 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
552 end
552 end
553 if to
553 if to
554 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
554 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
555 end
555 end
556 s.join(' AND ')
556 s.join(' AND ')
557 end
557 end
558 end
558 end
@@ -1,163 +1,205
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Version < ActiveRecord::Base
18 class Version < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 after_update :update_issue_versions
20 belongs_to :project
21 belongs_to :project
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
22 acts_as_customizable
23 acts_as_customizable
23 acts_as_attachable :view_permission => :view_files,
24 acts_as_attachable :view_permission => :view_files,
24 :delete_permission => :manage_files
25 :delete_permission => :manage_files
25
26
26 VERSION_STATUSES = %w(open locked closed)
27 VERSION_STATUSES = %w(open locked closed)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
27
29
28 validates_presence_of :name
30 validates_presence_of :name
29 validates_uniqueness_of :name, :scope => [:project_id]
31 validates_uniqueness_of :name, :scope => [:project_id]
30 validates_length_of :name, :maximum => 60
32 validates_length_of :name, :maximum => 60
31 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
32 validates_inclusion_of :status, :in => VERSION_STATUSES
34 validates_inclusion_of :status, :in => VERSION_STATUSES
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
33
36
34 named_scope :open, :conditions => {:status => 'open'}
37 named_scope :open, :conditions => {:status => 'open'}
35
38 named_scope :visible, lambda {|*args| { :include => :project,
39 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
40
41 # Returns true if +user+ or current user is allowed to view the version
42 def visible?(user=User.current)
43 user.allowed_to?(:view_issues, self.project)
44 end
45
36 def start_date
46 def start_date
37 effective_date
47 effective_date
38 end
48 end
39
49
40 def due_date
50 def due_date
41 effective_date
51 effective_date
42 end
52 end
43
53
44 # Returns the total estimated time for this version
54 # Returns the total estimated time for this version
45 def estimated_hours
55 def estimated_hours
46 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
56 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
47 end
57 end
48
58
49 # Returns the total reported time for this version
59 # Returns the total reported time for this version
50 def spent_hours
60 def spent_hours
51 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
61 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
52 end
62 end
53
63
54 def closed?
64 def closed?
55 status == 'closed'
65 status == 'closed'
56 end
66 end
67
68 def open?
69 status == 'open'
70 end
57
71
58 # Returns true if the version is completed: due date reached and no open issues
72 # Returns true if the version is completed: due date reached and no open issues
59 def completed?
73 def completed?
60 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
74 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
61 end
75 end
62
76
63 # Returns the completion percentage of this version based on the amount of open/closed issues
77 # Returns the completion percentage of this version based on the amount of open/closed issues
64 # and the time spent on the open issues.
78 # and the time spent on the open issues.
65 def completed_pourcent
79 def completed_pourcent
66 if issues_count == 0
80 if issues_count == 0
67 0
81 0
68 elsif open_issues_count == 0
82 elsif open_issues_count == 0
69 100
83 100
70 else
84 else
71 issues_progress(false) + issues_progress(true)
85 issues_progress(false) + issues_progress(true)
72 end
86 end
73 end
87 end
74
88
75 # Returns the percentage of issues that have been marked as 'closed'.
89 # Returns the percentage of issues that have been marked as 'closed'.
76 def closed_pourcent
90 def closed_pourcent
77 if issues_count == 0
91 if issues_count == 0
78 0
92 0
79 else
93 else
80 issues_progress(false)
94 issues_progress(false)
81 end
95 end
82 end
96 end
83
97
84 # Returns true if the version is overdue: due date reached and some open issues
98 # Returns true if the version is overdue: due date reached and some open issues
85 def overdue?
99 def overdue?
86 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
100 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
87 end
101 end
88
102
89 # Returns assigned issues count
103 # Returns assigned issues count
90 def issues_count
104 def issues_count
91 @issue_count ||= fixed_issues.count
105 @issue_count ||= fixed_issues.count
92 end
106 end
93
107
94 # Returns the total amount of open issues for this version.
108 # Returns the total amount of open issues for this version.
95 def open_issues_count
109 def open_issues_count
96 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
110 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
97 end
111 end
98
112
99 # Returns the total amount of closed issues for this version.
113 # Returns the total amount of closed issues for this version.
100 def closed_issues_count
114 def closed_issues_count
101 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
115 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
102 end
116 end
103
117
104 def wiki_page
118 def wiki_page
105 if project.wiki && !wiki_page_title.blank?
119 if project.wiki && !wiki_page_title.blank?
106 @wiki_page ||= project.wiki.find_page(wiki_page_title)
120 @wiki_page ||= project.wiki.find_page(wiki_page_title)
107 end
121 end
108 @wiki_page
122 @wiki_page
109 end
123 end
110
124
111 def to_s; name end
125 def to_s; name end
112
126
113 # Versions are sorted by effective_date and name
127 # Versions are sorted by effective_date and name
114 # Those with no effective_date are at the end, sorted by name
128 # Those with no effective_date are at the end, sorted by name
115 def <=>(version)
129 def <=>(version)
116 if self.effective_date
130 if self.effective_date
117 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
131 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
118 else
132 else
119 version.effective_date ? 1 : (self.name <=> version.name)
133 version.effective_date ? 1 : (self.name <=> version.name)
120 end
134 end
121 end
135 end
122
136
137 # Returns the sharings that +user+ can set the version to
138 def allowed_sharings(user = User.current)
139 VERSION_SHARINGS.select do |s|
140 if sharing == s
141 true
142 else
143 case s
144 when 'system'
145 # Only admin users can set a systemwide sharing
146 user.admin?
147 when 'hierarchy', 'tree'
148 # Only users allowed to manage versions of the root project can
149 # set sharing to hierarchy or tree
150 project.nil? || user.allowed_to?(:manage_versions, project.root)
151 else
152 true
153 end
154 end
155 end
156 end
157
123 private
158 private
124 def check_integrity
159 def check_integrity
125 raise "Can't delete version" if self.fixed_issues.find(:first)
160 raise "Can't delete version" if self.fixed_issues.find(:first)
126 end
161 end
162
163 # Update the issue's fixed versions. Used if a version's sharing changes.
164 def update_issue_versions
165 if sharing_changed?
166 Issue.update_fixed_versions_from_project_hierarchy_change
167 end
168 end
127
169
128 # Returns the average estimated time of assigned issues
170 # Returns the average estimated time of assigned issues
129 # or 1 if no issue has an estimated time
171 # or 1 if no issue has an estimated time
130 # Used to weigth unestimated issues in progress calculation
172 # Used to weigth unestimated issues in progress calculation
131 def estimated_average
173 def estimated_average
132 if @estimated_average.nil?
174 if @estimated_average.nil?
133 average = fixed_issues.average(:estimated_hours).to_f
175 average = fixed_issues.average(:estimated_hours).to_f
134 if average == 0
176 if average == 0
135 average = 1
177 average = 1
136 end
178 end
137 @estimated_average = average
179 @estimated_average = average
138 end
180 end
139 @estimated_average
181 @estimated_average
140 end
182 end
141
183
142 # Returns the total progress of open or closed issues. The returned percentage takes into account
184 # Returns the total progress of open or closed issues. The returned percentage takes into account
143 # the amount of estimated time set for this version.
185 # the amount of estimated time set for this version.
144 #
186 #
145 # Examples:
187 # Examples:
146 # issues_progress(true) => returns the progress percentage for open issues.
188 # issues_progress(true) => returns the progress percentage for open issues.
147 # issues_progress(false) => returns the progress percentage for closed issues.
189 # issues_progress(false) => returns the progress percentage for closed issues.
148 def issues_progress(open)
190 def issues_progress(open)
149 @issues_progress ||= {}
191 @issues_progress ||= {}
150 @issues_progress[open] ||= begin
192 @issues_progress[open] ||= begin
151 progress = 0
193 progress = 0
152 if issues_count > 0
194 if issues_count > 0
153 ratio = open ? 'done_ratio' : 100
195 ratio = open ? 'done_ratio' : 100
154
196
155 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
197 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
156 :include => :status,
198 :include => :status,
157 :conditions => ["is_closed = ?", !open]).to_f
199 :conditions => ["is_closed = ?", !open]).to_f
158 progress = done / (estimated_average * issues_count)
200 progress = done / (estimated_average * issues_count)
159 end
201 end
160 progress
202 progress
161 end
203 end
162 end
204 end
163 end
205 end
@@ -1,36 +1,36
1 <% fields_for :issue, @issue, :builder => TabularFormBuilder do |f| %>
1 <% fields_for :issue, @issue, :builder => TabularFormBuilder do |f| %>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <% if @issue.new_record? || @allowed_statuses.any? %>
4 <% if @issue.new_record? || @allowed_statuses.any? %>
5 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
5 <p><%= f.select :status_id, (@allowed_statuses.collect {|p| [p.name, p.id]}), :required => true %></p>
6 <% else %>
6 <% else %>
7 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
7 <p><label><%= l(:field_status) %></label> <%= @issue.status.name %></p>
8 <% end %>
8 <% end %>
9
9
10 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
10 <p><%= f.select :priority_id, (@priorities.collect {|p| [p.name, p.id]}), :required => true %></p>
11 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
11 <p><%= f.select :assigned_to_id, (@issue.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true %></p>
12 <% unless @project.issue_categories.empty? %>
12 <% unless @project.issue_categories.empty? %>
13 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
13 <p><%= f.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>
14 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
14 <%= prompt_to_remote(image_tag('add.png', :style => 'vertical-align: middle;'),
15 l(:label_issue_category_new),
15 l(:label_issue_category_new),
16 'category[name]',
16 'category[name]',
17 {:controller => 'projects', :action => 'add_issue_category', :id => @project},
17 {:controller => 'projects', :action => 'add_issue_category', :id => @project},
18 :title => l(:label_issue_category_new),
18 :title => l(:label_issue_category_new),
19 :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
19 :tabindex => 199) if authorize_for('projects', 'add_issue_category') %></p>
20 <% end %>
20 <% end %>
21 <% unless @issue.assignable_versions.empty? %>
21 <% unless @issue.assignable_versions.empty? %>
22 <p><%= f.select :fixed_version_id, (@issue.assignable_versions.collect {|v| [v.name, v.id]}), :include_blank => true %></p>
22 <p><%= f.select :fixed_version_id, version_options_for_select(@issue.assignable_versions, @issue.fixed_version), :include_blank => true %></p>
23 <% end %>
23 <% end %>
24 </div>
24 </div>
25
25
26 <div class="splitcontentright">
26 <div class="splitcontentright">
27 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
27 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
28 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
28 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
29 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
29 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
30 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
30 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
31 </div>
31 </div>
32
32
33 <div style="clear:both;"> </div>
33 <div style="clear:both;"> </div>
34 <%= render :partial => 'form_custom_fields' %>
34 <%= render :partial => 'form_custom_fields' %>
35
35
36 <% end %>
36 <% end %>
@@ -1,62 +1,62
1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
1 <h2><%= l(:label_bulk_edit_selected_issues) %></h2>
2
2
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
3 <ul><%= @issues.collect {|i| content_tag('li', link_to(h("#{i.tracker} ##{i.id}"), { :action => 'show', :id => i }) + h(": #{i.subject}")) }.join("\n") %></ul>
4
4
5 <% form_tag() do %>
5 <% form_tag() do %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
6 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join %>
7 <div class="box">
7 <div class="box">
8 <fieldset>
8 <fieldset>
9 <legend><%= l(:label_change_properties) %></legend>
9 <legend><%= l(:label_change_properties) %></legend>
10 <p>
10 <p>
11 <label><%= l(:field_tracker) %>:
11 <label><%= l(:field_tracker) %>:
12 <%= select_tag('tracker_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.trackers, :id, :name)) %></label>
12 <%= select_tag('tracker_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@project.trackers, :id, :name)) %></label>
13 <% if @available_statuses.any? %>
13 <% if @available_statuses.any? %>
14 <label><%= l(:field_status) %>:
14 <label><%= l(:field_status) %>:
15 <%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
15 <%= select_tag('status_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(@available_statuses, :id, :name)) %></label>
16 <% end %>
16 <% end %>
17 </p>
17 </p>
18 <p>
18 <p>
19 <label><%= l(:field_priority) %>:
19 <label><%= l(:field_priority) %>:
20 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(IssuePriority.all, :id, :name)) %></label>
20 <%= select_tag('priority_id', "<option value=\"\">#{l(:label_no_change_option)}</option>" + options_from_collection_for_select(IssuePriority.all, :id, :name)) %></label>
21 <label><%= l(:field_category) %>:
21 <label><%= l(:field_category) %>:
22 <%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') +
22 <%= select_tag('category_id', content_tag('option', l(:label_no_change_option), :value => '') +
23 content_tag('option', l(:label_none), :value => 'none') +
23 content_tag('option', l(:label_none), :value => 'none') +
24 options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
24 options_from_collection_for_select(@project.issue_categories, :id, :name)) %></label>
25 </p>
25 </p>
26 <p>
26 <p>
27 <label><%= l(:field_assigned_to) %>:
27 <label><%= l(:field_assigned_to) %>:
28 <%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
28 <%= select_tag('assigned_to_id', content_tag('option', l(:label_no_change_option), :value => '') +
29 content_tag('option', l(:label_nobody), :value => 'none') +
29 content_tag('option', l(:label_nobody), :value => 'none') +
30 options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
30 options_from_collection_for_select(@project.assignable_users, :id, :name)) %></label>
31 <label><%= l(:field_fixed_version) %>:
31 <label><%= l(:field_fixed_version) %>:
32 <%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
32 <%= select_tag('fixed_version_id', content_tag('option', l(:label_no_change_option), :value => '') +
33 content_tag('option', l(:label_none), :value => 'none') +
33 content_tag('option', l(:label_none), :value => 'none') +
34 options_from_collection_for_select(@project.versions.open.sort, :id, :name)) %></label>
34 version_options_for_select(@project.shared_versions.open)) %></label>
35 </p>
35 </p>
36
36
37 <p>
37 <p>
38 <label><%= l(:field_start_date) %>:
38 <label><%= l(:field_start_date) %>:
39 <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
39 <%= text_field_tag 'start_date', '', :size => 10 %><%= calendar_for('start_date') %></label>
40 <label><%= l(:field_due_date) %>:
40 <label><%= l(:field_due_date) %>:
41 <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
41 <%= text_field_tag 'due_date', '', :size => 10 %><%= calendar_for('due_date') %></label>
42 <label><%= l(:field_done_ratio) %>:
42 <label><%= l(:field_done_ratio) %>:
43 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
43 <%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label>
44 </p>
44 </p>
45
45
46 <% @custom_fields.each do |custom_field| %>
46 <% @custom_fields.each do |custom_field| %>
47 <p><label><%= h(custom_field.name) %></label>
47 <p><label><%= h(custom_field.name) %></label>
48 <%= select_tag "custom_field_values[#{custom_field.id}]", options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values) %></label>
48 <%= select_tag "custom_field_values[#{custom_field.id}]", options_for_select([[l(:label_no_change_option), '']] + custom_field.possible_values) %></label>
49 </p>
49 </p>
50 <% end %>
50 <% end %>
51
51
52 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
52 <%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %>
53 </fieldset>
53 </fieldset>
54
54
55 <fieldset><legend><%= l(:field_notes) %></legend>
55 <fieldset><legend><%= l(:field_notes) %></legend>
56 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
56 <%= text_area_tag 'notes', @notes, :cols => 60, :rows => 10, :class => 'wiki-edit' %>
57 <%= wikitoolbar_for 'notes' %>
57 <%= wikitoolbar_for 'notes' %>
58 </fieldset>
58 </fieldset>
59 </div>
59 </div>
60
60
61 <p><%= submit_tag l(:button_submit) %>
61 <p><%= submit_tag l(:button_submit) %>
62 <% end %>
62 <% end %>
@@ -1,114 +1,114
1 <ul>
1 <ul>
2 <%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %>
2 <%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %>
3
3
4 <% if !@issue.nil? -%>
4 <% if !@issue.nil? -%>
5 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
5 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
6 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
6 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
7 <li class="folder">
7 <li class="folder">
8 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
8 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
9 <ul>
9 <ul>
10 <% @statuses.each do |s| -%>
10 <% @statuses.each do |s| -%>
11 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_to => @back}, :method => :post,
11 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_to => @back}, :method => :post,
12 :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
12 :selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
13 <% end -%>
13 <% end -%>
14 </ul>
14 </ul>
15 </li>
15 </li>
16 <% else %>
16 <% else %>
17 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
17 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
18 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
18 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
19 <% end %>
19 <% end %>
20
20
21 <% unless @trackers.nil? %>
21 <% unless @trackers.nil? %>
22 <li class="folder">
22 <li class="folder">
23 <a href="#" class="submenu"><%= l(:field_tracker) %></a>
23 <a href="#" class="submenu"><%= l(:field_tracker) %></a>
24 <ul>
24 <ul>
25 <% @trackers.each do |t| -%>
25 <% @trackers.each do |t| -%>
26 <li><%= context_menu_link t.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'tracker_id' => t, :back_to => @back}, :method => :post,
26 <li><%= context_menu_link t.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'tracker_id' => t, :back_to => @back}, :method => :post,
27 :selected => (@issue && t == @issue.tracker), :disabled => !@can[:edit] %></li>
27 :selected => (@issue && t == @issue.tracker), :disabled => !@can[:edit] %></li>
28 <% end -%>
28 <% end -%>
29 </ul>
29 </ul>
30 </li>
30 </li>
31 <% end %>
31 <% end %>
32 <li class="folder">
32 <li class="folder">
33 <a href="#" class="submenu"><%= l(:field_priority) %></a>
33 <a href="#" class="submenu"><%= l(:field_priority) %></a>
34 <ul>
34 <ul>
35 <% @priorities.each do |p| -%>
35 <% @priorities.each do |p| -%>
36 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'priority_id' => p, :back_to => @back}, :method => :post,
36 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'priority_id' => p, :back_to => @back}, :method => :post,
37 :selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %></li>
37 :selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %></li>
38 <% end -%>
38 <% end -%>
39 </ul>
39 </ul>
40 </li>
40 </li>
41 <% unless @project.nil? || @project.versions.open.empty? -%>
41 <% unless @project.nil? || @project.shared_versions.open.empty? -%>
42 <li class="folder">
42 <li class="folder">
43 <a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
43 <a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
44 <ul>
44 <ul>
45 <% @project.versions.open.sort.each do |v| -%>
45 <% @project.shared_versions.open.sort.each do |v| -%>
46 <li><%= context_menu_link v.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post,
46 <li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post,
47 :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
47 :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
48 <% end -%>
48 <% end -%>
49 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post,
49 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post,
50 :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
50 :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
51 </ul>
51 </ul>
52 </li>
52 </li>
53 <% end %>
53 <% end %>
54 <% unless @assignables.nil? || @assignables.empty? -%>
54 <% unless @assignables.nil? || @assignables.empty? -%>
55 <li class="folder">
55 <li class="folder">
56 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
56 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
57 <ul>
57 <ul>
58 <% @assignables.each do |u| -%>
58 <% @assignables.each do |u| -%>
59 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => u, :back_to => @back}, :method => :post,
59 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => u, :back_to => @back}, :method => :post,
60 :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li>
60 :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li>
61 <% end -%>
61 <% end -%>
62 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => 'none', :back_to => @back}, :method => :post,
62 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => 'none', :back_to => @back}, :method => :post,
63 :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
63 :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
64 </ul>
64 </ul>
65 </li>
65 </li>
66 <% end %>
66 <% end %>
67 <% unless @project.nil? || @project.issue_categories.empty? -%>
67 <% unless @project.nil? || @project.issue_categories.empty? -%>
68 <li class="folder">
68 <li class="folder">
69 <a href="#" class="submenu"><%= l(:field_category) %></a>
69 <a href="#" class="submenu"><%= l(:field_category) %></a>
70 <ul>
70 <ul>
71 <% @project.issue_categories.each do |u| -%>
71 <% @project.issue_categories.each do |u| -%>
72 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => u, :back_to => @back}, :method => :post,
72 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => u, :back_to => @back}, :method => :post,
73 :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li>
73 :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li>
74 <% end -%>
74 <% end -%>
75 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => 'none', :back_to => @back}, :method => :post,
75 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => 'none', :back_to => @back}, :method => :post,
76 :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
76 :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
77 </ul>
77 </ul>
78 </li>
78 </li>
79 <% end -%>
79 <% end -%>
80 <li class="folder">
80 <li class="folder">
81 <a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
81 <a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
82 <ul>
82 <ul>
83 <% (0..10).map{|x|x*10}.each do |p| -%>
83 <% (0..10).map{|x|x*10}.each do |p| -%>
84 <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post,
84 <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post,
85 :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
85 :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
86 <% end -%>
86 <% end -%>
87 </ul>
87 </ul>
88 </li>
88 </li>
89
89
90 <% if !@issue.nil? %>
90 <% if !@issue.nil? %>
91 <% if @can[:log_time] -%>
91 <% if @can[:log_time] -%>
92 <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue},
92 <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue},
93 :class => 'icon-time-add' %></li>
93 :class => 'icon-time-add' %></li>
94 <% end %>
94 <% end %>
95 <% if User.current.logged? %>
95 <% if User.current.logged? %>
96 <li><%= watcher_link(@issue, User.current) %></li>
96 <li><%= watcher_link(@issue, User.current) %></li>
97 <% end %>
97 <% end %>
98 <% end %>
98 <% end %>
99
99
100 <% if @issue.present? %>
100 <% if @issue.present? %>
101 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
101 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
102 :class => 'icon-copy', :disabled => !@can[:copy] %></li>
102 :class => 'icon-copy', :disabled => !@can[:copy] %></li>
103 <% else %>
103 <% else %>
104 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id), :copy_options => {:copy => 't'}},
104 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id), :copy_options => {:copy => 't'}},
105 :class => 'icon-copy', :disabled => !@can[:move] %></li>
105 :class => 'icon-copy', :disabled => !@can[:move] %></li>
106 <% end %>
106 <% end %>
107
107
108 <li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)},
108 <li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)},
109 :class => 'icon-move', :disabled => !@can[:move] %></li>
109 :class => 'icon-move', :disabled => !@can[:move] %></li>
110 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
110 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
111 :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
111 :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
112
112
113 <%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>
113 <%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>
114 </ul>
114 </ul>
@@ -1,61 +1,61
1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
1 <h2><%= @author.nil? ? l(:label_activity) : l(:label_user_activity, link_to_user(@author)) %></h2>
2 <p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
2 <p class="subtitle"><%= l(:label_date_from_to, :start => format_date(@date_to - @days), :end => format_date(@date_to-1)) %></p>
3
3
4 <div id="activity">
4 <div id="activity">
5 <% @events_by_day.keys.sort.reverse.each do |day| %>
5 <% @events_by_day.keys.sort.reverse.each do |day| %>
6 <h3><%= format_activity_day(day) %></h3>
6 <h3><%= format_activity_day(day) %></h3>
7 <dl>
7 <dl>
8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
8 <% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
9 <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
10 <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %>
11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
11 <span class="time"><%= format_time(e.event_datetime, false) %></span>
12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
12 <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
13 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
13 <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
14 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
14 <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
15 <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd>
15 <span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd>
16 <% end -%>
16 <% end -%>
17 </dl>
17 </dl>
18 <% end -%>
18 <% end -%>
19 </div>
19 </div>
20
20
21 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
21 <%= content_tag('p', l(:label_no_data), :class => 'nodata') if @events_by_day.empty? %>
22
22
23 <div style="float:left;">
23 <div style="float:left;">
24 <%= link_to_remote(('&#171; ' + l(:label_previous)),
24 <%= link_to_remote(('&#171; ' + l(:label_previous)),
25 {:update => "content", :url => params.merge(:from => @date_to - @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
25 {:update => "content", :url => params.merge(:from => @date_to - @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
26 {:href => url_for(params.merge(:from => @date_to - @days - 1)),
26 {:href => url_for(params.merge(:from => @date_to - @days - 1)),
27 :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))}) %>
27 :title => l(:label_date_from_to, :start => format_date(@date_to - 2*@days), :end => format_date(@date_to - @days - 1))}) %>
28 </div>
28 </div>
29 <div style="float:right;">
29 <div style="float:right;">
30 <%= link_to_remote((l(:label_next) + ' &#187;'),
30 <%= link_to_remote((l(:label_next) + ' &#187;'),
31 {:update => "content", :url => params.merge(:from => @date_to + @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
31 {:update => "content", :url => params.merge(:from => @date_to + @days - 1), :method => :get, :complete => 'window.scrollTo(0,0)'},
32 {:href => url_for(params.merge(:from => @date_to + @days - 1)),
32 {:href => url_for(params.merge(:from => @date_to + @days - 1)),
33 :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))}) unless @date_to >= Date.today %>
33 :title => l(:label_date_from_to, :start => format_date(@date_to), :end => format_date(@date_to + @days - 1))}) unless @date_to >= Date.today %>
34 </div>
34 </div>
35 &nbsp;
35 &nbsp;
36 <% other_formats_links do |f| %>
36 <% other_formats_links do |f| %>
37 <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %>
37 <%= f.link_to 'Atom', :url => params.merge(:from => nil, :key => User.current.rss_key) %>
38 <% end %>
38 <% end %>
39
39
40 <% content_for :header_tags do %>
40 <% content_for :header_tags do %>
41 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
41 <%= auto_discovery_link_tag(:atom, params.merge(:format => 'atom', :from => nil, :key => User.current.rss_key)) %>
42 <% end %>
42 <% end %>
43
43
44 <% content_for :sidebar do %>
44 <% content_for :sidebar do %>
45 <% form_tag({}, :method => :get) do %>
45 <% form_tag({}, :method => :get) do %>
46 <h3><%= l(:label_activity) %></h3>
46 <h3><%= l(:label_activity) %></h3>
47 <p><% @activity.event_types.each do |t| %>
47 <p><% @activity.event_types.each do |t| %>
48 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
48 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
49 <%= link_to(l("label_#{t.singularize}_plural"), {"show_#{t}" => 1, :user_id => params[:user_id]})%>
49 <%= link_to(l("label_#{t.singularize}_plural"), {"show_#{t}" => 1, :user_id => params[:user_id]})%>
50 <br />
50 <br />
51 <% end %></p>
51 <% end %></p>
52 <% if @project && @project.descendants.active.any? %>
52 <% if @project && @project.descendants.active.any? %>
53 <%= hidden_field_tag 'with_subprojects', 0 %>
53 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
54 <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
54 <%= hidden_field_tag 'with_subprojects', 0 %>
55 <% end %>
55 <% end %>
56 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
56 <%= hidden_field_tag('user_id', params[:user_id]) unless params[:user_id].blank? %>
57 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
57 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
58 <% end %>
58 <% end %>
59 <% end %>
59 <% end %>
60
60
61 <% html_title(l(:label_activity), @author) -%>
61 <% html_title(l(:label_activity), @author) -%>
@@ -1,43 +1,47
1 <h2><%=l(:label_change_log)%></h2>
1 <h2><%=l(:label_change_log)%></h2>
2
2
3 <% if @versions.empty? %>
3 <% if @versions.empty? %>
4 <p class="nodata"><%= l(:label_no_data) %></p>
4 <p class="nodata"><%= l(:label_no_data) %></p>
5 <% end %>
5 <% end %>
6
6
7 <% @versions.each do |version| %>
7 <% @versions.each do |version| %>
8 <%= tag 'a', :name => version.name %>
8 <%= tag 'a', :name => version.name %>
9 <h3 class="icon22 icon22-package"><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3>
9 <h3 class="icon22 icon22-package"><%= link_to_version version %></h3>
10 <% if version.effective_date %>
10 <% if version.effective_date %>
11 <p><%= format_date(version.effective_date) %></p>
11 <p><%= format_date(version.effective_date) %></p>
12 <% end %>
12 <% end %>
13 <p><%=h version.description %></p>
13 <p><%=h version.description %></p>
14 <% issues = version.fixed_issues.find(:all,
14 <% issues = version.fixed_issues.visible.find(:all,
15 :include => [:status, :tracker, :priority],
15 :include => [:status, :tracker, :priority],
16 :conditions => ["#{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')})", true],
16 :conditions => ["#{Issue.table_name}.project_id = ? AND #{IssueStatus.table_name}.is_closed=? AND #{Issue.table_name}.tracker_id in (?)", @project.id, true, @selected_tracker_ids],
17 :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty?
17 :order => "#{Tracker.table_name}.position") unless @selected_tracker_ids.empty?
18 issues ||= []
18 issues ||= []
19 %>
19 %>
20 <% if !issues.empty? %>
20 <% if !issues.empty? %>
21 <ul>
21 <ul>
22 <% issues.each do |issue| %>
22 <% issues.each do |issue| %>
23 <li><%= link_to_issue(issue) %></li>
23 <li><%= link_to_issue(issue) %></li>
24 <% end %>
24 <% end %>
25 </ul>
25 </ul>
26 <% end %>
26 <% end %>
27 <% end %>
27 <% end %>
28
28
29 <% content_for :sidebar do %>
29 <% content_for :sidebar do %>
30 <% form_tag({},:method => :get) do %>
30 <% form_tag({},:method => :get) do %>
31 <h3><%= l(:label_change_log) %></h3>
31 <h3><%= l(:label_change_log) %></h3>
32 <% @trackers.each do |tracker| %>
32 <% @trackers.each do |tracker| %>
33 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %>
33 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %>
34 <%= tracker.name %></label><br />
34 <%= tracker.name %></label><br />
35 <% end %>
35 <% end %>
36 <% if @project.descendants.active.any? %>
37 <%= hidden_field_tag 'with_subprojects', 0 %>
38 <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
39 <% end %>
36 <p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
40 <p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
37 <% end %>
41 <% end %>
38
42
39 <h3><%= l(:label_version_plural) %></h3>
43 <h3><%= l(:label_version_plural) %></h3>
40 <% @versions.each do |version| %>
44 <% @versions.each do |version| %>
41 <%= link_to version.name, :anchor => version.name %><br />
45 <%= link_to format_version_name(version), :anchor => version.name %><br />
42 <% end %>
46 <% end %>
43 <% end %>
47 <% end %>
@@ -1,51 +1,49
1 <h2><%=l(:label_roadmap)%></h2>
1 <h2><%=l(:label_roadmap)%></h2>
2
2
3 <% if @versions.empty? %>
3 <% if @versions.empty? %>
4 <p class="nodata"><%= l(:label_no_data) %></p>
4 <p class="nodata"><%= l(:label_no_data) %></p>
5 <% else %>
5 <% else %>
6 <div id="roadmap">
6 <div id="roadmap">
7 <% @versions.each do |version| %>
7 <% @versions.each do |version| %>
8 <%= tag 'a', :name => version.name %>
8 <%= tag 'a', :name => version.name %>
9 <h3 class="icon22 icon22-package"><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></h3>
9 <h3 class="icon22 icon22-package"><%= link_to_version version %></h3>
10 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
10 <%= render :partial => 'versions/overview', :locals => {:version => version} %>
11 <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
11 <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
12
12
13 <% issues = version.fixed_issues.find(:all,
13 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
14 :include => [:status, :tracker, :priority],
15 :conditions => ["tracker_id in (#{@selected_tracker_ids.join(',')})"],
16 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") unless @selected_tracker_ids.empty?
17 issues ||= []
18 %>
19 <% if issues.size > 0 %>
20 <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
14 <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
21 <ul>
15 <ul>
22 <%- issues.each do |issue| -%>
16 <%- issues.each do |issue| -%>
23 <li><%= link_to_issue(issue) %></li>
17 <li><%= link_to_issue(issue) %></li>
24 <%- end -%>
18 <%- end -%>
25 </ul>
19 </ul>
26 </fieldset>
20 </fieldset>
27 <% end %>
21 <% end %>
28 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
22 <%= call_hook :view_projects_roadmap_version_bottom, :version => version %>
29 <% end %>
23 <% end %>
30 </div>
24 </div>
31 <% end %>
25 <% end %>
32
26
33 <% content_for :sidebar do %>
27 <% content_for :sidebar do %>
34 <% form_tag({}, :method => :get) do %>
28 <% form_tag({}, :method => :get) do %>
35 <h3><%= l(:label_roadmap) %></h3>
29 <h3><%= l(:label_roadmap) %></h3>
36 <% @trackers.each do |tracker| %>
30 <% @trackers.each do |tracker| %>
37 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
31 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
38 <%= tracker.name %></label><br />
32 <%= tracker.name %></label><br />
39 <% end %>
33 <% end %>
40 <br />
34 <br />
41 <label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
35 <label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
36 <% if @project.descendants.active.any? %>
37 <%= hidden_field_tag 'with_subprojects', 0 %>
38 <br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
39 <% end %>
42 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
40 <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
43 <% end %>
41 <% end %>
44
42
45 <h3><%= l(:label_version_plural) %></h3>
43 <h3><%= l(:label_version_plural) %></h3>
46 <% @versions.each do |version| %>
44 <% @versions.each do |version| %>
47 <%= link_to version.name, "##{version.name}" %><br />
45 <%= link_to format_version_name(version), "##{version.name}" %><br />
48 <% end %>
46 <% end %>
49 <% end %>
47 <% end %>
50
48
51 <% html_title(l(:label_roadmap)) %>
49 <% html_title(l(:label_roadmap)) %>
@@ -1,37 +1,39
1 <% if @project.versions.any? %>
1 <% if @project.versions.any? %>
2 <table class="list versions">
2 <table class="list versions">
3 <thead>
3 <thead>
4 <th><%= l(:label_version) %></th>
4 <th><%= l(:label_version) %></th>
5 <th><%= l(:field_effective_date) %></th>
5 <th><%= l(:field_effective_date) %></th>
6 <th><%= l(:field_description) %></th>
6 <th><%= l(:field_description) %></th>
7 <th><%= l(:field_status) %></th>
7 <th><%= l(:field_status) %></th>
8 <th><%= l(:field_sharing) %></th>
8 <th><%= l(:label_wiki_page) unless @project.wiki.nil? %></th>
9 <th><%= l(:label_wiki_page) unless @project.wiki.nil? %></th>
9 <th style="width:15%"></th>
10 <th style="width:15%"></th>
10 </thead>
11 </thead>
11 <tbody>
12 <tbody>
12 <% for version in @project.versions.sort %>
13 <% for version in @project.versions.sort %>
13 <tr class="version <%= cycle 'odd', 'even' %> <%=h version.status %>">
14 <tr class="version <%= cycle 'odd', 'even' %> <%=h version.status %>">
14 <td><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></td>
15 <td><%= link_to h(version.name), :controller => 'versions', :action => 'show', :id => version %></td>
15 <td align="center"><%= format_date(version.effective_date) %></td>
16 <td align="center"><%= format_date(version.effective_date) %></td>
16 <td><%=h version.description %></td>
17 <td><%=h version.description %></td>
17 <td><%= l("version_status_#{version.status}") %></td>
18 <td><%= l("version_status_#{version.status}") %></td>
19 <td><%=h format_version_sharing(version.sharing) %></td>
18 <td><%= link_to(h(version.wiki_page_title), :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %></td>
20 <td><%= link_to(h(version.wiki_page_title), :controller => 'wiki', :page => Wiki.titleize(version.wiki_page_title)) unless version.wiki_page_title.blank? || @project.wiki.nil? %></td>
19 <td class="buttons">
21 <td class="buttons">
20 <%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %>
22 <%= link_to_if_authorized l(:button_edit), {:controller => 'versions', :action => 'edit', :id => version }, :class => 'icon icon-edit' %>
21 <%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
23 <%= link_to_if_authorized l(:button_delete), {:controller => 'versions', :action => 'destroy', :id => version}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
22 </td>
24 </td>
23 </tr>
25 </tr>
24 <% end; reset_cycle %>
26 <% end; reset_cycle %>
25 </tbody>
27 </tbody>
26 </table>
28 </table>
27 <% else %>
29 <% else %>
28 <p class="nodata"><%= l(:label_no_data) %></p>
30 <p class="nodata"><%= l(:label_no_data) %></p>
29 <% end %>
31 <% end %>
30
32
31 <div class="contextual">
33 <div class="contextual">
32 <% if @project.versions.any? %>
34 <% if @project.versions.any? %>
33 <%= link_to 'Close completed versions', {:controller => 'versions', :action => 'close_completed', :project_id => @project}, :method => :post %>
35 <%= link_to 'Close completed versions', {:controller => 'versions', :action => 'close_completed', :project_id => @project}, :method => :post %>
34 <% end %>
36 <% end %>
35 </div>
37 </div>
36
38
37 <p><%= link_to_if_authorized l(:label_version_new), :controller => 'projects', :action => 'add_version', :id => @project %></p>
39 <p><%= link_to_if_authorized l(:label_version_new), :controller => 'projects', :action => 'add_version', :id => @project %></p>
@@ -1,13 +1,15
1 <%= error_messages_for 'version' %>
1 <%= error_messages_for 'version' %>
2
2
3 <div class="box">
3 <div class="box">
4 <p><%= f.text_field :name, :size => 60, :required => true %></p>
4 <p><%= f.text_field :name, :size => 60, :required => true %></p>
5 <p><%= f.text_field :description, :size => 60 %></p>
5 <p><%= f.text_field :description, :size => 60 %></p>
6 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
6 <p><%= f.select :status, Version::VERSION_STATUSES.collect {|s| [l("version_status_#{s}"), s]} %></p>
7 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
7 <p><%= f.text_field :wiki_page_title, :label => :label_wiki_page, :size => 60, :disabled => @project.wiki.nil? %></p>
8 <p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
8 <p><%= f.text_field :effective_date, :size => 10 %><%= calendar_for('version_effective_date') %></p>
9 <p><%= f.select :sharing, @version.allowed_sharings.collect {|v| [format_version_sharing(v), v]} %></p>
9
10
10 <% @version.custom_field_values.each do |value| %>
11 <% @version.custom_field_values.each do |value| %>
11 <p><%= custom_field_tag_with_label :version, value %></p>
12 <p><%= custom_field_tag_with_label :version, value %></p>
12 <% end %>
13 <% end %>
14
13 </div>
15 </div>
@@ -1,843 +1,850
1 en:
1 en:
2 date:
2 date:
3 formats:
3 formats:
4 # Use the strftime parameters for formats.
4 # Use the strftime parameters for formats.
5 # When no format has been given, it uses default.
5 # When no format has been given, it uses default.
6 # You can provide other formats here if you like!
6 # You can provide other formats here if you like!
7 default: "%m/%d/%Y"
7 default: "%m/%d/%Y"
8 short: "%b %d"
8 short: "%b %d"
9 long: "%B %d, %Y"
9 long: "%B %d, %Y"
10
10
11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
11 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
12 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
13
13
14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
14 # Don't forget the nil at the beginning; there's no such thing as a 0th month
15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
15 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
16 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
17 # Used in date_select and datime_select.
17 # Used in date_select and datime_select.
18 order: [ :year, :month, :day ]
18 order: [ :year, :month, :day ]
19
19
20 time:
20 time:
21 formats:
21 formats:
22 default: "%m/%d/%Y %I:%M %p"
22 default: "%m/%d/%Y %I:%M %p"
23 time: "%I:%M %p"
23 time: "%I:%M %p"
24 short: "%d %b %H:%M"
24 short: "%d %b %H:%M"
25 long: "%B %d, %Y %H:%M"
25 long: "%B %d, %Y %H:%M"
26 am: "am"
26 am: "am"
27 pm: "pm"
27 pm: "pm"
28
28
29 datetime:
29 datetime:
30 distance_in_words:
30 distance_in_words:
31 half_a_minute: "half a minute"
31 half_a_minute: "half a minute"
32 less_than_x_seconds:
32 less_than_x_seconds:
33 one: "less than 1 second"
33 one: "less than 1 second"
34 other: "less than {{count}} seconds"
34 other: "less than {{count}} seconds"
35 x_seconds:
35 x_seconds:
36 one: "1 second"
36 one: "1 second"
37 other: "{{count}} seconds"
37 other: "{{count}} seconds"
38 less_than_x_minutes:
38 less_than_x_minutes:
39 one: "less than a minute"
39 one: "less than a minute"
40 other: "less than {{count}} minutes"
40 other: "less than {{count}} minutes"
41 x_minutes:
41 x_minutes:
42 one: "1 minute"
42 one: "1 minute"
43 other: "{{count}} minutes"
43 other: "{{count}} minutes"
44 about_x_hours:
44 about_x_hours:
45 one: "about 1 hour"
45 one: "about 1 hour"
46 other: "about {{count}} hours"
46 other: "about {{count}} hours"
47 x_days:
47 x_days:
48 one: "1 day"
48 one: "1 day"
49 other: "{{count}} days"
49 other: "{{count}} days"
50 about_x_months:
50 about_x_months:
51 one: "about 1 month"
51 one: "about 1 month"
52 other: "about {{count}} months"
52 other: "about {{count}} months"
53 x_months:
53 x_months:
54 one: "1 month"
54 one: "1 month"
55 other: "{{count}} months"
55 other: "{{count}} months"
56 about_x_years:
56 about_x_years:
57 one: "about 1 year"
57 one: "about 1 year"
58 other: "about {{count}} years"
58 other: "about {{count}} years"
59 over_x_years:
59 over_x_years:
60 one: "over 1 year"
60 one: "over 1 year"
61 other: "over {{count}} years"
61 other: "over {{count}} years"
62
62
63 number:
63 number:
64 human:
64 human:
65 format:
65 format:
66 delimiter: ""
66 delimiter: ""
67 precision: 1
67 precision: 1
68 storage_units:
68 storage_units:
69 format: "%n %u"
69 format: "%n %u"
70 units:
70 units:
71 byte:
71 byte:
72 one: "Byte"
72 one: "Byte"
73 other: "Bytes"
73 other: "Bytes"
74 kb: "KB"
74 kb: "KB"
75 mb: "MB"
75 mb: "MB"
76 gb: "GB"
76 gb: "GB"
77 tb: "TB"
77 tb: "TB"
78
78
79
79
80 # Used in array.to_sentence.
80 # Used in array.to_sentence.
81 support:
81 support:
82 array:
82 array:
83 sentence_connector: "and"
83 sentence_connector: "and"
84 skip_last_comma: false
84 skip_last_comma: false
85
85
86 activerecord:
86 activerecord:
87 errors:
87 errors:
88 messages:
88 messages:
89 inclusion: "is not included in the list"
89 inclusion: "is not included in the list"
90 exclusion: "is reserved"
90 exclusion: "is reserved"
91 invalid: "is invalid"
91 invalid: "is invalid"
92 confirmation: "doesn't match confirmation"
92 confirmation: "doesn't match confirmation"
93 accepted: "must be accepted"
93 accepted: "must be accepted"
94 empty: "can't be empty"
94 empty: "can't be empty"
95 blank: "can't be blank"
95 blank: "can't be blank"
96 too_long: "is too long (maximum is {{count}} characters)"
96 too_long: "is too long (maximum is {{count}} characters)"
97 too_short: "is too short (minimum is {{count}} characters)"
97 too_short: "is too short (minimum is {{count}} characters)"
98 wrong_length: "is the wrong length (should be {{count}} characters)"
98 wrong_length: "is the wrong length (should be {{count}} characters)"
99 taken: "has already been taken"
99 taken: "has already been taken"
100 not_a_number: "is not a number"
100 not_a_number: "is not a number"
101 not_a_date: "is not a valid date"
101 not_a_date: "is not a valid date"
102 greater_than: "must be greater than {{count}}"
102 greater_than: "must be greater than {{count}}"
103 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
103 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
104 equal_to: "must be equal to {{count}}"
104 equal_to: "must be equal to {{count}}"
105 less_than: "must be less than {{count}}"
105 less_than: "must be less than {{count}}"
106 less_than_or_equal_to: "must be less than or equal to {{count}}"
106 less_than_or_equal_to: "must be less than or equal to {{count}}"
107 odd: "must be odd"
107 odd: "must be odd"
108 even: "must be even"
108 even: "must be even"
109 greater_than_start_date: "must be greater than start date"
109 greater_than_start_date: "must be greater than start date"
110 not_same_project: "doesn't belong to the same project"
110 not_same_project: "doesn't belong to the same project"
111 circular_dependency: "This relation would create a circular dependency"
111 circular_dependency: "This relation would create a circular dependency"
112
112
113 actionview_instancetag_blank_option: Please select
113 actionview_instancetag_blank_option: Please select
114
114
115 general_text_No: 'No'
115 general_text_No: 'No'
116 general_text_Yes: 'Yes'
116 general_text_Yes: 'Yes'
117 general_text_no: 'no'
117 general_text_no: 'no'
118 general_text_yes: 'yes'
118 general_text_yes: 'yes'
119 general_lang_name: 'English'
119 general_lang_name: 'English'
120 general_csv_separator: ','
120 general_csv_separator: ','
121 general_csv_decimal_separator: '.'
121 general_csv_decimal_separator: '.'
122 general_csv_encoding: ISO-8859-1
122 general_csv_encoding: ISO-8859-1
123 general_pdf_encoding: ISO-8859-1
123 general_pdf_encoding: ISO-8859-1
124 general_first_day_of_week: '7'
124 general_first_day_of_week: '7'
125
125
126 notice_account_updated: Account was successfully updated.
126 notice_account_updated: Account was successfully updated.
127 notice_account_invalid_creditentials: Invalid user or password
127 notice_account_invalid_creditentials: Invalid user or password
128 notice_account_password_updated: Password was successfully updated.
128 notice_account_password_updated: Password was successfully updated.
129 notice_account_wrong_password: Wrong password
129 notice_account_wrong_password: Wrong password
130 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
130 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
131 notice_account_unknown_email: Unknown user.
131 notice_account_unknown_email: Unknown user.
132 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
132 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
133 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
133 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
134 notice_account_activated: Your account has been activated. You can now log in.
134 notice_account_activated: Your account has been activated. You can now log in.
135 notice_successful_create: Successful creation.
135 notice_successful_create: Successful creation.
136 notice_successful_update: Successful update.
136 notice_successful_update: Successful update.
137 notice_successful_delete: Successful deletion.
137 notice_successful_delete: Successful deletion.
138 notice_successful_connection: Successful connection.
138 notice_successful_connection: Successful connection.
139 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
139 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
140 notice_locking_conflict: Data has been updated by another user.
140 notice_locking_conflict: Data has been updated by another user.
141 notice_not_authorized: You are not authorized to access this page.
141 notice_not_authorized: You are not authorized to access this page.
142 notice_email_sent: "An email was sent to {{value}}"
142 notice_email_sent: "An email was sent to {{value}}"
143 notice_email_error: "An error occurred while sending mail ({{value}})"
143 notice_email_error: "An error occurred while sending mail ({{value}})"
144 notice_feeds_access_key_reseted: Your RSS access key was reset.
144 notice_feeds_access_key_reseted: Your RSS access key was reset.
145 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
145 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
146 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
146 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
147 notice_account_pending: "Your account was created and is now pending administrator approval."
147 notice_account_pending: "Your account was created and is now pending administrator approval."
148 notice_default_data_loaded: Default configuration successfully loaded.
148 notice_default_data_loaded: Default configuration successfully loaded.
149 notice_unable_delete_version: Unable to delete version.
149 notice_unable_delete_version: Unable to delete version.
150
150
151 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
151 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
152 error_scm_not_found: "The entry or revision was not found in the repository."
152 error_scm_not_found: "The entry or revision was not found in the repository."
153 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
153 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
154 error_scm_annotate: "The entry does not exist or can not be annotated."
154 error_scm_annotate: "The entry does not exist or can not be annotated."
155 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
155 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
156 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
156 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
157 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
157 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
158 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
158 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
159 error_can_not_archive_project: This project can not be archived
159
160
160 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
161 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
161
162
162 mail_subject_lost_password: "Your {{value}} password"
163 mail_subject_lost_password: "Your {{value}} password"
163 mail_body_lost_password: 'To change your password, click on the following link:'
164 mail_body_lost_password: 'To change your password, click on the following link:'
164 mail_subject_register: "Your {{value}} account activation"
165 mail_subject_register: "Your {{value}} account activation"
165 mail_body_register: 'To activate your account, click on the following link:'
166 mail_body_register: 'To activate your account, click on the following link:'
166 mail_body_account_information_external: "You can use your {{value}} account to log in."
167 mail_body_account_information_external: "You can use your {{value}} account to log in."
167 mail_body_account_information: Your account information
168 mail_body_account_information: Your account information
168 mail_subject_account_activation_request: "{{value}} account activation request"
169 mail_subject_account_activation_request: "{{value}} account activation request"
169 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
170 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
170 mail_subject_reminder: "{{count}} issue(s) due in the next days"
171 mail_subject_reminder: "{{count}} issue(s) due in the next days"
171 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
172 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
172 mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
173 mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
173 mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
174 mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
174 mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
175 mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
175 mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
176 mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
176
177
177 gui_validation_error: 1 error
178 gui_validation_error: 1 error
178 gui_validation_error_plural: "{{count}} errors"
179 gui_validation_error_plural: "{{count}} errors"
179
180
180 field_name: Name
181 field_name: Name
181 field_description: Description
182 field_description: Description
182 field_summary: Summary
183 field_summary: Summary
183 field_is_required: Required
184 field_is_required: Required
184 field_firstname: Firstname
185 field_firstname: Firstname
185 field_lastname: Lastname
186 field_lastname: Lastname
186 field_mail: Email
187 field_mail: Email
187 field_filename: File
188 field_filename: File
188 field_filesize: Size
189 field_filesize: Size
189 field_downloads: Downloads
190 field_downloads: Downloads
190 field_author: Author
191 field_author: Author
191 field_created_on: Created
192 field_created_on: Created
192 field_updated_on: Updated
193 field_updated_on: Updated
193 field_field_format: Format
194 field_field_format: Format
194 field_is_for_all: For all projects
195 field_is_for_all: For all projects
195 field_possible_values: Possible values
196 field_possible_values: Possible values
196 field_regexp: Regular expression
197 field_regexp: Regular expression
197 field_min_length: Minimum length
198 field_min_length: Minimum length
198 field_max_length: Maximum length
199 field_max_length: Maximum length
199 field_value: Value
200 field_value: Value
200 field_category: Category
201 field_category: Category
201 field_title: Title
202 field_title: Title
202 field_project: Project
203 field_project: Project
203 field_issue: Issue
204 field_issue: Issue
204 field_status: Status
205 field_status: Status
205 field_notes: Notes
206 field_notes: Notes
206 field_is_closed: Issue closed
207 field_is_closed: Issue closed
207 field_is_default: Default value
208 field_is_default: Default value
208 field_tracker: Tracker
209 field_tracker: Tracker
209 field_subject: Subject
210 field_subject: Subject
210 field_due_date: Due date
211 field_due_date: Due date
211 field_assigned_to: Assigned to
212 field_assigned_to: Assigned to
212 field_priority: Priority
213 field_priority: Priority
213 field_fixed_version: Target version
214 field_fixed_version: Target version
214 field_user: User
215 field_user: User
215 field_role: Role
216 field_role: Role
216 field_homepage: Homepage
217 field_homepage: Homepage
217 field_is_public: Public
218 field_is_public: Public
218 field_parent: Subproject of
219 field_parent: Subproject of
219 field_is_in_chlog: Issues displayed in changelog
220 field_is_in_chlog: Issues displayed in changelog
220 field_is_in_roadmap: Issues displayed in roadmap
221 field_is_in_roadmap: Issues displayed in roadmap
221 field_login: Login
222 field_login: Login
222 field_mail_notification: Email notifications
223 field_mail_notification: Email notifications
223 field_admin: Administrator
224 field_admin: Administrator
224 field_last_login_on: Last connection
225 field_last_login_on: Last connection
225 field_language: Language
226 field_language: Language
226 field_effective_date: Date
227 field_effective_date: Date
227 field_password: Password
228 field_password: Password
228 field_new_password: New password
229 field_new_password: New password
229 field_password_confirmation: Confirmation
230 field_password_confirmation: Confirmation
230 field_version: Version
231 field_version: Version
231 field_type: Type
232 field_type: Type
232 field_host: Host
233 field_host: Host
233 field_port: Port
234 field_port: Port
234 field_account: Account
235 field_account: Account
235 field_base_dn: Base DN
236 field_base_dn: Base DN
236 field_attr_login: Login attribute
237 field_attr_login: Login attribute
237 field_attr_firstname: Firstname attribute
238 field_attr_firstname: Firstname attribute
238 field_attr_lastname: Lastname attribute
239 field_attr_lastname: Lastname attribute
239 field_attr_mail: Email attribute
240 field_attr_mail: Email attribute
240 field_onthefly: On-the-fly user creation
241 field_onthefly: On-the-fly user creation
241 field_start_date: Start
242 field_start_date: Start
242 field_done_ratio: % Done
243 field_done_ratio: % Done
243 field_auth_source: Authentication mode
244 field_auth_source: Authentication mode
244 field_hide_mail: Hide my email address
245 field_hide_mail: Hide my email address
245 field_comments: Comment
246 field_comments: Comment
246 field_url: URL
247 field_url: URL
247 field_start_page: Start page
248 field_start_page: Start page
248 field_subproject: Subproject
249 field_subproject: Subproject
249 field_hours: Hours
250 field_hours: Hours
250 field_activity: Activity
251 field_activity: Activity
251 field_spent_on: Date
252 field_spent_on: Date
252 field_identifier: Identifier
253 field_identifier: Identifier
253 field_is_filter: Used as a filter
254 field_is_filter: Used as a filter
254 field_issue_to: Related issue
255 field_issue_to: Related issue
255 field_delay: Delay
256 field_delay: Delay
256 field_assignable: Issues can be assigned to this role
257 field_assignable: Issues can be assigned to this role
257 field_redirect_existing_links: Redirect existing links
258 field_redirect_existing_links: Redirect existing links
258 field_estimated_hours: Estimated time
259 field_estimated_hours: Estimated time
259 field_column_names: Columns
260 field_column_names: Columns
260 field_time_zone: Time zone
261 field_time_zone: Time zone
261 field_searchable: Searchable
262 field_searchable: Searchable
262 field_default_value: Default value
263 field_default_value: Default value
263 field_comments_sorting: Display comments
264 field_comments_sorting: Display comments
264 field_parent_title: Parent page
265 field_parent_title: Parent page
265 field_editable: Editable
266 field_editable: Editable
266 field_watcher: Watcher
267 field_watcher: Watcher
267 field_identity_url: OpenID URL
268 field_identity_url: OpenID URL
268 field_content: Content
269 field_content: Content
269 field_group_by: Group results by
270 field_group_by: Group results by
271 field_sharing: Sharing
270
272
271 setting_app_title: Application title
273 setting_app_title: Application title
272 setting_app_subtitle: Application subtitle
274 setting_app_subtitle: Application subtitle
273 setting_welcome_text: Welcome text
275 setting_welcome_text: Welcome text
274 setting_default_language: Default language
276 setting_default_language: Default language
275 setting_login_required: Authentication required
277 setting_login_required: Authentication required
276 setting_self_registration: Self-registration
278 setting_self_registration: Self-registration
277 setting_attachment_max_size: Attachment max. size
279 setting_attachment_max_size: Attachment max. size
278 setting_issues_export_limit: Issues export limit
280 setting_issues_export_limit: Issues export limit
279 setting_mail_from: Emission email address
281 setting_mail_from: Emission email address
280 setting_bcc_recipients: Blind carbon copy recipients (bcc)
282 setting_bcc_recipients: Blind carbon copy recipients (bcc)
281 setting_plain_text_mail: Plain text mail (no HTML)
283 setting_plain_text_mail: Plain text mail (no HTML)
282 setting_host_name: Host name and path
284 setting_host_name: Host name and path
283 setting_text_formatting: Text formatting
285 setting_text_formatting: Text formatting
284 setting_wiki_compression: Wiki history compression
286 setting_wiki_compression: Wiki history compression
285 setting_feeds_limit: Feed content limit
287 setting_feeds_limit: Feed content limit
286 setting_default_projects_public: New projects are public by default
288 setting_default_projects_public: New projects are public by default
287 setting_autofetch_changesets: Autofetch commits
289 setting_autofetch_changesets: Autofetch commits
288 setting_sys_api_enabled: Enable WS for repository management
290 setting_sys_api_enabled: Enable WS for repository management
289 setting_commit_ref_keywords: Referencing keywords
291 setting_commit_ref_keywords: Referencing keywords
290 setting_commit_fix_keywords: Fixing keywords
292 setting_commit_fix_keywords: Fixing keywords
291 setting_autologin: Autologin
293 setting_autologin: Autologin
292 setting_date_format: Date format
294 setting_date_format: Date format
293 setting_time_format: Time format
295 setting_time_format: Time format
294 setting_cross_project_issue_relations: Allow cross-project issue relations
296 setting_cross_project_issue_relations: Allow cross-project issue relations
295 setting_issue_list_default_columns: Default columns displayed on the issue list
297 setting_issue_list_default_columns: Default columns displayed on the issue list
296 setting_repositories_encodings: Repositories encodings
298 setting_repositories_encodings: Repositories encodings
297 setting_commit_logs_encoding: Commit messages encoding
299 setting_commit_logs_encoding: Commit messages encoding
298 setting_emails_footer: Emails footer
300 setting_emails_footer: Emails footer
299 setting_protocol: Protocol
301 setting_protocol: Protocol
300 setting_per_page_options: Objects per page options
302 setting_per_page_options: Objects per page options
301 setting_user_format: Users display format
303 setting_user_format: Users display format
302 setting_activity_days_default: Days displayed on project activity
304 setting_activity_days_default: Days displayed on project activity
303 setting_display_subprojects_issues: Display subprojects issues on main projects by default
305 setting_display_subprojects_issues: Display subprojects issues on main projects by default
304 setting_enabled_scm: Enabled SCM
306 setting_enabled_scm: Enabled SCM
305 setting_mail_handler_api_enabled: Enable WS for incoming emails
307 setting_mail_handler_api_enabled: Enable WS for incoming emails
306 setting_mail_handler_api_key: API key
308 setting_mail_handler_api_key: API key
307 setting_sequential_project_identifiers: Generate sequential project identifiers
309 setting_sequential_project_identifiers: Generate sequential project identifiers
308 setting_gravatar_enabled: Use Gravatar user icons
310 setting_gravatar_enabled: Use Gravatar user icons
309 setting_gravatar_default: Default Gravatar image
311 setting_gravatar_default: Default Gravatar image
310 setting_diff_max_lines_displayed: Max number of diff lines displayed
312 setting_diff_max_lines_displayed: Max number of diff lines displayed
311 setting_file_max_size_displayed: Max size of text files displayed inline
313 setting_file_max_size_displayed: Max size of text files displayed inline
312 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
314 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
313 setting_openid: Allow OpenID login and registration
315 setting_openid: Allow OpenID login and registration
314 setting_password_min_length: Minimum password length
316 setting_password_min_length: Minimum password length
315 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
317 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
316 setting_default_projects_modules: Default enabled modules for new projects
318 setting_default_projects_modules: Default enabled modules for new projects
317
319
318 permission_add_project: Create project
320 permission_add_project: Create project
319 permission_edit_project: Edit project
321 permission_edit_project: Edit project
320 permission_select_project_modules: Select project modules
322 permission_select_project_modules: Select project modules
321 permission_manage_members: Manage members
323 permission_manage_members: Manage members
322 permission_manage_versions: Manage versions
324 permission_manage_versions: Manage versions
323 permission_manage_categories: Manage issue categories
325 permission_manage_categories: Manage issue categories
324 permission_add_issues: Add issues
326 permission_add_issues: Add issues
325 permission_edit_issues: Edit issues
327 permission_edit_issues: Edit issues
326 permission_manage_issue_relations: Manage issue relations
328 permission_manage_issue_relations: Manage issue relations
327 permission_add_issue_notes: Add notes
329 permission_add_issue_notes: Add notes
328 permission_edit_issue_notes: Edit notes
330 permission_edit_issue_notes: Edit notes
329 permission_edit_own_issue_notes: Edit own notes
331 permission_edit_own_issue_notes: Edit own notes
330 permission_move_issues: Move issues
332 permission_move_issues: Move issues
331 permission_delete_issues: Delete issues
333 permission_delete_issues: Delete issues
332 permission_manage_public_queries: Manage public queries
334 permission_manage_public_queries: Manage public queries
333 permission_save_queries: Save queries
335 permission_save_queries: Save queries
334 permission_view_gantt: View gantt chart
336 permission_view_gantt: View gantt chart
335 permission_view_calendar: View calendar
337 permission_view_calendar: View calendar
336 permission_view_issue_watchers: View watchers list
338 permission_view_issue_watchers: View watchers list
337 permission_add_issue_watchers: Add watchers
339 permission_add_issue_watchers: Add watchers
338 permission_delete_issue_watchers: Delete watchers
340 permission_delete_issue_watchers: Delete watchers
339 permission_log_time: Log spent time
341 permission_log_time: Log spent time
340 permission_view_time_entries: View spent time
342 permission_view_time_entries: View spent time
341 permission_edit_time_entries: Edit time logs
343 permission_edit_time_entries: Edit time logs
342 permission_edit_own_time_entries: Edit own time logs
344 permission_edit_own_time_entries: Edit own time logs
343 permission_manage_news: Manage news
345 permission_manage_news: Manage news
344 permission_comment_news: Comment news
346 permission_comment_news: Comment news
345 permission_manage_documents: Manage documents
347 permission_manage_documents: Manage documents
346 permission_view_documents: View documents
348 permission_view_documents: View documents
347 permission_manage_files: Manage files
349 permission_manage_files: Manage files
348 permission_view_files: View files
350 permission_view_files: View files
349 permission_manage_wiki: Manage wiki
351 permission_manage_wiki: Manage wiki
350 permission_rename_wiki_pages: Rename wiki pages
352 permission_rename_wiki_pages: Rename wiki pages
351 permission_delete_wiki_pages: Delete wiki pages
353 permission_delete_wiki_pages: Delete wiki pages
352 permission_view_wiki_pages: View wiki
354 permission_view_wiki_pages: View wiki
353 permission_view_wiki_edits: View wiki history
355 permission_view_wiki_edits: View wiki history
354 permission_edit_wiki_pages: Edit wiki pages
356 permission_edit_wiki_pages: Edit wiki pages
355 permission_delete_wiki_pages_attachments: Delete attachments
357 permission_delete_wiki_pages_attachments: Delete attachments
356 permission_protect_wiki_pages: Protect wiki pages
358 permission_protect_wiki_pages: Protect wiki pages
357 permission_manage_repository: Manage repository
359 permission_manage_repository: Manage repository
358 permission_browse_repository: Browse repository
360 permission_browse_repository: Browse repository
359 permission_view_changesets: View changesets
361 permission_view_changesets: View changesets
360 permission_commit_access: Commit access
362 permission_commit_access: Commit access
361 permission_manage_boards: Manage boards
363 permission_manage_boards: Manage boards
362 permission_view_messages: View messages
364 permission_view_messages: View messages
363 permission_add_messages: Post messages
365 permission_add_messages: Post messages
364 permission_edit_messages: Edit messages
366 permission_edit_messages: Edit messages
365 permission_edit_own_messages: Edit own messages
367 permission_edit_own_messages: Edit own messages
366 permission_delete_messages: Delete messages
368 permission_delete_messages: Delete messages
367 permission_delete_own_messages: Delete own messages
369 permission_delete_own_messages: Delete own messages
368
370
369 project_module_issue_tracking: Issue tracking
371 project_module_issue_tracking: Issue tracking
370 project_module_time_tracking: Time tracking
372 project_module_time_tracking: Time tracking
371 project_module_news: News
373 project_module_news: News
372 project_module_documents: Documents
374 project_module_documents: Documents
373 project_module_files: Files
375 project_module_files: Files
374 project_module_wiki: Wiki
376 project_module_wiki: Wiki
375 project_module_repository: Repository
377 project_module_repository: Repository
376 project_module_boards: Boards
378 project_module_boards: Boards
377
379
378 label_user: User
380 label_user: User
379 label_user_plural: Users
381 label_user_plural: Users
380 label_user_new: New user
382 label_user_new: New user
381 label_user_anonymous: Anonymous
383 label_user_anonymous: Anonymous
382 label_project: Project
384 label_project: Project
383 label_project_new: New project
385 label_project_new: New project
384 label_project_plural: Projects
386 label_project_plural: Projects
385 label_x_projects:
387 label_x_projects:
386 zero: no projects
388 zero: no projects
387 one: 1 project
389 one: 1 project
388 other: "{{count}} projects"
390 other: "{{count}} projects"
389 label_project_all: All Projects
391 label_project_all: All Projects
390 label_project_latest: Latest projects
392 label_project_latest: Latest projects
391 label_issue: Issue
393 label_issue: Issue
392 label_issue_new: New issue
394 label_issue_new: New issue
393 label_issue_plural: Issues
395 label_issue_plural: Issues
394 label_issue_view_all: View all issues
396 label_issue_view_all: View all issues
395 label_issues_by: "Issues by {{value}}"
397 label_issues_by: "Issues by {{value}}"
396 label_issue_added: Issue added
398 label_issue_added: Issue added
397 label_issue_updated: Issue updated
399 label_issue_updated: Issue updated
398 label_document: Document
400 label_document: Document
399 label_document_new: New document
401 label_document_new: New document
400 label_document_plural: Documents
402 label_document_plural: Documents
401 label_document_added: Document added
403 label_document_added: Document added
402 label_role: Role
404 label_role: Role
403 label_role_plural: Roles
405 label_role_plural: Roles
404 label_role_new: New role
406 label_role_new: New role
405 label_role_and_permissions: Roles and permissions
407 label_role_and_permissions: Roles and permissions
406 label_member: Member
408 label_member: Member
407 label_member_new: New member
409 label_member_new: New member
408 label_member_plural: Members
410 label_member_plural: Members
409 label_tracker: Tracker
411 label_tracker: Tracker
410 label_tracker_plural: Trackers
412 label_tracker_plural: Trackers
411 label_tracker_new: New tracker
413 label_tracker_new: New tracker
412 label_workflow: Workflow
414 label_workflow: Workflow
413 label_issue_status: Issue status
415 label_issue_status: Issue status
414 label_issue_status_plural: Issue statuses
416 label_issue_status_plural: Issue statuses
415 label_issue_status_new: New status
417 label_issue_status_new: New status
416 label_issue_category: Issue category
418 label_issue_category: Issue category
417 label_issue_category_plural: Issue categories
419 label_issue_category_plural: Issue categories
418 label_issue_category_new: New category
420 label_issue_category_new: New category
419 label_custom_field: Custom field
421 label_custom_field: Custom field
420 label_custom_field_plural: Custom fields
422 label_custom_field_plural: Custom fields
421 label_custom_field_new: New custom field
423 label_custom_field_new: New custom field
422 label_enumerations: Enumerations
424 label_enumerations: Enumerations
423 label_enumeration_new: New value
425 label_enumeration_new: New value
424 label_information: Information
426 label_information: Information
425 label_information_plural: Information
427 label_information_plural: Information
426 label_please_login: Please log in
428 label_please_login: Please log in
427 label_register: Register
429 label_register: Register
428 label_login_with_open_id_option: or login with OpenID
430 label_login_with_open_id_option: or login with OpenID
429 label_password_lost: Lost password
431 label_password_lost: Lost password
430 label_home: Home
432 label_home: Home
431 label_my_page: My page
433 label_my_page: My page
432 label_my_account: My account
434 label_my_account: My account
433 label_my_projects: My projects
435 label_my_projects: My projects
434 label_administration: Administration
436 label_administration: Administration
435 label_login: Sign in
437 label_login: Sign in
436 label_logout: Sign out
438 label_logout: Sign out
437 label_help: Help
439 label_help: Help
438 label_reported_issues: Reported issues
440 label_reported_issues: Reported issues
439 label_assigned_to_me_issues: Issues assigned to me
441 label_assigned_to_me_issues: Issues assigned to me
440 label_last_login: Last connection
442 label_last_login: Last connection
441 label_registered_on: Registered on
443 label_registered_on: Registered on
442 label_activity: Activity
444 label_activity: Activity
443 label_overall_activity: Overall activity
445 label_overall_activity: Overall activity
444 label_user_activity: "{{value}}'s activity"
446 label_user_activity: "{{value}}'s activity"
445 label_new: New
447 label_new: New
446 label_logged_as: Logged in as
448 label_logged_as: Logged in as
447 label_environment: Environment
449 label_environment: Environment
448 label_authentication: Authentication
450 label_authentication: Authentication
449 label_auth_source: Authentication mode
451 label_auth_source: Authentication mode
450 label_auth_source_new: New authentication mode
452 label_auth_source_new: New authentication mode
451 label_auth_source_plural: Authentication modes
453 label_auth_source_plural: Authentication modes
452 label_subproject_plural: Subprojects
454 label_subproject_plural: Subprojects
453 label_and_its_subprojects: "{{value}} and its subprojects"
455 label_and_its_subprojects: "{{value}} and its subprojects"
454 label_min_max_length: Min - Max length
456 label_min_max_length: Min - Max length
455 label_list: List
457 label_list: List
456 label_date: Date
458 label_date: Date
457 label_integer: Integer
459 label_integer: Integer
458 label_float: Float
460 label_float: Float
459 label_boolean: Boolean
461 label_boolean: Boolean
460 label_string: Text
462 label_string: Text
461 label_text: Long text
463 label_text: Long text
462 label_attribute: Attribute
464 label_attribute: Attribute
463 label_attribute_plural: Attributes
465 label_attribute_plural: Attributes
464 label_download: "{{count}} Download"
466 label_download: "{{count}} Download"
465 label_download_plural: "{{count}} Downloads"
467 label_download_plural: "{{count}} Downloads"
466 label_no_data: No data to display
468 label_no_data: No data to display
467 label_change_status: Change status
469 label_change_status: Change status
468 label_history: History
470 label_history: History
469 label_attachment: File
471 label_attachment: File
470 label_attachment_new: New file
472 label_attachment_new: New file
471 label_attachment_delete: Delete file
473 label_attachment_delete: Delete file
472 label_attachment_plural: Files
474 label_attachment_plural: Files
473 label_file_added: File added
475 label_file_added: File added
474 label_report: Report
476 label_report: Report
475 label_report_plural: Reports
477 label_report_plural: Reports
476 label_news: News
478 label_news: News
477 label_news_new: Add news
479 label_news_new: Add news
478 label_news_plural: News
480 label_news_plural: News
479 label_news_latest: Latest news
481 label_news_latest: Latest news
480 label_news_view_all: View all news
482 label_news_view_all: View all news
481 label_news_added: News added
483 label_news_added: News added
482 label_change_log: Change log
484 label_change_log: Change log
483 label_settings: Settings
485 label_settings: Settings
484 label_overview: Overview
486 label_overview: Overview
485 label_version: Version
487 label_version: Version
486 label_version_new: New version
488 label_version_new: New version
487 label_version_plural: Versions
489 label_version_plural: Versions
488 label_confirmation: Confirmation
490 label_confirmation: Confirmation
489 label_export_to: 'Also available in:'
491 label_export_to: 'Also available in:'
490 label_read: Read...
492 label_read: Read...
491 label_public_projects: Public projects
493 label_public_projects: Public projects
492 label_open_issues: open
494 label_open_issues: open
493 label_open_issues_plural: open
495 label_open_issues_plural: open
494 label_closed_issues: closed
496 label_closed_issues: closed
495 label_closed_issues_plural: closed
497 label_closed_issues_plural: closed
496 label_x_open_issues_abbr_on_total:
498 label_x_open_issues_abbr_on_total:
497 zero: 0 open / {{total}}
499 zero: 0 open / {{total}}
498 one: 1 open / {{total}}
500 one: 1 open / {{total}}
499 other: "{{count}} open / {{total}}"
501 other: "{{count}} open / {{total}}"
500 label_x_open_issues_abbr:
502 label_x_open_issues_abbr:
501 zero: 0 open
503 zero: 0 open
502 one: 1 open
504 one: 1 open
503 other: "{{count}} open"
505 other: "{{count}} open"
504 label_x_closed_issues_abbr:
506 label_x_closed_issues_abbr:
505 zero: 0 closed
507 zero: 0 closed
506 one: 1 closed
508 one: 1 closed
507 other: "{{count}} closed"
509 other: "{{count}} closed"
508 label_total: Total
510 label_total: Total
509 label_permissions: Permissions
511 label_permissions: Permissions
510 label_current_status: Current status
512 label_current_status: Current status
511 label_new_statuses_allowed: New statuses allowed
513 label_new_statuses_allowed: New statuses allowed
512 label_all: all
514 label_all: all
513 label_none: none
515 label_none: none
514 label_nobody: nobody
516 label_nobody: nobody
515 label_next: Next
517 label_next: Next
516 label_previous: Previous
518 label_previous: Previous
517 label_used_by: Used by
519 label_used_by: Used by
518 label_details: Details
520 label_details: Details
519 label_add_note: Add a note
521 label_add_note: Add a note
520 label_per_page: Per page
522 label_per_page: Per page
521 label_calendar: Calendar
523 label_calendar: Calendar
522 label_months_from: months from
524 label_months_from: months from
523 label_gantt: Gantt
525 label_gantt: Gantt
524 label_internal: Internal
526 label_internal: Internal
525 label_last_changes: "last {{count}} changes"
527 label_last_changes: "last {{count}} changes"
526 label_change_view_all: View all changes
528 label_change_view_all: View all changes
527 label_personalize_page: Personalize this page
529 label_personalize_page: Personalize this page
528 label_comment: Comment
530 label_comment: Comment
529 label_comment_plural: Comments
531 label_comment_plural: Comments
530 label_x_comments:
532 label_x_comments:
531 zero: no comments
533 zero: no comments
532 one: 1 comment
534 one: 1 comment
533 other: "{{count}} comments"
535 other: "{{count}} comments"
534 label_comment_add: Add a comment
536 label_comment_add: Add a comment
535 label_comment_added: Comment added
537 label_comment_added: Comment added
536 label_comment_delete: Delete comments
538 label_comment_delete: Delete comments
537 label_query: Custom query
539 label_query: Custom query
538 label_query_plural: Custom queries
540 label_query_plural: Custom queries
539 label_query_new: New query
541 label_query_new: New query
540 label_filter_add: Add filter
542 label_filter_add: Add filter
541 label_filter_plural: Filters
543 label_filter_plural: Filters
542 label_equals: is
544 label_equals: is
543 label_not_equals: is not
545 label_not_equals: is not
544 label_in_less_than: in less than
546 label_in_less_than: in less than
545 label_in_more_than: in more than
547 label_in_more_than: in more than
546 label_greater_or_equal: '>='
548 label_greater_or_equal: '>='
547 label_less_or_equal: '<='
549 label_less_or_equal: '<='
548 label_in: in
550 label_in: in
549 label_today: today
551 label_today: today
550 label_all_time: all time
552 label_all_time: all time
551 label_yesterday: yesterday
553 label_yesterday: yesterday
552 label_this_week: this week
554 label_this_week: this week
553 label_last_week: last week
555 label_last_week: last week
554 label_last_n_days: "last {{count}} days"
556 label_last_n_days: "last {{count}} days"
555 label_this_month: this month
557 label_this_month: this month
556 label_last_month: last month
558 label_last_month: last month
557 label_this_year: this year
559 label_this_year: this year
558 label_date_range: Date range
560 label_date_range: Date range
559 label_less_than_ago: less than days ago
561 label_less_than_ago: less than days ago
560 label_more_than_ago: more than days ago
562 label_more_than_ago: more than days ago
561 label_ago: days ago
563 label_ago: days ago
562 label_contains: contains
564 label_contains: contains
563 label_not_contains: doesn't contain
565 label_not_contains: doesn't contain
564 label_day_plural: days
566 label_day_plural: days
565 label_repository: Repository
567 label_repository: Repository
566 label_repository_plural: Repositories
568 label_repository_plural: Repositories
567 label_browse: Browse
569 label_browse: Browse
568 label_modification: "{{count}} change"
570 label_modification: "{{count}} change"
569 label_modification_plural: "{{count}} changes"
571 label_modification_plural: "{{count}} changes"
570 label_branch: Branch
572 label_branch: Branch
571 label_tag: Tag
573 label_tag: Tag
572 label_revision: Revision
574 label_revision: Revision
573 label_revision_plural: Revisions
575 label_revision_plural: Revisions
574 label_associated_revisions: Associated revisions
576 label_associated_revisions: Associated revisions
575 label_added: added
577 label_added: added
576 label_modified: modified
578 label_modified: modified
577 label_copied: copied
579 label_copied: copied
578 label_renamed: renamed
580 label_renamed: renamed
579 label_deleted: deleted
581 label_deleted: deleted
580 label_latest_revision: Latest revision
582 label_latest_revision: Latest revision
581 label_latest_revision_plural: Latest revisions
583 label_latest_revision_plural: Latest revisions
582 label_view_revisions: View revisions
584 label_view_revisions: View revisions
583 label_view_all_revisions: View all revisions
585 label_view_all_revisions: View all revisions
584 label_max_size: Maximum size
586 label_max_size: Maximum size
585 label_sort_highest: Move to top
587 label_sort_highest: Move to top
586 label_sort_higher: Move up
588 label_sort_higher: Move up
587 label_sort_lower: Move down
589 label_sort_lower: Move down
588 label_sort_lowest: Move to bottom
590 label_sort_lowest: Move to bottom
589 label_roadmap: Roadmap
591 label_roadmap: Roadmap
590 label_roadmap_due_in: "Due in {{value}}"
592 label_roadmap_due_in: "Due in {{value}}"
591 label_roadmap_overdue: "{{value}} late"
593 label_roadmap_overdue: "{{value}} late"
592 label_roadmap_no_issues: No issues for this version
594 label_roadmap_no_issues: No issues for this version
593 label_search: Search
595 label_search: Search
594 label_result_plural: Results
596 label_result_plural: Results
595 label_all_words: All words
597 label_all_words: All words
596 label_wiki: Wiki
598 label_wiki: Wiki
597 label_wiki_edit: Wiki edit
599 label_wiki_edit: Wiki edit
598 label_wiki_edit_plural: Wiki edits
600 label_wiki_edit_plural: Wiki edits
599 label_wiki_page: Wiki page
601 label_wiki_page: Wiki page
600 label_wiki_page_plural: Wiki pages
602 label_wiki_page_plural: Wiki pages
601 label_index_by_title: Index by title
603 label_index_by_title: Index by title
602 label_index_by_date: Index by date
604 label_index_by_date: Index by date
603 label_current_version: Current version
605 label_current_version: Current version
604 label_preview: Preview
606 label_preview: Preview
605 label_feed_plural: Feeds
607 label_feed_plural: Feeds
606 label_changes_details: Details of all changes
608 label_changes_details: Details of all changes
607 label_issue_tracking: Issue tracking
609 label_issue_tracking: Issue tracking
608 label_spent_time: Spent time
610 label_spent_time: Spent time
609 label_f_hour: "{{value}} hour"
611 label_f_hour: "{{value}} hour"
610 label_f_hour_plural: "{{value}} hours"
612 label_f_hour_plural: "{{value}} hours"
611 label_time_tracking: Time tracking
613 label_time_tracking: Time tracking
612 label_change_plural: Changes
614 label_change_plural: Changes
613 label_statistics: Statistics
615 label_statistics: Statistics
614 label_commits_per_month: Commits per month
616 label_commits_per_month: Commits per month
615 label_commits_per_author: Commits per author
617 label_commits_per_author: Commits per author
616 label_view_diff: View differences
618 label_view_diff: View differences
617 label_diff_inline: inline
619 label_diff_inline: inline
618 label_diff_side_by_side: side by side
620 label_diff_side_by_side: side by side
619 label_options: Options
621 label_options: Options
620 label_copy_workflow_from: Copy workflow from
622 label_copy_workflow_from: Copy workflow from
621 label_permissions_report: Permissions report
623 label_permissions_report: Permissions report
622 label_watched_issues: Watched issues
624 label_watched_issues: Watched issues
623 label_related_issues: Related issues
625 label_related_issues: Related issues
624 label_applied_status: Applied status
626 label_applied_status: Applied status
625 label_loading: Loading...
627 label_loading: Loading...
626 label_relation_new: New relation
628 label_relation_new: New relation
627 label_relation_delete: Delete relation
629 label_relation_delete: Delete relation
628 label_relates_to: related to
630 label_relates_to: related to
629 label_duplicates: duplicates
631 label_duplicates: duplicates
630 label_duplicated_by: duplicated by
632 label_duplicated_by: duplicated by
631 label_blocks: blocks
633 label_blocks: blocks
632 label_blocked_by: blocked by
634 label_blocked_by: blocked by
633 label_precedes: precedes
635 label_precedes: precedes
634 label_follows: follows
636 label_follows: follows
635 label_end_to_start: end to start
637 label_end_to_start: end to start
636 label_end_to_end: end to end
638 label_end_to_end: end to end
637 label_start_to_start: start to start
639 label_start_to_start: start to start
638 label_start_to_end: start to end
640 label_start_to_end: start to end
639 label_stay_logged_in: Stay logged in
641 label_stay_logged_in: Stay logged in
640 label_disabled: disabled
642 label_disabled: disabled
641 label_show_completed_versions: Show completed versions
643 label_show_completed_versions: Show completed versions
642 label_me: me
644 label_me: me
643 label_board: Forum
645 label_board: Forum
644 label_board_new: New forum
646 label_board_new: New forum
645 label_board_plural: Forums
647 label_board_plural: Forums
646 label_topic_plural: Topics
648 label_topic_plural: Topics
647 label_message_plural: Messages
649 label_message_plural: Messages
648 label_message_last: Last message
650 label_message_last: Last message
649 label_message_new: New message
651 label_message_new: New message
650 label_message_posted: Message added
652 label_message_posted: Message added
651 label_reply_plural: Replies
653 label_reply_plural: Replies
652 label_send_information: Send account information to the user
654 label_send_information: Send account information to the user
653 label_year: Year
655 label_year: Year
654 label_month: Month
656 label_month: Month
655 label_week: Week
657 label_week: Week
656 label_date_from: From
658 label_date_from: From
657 label_date_to: To
659 label_date_to: To
658 label_language_based: Based on user's language
660 label_language_based: Based on user's language
659 label_sort_by: "Sort by {{value}}"
661 label_sort_by: "Sort by {{value}}"
660 label_send_test_email: Send a test email
662 label_send_test_email: Send a test email
661 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
663 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
662 label_module_plural: Modules
664 label_module_plural: Modules
663 label_added_time_by: "Added by {{author}} {{age}} ago"
665 label_added_time_by: "Added by {{author}} {{age}} ago"
664 label_updated_time_by: "Updated by {{author}} {{age}} ago"
666 label_updated_time_by: "Updated by {{author}} {{age}} ago"
665 label_updated_time: "Updated {{value}} ago"
667 label_updated_time: "Updated {{value}} ago"
666 label_jump_to_a_project: Jump to a project...
668 label_jump_to_a_project: Jump to a project...
667 label_file_plural: Files
669 label_file_plural: Files
668 label_changeset_plural: Changesets
670 label_changeset_plural: Changesets
669 label_default_columns: Default columns
671 label_default_columns: Default columns
670 label_no_change_option: (No change)
672 label_no_change_option: (No change)
671 label_bulk_edit_selected_issues: Bulk edit selected issues
673 label_bulk_edit_selected_issues: Bulk edit selected issues
672 label_theme: Theme
674 label_theme: Theme
673 label_default: Default
675 label_default: Default
674 label_search_titles_only: Search titles only
676 label_search_titles_only: Search titles only
675 label_user_mail_option_all: "For any event on all my projects"
677 label_user_mail_option_all: "For any event on all my projects"
676 label_user_mail_option_selected: "For any event on the selected projects only..."
678 label_user_mail_option_selected: "For any event on the selected projects only..."
677 label_user_mail_option_none: "Only for things I watch or I'm involved in"
679 label_user_mail_option_none: "Only for things I watch or I'm involved in"
678 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
680 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
679 label_registration_activation_by_email: account activation by email
681 label_registration_activation_by_email: account activation by email
680 label_registration_manual_activation: manual account activation
682 label_registration_manual_activation: manual account activation
681 label_registration_automatic_activation: automatic account activation
683 label_registration_automatic_activation: automatic account activation
682 label_display_per_page: "Per page: {{value}}"
684 label_display_per_page: "Per page: {{value}}"
683 label_age: Age
685 label_age: Age
684 label_change_properties: Change properties
686 label_change_properties: Change properties
685 label_general: General
687 label_general: General
686 label_more: More
688 label_more: More
687 label_scm: SCM
689 label_scm: SCM
688 label_plugins: Plugins
690 label_plugins: Plugins
689 label_ldap_authentication: LDAP authentication
691 label_ldap_authentication: LDAP authentication
690 label_downloads_abbr: D/L
692 label_downloads_abbr: D/L
691 label_optional_description: Optional description
693 label_optional_description: Optional description
692 label_add_another_file: Add another file
694 label_add_another_file: Add another file
693 label_preferences: Preferences
695 label_preferences: Preferences
694 label_chronological_order: In chronological order
696 label_chronological_order: In chronological order
695 label_reverse_chronological_order: In reverse chronological order
697 label_reverse_chronological_order: In reverse chronological order
696 label_planning: Planning
698 label_planning: Planning
697 label_incoming_emails: Incoming emails
699 label_incoming_emails: Incoming emails
698 label_generate_key: Generate a key
700 label_generate_key: Generate a key
699 label_issue_watchers: Watchers
701 label_issue_watchers: Watchers
700 label_example: Example
702 label_example: Example
701 label_display: Display
703 label_display: Display
702 label_sort: Sort
704 label_sort: Sort
703 label_ascending: Ascending
705 label_ascending: Ascending
704 label_descending: Descending
706 label_descending: Descending
705 label_date_from_to: From {{start}} to {{end}}
707 label_date_from_to: From {{start}} to {{end}}
706 label_wiki_content_added: Wiki page added
708 label_wiki_content_added: Wiki page added
707 label_wiki_content_updated: Wiki page updated
709 label_wiki_content_updated: Wiki page updated
708 label_group: Group
710 label_group: Group
709 label_group_plural: Groups
711 label_group_plural: Groups
710 label_group_new: New group
712 label_group_new: New group
711 label_time_entry_plural: Spent time
713 label_time_entry_plural: Spent time
714 label_version_sharing_none: Not shared
715 label_version_sharing_descendants: With subprojects
716 label_version_sharing_hierarchy: With project hierarchy
717 label_version_sharing_tree: With project tree
718 label_version_sharing_system: With all projects
712
719
713 button_login: Login
720 button_login: Login
714 button_submit: Submit
721 button_submit: Submit
715 button_save: Save
722 button_save: Save
716 button_check_all: Check all
723 button_check_all: Check all
717 button_uncheck_all: Uncheck all
724 button_uncheck_all: Uncheck all
718 button_delete: Delete
725 button_delete: Delete
719 button_create: Create
726 button_create: Create
720 button_create_and_continue: Create and continue
727 button_create_and_continue: Create and continue
721 button_test: Test
728 button_test: Test
722 button_edit: Edit
729 button_edit: Edit
723 button_add: Add
730 button_add: Add
724 button_change: Change
731 button_change: Change
725 button_apply: Apply
732 button_apply: Apply
726 button_clear: Clear
733 button_clear: Clear
727 button_lock: Lock
734 button_lock: Lock
728 button_unlock: Unlock
735 button_unlock: Unlock
729 button_download: Download
736 button_download: Download
730 button_list: List
737 button_list: List
731 button_view: View
738 button_view: View
732 button_move: Move
739 button_move: Move
733 button_move_and_follow: Move and follow
740 button_move_and_follow: Move and follow
734 button_back: Back
741 button_back: Back
735 button_cancel: Cancel
742 button_cancel: Cancel
736 button_activate: Activate
743 button_activate: Activate
737 button_sort: Sort
744 button_sort: Sort
738 button_log_time: Log time
745 button_log_time: Log time
739 button_rollback: Rollback to this version
746 button_rollback: Rollback to this version
740 button_watch: Watch
747 button_watch: Watch
741 button_unwatch: Unwatch
748 button_unwatch: Unwatch
742 button_reply: Reply
749 button_reply: Reply
743 button_archive: Archive
750 button_archive: Archive
744 button_unarchive: Unarchive
751 button_unarchive: Unarchive
745 button_reset: Reset
752 button_reset: Reset
746 button_rename: Rename
753 button_rename: Rename
747 button_change_password: Change password
754 button_change_password: Change password
748 button_copy: Copy
755 button_copy: Copy
749 button_annotate: Annotate
756 button_annotate: Annotate
750 button_update: Update
757 button_update: Update
751 button_configure: Configure
758 button_configure: Configure
752 button_quote: Quote
759 button_quote: Quote
753
760
754 status_active: active
761 status_active: active
755 status_registered: registered
762 status_registered: registered
756 status_locked: locked
763 status_locked: locked
757
764
758 version_status_open: open
765 version_status_open: open
759 version_status_locked: locked
766 version_status_locked: locked
760 version_status_closed: closed
767 version_status_closed: closed
761
768
762 field_active: Active
769 field_active: Active
763
770
764 text_select_mail_notifications: Select actions for which email notifications should be sent.
771 text_select_mail_notifications: Select actions for which email notifications should be sent.
765 text_regexp_info: eg. ^[A-Z0-9]+$
772 text_regexp_info: eg. ^[A-Z0-9]+$
766 text_min_max_length_info: 0 means no restriction
773 text_min_max_length_info: 0 means no restriction
767 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
774 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
768 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
775 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
769 text_workflow_edit: Select a role and a tracker to edit the workflow
776 text_workflow_edit: Select a role and a tracker to edit the workflow
770 text_are_you_sure: Are you sure ?
777 text_are_you_sure: Are you sure ?
771 text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
778 text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
772 text_journal_set_to: "{{label}} set to {{value}}"
779 text_journal_set_to: "{{label}} set to {{value}}"
773 text_journal_deleted: "{{label}} deleted ({{old}})"
780 text_journal_deleted: "{{label}} deleted ({{old}})"
774 text_journal_added: "{{label}} {{value}} added"
781 text_journal_added: "{{label}} {{value}} added"
775 text_tip_task_begin_day: task beginning this day
782 text_tip_task_begin_day: task beginning this day
776 text_tip_task_end_day: task ending this day
783 text_tip_task_end_day: task ending this day
777 text_tip_task_begin_end_day: task beginning and ending this day
784 text_tip_task_begin_end_day: task beginning and ending this day
778 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
785 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
779 text_caracters_maximum: "{{count}} characters maximum."
786 text_caracters_maximum: "{{count}} characters maximum."
780 text_caracters_minimum: "Must be at least {{count}} characters long."
787 text_caracters_minimum: "Must be at least {{count}} characters long."
781 text_length_between: "Length between {{min}} and {{max}} characters."
788 text_length_between: "Length between {{min}} and {{max}} characters."
782 text_tracker_no_workflow: No workflow defined for this tracker
789 text_tracker_no_workflow: No workflow defined for this tracker
783 text_unallowed_characters: Unallowed characters
790 text_unallowed_characters: Unallowed characters
784 text_comma_separated: Multiple values allowed (comma separated).
791 text_comma_separated: Multiple values allowed (comma separated).
785 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
792 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
786 text_issue_added: "Issue {{id}} has been reported by {{author}}."
793 text_issue_added: "Issue {{id}} has been reported by {{author}}."
787 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
794 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
788 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
795 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
789 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
796 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
790 text_issue_category_destroy_assignments: Remove category assignments
797 text_issue_category_destroy_assignments: Remove category assignments
791 text_issue_category_reassign_to: Reassign issues to this category
798 text_issue_category_reassign_to: Reassign issues to this category
792 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
799 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
793 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
800 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
794 text_load_default_configuration: Load the default configuration
801 text_load_default_configuration: Load the default configuration
795 text_status_changed_by_changeset: "Applied in changeset {{value}}."
802 text_status_changed_by_changeset: "Applied in changeset {{value}}."
796 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
803 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
797 text_select_project_modules: 'Select modules to enable for this project:'
804 text_select_project_modules: 'Select modules to enable for this project:'
798 text_default_administrator_account_changed: Default administrator account changed
805 text_default_administrator_account_changed: Default administrator account changed
799 text_file_repository_writable: Attachments directory writable
806 text_file_repository_writable: Attachments directory writable
800 text_plugin_assets_writable: Plugin assets directory writable
807 text_plugin_assets_writable: Plugin assets directory writable
801 text_rmagick_available: RMagick available (optional)
808 text_rmagick_available: RMagick available (optional)
802 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
809 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
803 text_destroy_time_entries: Delete reported hours
810 text_destroy_time_entries: Delete reported hours
804 text_assign_time_entries_to_project: Assign reported hours to the project
811 text_assign_time_entries_to_project: Assign reported hours to the project
805 text_reassign_time_entries: 'Reassign reported hours to this issue:'
812 text_reassign_time_entries: 'Reassign reported hours to this issue:'
806 text_user_wrote: "{{value}} wrote:"
813 text_user_wrote: "{{value}} wrote:"
807 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
814 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
808 text_enumeration_category_reassign_to: 'Reassign them to this value:'
815 text_enumeration_category_reassign_to: 'Reassign them to this value:'
809 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
816 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
810 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
817 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
811 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
818 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
812 text_custom_field_possible_values_info: 'One line for each value'
819 text_custom_field_possible_values_info: 'One line for each value'
813 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
820 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
814 text_wiki_page_nullify_children: "Keep child pages as root pages"
821 text_wiki_page_nullify_children: "Keep child pages as root pages"
815 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
822 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
816 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
823 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
817
824
818 default_role_manager: Manager
825 default_role_manager: Manager
819 default_role_developper: Developer
826 default_role_developper: Developer
820 default_role_reporter: Reporter
827 default_role_reporter: Reporter
821 default_tracker_bug: Bug
828 default_tracker_bug: Bug
822 default_tracker_feature: Feature
829 default_tracker_feature: Feature
823 default_tracker_support: Support
830 default_tracker_support: Support
824 default_issue_status_new: New
831 default_issue_status_new: New
825 default_issue_status_in_progress: In Progress
832 default_issue_status_in_progress: In Progress
826 default_issue_status_resolved: Resolved
833 default_issue_status_resolved: Resolved
827 default_issue_status_feedback: Feedback
834 default_issue_status_feedback: Feedback
828 default_issue_status_closed: Closed
835 default_issue_status_closed: Closed
829 default_issue_status_rejected: Rejected
836 default_issue_status_rejected: Rejected
830 default_doc_category_user: User documentation
837 default_doc_category_user: User documentation
831 default_doc_category_tech: Technical documentation
838 default_doc_category_tech: Technical documentation
832 default_priority_low: Low
839 default_priority_low: Low
833 default_priority_normal: Normal
840 default_priority_normal: Normal
834 default_priority_high: High
841 default_priority_high: High
835 default_priority_urgent: Urgent
842 default_priority_urgent: Urgent
836 default_priority_immediate: Immediate
843 default_priority_immediate: Immediate
837 default_activity_design: Design
844 default_activity_design: Design
838 default_activity_development: Development
845 default_activity_development: Development
839
846
840 enumeration_issue_priorities: Issue priorities
847 enumeration_issue_priorities: Issue priorities
841 enumeration_doc_categories: Document categories
848 enumeration_doc_categories: Document categories
842 enumeration_activities: Activities (time tracking)
849 enumeration_activities: Activities (time tracking)
843 enumeration_system_activity: System Activity
850 enumeration_system_activity: System Activity
@@ -1,866 +1,873
1 # French translations for Ruby on Rails
1 # French translations for Ruby on Rails
2 # by Christian Lescuyer (christian@flyingcoders.com)
2 # by Christian Lescuyer (christian@flyingcoders.com)
3 # contributor: Sebastien Grosjean - ZenCocoon.com
3 # contributor: Sebastien Grosjean - ZenCocoon.com
4
4
5 fr:
5 fr:
6 date:
6 date:
7 formats:
7 formats:
8 default: "%d/%m/%Y"
8 default: "%d/%m/%Y"
9 short: "%e %b"
9 short: "%e %b"
10 long: "%e %B %Y"
10 long: "%e %B %Y"
11 long_ordinal: "%e %B %Y"
11 long_ordinal: "%e %B %Y"
12 only_day: "%e"
12 only_day: "%e"
13
13
14 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
14 day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi]
15 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
15 abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam]
16 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
16 month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre]
17 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
17 abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.]
18 order: [ :day, :month, :year ]
18 order: [ :day, :month, :year ]
19
19
20 time:
20 time:
21 formats:
21 formats:
22 default: "%d/%m/%Y %H:%M"
22 default: "%d/%m/%Y %H:%M"
23 time: "%H:%M"
23 time: "%H:%M"
24 short: "%d %b %H:%M"
24 short: "%d %b %H:%M"
25 long: "%A %d %B %Y %H:%M:%S %Z"
25 long: "%A %d %B %Y %H:%M:%S %Z"
26 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
26 long_ordinal: "%A %d %B %Y %H:%M:%S %Z"
27 only_second: "%S"
27 only_second: "%S"
28 am: 'am'
28 am: 'am'
29 pm: 'pm'
29 pm: 'pm'
30
30
31 datetime:
31 datetime:
32 distance_in_words:
32 distance_in_words:
33 half_a_minute: "30 secondes"
33 half_a_minute: "30 secondes"
34 less_than_x_seconds:
34 less_than_x_seconds:
35 zero: "moins d'une seconde"
35 zero: "moins d'une seconde"
36 one: "moins de 1 seconde"
36 one: "moins de 1 seconde"
37 other: "moins de {{count}} secondes"
37 other: "moins de {{count}} secondes"
38 x_seconds:
38 x_seconds:
39 one: "1 seconde"
39 one: "1 seconde"
40 other: "{{count}} secondes"
40 other: "{{count}} secondes"
41 less_than_x_minutes:
41 less_than_x_minutes:
42 zero: "moins d'une minute"
42 zero: "moins d'une minute"
43 one: "moins de 1 minute"
43 one: "moins de 1 minute"
44 other: "moins de {{count}} minutes"
44 other: "moins de {{count}} minutes"
45 x_minutes:
45 x_minutes:
46 one: "1 minute"
46 one: "1 minute"
47 other: "{{count}} minutes"
47 other: "{{count}} minutes"
48 about_x_hours:
48 about_x_hours:
49 one: "environ une heure"
49 one: "environ une heure"
50 other: "environ {{count}} heures"
50 other: "environ {{count}} heures"
51 x_days:
51 x_days:
52 one: "1 jour"
52 one: "1 jour"
53 other: "{{count}} jours"
53 other: "{{count}} jours"
54 about_x_months:
54 about_x_months:
55 one: "environ un mois"
55 one: "environ un mois"
56 other: "environ {{count}} mois"
56 other: "environ {{count}} mois"
57 x_months:
57 x_months:
58 one: "1 mois"
58 one: "1 mois"
59 other: "{{count}} mois"
59 other: "{{count}} mois"
60 about_x_years:
60 about_x_years:
61 one: "environ un an"
61 one: "environ un an"
62 other: "environ {{count}} ans"
62 other: "environ {{count}} ans"
63 over_x_years:
63 over_x_years:
64 one: "plus d'un an"
64 one: "plus d'un an"
65 other: "plus de {{count}} ans"
65 other: "plus de {{count}} ans"
66 prompts:
66 prompts:
67 year: "Année"
67 year: "Année"
68 month: "Mois"
68 month: "Mois"
69 day: "Jour"
69 day: "Jour"
70 hour: "Heure"
70 hour: "Heure"
71 minute: "Minute"
71 minute: "Minute"
72 second: "Seconde"
72 second: "Seconde"
73
73
74 number:
74 number:
75 format:
75 format:
76 precision: 3
76 precision: 3
77 separator: ','
77 separator: ','
78 delimiter: ' '
78 delimiter: ' '
79 currency:
79 currency:
80 format:
80 format:
81 unit: '€'
81 unit: '€'
82 precision: 2
82 precision: 2
83 format: '%n %u'
83 format: '%n %u'
84 human:
84 human:
85 format:
85 format:
86 precision: 2
86 precision: 2
87 storage_units:
87 storage_units:
88 format: "%n %u"
88 format: "%n %u"
89 units:
89 units:
90 byte:
90 byte:
91 one: "Octet"
91 one: "Octet"
92 other: "Octet"
92 other: "Octet"
93 kb: "ko"
93 kb: "ko"
94 mb: "Mo"
94 mb: "Mo"
95 gb: "Go"
95 gb: "Go"
96 tb: "To"
96 tb: "To"
97
97
98 support:
98 support:
99 array:
99 array:
100 sentence_connector: 'et'
100 sentence_connector: 'et'
101 skip_last_comma: true
101 skip_last_comma: true
102 word_connector: ", "
102 word_connector: ", "
103 two_words_connector: " et "
103 two_words_connector: " et "
104 last_word_connector: " et "
104 last_word_connector: " et "
105
105
106 activerecord:
106 activerecord:
107 errors:
107 errors:
108 template:
108 template:
109 header:
109 header:
110 one: "Impossible d'enregistrer {{model}}: 1 erreur"
110 one: "Impossible d'enregistrer {{model}}: 1 erreur"
111 other: "Impossible d'enregistrer {{model}}: {{count}} erreurs."
111 other: "Impossible d'enregistrer {{model}}: {{count}} erreurs."
112 body: "Veuillez vérifier les champs suivants :"
112 body: "Veuillez vérifier les champs suivants :"
113 messages:
113 messages:
114 inclusion: "n'est pas inclus(e) dans la liste"
114 inclusion: "n'est pas inclus(e) dans la liste"
115 exclusion: "n'est pas disponible"
115 exclusion: "n'est pas disponible"
116 invalid: "n'est pas valide"
116 invalid: "n'est pas valide"
117 confirmation: "ne concorde pas avec la confirmation"
117 confirmation: "ne concorde pas avec la confirmation"
118 accepted: "doit être accepté(e)"
118 accepted: "doit être accepté(e)"
119 empty: "doit être renseigné(e)"
119 empty: "doit être renseigné(e)"
120 blank: "doit être renseigné(e)"
120 blank: "doit être renseigné(e)"
121 too_long: "est trop long (pas plus de {{count}} caractères)"
121 too_long: "est trop long (pas plus de {{count}} caractères)"
122 too_short: "est trop court (au moins {{count}} caractères)"
122 too_short: "est trop court (au moins {{count}} caractères)"
123 wrong_length: "ne fait pas la bonne longueur (doit comporter {{count}} caractères)"
123 wrong_length: "ne fait pas la bonne longueur (doit comporter {{count}} caractères)"
124 taken: "est déjà utilisé"
124 taken: "est déjà utilisé"
125 not_a_number: "n'est pas un nombre"
125 not_a_number: "n'est pas un nombre"
126 greater_than: "doit être supérieur à {{count}}"
126 greater_than: "doit être supérieur à {{count}}"
127 greater_than_or_equal_to: "doit être supérieur ou égal à {{count}}"
127 greater_than_or_equal_to: "doit être supérieur ou égal à {{count}}"
128 equal_to: "doit être égal à {{count}}"
128 equal_to: "doit être égal à {{count}}"
129 less_than: "doit être inférieur à {{count}}"
129 less_than: "doit être inférieur à {{count}}"
130 less_than_or_equal_to: "doit être inférieur ou égal à {{count}}"
130 less_than_or_equal_to: "doit être inférieur ou égal à {{count}}"
131 odd: "doit être impair"
131 odd: "doit être impair"
132 even: "doit être pair"
132 even: "doit être pair"
133 greater_than_start_date: "doit être postérieure à la date de début"
133 greater_than_start_date: "doit être postérieure à la date de début"
134 not_same_project: "n'appartient pas au même projet"
134 not_same_project: "n'appartient pas au même projet"
135 circular_dependency: "Cette relation créerait une dépendance circulaire"
135 circular_dependency: "Cette relation créerait une dépendance circulaire"
136
136
137 actionview_instancetag_blank_option: Choisir
137 actionview_instancetag_blank_option: Choisir
138
138
139 general_text_No: 'Non'
139 general_text_No: 'Non'
140 general_text_Yes: 'Oui'
140 general_text_Yes: 'Oui'
141 general_text_no: 'non'
141 general_text_no: 'non'
142 general_text_yes: 'oui'
142 general_text_yes: 'oui'
143 general_lang_name: 'Français'
143 general_lang_name: 'Français'
144 general_csv_separator: ';'
144 general_csv_separator: ';'
145 general_csv_decimal_separator: ','
145 general_csv_decimal_separator: ','
146 general_csv_encoding: ISO-8859-1
146 general_csv_encoding: ISO-8859-1
147 general_pdf_encoding: ISO-8859-1
147 general_pdf_encoding: ISO-8859-1
148 general_first_day_of_week: '1'
148 general_first_day_of_week: '1'
149
149
150 notice_account_updated: Le compte a été mis à jour avec succès.
150 notice_account_updated: Le compte a été mis à jour avec succès.
151 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
151 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
152 notice_account_password_updated: Mot de passe mis à jour avec succès.
152 notice_account_password_updated: Mot de passe mis à jour avec succès.
153 notice_account_wrong_password: Mot de passe incorrect
153 notice_account_wrong_password: Mot de passe incorrect
154 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé.
154 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé.
155 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
155 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
156 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
156 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
157 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
157 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
158 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
158 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
159 notice_successful_create: Création effectuée avec succès.
159 notice_successful_create: Création effectuée avec succès.
160 notice_successful_update: Mise à jour effectuée avec succès.
160 notice_successful_update: Mise à jour effectuée avec succès.
161 notice_successful_delete: Suppression effectuée avec succès.
161 notice_successful_delete: Suppression effectuée avec succès.
162 notice_successful_connection: Connection réussie.
162 notice_successful_connection: Connection réussie.
163 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
163 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
164 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
164 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
165 notice_not_authorized: "Vous n'êtes pas autorisés à accéder à cette page."
165 notice_not_authorized: "Vous n'êtes pas autorisés à accéder à cette page."
166 notice_email_sent: "Un email a été envoyé à {{value}}"
166 notice_email_sent: "Un email a été envoyé à {{value}}"
167 notice_email_error: "Erreur lors de l'envoi de l'email ({{value}})"
167 notice_email_error: "Erreur lors de l'envoi de l'email ({{value}})"
168 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
168 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
169 notice_failed_to_save_issues: "{{count}} demande(s) sur les {{total}} sélectionnées n'ont pas pu être mise(s) à jour: {{ids}}."
169 notice_failed_to_save_issues: "{{count}} demande(s) sur les {{total}} sélectionnées n'ont pas pu être mise(s) à jour: {{ids}}."
170 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
170 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
171 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
171 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
172 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
172 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
173 notice_unable_delete_version: Impossible de supprimer cette version.
173 notice_unable_delete_version: Impossible de supprimer cette version.
174
174
175 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage: {{value}}"
175 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage: {{value}}"
176 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
176 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
177 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt: {{value}}"
177 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt: {{value}}"
178 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
178 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
179 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
179 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
180 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
180 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
181 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
181
182
182 warning_attachments_not_saved: "{{count}} fichier(s) n'ont pas pu être sauvegardés."
183 warning_attachments_not_saved: "{{count}} fichier(s) n'ont pas pu être sauvegardés."
183
184
184 mail_subject_lost_password: "Votre mot de passe {{value}}"
185 mail_subject_lost_password: "Votre mot de passe {{value}}"
185 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant:'
186 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant:'
186 mail_subject_register: "Activation de votre compte {{value}}"
187 mail_subject_register: "Activation de votre compte {{value}}"
187 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant:'
188 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant:'
188 mail_body_account_information_external: "Vous pouvez utiliser votre compte {{value}} pour vous connecter."
189 mail_body_account_information_external: "Vous pouvez utiliser votre compte {{value}} pour vous connecter."
189 mail_body_account_information: Paramètres de connexion de votre compte
190 mail_body_account_information: Paramètres de connexion de votre compte
190 mail_subject_account_activation_request: "Demande d'activation d'un compte {{value}}"
191 mail_subject_account_activation_request: "Demande d'activation d'un compte {{value}}"
191 mail_body_account_activation_request: "Un nouvel utilisateur ({{value}}) s'est inscrit. Son compte nécessite votre approbation:"
192 mail_body_account_activation_request: "Un nouvel utilisateur ({{value}}) s'est inscrit. Son compte nécessite votre approbation:"
192 mail_subject_reminder: "{{count}} demande(s) arrivent à échéance"
193 mail_subject_reminder: "{{count}} demande(s) arrivent à échéance"
193 mail_body_reminder: "{{count}} demande(s) qui vous sont assignées arrivent à échéance dans les {{days}} prochains jours:"
194 mail_body_reminder: "{{count}} demande(s) qui vous sont assignées arrivent à échéance dans les {{days}} prochains jours:"
194 mail_subject_wiki_content_added: "Page wiki '{{page}}' ajoutée"
195 mail_subject_wiki_content_added: "Page wiki '{{page}}' ajoutée"
195 mail_body_wiki_content_added: "La page wiki '{{page}}' a été ajoutée par {{author}}."
196 mail_body_wiki_content_added: "La page wiki '{{page}}' a été ajoutée par {{author}}."
196 mail_subject_wiki_content_updated: "Page wiki '{{page}}' mise à jour"
197 mail_subject_wiki_content_updated: "Page wiki '{{page}}' mise à jour"
197 mail_body_wiki_content_updated: "La page wiki '{{page}}' a été mise à jour par {{author}}."
198 mail_body_wiki_content_updated: "La page wiki '{{page}}' a été mise à jour par {{author}}."
198
199
199 gui_validation_error: 1 erreur
200 gui_validation_error: 1 erreur
200 gui_validation_error_plural: "{{count}} erreurs"
201 gui_validation_error_plural: "{{count}} erreurs"
201
202
202 field_name: Nom
203 field_name: Nom
203 field_description: Description
204 field_description: Description
204 field_summary: Résumé
205 field_summary: Résumé
205 field_is_required: Obligatoire
206 field_is_required: Obligatoire
206 field_firstname: Prénom
207 field_firstname: Prénom
207 field_lastname: Nom
208 field_lastname: Nom
208 field_mail: Email
209 field_mail: Email
209 field_filename: Fichier
210 field_filename: Fichier
210 field_filesize: Taille
211 field_filesize: Taille
211 field_downloads: Téléchargements
212 field_downloads: Téléchargements
212 field_author: Auteur
213 field_author: Auteur
213 field_created_on: Créé
214 field_created_on: Créé
214 field_updated_on: Mis à jour
215 field_updated_on: Mis à jour
215 field_field_format: Format
216 field_field_format: Format
216 field_is_for_all: Pour tous les projets
217 field_is_for_all: Pour tous les projets
217 field_possible_values: Valeurs possibles
218 field_possible_values: Valeurs possibles
218 field_regexp: Expression régulière
219 field_regexp: Expression régulière
219 field_min_length: Longueur minimum
220 field_min_length: Longueur minimum
220 field_max_length: Longueur maximum
221 field_max_length: Longueur maximum
221 field_value: Valeur
222 field_value: Valeur
222 field_category: Catégorie
223 field_category: Catégorie
223 field_title: Titre
224 field_title: Titre
224 field_project: Projet
225 field_project: Projet
225 field_issue: Demande
226 field_issue: Demande
226 field_status: Statut
227 field_status: Statut
227 field_notes: Notes
228 field_notes: Notes
228 field_is_closed: Demande fermée
229 field_is_closed: Demande fermée
229 field_is_default: Valeur par défaut
230 field_is_default: Valeur par défaut
230 field_tracker: Tracker
231 field_tracker: Tracker
231 field_subject: Sujet
232 field_subject: Sujet
232 field_due_date: Echéance
233 field_due_date: Echéance
233 field_assigned_to: Assigné à
234 field_assigned_to: Assigné à
234 field_priority: Priorité
235 field_priority: Priorité
235 field_fixed_version: Version cible
236 field_fixed_version: Version cible
236 field_user: Utilisateur
237 field_user: Utilisateur
237 field_role: Rôle
238 field_role: Rôle
238 field_homepage: Site web
239 field_homepage: Site web
239 field_is_public: Public
240 field_is_public: Public
240 field_parent: Sous-projet de
241 field_parent: Sous-projet de
241 field_is_in_chlog: Demandes affichées dans l'historique
242 field_is_in_chlog: Demandes affichées dans l'historique
242 field_is_in_roadmap: Demandes affichées dans la roadmap
243 field_is_in_roadmap: Demandes affichées dans la roadmap
243 field_login: Identifiant
244 field_login: Identifiant
244 field_mail_notification: Notifications par mail
245 field_mail_notification: Notifications par mail
245 field_admin: Administrateur
246 field_admin: Administrateur
246 field_last_login_on: Dernière connexion
247 field_last_login_on: Dernière connexion
247 field_language: Langue
248 field_language: Langue
248 field_effective_date: Date
249 field_effective_date: Date
249 field_password: Mot de passe
250 field_password: Mot de passe
250 field_new_password: Nouveau mot de passe
251 field_new_password: Nouveau mot de passe
251 field_password_confirmation: Confirmation
252 field_password_confirmation: Confirmation
252 field_version: Version
253 field_version: Version
253 field_type: Type
254 field_type: Type
254 field_host: Hôte
255 field_host: Hôte
255 field_port: Port
256 field_port: Port
256 field_account: Compte
257 field_account: Compte
257 field_base_dn: Base DN
258 field_base_dn: Base DN
258 field_attr_login: Attribut Identifiant
259 field_attr_login: Attribut Identifiant
259 field_attr_firstname: Attribut Prénom
260 field_attr_firstname: Attribut Prénom
260 field_attr_lastname: Attribut Nom
261 field_attr_lastname: Attribut Nom
261 field_attr_mail: Attribut Email
262 field_attr_mail: Attribut Email
262 field_onthefly: Création des utilisateurs à la volée
263 field_onthefly: Création des utilisateurs à la volée
263 field_start_date: Début
264 field_start_date: Début
264 field_done_ratio: % Réalisé
265 field_done_ratio: % Réalisé
265 field_auth_source: Mode d'authentification
266 field_auth_source: Mode d'authentification
266 field_hide_mail: Cacher mon adresse mail
267 field_hide_mail: Cacher mon adresse mail
267 field_comments: Commentaire
268 field_comments: Commentaire
268 field_url: URL
269 field_url: URL
269 field_start_page: Page de démarrage
270 field_start_page: Page de démarrage
270 field_subproject: Sous-projet
271 field_subproject: Sous-projet
271 field_hours: Heures
272 field_hours: Heures
272 field_activity: Activité
273 field_activity: Activité
273 field_spent_on: Date
274 field_spent_on: Date
274 field_identifier: Identifiant
275 field_identifier: Identifiant
275 field_is_filter: Utilisé comme filtre
276 field_is_filter: Utilisé comme filtre
276 field_issue_to: Demande liée
277 field_issue_to: Demande liée
277 field_delay: Retard
278 field_delay: Retard
278 field_assignable: Demandes assignables à ce rôle
279 field_assignable: Demandes assignables à ce rôle
279 field_redirect_existing_links: Rediriger les liens existants
280 field_redirect_existing_links: Rediriger les liens existants
280 field_estimated_hours: Temps estimé
281 field_estimated_hours: Temps estimé
281 field_column_names: Colonnes
282 field_column_names: Colonnes
282 field_time_zone: Fuseau horaire
283 field_time_zone: Fuseau horaire
283 field_searchable: Utilisé pour les recherches
284 field_searchable: Utilisé pour les recherches
284 field_default_value: Valeur par défaut
285 field_default_value: Valeur par défaut
285 field_comments_sorting: Afficher les commentaires
286 field_comments_sorting: Afficher les commentaires
286 field_parent_title: Page parent
287 field_parent_title: Page parent
287 field_editable: Modifiable
288 field_editable: Modifiable
288 field_watcher: Observateur
289 field_watcher: Observateur
289 field_identity_url: URL OpenID
290 field_identity_url: URL OpenID
290 field_content: Contenu
291 field_content: Contenu
291 field_group_by: Grouper par
292 field_group_by: Grouper par
293 field_sharing: Partage
292
294
293 setting_app_title: Titre de l'application
295 setting_app_title: Titre de l'application
294 setting_app_subtitle: Sous-titre de l'application
296 setting_app_subtitle: Sous-titre de l'application
295 setting_welcome_text: Texte d'accueil
297 setting_welcome_text: Texte d'accueil
296 setting_default_language: Langue par défaut
298 setting_default_language: Langue par défaut
297 setting_login_required: Authentification obligatoire
299 setting_login_required: Authentification obligatoire
298 setting_self_registration: Inscription des nouveaux utilisateurs
300 setting_self_registration: Inscription des nouveaux utilisateurs
299 setting_attachment_max_size: Taille max des fichiers
301 setting_attachment_max_size: Taille max des fichiers
300 setting_issues_export_limit: Limite export demandes
302 setting_issues_export_limit: Limite export demandes
301 setting_mail_from: Adresse d'émission
303 setting_mail_from: Adresse d'émission
302 setting_bcc_recipients: Destinataires en copie cachée (cci)
304 setting_bcc_recipients: Destinataires en copie cachée (cci)
303 setting_plain_text_mail: Mail texte brut (non HTML)
305 setting_plain_text_mail: Mail texte brut (non HTML)
304 setting_host_name: Nom d'hôte et chemin
306 setting_host_name: Nom d'hôte et chemin
305 setting_text_formatting: Formatage du texte
307 setting_text_formatting: Formatage du texte
306 setting_wiki_compression: Compression historique wiki
308 setting_wiki_compression: Compression historique wiki
307 setting_feeds_limit: Limite du contenu des flux RSS
309 setting_feeds_limit: Limite du contenu des flux RSS
308 setting_default_projects_public: Définir les nouveaux projects comme publics par défaut
310 setting_default_projects_public: Définir les nouveaux projects comme publics par défaut
309 setting_autofetch_changesets: Récupération auto. des commits
311 setting_autofetch_changesets: Récupération auto. des commits
310 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
312 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
311 setting_commit_ref_keywords: Mot-clés de référencement
313 setting_commit_ref_keywords: Mot-clés de référencement
312 setting_commit_fix_keywords: Mot-clés de résolution
314 setting_commit_fix_keywords: Mot-clés de résolution
313 setting_autologin: Autologin
315 setting_autologin: Autologin
314 setting_date_format: Format de date
316 setting_date_format: Format de date
315 setting_time_format: Format d'heure
317 setting_time_format: Format d'heure
316 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
318 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
317 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
319 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
318 setting_repositories_encodings: Encodages des dépôts
320 setting_repositories_encodings: Encodages des dépôts
319 setting_commit_logs_encoding: Encodage des messages de commit
321 setting_commit_logs_encoding: Encodage des messages de commit
320 setting_emails_footer: Pied-de-page des emails
322 setting_emails_footer: Pied-de-page des emails
321 setting_protocol: Protocole
323 setting_protocol: Protocole
322 setting_per_page_options: Options d'objets affichés par page
324 setting_per_page_options: Options d'objets affichés par page
323 setting_user_format: Format d'affichage des utilisateurs
325 setting_user_format: Format d'affichage des utilisateurs
324 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
326 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
325 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
327 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
326 setting_enabled_scm: SCM activés
328 setting_enabled_scm: SCM activés
327 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
329 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
328 setting_mail_handler_api_key: Clé de protection de l'API
330 setting_mail_handler_api_key: Clé de protection de l'API
329 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
331 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
330 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
332 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
331 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
333 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
332 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
334 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
333 setting_repository_log_display_limit: "Nombre maximum de revisions affichées sur l'historique d'un fichier"
335 setting_repository_log_display_limit: "Nombre maximum de revisions affichées sur l'historique d'un fichier"
334 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
336 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
335 setting_password_min_length: Longueur minimum des mots de passe
337 setting_password_min_length: Longueur minimum des mots de passe
336 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
338 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
337 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
339 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
338
340
339 permission_add_project: Créer un projet
341 permission_add_project: Créer un projet
340 permission_edit_project: Modifier le projet
342 permission_edit_project: Modifier le projet
341 permission_select_project_modules: Choisir les modules
343 permission_select_project_modules: Choisir les modules
342 permission_manage_members: Gérer les members
344 permission_manage_members: Gérer les members
343 permission_manage_versions: Gérer les versions
345 permission_manage_versions: Gérer les versions
344 permission_manage_categories: Gérer les catégories de demandes
346 permission_manage_categories: Gérer les catégories de demandes
345 permission_add_issues: Créer des demandes
347 permission_add_issues: Créer des demandes
346 permission_edit_issues: Modifier les demandes
348 permission_edit_issues: Modifier les demandes
347 permission_manage_issue_relations: Gérer les relations
349 permission_manage_issue_relations: Gérer les relations
348 permission_add_issue_notes: Ajouter des notes
350 permission_add_issue_notes: Ajouter des notes
349 permission_edit_issue_notes: Modifier les notes
351 permission_edit_issue_notes: Modifier les notes
350 permission_edit_own_issue_notes: Modifier ses propres notes
352 permission_edit_own_issue_notes: Modifier ses propres notes
351 permission_move_issues: Déplacer les demandes
353 permission_move_issues: Déplacer les demandes
352 permission_delete_issues: Supprimer les demandes
354 permission_delete_issues: Supprimer les demandes
353 permission_manage_public_queries: Gérer les requêtes publiques
355 permission_manage_public_queries: Gérer les requêtes publiques
354 permission_save_queries: Sauvegarder les requêtes
356 permission_save_queries: Sauvegarder les requêtes
355 permission_view_gantt: Voir le gantt
357 permission_view_gantt: Voir le gantt
356 permission_view_calendar: Voir le calendrier
358 permission_view_calendar: Voir le calendrier
357 permission_view_issue_watchers: Voir la liste des observateurs
359 permission_view_issue_watchers: Voir la liste des observateurs
358 permission_add_issue_watchers: Ajouter des observateurs
360 permission_add_issue_watchers: Ajouter des observateurs
359 permission_delete_issue_watchers: Supprimer des observateurs
361 permission_delete_issue_watchers: Supprimer des observateurs
360 permission_log_time: Saisir le temps passé
362 permission_log_time: Saisir le temps passé
361 permission_view_time_entries: Voir le temps passé
363 permission_view_time_entries: Voir le temps passé
362 permission_edit_time_entries: Modifier les temps passés
364 permission_edit_time_entries: Modifier les temps passés
363 permission_edit_own_time_entries: Modifier son propre temps passé
365 permission_edit_own_time_entries: Modifier son propre temps passé
364 permission_manage_news: Gérer les annonces
366 permission_manage_news: Gérer les annonces
365 permission_comment_news: Commenter les annonces
367 permission_comment_news: Commenter les annonces
366 permission_manage_documents: Gérer les documents
368 permission_manage_documents: Gérer les documents
367 permission_view_documents: Voir les documents
369 permission_view_documents: Voir les documents
368 permission_manage_files: Gérer les fichiers
370 permission_manage_files: Gérer les fichiers
369 permission_view_files: Voir les fichiers
371 permission_view_files: Voir les fichiers
370 permission_manage_wiki: Gérer le wiki
372 permission_manage_wiki: Gérer le wiki
371 permission_rename_wiki_pages: Renommer les pages
373 permission_rename_wiki_pages: Renommer les pages
372 permission_delete_wiki_pages: Supprimer les pages
374 permission_delete_wiki_pages: Supprimer les pages
373 permission_view_wiki_pages: Voir le wiki
375 permission_view_wiki_pages: Voir le wiki
374 permission_view_wiki_edits: "Voir l'historique des modifications"
376 permission_view_wiki_edits: "Voir l'historique des modifications"
375 permission_edit_wiki_pages: Modifier les pages
377 permission_edit_wiki_pages: Modifier les pages
376 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
378 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
377 permission_protect_wiki_pages: Protéger les pages
379 permission_protect_wiki_pages: Protéger les pages
378 permission_manage_repository: Gérer le dépôt de sources
380 permission_manage_repository: Gérer le dépôt de sources
379 permission_browse_repository: Parcourir les sources
381 permission_browse_repository: Parcourir les sources
380 permission_view_changesets: Voir les révisions
382 permission_view_changesets: Voir les révisions
381 permission_commit_access: Droit de commit
383 permission_commit_access: Droit de commit
382 permission_manage_boards: Gérer les forums
384 permission_manage_boards: Gérer les forums
383 permission_view_messages: Voir les messages
385 permission_view_messages: Voir les messages
384 permission_add_messages: Poster un message
386 permission_add_messages: Poster un message
385 permission_edit_messages: Modifier les messages
387 permission_edit_messages: Modifier les messages
386 permission_edit_own_messages: Modifier ses propres messages
388 permission_edit_own_messages: Modifier ses propres messages
387 permission_delete_messages: Supprimer les messages
389 permission_delete_messages: Supprimer les messages
388 permission_delete_own_messages: Supprimer ses propres messages
390 permission_delete_own_messages: Supprimer ses propres messages
389
391
390 project_module_issue_tracking: Suivi des demandes
392 project_module_issue_tracking: Suivi des demandes
391 project_module_time_tracking: Suivi du temps passé
393 project_module_time_tracking: Suivi du temps passé
392 project_module_news: Publication d'annonces
394 project_module_news: Publication d'annonces
393 project_module_documents: Publication de documents
395 project_module_documents: Publication de documents
394 project_module_files: Publication de fichiers
396 project_module_files: Publication de fichiers
395 project_module_wiki: Wiki
397 project_module_wiki: Wiki
396 project_module_repository: Dépôt de sources
398 project_module_repository: Dépôt de sources
397 project_module_boards: Forums de discussion
399 project_module_boards: Forums de discussion
398
400
399 label_user: Utilisateur
401 label_user: Utilisateur
400 label_user_plural: Utilisateurs
402 label_user_plural: Utilisateurs
401 label_user_new: Nouvel utilisateur
403 label_user_new: Nouvel utilisateur
402 label_user_anonymous: Anonyme
404 label_user_anonymous: Anonyme
403 label_project: Projet
405 label_project: Projet
404 label_project_new: Nouveau projet
406 label_project_new: Nouveau projet
405 label_project_plural: Projets
407 label_project_plural: Projets
406 label_x_projects:
408 label_x_projects:
407 zero: aucun projet
409 zero: aucun projet
408 one: 1 projet
410 one: 1 projet
409 other: "{{count}} projets"
411 other: "{{count}} projets"
410 label_project_all: Tous les projets
412 label_project_all: Tous les projets
411 label_project_latest: Derniers projets
413 label_project_latest: Derniers projets
412 label_issue: Demande
414 label_issue: Demande
413 label_issue_new: Nouvelle demande
415 label_issue_new: Nouvelle demande
414 label_issue_plural: Demandes
416 label_issue_plural: Demandes
415 label_issue_view_all: Voir toutes les demandes
417 label_issue_view_all: Voir toutes les demandes
416 label_issue_added: Demande ajoutée
418 label_issue_added: Demande ajoutée
417 label_issue_updated: Demande mise à jour
419 label_issue_updated: Demande mise à jour
418 label_issues_by: "Demandes par {{value}}"
420 label_issues_by: "Demandes par {{value}}"
419 label_document: Document
421 label_document: Document
420 label_document_new: Nouveau document
422 label_document_new: Nouveau document
421 label_document_plural: Documents
423 label_document_plural: Documents
422 label_document_added: Document ajouté
424 label_document_added: Document ajouté
423 label_role: Rôle
425 label_role: Rôle
424 label_role_plural: Rôles
426 label_role_plural: Rôles
425 label_role_new: Nouveau rôle
427 label_role_new: Nouveau rôle
426 label_role_and_permissions: Rôles et permissions
428 label_role_and_permissions: Rôles et permissions
427 label_member: Membre
429 label_member: Membre
428 label_member_new: Nouveau membre
430 label_member_new: Nouveau membre
429 label_member_plural: Membres
431 label_member_plural: Membres
430 label_tracker: Tracker
432 label_tracker: Tracker
431 label_tracker_plural: Trackers
433 label_tracker_plural: Trackers
432 label_tracker_new: Nouveau tracker
434 label_tracker_new: Nouveau tracker
433 label_workflow: Workflow
435 label_workflow: Workflow
434 label_issue_status: Statut de demandes
436 label_issue_status: Statut de demandes
435 label_issue_status_plural: Statuts de demandes
437 label_issue_status_plural: Statuts de demandes
436 label_issue_status_new: Nouveau statut
438 label_issue_status_new: Nouveau statut
437 label_issue_category: Catégorie de demandes
439 label_issue_category: Catégorie de demandes
438 label_issue_category_plural: Catégories de demandes
440 label_issue_category_plural: Catégories de demandes
439 label_issue_category_new: Nouvelle catégorie
441 label_issue_category_new: Nouvelle catégorie
440 label_custom_field: Champ personnalisé
442 label_custom_field: Champ personnalisé
441 label_custom_field_plural: Champs personnalisés
443 label_custom_field_plural: Champs personnalisés
442 label_custom_field_new: Nouveau champ personnalisé
444 label_custom_field_new: Nouveau champ personnalisé
443 label_enumerations: Listes de valeurs
445 label_enumerations: Listes de valeurs
444 label_enumeration_new: Nouvelle valeur
446 label_enumeration_new: Nouvelle valeur
445 label_information: Information
447 label_information: Information
446 label_information_plural: Informations
448 label_information_plural: Informations
447 label_please_login: Identification
449 label_please_login: Identification
448 label_register: S'enregistrer
450 label_register: S'enregistrer
449 label_login_with_open_id_option: S'authentifier avec OpenID
451 label_login_with_open_id_option: S'authentifier avec OpenID
450 label_password_lost: Mot de passe perdu
452 label_password_lost: Mot de passe perdu
451 label_home: Accueil
453 label_home: Accueil
452 label_my_page: Ma page
454 label_my_page: Ma page
453 label_my_account: Mon compte
455 label_my_account: Mon compte
454 label_my_projects: Mes projets
456 label_my_projects: Mes projets
455 label_administration: Administration
457 label_administration: Administration
456 label_login: Connexion
458 label_login: Connexion
457 label_logout: Déconnexion
459 label_logout: Déconnexion
458 label_help: Aide
460 label_help: Aide
459 label_reported_issues: Demandes soumises
461 label_reported_issues: Demandes soumises
460 label_assigned_to_me_issues: Demandes qui me sont assignées
462 label_assigned_to_me_issues: Demandes qui me sont assignées
461 label_last_login: Dernière connexion
463 label_last_login: Dernière connexion
462 label_registered_on: Inscrit le
464 label_registered_on: Inscrit le
463 label_activity: Activité
465 label_activity: Activité
464 label_overall_activity: Activité globale
466 label_overall_activity: Activité globale
465 label_user_activity: "Activité de {{value}}"
467 label_user_activity: "Activité de {{value}}"
466 label_new: Nouveau
468 label_new: Nouveau
467 label_logged_as: Connecté en tant que
469 label_logged_as: Connecté en tant que
468 label_environment: Environnement
470 label_environment: Environnement
469 label_authentication: Authentification
471 label_authentication: Authentification
470 label_auth_source: Mode d'authentification
472 label_auth_source: Mode d'authentification
471 label_auth_source_new: Nouveau mode d'authentification
473 label_auth_source_new: Nouveau mode d'authentification
472 label_auth_source_plural: Modes d'authentification
474 label_auth_source_plural: Modes d'authentification
473 label_subproject_plural: Sous-projets
475 label_subproject_plural: Sous-projets
474 label_and_its_subprojects: "{{value}} et ses sous-projets"
476 label_and_its_subprojects: "{{value}} et ses sous-projets"
475 label_min_max_length: Longueurs mini - maxi
477 label_min_max_length: Longueurs mini - maxi
476 label_list: Liste
478 label_list: Liste
477 label_date: Date
479 label_date: Date
478 label_integer: Entier
480 label_integer: Entier
479 label_float: Nombre décimal
481 label_float: Nombre décimal
480 label_boolean: Booléen
482 label_boolean: Booléen
481 label_string: Texte
483 label_string: Texte
482 label_text: Texte long
484 label_text: Texte long
483 label_attribute: Attribut
485 label_attribute: Attribut
484 label_attribute_plural: Attributs
486 label_attribute_plural: Attributs
485 label_download: "{{count}} Téléchargement"
487 label_download: "{{count}} Téléchargement"
486 label_download_plural: "{{count}} Téléchargements"
488 label_download_plural: "{{count}} Téléchargements"
487 label_no_data: Aucune donnée à afficher
489 label_no_data: Aucune donnée à afficher
488 label_change_status: Changer le statut
490 label_change_status: Changer le statut
489 label_history: Historique
491 label_history: Historique
490 label_attachment: Fichier
492 label_attachment: Fichier
491 label_attachment_new: Nouveau fichier
493 label_attachment_new: Nouveau fichier
492 label_attachment_delete: Supprimer le fichier
494 label_attachment_delete: Supprimer le fichier
493 label_attachment_plural: Fichiers
495 label_attachment_plural: Fichiers
494 label_file_added: Fichier ajouté
496 label_file_added: Fichier ajouté
495 label_report: Rapport
497 label_report: Rapport
496 label_report_plural: Rapports
498 label_report_plural: Rapports
497 label_news: Annonce
499 label_news: Annonce
498 label_news_new: Nouvelle annonce
500 label_news_new: Nouvelle annonce
499 label_news_plural: Annonces
501 label_news_plural: Annonces
500 label_news_latest: Dernières annonces
502 label_news_latest: Dernières annonces
501 label_news_view_all: Voir toutes les annonces
503 label_news_view_all: Voir toutes les annonces
502 label_news_added: Annonce ajoutée
504 label_news_added: Annonce ajoutée
503 label_change_log: Historique
505 label_change_log: Historique
504 label_settings: Configuration
506 label_settings: Configuration
505 label_overview: Aperçu
507 label_overview: Aperçu
506 label_version: Version
508 label_version: Version
507 label_version_new: Nouvelle version
509 label_version_new: Nouvelle version
508 label_version_plural: Versions
510 label_version_plural: Versions
509 label_confirmation: Confirmation
511 label_confirmation: Confirmation
510 label_export_to: 'Formats disponibles:'
512 label_export_to: 'Formats disponibles:'
511 label_read: Lire...
513 label_read: Lire...
512 label_public_projects: Projets publics
514 label_public_projects: Projets publics
513 label_open_issues: ouvert
515 label_open_issues: ouvert
514 label_open_issues_plural: ouverts
516 label_open_issues_plural: ouverts
515 label_closed_issues: fermé
517 label_closed_issues: fermé
516 label_closed_issues_plural: fermés
518 label_closed_issues_plural: fermés
517 label_x_open_issues_abbr_on_total:
519 label_x_open_issues_abbr_on_total:
518 zero: 0 ouvert sur {{total}}
520 zero: 0 ouvert sur {{total}}
519 one: 1 ouvert sur {{total}}
521 one: 1 ouvert sur {{total}}
520 other: "{{count}} ouverts sur {{total}}"
522 other: "{{count}} ouverts sur {{total}}"
521 label_x_open_issues_abbr:
523 label_x_open_issues_abbr:
522 zero: 0 ouvert
524 zero: 0 ouvert
523 one: 1 ouvert
525 one: 1 ouvert
524 other: "{{count}} ouverts"
526 other: "{{count}} ouverts"
525 label_x_closed_issues_abbr:
527 label_x_closed_issues_abbr:
526 zero: 0 fermé
528 zero: 0 fermé
527 one: 1 fermé
529 one: 1 fermé
528 other: "{{count}} fermés"
530 other: "{{count}} fermés"
529 label_total: Total
531 label_total: Total
530 label_permissions: Permissions
532 label_permissions: Permissions
531 label_current_status: Statut actuel
533 label_current_status: Statut actuel
532 label_new_statuses_allowed: Nouveaux statuts autorisés
534 label_new_statuses_allowed: Nouveaux statuts autorisés
533 label_all: tous
535 label_all: tous
534 label_none: aucun
536 label_none: aucun
535 label_nobody: personne
537 label_nobody: personne
536 label_next: Suivant
538 label_next: Suivant
537 label_previous: Précédent
539 label_previous: Précédent
538 label_used_by: Utilisé par
540 label_used_by: Utilisé par
539 label_details: Détails
541 label_details: Détails
540 label_add_note: Ajouter une note
542 label_add_note: Ajouter une note
541 label_per_page: Par page
543 label_per_page: Par page
542 label_calendar: Calendrier
544 label_calendar: Calendrier
543 label_months_from: mois depuis
545 label_months_from: mois depuis
544 label_gantt: Gantt
546 label_gantt: Gantt
545 label_internal: Interne
547 label_internal: Interne
546 label_last_changes: "{{count}} derniers changements"
548 label_last_changes: "{{count}} derniers changements"
547 label_change_view_all: Voir tous les changements
549 label_change_view_all: Voir tous les changements
548 label_personalize_page: Personnaliser cette page
550 label_personalize_page: Personnaliser cette page
549 label_comment: Commentaire
551 label_comment: Commentaire
550 label_comment_plural: Commentaires
552 label_comment_plural: Commentaires
551 label_x_comments:
553 label_x_comments:
552 zero: aucun commentaire
554 zero: aucun commentaire
553 one: 1 commentaire
555 one: 1 commentaire
554 other: "{{count}} commentaires"
556 other: "{{count}} commentaires"
555 label_comment_add: Ajouter un commentaire
557 label_comment_add: Ajouter un commentaire
556 label_comment_added: Commentaire ajouté
558 label_comment_added: Commentaire ajouté
557 label_comment_delete: Supprimer les commentaires
559 label_comment_delete: Supprimer les commentaires
558 label_query: Rapport personnalisé
560 label_query: Rapport personnalisé
559 label_query_plural: Rapports personnalisés
561 label_query_plural: Rapports personnalisés
560 label_query_new: Nouveau rapport
562 label_query_new: Nouveau rapport
561 label_filter_add: Ajouter le filtre
563 label_filter_add: Ajouter le filtre
562 label_filter_plural: Filtres
564 label_filter_plural: Filtres
563 label_equals: égal
565 label_equals: égal
564 label_not_equals: différent
566 label_not_equals: différent
565 label_in_less_than: dans moins de
567 label_in_less_than: dans moins de
566 label_in_more_than: dans plus de
568 label_in_more_than: dans plus de
567 label_in: dans
569 label_in: dans
568 label_today: aujourd'hui
570 label_today: aujourd'hui
569 label_all_time: toute la période
571 label_all_time: toute la période
570 label_yesterday: hier
572 label_yesterday: hier
571 label_this_week: cette semaine
573 label_this_week: cette semaine
572 label_last_week: la semaine dernière
574 label_last_week: la semaine dernière
573 label_last_n_days: "les {{count}} derniers jours"
575 label_last_n_days: "les {{count}} derniers jours"
574 label_this_month: ce mois-ci
576 label_this_month: ce mois-ci
575 label_last_month: le mois dernier
577 label_last_month: le mois dernier
576 label_this_year: cette année
578 label_this_year: cette année
577 label_date_range: Période
579 label_date_range: Période
578 label_less_than_ago: il y a moins de
580 label_less_than_ago: il y a moins de
579 label_more_than_ago: il y a plus de
581 label_more_than_ago: il y a plus de
580 label_ago: il y a
582 label_ago: il y a
581 label_contains: contient
583 label_contains: contient
582 label_not_contains: ne contient pas
584 label_not_contains: ne contient pas
583 label_day_plural: jours
585 label_day_plural: jours
584 label_repository: Dépôt
586 label_repository: Dépôt
585 label_repository_plural: Dépôts
587 label_repository_plural: Dépôts
586 label_browse: Parcourir
588 label_browse: Parcourir
587 label_modification: "{{count}} modification"
589 label_modification: "{{count}} modification"
588 label_modification_plural: "{{count}} modifications"
590 label_modification_plural: "{{count}} modifications"
589 label_revision: Révision
591 label_revision: Révision
590 label_revision_plural: Révisions
592 label_revision_plural: Révisions
591 label_associated_revisions: Révisions associées
593 label_associated_revisions: Révisions associées
592 label_added: ajouté
594 label_added: ajouté
593 label_modified: modifié
595 label_modified: modifié
594 label_copied: copié
596 label_copied: copié
595 label_renamed: renommé
597 label_renamed: renommé
596 label_deleted: supprimé
598 label_deleted: supprimé
597 label_latest_revision: Dernière révision
599 label_latest_revision: Dernière révision
598 label_latest_revision_plural: Dernières révisions
600 label_latest_revision_plural: Dernières révisions
599 label_view_revisions: Voir les révisions
601 label_view_revisions: Voir les révisions
600 label_max_size: Taille maximale
602 label_max_size: Taille maximale
601 label_sort_highest: Remonter en premier
603 label_sort_highest: Remonter en premier
602 label_sort_higher: Remonter
604 label_sort_higher: Remonter
603 label_sort_lower: Descendre
605 label_sort_lower: Descendre
604 label_sort_lowest: Descendre en dernier
606 label_sort_lowest: Descendre en dernier
605 label_roadmap: Roadmap
607 label_roadmap: Roadmap
606 label_roadmap_due_in: "Echéance dans {{value}}"
608 label_roadmap_due_in: "Echéance dans {{value}}"
607 label_roadmap_overdue: "En retard de {{value}}"
609 label_roadmap_overdue: "En retard de {{value}}"
608 label_roadmap_no_issues: Aucune demande pour cette version
610 label_roadmap_no_issues: Aucune demande pour cette version
609 label_search: Recherche
611 label_search: Recherche
610 label_result_plural: Résultats
612 label_result_plural: Résultats
611 label_all_words: Tous les mots
613 label_all_words: Tous les mots
612 label_wiki: Wiki
614 label_wiki: Wiki
613 label_wiki_edit: Révision wiki
615 label_wiki_edit: Révision wiki
614 label_wiki_edit_plural: Révisions wiki
616 label_wiki_edit_plural: Révisions wiki
615 label_wiki_page: Page wiki
617 label_wiki_page: Page wiki
616 label_wiki_page_plural: Pages wiki
618 label_wiki_page_plural: Pages wiki
617 label_index_by_title: Index par titre
619 label_index_by_title: Index par titre
618 label_index_by_date: Index par date
620 label_index_by_date: Index par date
619 label_current_version: Version actuelle
621 label_current_version: Version actuelle
620 label_preview: Prévisualisation
622 label_preview: Prévisualisation
621 label_feed_plural: Flux RSS
623 label_feed_plural: Flux RSS
622 label_changes_details: Détails de tous les changements
624 label_changes_details: Détails de tous les changements
623 label_issue_tracking: Suivi des demandes
625 label_issue_tracking: Suivi des demandes
624 label_spent_time: Temps passé
626 label_spent_time: Temps passé
625 label_f_hour: "{{value}} heure"
627 label_f_hour: "{{value}} heure"
626 label_f_hour_plural: "{{value}} heures"
628 label_f_hour_plural: "{{value}} heures"
627 label_time_tracking: Suivi du temps
629 label_time_tracking: Suivi du temps
628 label_change_plural: Changements
630 label_change_plural: Changements
629 label_statistics: Statistiques
631 label_statistics: Statistiques
630 label_commits_per_month: Commits par mois
632 label_commits_per_month: Commits par mois
631 label_commits_per_author: Commits par auteur
633 label_commits_per_author: Commits par auteur
632 label_view_diff: Voir les différences
634 label_view_diff: Voir les différences
633 label_diff_inline: en ligne
635 label_diff_inline: en ligne
634 label_diff_side_by_side: côte à côte
636 label_diff_side_by_side: côte à côte
635 label_options: Options
637 label_options: Options
636 label_copy_workflow_from: Copier le workflow de
638 label_copy_workflow_from: Copier le workflow de
637 label_permissions_report: Synthèse des permissions
639 label_permissions_report: Synthèse des permissions
638 label_watched_issues: Demandes surveillées
640 label_watched_issues: Demandes surveillées
639 label_related_issues: Demandes liées
641 label_related_issues: Demandes liées
640 label_applied_status: Statut appliqué
642 label_applied_status: Statut appliqué
641 label_loading: Chargement...
643 label_loading: Chargement...
642 label_relation_new: Nouvelle relation
644 label_relation_new: Nouvelle relation
643 label_relation_delete: Supprimer la relation
645 label_relation_delete: Supprimer la relation
644 label_relates_to: lié à
646 label_relates_to: lié à
645 label_duplicates: duplique
647 label_duplicates: duplique
646 label_duplicated_by: dupliqué par
648 label_duplicated_by: dupliqué par
647 label_blocks: bloque
649 label_blocks: bloque
648 label_blocked_by: bloqué par
650 label_blocked_by: bloqué par
649 label_precedes: précède
651 label_precedes: précède
650 label_follows: suit
652 label_follows: suit
651 label_end_to_start: fin à début
653 label_end_to_start: fin à début
652 label_end_to_end: fin à fin
654 label_end_to_end: fin à fin
653 label_start_to_start: début à début
655 label_start_to_start: début à début
654 label_start_to_end: début à fin
656 label_start_to_end: début à fin
655 label_stay_logged_in: Rester connecté
657 label_stay_logged_in: Rester connecté
656 label_disabled: désactivé
658 label_disabled: désactivé
657 label_show_completed_versions: Voir les versions passées
659 label_show_completed_versions: Voir les versions passées
658 label_me: moi
660 label_me: moi
659 label_board: Forum
661 label_board: Forum
660 label_board_new: Nouveau forum
662 label_board_new: Nouveau forum
661 label_board_plural: Forums
663 label_board_plural: Forums
662 label_topic_plural: Discussions
664 label_topic_plural: Discussions
663 label_message_plural: Messages
665 label_message_plural: Messages
664 label_message_last: Dernier message
666 label_message_last: Dernier message
665 label_message_new: Nouveau message
667 label_message_new: Nouveau message
666 label_message_posted: Message ajouté
668 label_message_posted: Message ajouté
667 label_reply_plural: Réponses
669 label_reply_plural: Réponses
668 label_send_information: Envoyer les informations à l'utilisateur
670 label_send_information: Envoyer les informations à l'utilisateur
669 label_year: Année
671 label_year: Année
670 label_month: Mois
672 label_month: Mois
671 label_week: Semaine
673 label_week: Semaine
672 label_date_from: Du
674 label_date_from: Du
673 label_date_to: Au
675 label_date_to: Au
674 label_language_based: Basé sur la langue de l'utilisateur
676 label_language_based: Basé sur la langue de l'utilisateur
675 label_sort_by: "Trier par {{value}}"
677 label_sort_by: "Trier par {{value}}"
676 label_send_test_email: Envoyer un email de test
678 label_send_test_email: Envoyer un email de test
677 label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a {{value}}"
679 label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a {{value}}"
678 label_module_plural: Modules
680 label_module_plural: Modules
679 label_added_time_by: "Ajouté par {{author}} il y a {{age}}"
681 label_added_time_by: "Ajouté par {{author}} il y a {{age}}"
680 label_updated_time_by: "Mis à jour par {{author}} il y a {{age}}"
682 label_updated_time_by: "Mis à jour par {{author}} il y a {{age}}"
681 label_updated_time: "Mis à jour il y a {{value}}"
683 label_updated_time: "Mis à jour il y a {{value}}"
682 label_jump_to_a_project: Aller à un projet...
684 label_jump_to_a_project: Aller à un projet...
683 label_file_plural: Fichiers
685 label_file_plural: Fichiers
684 label_changeset_plural: Révisions
686 label_changeset_plural: Révisions
685 label_default_columns: Colonnes par défaut
687 label_default_columns: Colonnes par défaut
686 label_no_change_option: (Pas de changement)
688 label_no_change_option: (Pas de changement)
687 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
689 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
688 label_theme: Thème
690 label_theme: Thème
689 label_default: Défaut
691 label_default: Défaut
690 label_search_titles_only: Uniquement dans les titres
692 label_search_titles_only: Uniquement dans les titres
691 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
693 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
692 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
694 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
693 label_user_mail_option_none: "Seulement pour ce que je surveille ou à quoi je participe"
695 label_user_mail_option_none: "Seulement pour ce que je surveille ou à quoi je participe"
694 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
696 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
695 label_registration_activation_by_email: activation du compte par email
697 label_registration_activation_by_email: activation du compte par email
696 label_registration_manual_activation: activation manuelle du compte
698 label_registration_manual_activation: activation manuelle du compte
697 label_registration_automatic_activation: activation automatique du compte
699 label_registration_automatic_activation: activation automatique du compte
698 label_display_per_page: "Par page: {{value}}"
700 label_display_per_page: "Par page: {{value}}"
699 label_age: Age
701 label_age: Age
700 label_change_properties: Changer les propriétés
702 label_change_properties: Changer les propriétés
701 label_general: Général
703 label_general: Général
702 label_more: Plus
704 label_more: Plus
703 label_scm: SCM
705 label_scm: SCM
704 label_plugins: Plugins
706 label_plugins: Plugins
705 label_ldap_authentication: Authentification LDAP
707 label_ldap_authentication: Authentification LDAP
706 label_downloads_abbr: D/L
708 label_downloads_abbr: D/L
707 label_optional_description: Description facultative
709 label_optional_description: Description facultative
708 label_add_another_file: Ajouter un autre fichier
710 label_add_another_file: Ajouter un autre fichier
709 label_preferences: Préférences
711 label_preferences: Préférences
710 label_chronological_order: Dans l'ordre chronologique
712 label_chronological_order: Dans l'ordre chronologique
711 label_reverse_chronological_order: Dans l'ordre chronologique inverse
713 label_reverse_chronological_order: Dans l'ordre chronologique inverse
712 label_planning: Planning
714 label_planning: Planning
713 label_incoming_emails: Emails entrants
715 label_incoming_emails: Emails entrants
714 label_generate_key: Générer une clé
716 label_generate_key: Générer une clé
715 label_issue_watchers: Observateurs
717 label_issue_watchers: Observateurs
716 label_example: Exemple
718 label_example: Exemple
717 label_display: Affichage
719 label_display: Affichage
718 label_sort: Tri
720 label_sort: Tri
719 label_ascending: Croissant
721 label_ascending: Croissant
720 label_descending: Décroissant
722 label_descending: Décroissant
721 label_date_from_to: Du {{start}} au {{end}}
723 label_date_from_to: Du {{start}} au {{end}}
722 label_wiki_content_added: Page wiki ajoutée
724 label_wiki_content_added: Page wiki ajoutée
723 label_wiki_content_updated: Page wiki mise à jour
725 label_wiki_content_updated: Page wiki mise à jour
724 label_group_plural: Groupes
726 label_group_plural: Groupes
725 label_group: Groupe
727 label_group: Groupe
726 label_group_new: Nouveau groupe
728 label_group_new: Nouveau groupe
727 label_time_entry_plural: Temps passé
729 label_time_entry_plural: Temps passé
730 label_version_sharing_none: Non partagé
731 label_version_sharing_descendants: Avec les sous-projets
732 label_version_sharing_hierarchy: Avec toute la hiérarchie
733 label_version_sharing_tree: Avec tout l'arbre
734 label_version_sharing_system: Avec tous les projets
728
735
729 button_login: Connexion
736 button_login: Connexion
730 button_submit: Soumettre
737 button_submit: Soumettre
731 button_save: Sauvegarder
738 button_save: Sauvegarder
732 button_check_all: Tout cocher
739 button_check_all: Tout cocher
733 button_uncheck_all: Tout décocher
740 button_uncheck_all: Tout décocher
734 button_delete: Supprimer
741 button_delete: Supprimer
735 button_create: Créer
742 button_create: Créer
736 button_create_and_continue: Créer et continuer
743 button_create_and_continue: Créer et continuer
737 button_test: Tester
744 button_test: Tester
738 button_edit: Modifier
745 button_edit: Modifier
739 button_add: Ajouter
746 button_add: Ajouter
740 button_change: Changer
747 button_change: Changer
741 button_apply: Appliquer
748 button_apply: Appliquer
742 button_clear: Effacer
749 button_clear: Effacer
743 button_lock: Verrouiller
750 button_lock: Verrouiller
744 button_unlock: Déverrouiller
751 button_unlock: Déverrouiller
745 button_download: Télécharger
752 button_download: Télécharger
746 button_list: Lister
753 button_list: Lister
747 button_view: Voir
754 button_view: Voir
748 button_move: Déplacer
755 button_move: Déplacer
749 button_move_and_follow: Déplacer et suivre
756 button_move_and_follow: Déplacer et suivre
750 button_back: Retour
757 button_back: Retour
751 button_cancel: Annuler
758 button_cancel: Annuler
752 button_activate: Activer
759 button_activate: Activer
753 button_sort: Trier
760 button_sort: Trier
754 button_log_time: Saisir temps
761 button_log_time: Saisir temps
755 button_rollback: Revenir à cette version
762 button_rollback: Revenir à cette version
756 button_watch: Surveiller
763 button_watch: Surveiller
757 button_unwatch: Ne plus surveiller
764 button_unwatch: Ne plus surveiller
758 button_reply: Répondre
765 button_reply: Répondre
759 button_archive: Archiver
766 button_archive: Archiver
760 button_unarchive: Désarchiver
767 button_unarchive: Désarchiver
761 button_reset: Réinitialiser
768 button_reset: Réinitialiser
762 button_rename: Renommer
769 button_rename: Renommer
763 button_change_password: Changer de mot de passe
770 button_change_password: Changer de mot de passe
764 button_copy: Copier
771 button_copy: Copier
765 button_annotate: Annoter
772 button_annotate: Annoter
766 button_update: Mettre à jour
773 button_update: Mettre à jour
767 button_configure: Configurer
774 button_configure: Configurer
768 button_quote: Citer
775 button_quote: Citer
769
776
770 status_active: actif
777 status_active: actif
771 status_registered: enregistré
778 status_registered: enregistré
772 status_locked: vérouillé
779 status_locked: vérouillé
773
780
774 version_status_open: ouvert
781 version_status_open: ouvert
775 version_status_locked: vérouillé
782 version_status_locked: vérouillé
776 version_status_closed: fermé
783 version_status_closed: fermé
777
784
778 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
785 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
779 text_regexp_info: ex. ^[A-Z0-9]+$
786 text_regexp_info: ex. ^[A-Z0-9]+$
780 text_min_max_length_info: 0 pour aucune restriction
787 text_min_max_length_info: 0 pour aucune restriction
781 text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
788 text_project_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
782 text_subprojects_destroy_warning: "Ses sous-projets: {{value}} seront également supprimés."
789 text_subprojects_destroy_warning: "Ses sous-projets: {{value}} seront également supprimés."
783 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
790 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
784 text_are_you_sure: Etes-vous sûr ?
791 text_are_you_sure: Etes-vous sûr ?
785 text_tip_task_begin_day: tâche commençant ce jour
792 text_tip_task_begin_day: tâche commençant ce jour
786 text_tip_task_end_day: tâche finissant ce jour
793 text_tip_task_end_day: tâche finissant ce jour
787 text_tip_task_begin_end_day: tâche commençant et finissant ce jour
794 text_tip_task_begin_end_day: tâche commençant et finissant ce jour
788 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é.'
795 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é.'
789 text_caracters_maximum: "{{count}} caractères maximum."
796 text_caracters_maximum: "{{count}} caractères maximum."
790 text_caracters_minimum: "{{count}} caractères minimum."
797 text_caracters_minimum: "{{count}} caractères minimum."
791 text_length_between: "Longueur comprise entre {{min}} et {{max}} caractères."
798 text_length_between: "Longueur comprise entre {{min}} et {{max}} caractères."
792 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
799 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
793 text_unallowed_characters: Caractères non autorisés
800 text_unallowed_characters: Caractères non autorisés
794 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
801 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
795 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
802 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
796 text_issue_added: "La demande {{id}} a été soumise par {{author}}."
803 text_issue_added: "La demande {{id}} a été soumise par {{author}}."
797 text_issue_updated: "La demande {{id}} a été mise à jour par {{author}}."
804 text_issue_updated: "La demande {{id}} a été mise à jour par {{author}}."
798 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
805 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
799 text_issue_category_destroy_question: "{{count}} demandes sont affectées à cette catégories. Que voulez-vous faire ?"
806 text_issue_category_destroy_question: "{{count}} demandes sont affectées à cette catégories. Que voulez-vous faire ?"
800 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
807 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
801 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
808 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
802 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)."
809 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)."
803 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é."
810 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é."
804 text_load_default_configuration: Charger le paramétrage par défaut
811 text_load_default_configuration: Charger le paramétrage par défaut
805 text_status_changed_by_changeset: "Appliqué par commit {{value}}."
812 text_status_changed_by_changeset: "Appliqué par commit {{value}}."
806 text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) demandes(s) selectionnée(s) ?'
813 text_issues_destroy_confirmation: 'Etes-vous sûr de vouloir supprimer le(s) demandes(s) selectionnée(s) ?'
807 text_select_project_modules: 'Selectionner les modules à activer pour ce project:'
814 text_select_project_modules: 'Selectionner les modules à activer pour ce project:'
808 text_default_administrator_account_changed: Compte administrateur par défaut changé
815 text_default_administrator_account_changed: Compte administrateur par défaut changé
809 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
816 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
810 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
817 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
811 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
818 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
812 text_destroy_time_entries_question: "{{hours}} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
819 text_destroy_time_entries_question: "{{hours}} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
813 text_destroy_time_entries: Supprimer les heures
820 text_destroy_time_entries: Supprimer les heures
814 text_assign_time_entries_to_project: Reporter les heures sur le projet
821 text_assign_time_entries_to_project: Reporter les heures sur le projet
815 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
822 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
816 text_user_wrote: "{{value}} a écrit:"
823 text_user_wrote: "{{value}} a écrit:"
817 text_enumeration_destroy_question: "Cette valeur est affectée à {{count}} objets."
824 text_enumeration_destroy_question: "Cette valeur est affectée à {{count}} objets."
818 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
825 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
819 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."
826 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."
820 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."
827 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."
821 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
828 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
822 text_custom_field_possible_values_info: 'Une ligne par valeur'
829 text_custom_field_possible_values_info: 'Une ligne par valeur'
823 text_wiki_page_destroy_question: "Cette page possède {{descendants}} sous-page(s) et descendante(s). Que voulez-vous faire ?"
830 text_wiki_page_destroy_question: "Cette page possède {{descendants}} sous-page(s) et descendante(s). Que voulez-vous faire ?"
824 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
831 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
825 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
832 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
826 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
833 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
827
834
828 default_role_manager: Manager
835 default_role_manager: Manager
829 default_role_developper: Développeur
836 default_role_developper: Développeur
830 default_role_reporter: Rapporteur
837 default_role_reporter: Rapporteur
831 default_tracker_bug: Anomalie
838 default_tracker_bug: Anomalie
832 default_tracker_feature: Evolution
839 default_tracker_feature: Evolution
833 default_tracker_support: Assistance
840 default_tracker_support: Assistance
834 default_issue_status_new: Nouveau
841 default_issue_status_new: Nouveau
835 default_issue_status_in_progress: In Progress
842 default_issue_status_in_progress: In Progress
836 default_issue_status_resolved: Résolu
843 default_issue_status_resolved: Résolu
837 default_issue_status_feedback: Commentaire
844 default_issue_status_feedback: Commentaire
838 default_issue_status_closed: Fermé
845 default_issue_status_closed: Fermé
839 default_issue_status_rejected: Rejeté
846 default_issue_status_rejected: Rejeté
840 default_doc_category_user: Documentation utilisateur
847 default_doc_category_user: Documentation utilisateur
841 default_doc_category_tech: Documentation technique
848 default_doc_category_tech: Documentation technique
842 default_priority_low: Bas
849 default_priority_low: Bas
843 default_priority_normal: Normal
850 default_priority_normal: Normal
844 default_priority_high: Haut
851 default_priority_high: Haut
845 default_priority_urgent: Urgent
852 default_priority_urgent: Urgent
846 default_priority_immediate: Immédiat
853 default_priority_immediate: Immédiat
847 default_activity_design: Conception
854 default_activity_design: Conception
848 default_activity_development: Développement
855 default_activity_development: Développement
849
856
850 enumeration_issue_priorities: Priorités des demandes
857 enumeration_issue_priorities: Priorités des demandes
851 enumeration_doc_categories: Catégories des documents
858 enumeration_doc_categories: Catégories des documents
852 enumeration_activities: Activités (suivi du temps)
859 enumeration_activities: Activités (suivi du temps)
853 label_greater_or_equal: ">="
860 label_greater_or_equal: ">="
854 label_less_or_equal: "<="
861 label_less_or_equal: "<="
855 label_view_all_revisions: Voir toutes les révisions
862 label_view_all_revisions: Voir toutes les révisions
856 label_tag: Tag
863 label_tag: Tag
857 label_branch: Branche
864 label_branch: Branche
858 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
865 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
859 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
866 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
860 text_journal_changed: "{{label}} changé de {{old}} à {{new}}"
867 text_journal_changed: "{{label}} changé de {{old}} à {{new}}"
861 text_journal_set_to: "{{label}} mis à {{value}}"
868 text_journal_set_to: "{{label}} mis à {{value}}"
862 text_journal_deleted: "{{label}} {{old}} supprimé"
869 text_journal_deleted: "{{label}} {{old}} supprimé"
863 text_journal_added: "{{label}} {{value}} ajouté"
870 text_journal_added: "{{label}} {{value}} ajouté"
864 field_active: Actif
871 field_active: Actif
865 enumeration_system_activity: Activité système
872 enumeration_system_activity: Activité système
866 setting_gravatar_default: Default Gravatar image
873 setting_gravatar_default: Default Gravatar image
@@ -1,174 +1,174
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/activity'
3 require 'redmine/activity'
4 require 'redmine/mime_type'
4 require 'redmine/mime_type'
5 require 'redmine/core_ext'
5 require 'redmine/core_ext'
6 require 'redmine/themes'
6 require 'redmine/themes'
7 require 'redmine/hook'
7 require 'redmine/hook'
8 require 'redmine/plugin'
8 require 'redmine/plugin'
9 require 'redmine/wiki_formatting'
9 require 'redmine/wiki_formatting'
10
10
11 begin
11 begin
12 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
12 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
13 rescue LoadError
13 rescue LoadError
14 # RMagick is not available
14 # RMagick is not available
15 end
15 end
16
16
17 if RUBY_VERSION < '1.9'
17 if RUBY_VERSION < '1.9'
18 require 'faster_csv'
18 require 'faster_csv'
19 else
19 else
20 require 'csv'
20 require 'csv'
21 FCSV = CSV
21 FCSV = CSV
22 end
22 end
23
23
24 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
24 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
25
25
26 # Permissions
26 # Permissions
27 Redmine::AccessControl.map do |map|
27 Redmine::AccessControl.map do |map|
28 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
28 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
29 map.permission :search_project, {:search => :index}, :public => true
29 map.permission :search_project, {:search => :index}, :public => true
30 map.permission :add_project, {:projects => :add}, :require => :loggedin
30 map.permission :add_project, {:projects => :add}, :require => :loggedin
31 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
31 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
32 map.permission :select_project_modules, {:projects => :modules}, :require => :member
32 map.permission :select_project_modules, {:projects => :modules}, :require => :member
33 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
33 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
34 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :close_completed, :destroy]}, :require => :member
34 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :close_completed, :destroy]}, :require => :member
35
35
36 map.project_module :issue_tracking do |map|
36 map.project_module :issue_tracking do |map|
37 # Issue categories
37 # Issue categories
38 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
38 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
39 # Issues
39 # Issues
40 map.permission :view_issues, {:projects => [:changelog, :roadmap],
40 map.permission :view_issues, {:projects => [:changelog, :roadmap],
41 :issues => [:index, :changes, :show, :context_menu],
41 :issues => [:index, :changes, :show, :context_menu],
42 :versions => [:show, :status_by],
42 :versions => [:show, :status_by],
43 :queries => :index,
43 :queries => :index,
44 :reports => :issue_report}
44 :reports => :issue_report}
45 map.permission :add_issues, {:issues => [:new, :update_form]}
45 map.permission :add_issues, {:issues => [:new, :update_form]}
46 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :update_form]}
46 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :update_form]}
47 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
47 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
48 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
48 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
49 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
49 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
50 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
50 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
51 map.permission :move_issues, {:issues => :move}, :require => :loggedin
51 map.permission :move_issues, {:issues => :move}, :require => :loggedin
52 map.permission :delete_issues, {:issues => :destroy}, :require => :member
52 map.permission :delete_issues, {:issues => :destroy}, :require => :member
53 # Queries
53 # Queries
54 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
54 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
55 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
55 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
56 # Gantt & calendar
56 # Gantt & calendar
57 map.permission :view_gantt, :issues => :gantt
57 map.permission :view_gantt, :issues => :gantt
58 map.permission :view_calendar, :issues => :calendar
58 map.permission :view_calendar, :issues => :calendar
59 # Watchers
59 # Watchers
60 map.permission :view_issue_watchers, {}
60 map.permission :view_issue_watchers, {}
61 map.permission :add_issue_watchers, {:watchers => :new}
61 map.permission :add_issue_watchers, {:watchers => :new}
62 map.permission :delete_issue_watchers, {:watchers => :destroy}
62 map.permission :delete_issue_watchers, {:watchers => :destroy}
63 end
63 end
64
64
65 map.project_module :time_tracking do |map|
65 map.project_module :time_tracking do |map|
66 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
66 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
67 map.permission :view_time_entries, :timelog => [:details, :report]
67 map.permission :view_time_entries, :timelog => [:details, :report]
68 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
68 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
69 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
69 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
70 map.permission :manage_project_activities, {:projects => [:save_activities, :reset_activities]}, :require => :member
70 map.permission :manage_project_activities, {:projects => [:save_activities, :reset_activities]}, :require => :member
71 end
71 end
72
72
73 map.project_module :news do |map|
73 map.project_module :news do |map|
74 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
74 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
75 map.permission :view_news, {:news => [:index, :show]}, :public => true
75 map.permission :view_news, {:news => [:index, :show]}, :public => true
76 map.permission :comment_news, {:news => :add_comment}
76 map.permission :comment_news, {:news => :add_comment}
77 end
77 end
78
78
79 map.project_module :documents do |map|
79 map.project_module :documents do |map|
80 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
80 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment]}, :require => :loggedin
81 map.permission :view_documents, :documents => [:index, :show, :download]
81 map.permission :view_documents, :documents => [:index, :show, :download]
82 end
82 end
83
83
84 map.project_module :files do |map|
84 map.project_module :files do |map|
85 map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
85 map.permission :manage_files, {:projects => :add_file}, :require => :loggedin
86 map.permission :view_files, :projects => :list_files, :versions => :download
86 map.permission :view_files, :projects => :list_files, :versions => :download
87 end
87 end
88
88
89 map.project_module :wiki do |map|
89 map.project_module :wiki do |map|
90 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
90 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
91 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
91 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
92 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
92 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
93 map.permission :view_wiki_pages, :wiki => [:index, :special]
93 map.permission :view_wiki_pages, :wiki => [:index, :special]
94 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
94 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
95 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
95 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
96 map.permission :delete_wiki_pages_attachments, {}
96 map.permission :delete_wiki_pages_attachments, {}
97 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
97 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
98 end
98 end
99
99
100 map.project_module :repository do |map|
100 map.project_module :repository do |map|
101 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
101 map.permission :manage_repository, {:repositories => [:edit, :committers, :destroy]}, :require => :member
102 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
102 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
103 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
103 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
104 map.permission :commit_access, {}
104 map.permission :commit_access, {}
105 end
105 end
106
106
107 map.project_module :boards do |map|
107 map.project_module :boards do |map|
108 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
108 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
109 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
109 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
110 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
110 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
111 map.permission :edit_messages, {:messages => :edit}, :require => :member
111 map.permission :edit_messages, {:messages => :edit}, :require => :member
112 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
112 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
113 map.permission :delete_messages, {:messages => :destroy}, :require => :member
113 map.permission :delete_messages, {:messages => :destroy}, :require => :member
114 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
114 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
115 end
115 end
116 end
116 end
117
117
118 Redmine::MenuManager.map :top_menu do |menu|
118 Redmine::MenuManager.map :top_menu do |menu|
119 menu.push :home, :home_path
119 menu.push :home, :home_path
120 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
120 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
121 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
121 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
122 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
122 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
123 menu.push :help, Redmine::Info.help_url, :last => true
123 menu.push :help, Redmine::Info.help_url, :last => true
124 end
124 end
125
125
126 Redmine::MenuManager.map :account_menu do |menu|
126 Redmine::MenuManager.map :account_menu do |menu|
127 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
127 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
128 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
128 menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
129 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
129 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
130 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
130 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
131 end
131 end
132
132
133 Redmine::MenuManager.map :application_menu do |menu|
133 Redmine::MenuManager.map :application_menu do |menu|
134 # Empty
134 # Empty
135 end
135 end
136
136
137 Redmine::MenuManager.map :admin_menu do |menu|
137 Redmine::MenuManager.map :admin_menu do |menu|
138 # Empty
138 # Empty
139 end
139 end
140
140
141 Redmine::MenuManager.map :project_menu do |menu|
141 Redmine::MenuManager.map :project_menu do |menu|
142 menu.push :overview, { :controller => 'projects', :action => 'show' }
142 menu.push :overview, { :controller => 'projects', :action => 'show' }
143 menu.push :activity, { :controller => 'projects', :action => 'activity' }
143 menu.push :activity, { :controller => 'projects', :action => 'activity' }
144 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
144 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
145 :if => Proc.new { |p| p.versions.any? }
145 :if => Proc.new { |p| p.shared_versions.any? }
146 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
146 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
147 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
147 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
148 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
148 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
149 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
149 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
150 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
150 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
151 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
151 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
152 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
152 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
153 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
153 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
154 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
154 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
155 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
155 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
156 menu.push :repository, { :controller => 'repositories', :action => 'show' },
156 menu.push :repository, { :controller => 'repositories', :action => 'show' },
157 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
157 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
158 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
158 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
159 end
159 end
160
160
161 Redmine::Activity.map do |activity|
161 Redmine::Activity.map do |activity|
162 activity.register :issues, :class_name => %w(Issue Journal)
162 activity.register :issues, :class_name => %w(Issue Journal)
163 activity.register :changesets
163 activity.register :changesets
164 activity.register :news
164 activity.register :news
165 activity.register :documents, :class_name => %w(Document Attachment)
165 activity.register :documents, :class_name => %w(Document Attachment)
166 activity.register :files, :class_name => 'Attachment'
166 activity.register :files, :class_name => 'Attachment'
167 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
167 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
168 activity.register :messages, :default => false
168 activity.register :messages, :default => false
169 activity.register :time_entries, :default => false
169 activity.register :time_entries, :default => false
170 end
170 end
171
171
172 Redmine::WikiFormatting.map do |format|
172 Redmine::WikiFormatting.map do |format|
173 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
173 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
174 end
174 end
@@ -1,10 +1,11
1 class Version < ActiveRecord::Base
1 class Version < ActiveRecord::Base
2 generator_for :name, :method => :next_name
2 generator_for :name, :method => :next_name
3
3 generator_for :status => 'open'
4 def self.next_name
4
5 @last_name ||= 'Version 1.0.0'
5 def self.next_name
6 @last_name.succ!
6 @last_name ||= 'Version 1.0.0'
7 @last_name
7 @last_name.succ!
8 end
8 @last_name
9
9 end
10 end
10
11 end
@@ -1,136 +1,147
1 ---
1 ---
2 attachments_001:
2 attachments_001:
3 created_on: 2006-07-19 21:07:27 +02:00
3 created_on: 2006-07-19 21:07:27 +02:00
4 downloads: 0
4 downloads: 0
5 content_type: text/plain
5 content_type: text/plain
6 disk_filename: 060719210727_error281.txt
6 disk_filename: 060719210727_error281.txt
7 container_id: 3
7 container_id: 3
8 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
8 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
9 id: 1
9 id: 1
10 container_type: Issue
10 container_type: Issue
11 filesize: 28
11 filesize: 28
12 filename: error281.txt
12 filename: error281.txt
13 author_id: 2
13 author_id: 2
14 attachments_002:
14 attachments_002:
15 created_on: 2007-01-27 15:08:27 +01:00
15 created_on: 2007-01-27 15:08:27 +01:00
16 downloads: 0
16 downloads: 0
17 content_type: text/plain
17 content_type: text/plain
18 disk_filename: 060719210727_document.txt
18 disk_filename: 060719210727_document.txt
19 container_id: 1
19 container_id: 1
20 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
20 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
21 id: 2
21 id: 2
22 container_type: Document
22 container_type: Document
23 filesize: 28
23 filesize: 28
24 filename: document.txt
24 filename: document.txt
25 author_id: 2
25 author_id: 2
26 attachments_003:
26 attachments_003:
27 created_on: 2006-07-19 21:07:27 +02:00
27 created_on: 2006-07-19 21:07:27 +02:00
28 downloads: 0
28 downloads: 0
29 content_type: image/gif
29 content_type: image/gif
30 disk_filename: 060719210727_logo.gif
30 disk_filename: 060719210727_logo.gif
31 container_id: 4
31 container_id: 4
32 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
32 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
33 id: 3
33 id: 3
34 container_type: WikiPage
34 container_type: WikiPage
35 filesize: 280
35 filesize: 280
36 filename: logo.gif
36 filename: logo.gif
37 description: This is a logo
37 description: This is a logo
38 author_id: 2
38 author_id: 2
39 attachments_004:
39 attachments_004:
40 created_on: 2006-07-19 21:07:27 +02:00
40 created_on: 2006-07-19 21:07:27 +02:00
41 container_type: Issue
41 container_type: Issue
42 container_id: 3
42 container_id: 3
43 downloads: 0
43 downloads: 0
44 disk_filename: 060719210727_source.rb
44 disk_filename: 060719210727_source.rb
45 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
45 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
46 id: 4
46 id: 4
47 filesize: 153
47 filesize: 153
48 filename: source.rb
48 filename: source.rb
49 author_id: 2
49 author_id: 2
50 description: This is a Ruby source file
50 description: This is a Ruby source file
51 content_type: application/x-ruby
51 content_type: application/x-ruby
52 attachments_005:
52 attachments_005:
53 created_on: 2006-07-19 21:07:27 +02:00
53 created_on: 2006-07-19 21:07:27 +02:00
54 container_type: Issue
54 container_type: Issue
55 container_id: 3
55 container_id: 3
56 downloads: 0
56 downloads: 0
57 disk_filename: 060719210727_changeset.diff
57 disk_filename: 060719210727_changeset.diff
58 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
58 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
59 id: 5
59 id: 5
60 filesize: 687
60 filesize: 687
61 filename: changeset.diff
61 filename: changeset.diff
62 author_id: 2
62 author_id: 2
63 content_type: text/x-diff
63 content_type: text/x-diff
64 attachments_006:
64 attachments_006:
65 created_on: 2006-07-19 21:07:27 +02:00
65 created_on: 2006-07-19 21:07:27 +02:00
66 container_type: Issue
66 container_type: Issue
67 container_id: 3
67 container_id: 3
68 downloads: 0
68 downloads: 0
69 disk_filename: 060719210727_archive.zip
69 disk_filename: 060719210727_archive.zip
70 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
70 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
71 id: 6
71 id: 6
72 filesize: 157
72 filesize: 157
73 filename: archive.zip
73 filename: archive.zip
74 author_id: 2
74 author_id: 2
75 content_type: application/octet-stream
75 content_type: application/octet-stream
76 attachments_007:
76 attachments_007:
77 created_on: 2006-07-19 21:07:27 +02:00
77 created_on: 2006-07-19 21:07:27 +02:00
78 container_type: Issue
78 container_type: Issue
79 container_id: 4
79 container_id: 4
80 downloads: 0
80 downloads: 0
81 disk_filename: 060719210727_archive.zip
81 disk_filename: 060719210727_archive.zip
82 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
82 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
83 id: 7
83 id: 7
84 filesize: 157
84 filesize: 157
85 filename: archive.zip
85 filename: archive.zip
86 author_id: 1
86 author_id: 1
87 content_type: application/octet-stream
87 content_type: application/octet-stream
88 attachments_008:
88 attachments_008:
89 created_on: 2006-07-19 21:07:27 +02:00
89 created_on: 2006-07-19 21:07:27 +02:00
90 container_type: Project
90 container_type: Project
91 container_id: 1
91 container_id: 1
92 downloads: 0
92 downloads: 0
93 disk_filename: 060719210727_project_file.zip
93 disk_filename: 060719210727_project_file.zip
94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
94 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
95 id: 8
95 id: 8
96 filesize: 320
96 filesize: 320
97 filename: project_file.zip
97 filename: project_file.zip
98 author_id: 2
98 author_id: 2
99 content_type: application/octet-stream
99 content_type: application/octet-stream
100 attachments_009:
100 attachments_009:
101 created_on: 2006-07-19 21:07:27 +02:00
101 created_on: 2006-07-19 21:07:27 +02:00
102 container_type: Version
102 container_type: Version
103 container_id: 1
103 container_id: 1
104 downloads: 0
104 downloads: 0
105 disk_filename: 060719210727_version_file.zip
105 disk_filename: 060719210727_version_file.zip
106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
106 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
107 id: 9
107 id: 9
108 filesize: 452
108 filesize: 452
109 filename: version_file.zip
109 filename: version_file.zip
110 author_id: 2
110 author_id: 2
111 content_type: application/octet-stream
111 content_type: application/octet-stream
112 attachments_010:
112 attachments_010:
113 created_on: 2006-07-19 21:07:27 +02:00
113 created_on: 2006-07-19 21:07:27 +02:00
114 container_type: Issue
114 container_type: Issue
115 container_id: 2
115 container_id: 2
116 downloads: 0
116 downloads: 0
117 disk_filename: 060719210727_picture.jpg
117 disk_filename: 060719210727_picture.jpg
118 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
118 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
119 id: 10
119 id: 10
120 filesize: 452
120 filesize: 452
121 filename: picture.jpg
121 filename: picture.jpg
122 author_id: 2
122 author_id: 2
123 content_type: image/jpeg
123 content_type: image/jpeg
124 attachments_011:
124 attachments_011:
125 created_on: 2007-02-12 15:08:27 +01:00
125 created_on: 2007-02-12 15:08:27 +01:00
126 container_type: Document
126 container_type: Document
127 container_id: 1
127 container_id: 1
128 downloads: 0
128 downloads: 0
129 disk_filename: 060719210727_picture.jpg
129 disk_filename: 060719210727_picture.jpg
130 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
130 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
131 id: 11
131 id: 11
132 filesize: 452
132 filesize: 452
133 filename: picture.jpg
133 filename: picture.jpg
134 author_id: 2
134 author_id: 2
135 content_type: image/jpeg
135 content_type: image/jpeg
136 No newline at end of file
136 attachments_012:
137 created_on: 2006-07-19 21:07:27 +02:00
138 container_type: Version
139 container_id: 1
140 downloads: 0
141 disk_filename: 060719210727_version_file.zip
142 digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
143 id: 12
144 filesize: 452
145 filename: version_file.zip
146 author_id: 2
147 content_type: application/octet-stream
@@ -1,191 +1,205
1 ---
1 ---
2 issues_001:
2 issues_001:
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
4 project_id: 1
4 project_id: 1
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
6 priority_id: 4
6 priority_id: 4
7 subject: Can't print recipes
7 subject: Can't print recipes
8 id: 1
8 id: 1
9 fixed_version_id:
9 fixed_version_id:
10 category_id: 1
10 category_id: 1
11 description: Unable to print recipes
11 description: Unable to print recipes
12 tracker_id: 1
12 tracker_id: 1
13 assigned_to_id:
13 assigned_to_id:
14 author_id: 2
14 author_id: 2
15 status_id: 1
15 status_id: 1
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
18 issues_002:
18 issues_002:
19 created_on: 2006-07-19 21:04:21 +02:00
19 created_on: 2006-07-19 21:04:21 +02:00
20 project_id: 1
20 project_id: 1
21 updated_on: 2006-07-19 21:09:50 +02:00
21 updated_on: 2006-07-19 21:09:50 +02:00
22 priority_id: 5
22 priority_id: 5
23 subject: Add ingredients categories
23 subject: Add ingredients categories
24 id: 2
24 id: 2
25 fixed_version_id: 2
25 fixed_version_id: 2
26 category_id:
26 category_id:
27 description: Ingredients of the recipe should be classified by categories
27 description: Ingredients of the recipe should be classified by categories
28 tracker_id: 2
28 tracker_id: 2
29 assigned_to_id: 3
29 assigned_to_id: 3
30 author_id: 2
30 author_id: 2
31 status_id: 2
31 status_id: 2
32 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
32 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
33 due_date:
33 due_date:
34 issues_003:
34 issues_003:
35 created_on: 2006-07-19 21:07:27 +02:00
35 created_on: 2006-07-19 21:07:27 +02:00
36 project_id: 1
36 project_id: 1
37 updated_on: 2006-07-19 21:07:27 +02:00
37 updated_on: 2006-07-19 21:07:27 +02:00
38 priority_id: 4
38 priority_id: 4
39 subject: Error 281 when updating a recipe
39 subject: Error 281 when updating a recipe
40 id: 3
40 id: 3
41 fixed_version_id:
41 fixed_version_id:
42 category_id:
42 category_id:
43 description: Error 281 is encountered when saving a recipe
43 description: Error 281 is encountered when saving a recipe
44 tracker_id: 1
44 tracker_id: 1
45 assigned_to_id: 3
45 assigned_to_id: 3
46 author_id: 2
46 author_id: 2
47 status_id: 1
47 status_id: 1
48 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
48 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
49 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
49 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
50 issues_004:
50 issues_004:
51 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
51 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
52 project_id: 2
52 project_id: 2
53 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
53 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
54 priority_id: 4
54 priority_id: 4
55 subject: Issue on project 2
55 subject: Issue on project 2
56 id: 4
56 id: 4
57 fixed_version_id:
57 fixed_version_id:
58 category_id:
58 category_id:
59 description: Issue on project 2
59 description: Issue on project 2
60 tracker_id: 1
60 tracker_id: 1
61 assigned_to_id: 2
61 assigned_to_id: 2
62 author_id: 2
62 author_id: 2
63 status_id: 1
63 status_id: 1
64 issues_005:
64 issues_005:
65 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
65 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
66 project_id: 3
66 project_id: 3
67 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
67 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
68 priority_id: 4
68 priority_id: 4
69 subject: Subproject issue
69 subject: Subproject issue
70 id: 5
70 id: 5
71 fixed_version_id:
71 fixed_version_id:
72 category_id:
72 category_id:
73 description: This is an issue on a cookbook subproject
73 description: This is an issue on a cookbook subproject
74 tracker_id: 1
74 tracker_id: 1
75 assigned_to_id:
75 assigned_to_id:
76 author_id: 2
76 author_id: 2
77 status_id: 1
77 status_id: 1
78 issues_006:
78 issues_006:
79 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
79 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
80 project_id: 5
80 project_id: 5
81 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
81 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
82 priority_id: 4
82 priority_id: 4
83 subject: Issue of a private subproject
83 subject: Issue of a private subproject
84 id: 6
84 id: 6
85 fixed_version_id:
85 fixed_version_id:
86 category_id:
86 category_id:
87 description: This is an issue of a private subproject of cookbook
87 description: This is an issue of a private subproject of cookbook
88 tracker_id: 1
88 tracker_id: 1
89 assigned_to_id:
89 assigned_to_id:
90 author_id: 2
90 author_id: 2
91 status_id: 1
91 status_id: 1
92 start_date: <%= Date.today.to_s(:db) %>
92 start_date: <%= Date.today.to_s(:db) %>
93 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
93 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
94 issues_007:
94 issues_007:
95 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
95 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
96 project_id: 1
96 project_id: 1
97 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
97 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
98 priority_id: 5
98 priority_id: 5
99 subject: Issue due today
99 subject: Issue due today
100 id: 7
100 id: 7
101 fixed_version_id:
101 fixed_version_id:
102 category_id:
102 category_id:
103 description: This is an issue that is due today
103 description: This is an issue that is due today
104 tracker_id: 1
104 tracker_id: 1
105 assigned_to_id:
105 assigned_to_id:
106 author_id: 2
106 author_id: 2
107 status_id: 1
107 status_id: 1
108 start_date: <%= 10.days.ago.to_s(:db) %>
108 start_date: <%= 10.days.ago.to_s(:db) %>
109 due_date: <%= Date.today.to_s(:db) %>
109 due_date: <%= Date.today.to_s(:db) %>
110 lock_version: 0
110 lock_version: 0
111 issues_008:
111 issues_008:
112 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
112 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
113 project_id: 1
113 project_id: 1
114 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
114 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
115 priority_id: 5
115 priority_id: 5
116 subject: Closed issue
116 subject: Closed issue
117 id: 8
117 id: 8
118 fixed_version_id:
118 fixed_version_id:
119 category_id:
119 category_id:
120 description: This is a closed issue.
120 description: This is a closed issue.
121 tracker_id: 1
121 tracker_id: 1
122 assigned_to_id:
122 assigned_to_id:
123 author_id: 2
123 author_id: 2
124 status_id: 5
124 status_id: 5
125 start_date:
125 start_date:
126 due_date:
126 due_date:
127 lock_version: 0
127 lock_version: 0
128 issues_009:
128 issues_009:
129 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
129 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
130 project_id: 5
130 project_id: 5
131 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
131 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
132 priority_id: 5
132 priority_id: 5
133 subject: Blocked Issue
133 subject: Blocked Issue
134 id: 9
134 id: 9
135 fixed_version_id:
135 fixed_version_id:
136 category_id:
136 category_id:
137 description: This is an issue that is blocked by issue #10
137 description: This is an issue that is blocked by issue #10
138 tracker_id: 1
138 tracker_id: 1
139 assigned_to_id:
139 assigned_to_id:
140 author_id: 2
140 author_id: 2
141 status_id: 1
141 status_id: 1
142 start_date: <%= Date.today.to_s(:db) %>
142 start_date: <%= Date.today.to_s(:db) %>
143 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
143 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
144 issues_010:
144 issues_010:
145 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
145 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
146 project_id: 5
146 project_id: 5
147 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
147 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
148 priority_id: 5
148 priority_id: 5
149 subject: Issue Doing the Blocking
149 subject: Issue Doing the Blocking
150 id: 10
150 id: 10
151 fixed_version_id:
151 fixed_version_id:
152 category_id:
152 category_id:
153 description: This is an issue that blocks issue #9
153 description: This is an issue that blocks issue #9
154 tracker_id: 1
154 tracker_id: 1
155 assigned_to_id:
155 assigned_to_id:
156 author_id: 2
156 author_id: 2
157 status_id: 1
157 status_id: 1
158 start_date: <%= Date.today.to_s(:db) %>
158 start_date: <%= Date.today.to_s(:db) %>
159 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
159 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
160 issues_011:
160 issues_011:
161 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
161 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
162 project_id: 1
162 project_id: 1
163 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
163 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
164 priority_id: 5
164 priority_id: 5
165 subject: Closed issue on a closed version
165 subject: Closed issue on a closed version
166 id: 11
166 id: 11
167 fixed_version_id: 1
167 fixed_version_id: 1
168 category_id: 1
168 category_id: 1
169 description:
169 description:
170 tracker_id: 1
170 tracker_id: 1
171 assigned_to_id:
171 assigned_to_id:
172 author_id: 2
172 author_id: 2
173 status_id: 5
173 status_id: 5
174 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
174 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
175 due_date:
175 due_date:
176 issues_012:
176 issues_012:
177 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
177 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
178 project_id: 1
178 project_id: 1
179 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
179 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
180 priority_id: 5
180 priority_id: 5
181 subject: Closed issue on a locked version
181 subject: Closed issue on a locked version
182 id: 12
182 id: 12
183 fixed_version_id: 2
183 fixed_version_id: 2
184 category_id: 1
184 category_id: 1
185 description:
185 description:
186 tracker_id: 1
186 tracker_id: 1
187 assigned_to_id:
187 assigned_to_id:
188 author_id: 3
188 author_id: 3
189 status_id: 5
189 status_id: 5
190 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
190 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
191 due_date:
191 due_date:
192 issues_013:
193 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
194 project_id: 3
195 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
196 priority_id: 4
197 subject: Subproject issue two
198 id: 13
199 fixed_version_id:
200 category_id:
201 description: This is a second issue on a cookbook subproject
202 tracker_id: 1
203 assigned_to_id:
204 author_id: 2
205 status_id: 1
@@ -1,15 +1,22
1 ---
1 ---
2 journal_details_001:
2 journal_details_001:
3 old_value: "1"
3 old_value: "1"
4 property: attr
4 property: attr
5 id: 1
5 id: 1
6 value: "2"
6 value: "2"
7 prop_key: status_id
7 prop_key: status_id
8 journal_id: 1
8 journal_id: 1
9 journal_details_002:
9 journal_details_002:
10 old_value: "40"
10 old_value: "40"
11 property: attr
11 property: attr
12 id: 2
12 id: 2
13 value: "30"
13 value: "30"
14 prop_key: done_ratio
14 prop_key: done_ratio
15 journal_id: 1
15 journal_id: 1
16 journal_details_003:
17 old_value: nil
18 property: attr
19 id: 3
20 value: "6"
21 prop_key: fixed_version_id
22 journal_id: 4
@@ -1,23 +1,29
1 ---
1 ---
2 journals_001:
2 journals_001:
3 created_on: <%= 2.days.ago.to_date.to_s(:db) %>
3 created_on: <%= 2.days.ago.to_date.to_s(:db) %>
4 notes: "Journal notes"
4 notes: "Journal notes"
5 id: 1
5 id: 1
6 journalized_type: Issue
6 journalized_type: Issue
7 user_id: 1
7 user_id: 1
8 journalized_id: 1
8 journalized_id: 1
9 journals_002:
9 journals_002:
10 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
10 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
11 notes: "Some notes with Redmine links: #2, r2."
11 notes: "Some notes with Redmine links: #2, r2."
12 id: 2
12 id: 2
13 journalized_type: Issue
13 journalized_type: Issue
14 user_id: 2
14 user_id: 2
15 journalized_id: 1
15 journalized_id: 1
16 journals_003:
16 journals_003:
17 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
17 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
18 notes: "A comment with inline image: !picture.jpg!"
18 notes: "A comment with inline image: !picture.jpg!"
19 id: 3
19 id: 3
20 journalized_type: Issue
20 journalized_type: Issue
21 user_id: 2
21 user_id: 2
22 journalized_id: 2
22 journalized_id: 2
23 No newline at end of file
23 journals_004:
24 created_on: <%= 1.days.ago.to_date.to_s(:db) %>
25 notes: "A comment with a private version."
26 id: 4
27 journalized_type: Issue
28 user_id: 1
29 journalized_id: 6
@@ -1,45 +1,50
1 ---
1 ---
2 members_001:
2 members_001:
3 created_on: 2006-07-19 19:35:33 +02:00
3 created_on: 2006-07-19 19:35:33 +02:00
4 project_id: 1
4 project_id: 1
5 id: 1
5 id: 1
6 user_id: 2
6 user_id: 2
7 mail_notification: true
7 mail_notification: true
8 members_002:
8 members_002:
9 created_on: 2006-07-19 19:35:36 +02:00
9 created_on: 2006-07-19 19:35:36 +02:00
10 project_id: 1
10 project_id: 1
11 id: 2
11 id: 2
12 user_id: 3
12 user_id: 3
13 mail_notification: true
13 mail_notification: true
14 members_003:
14 members_003:
15 created_on: 2006-07-19 19:35:36 +02:00
15 created_on: 2006-07-19 19:35:36 +02:00
16 project_id: 2
16 project_id: 2
17 id: 3
17 id: 3
18 user_id: 2
18 user_id: 2
19 mail_notification: true
19 mail_notification: true
20 members_004:
20 members_004:
21 id: 4
21 id: 4
22 created_on: 2006-07-19 19:35:36 +02:00
22 created_on: 2006-07-19 19:35:36 +02:00
23 project_id: 1
23 project_id: 1
24 # Locked user
24 # Locked user
25 user_id: 5
25 user_id: 5
26 mail_notification: true
26 mail_notification: true
27 members_005:
27 members_005:
28 id: 5
28 id: 5
29 created_on: 2006-07-19 19:35:33 +02:00
29 created_on: 2006-07-19 19:35:33 +02:00
30 project_id: 5
30 project_id: 5
31 user_id: 2
31 user_id: 2
32 mail_notification: true
32 mail_notification: true
33 members_006:
33 members_006:
34 id: 6
34 id: 6
35 created_on: 2006-07-19 19:35:33 +02:00
35 created_on: 2006-07-19 19:35:33 +02:00
36 project_id: 5
36 project_id: 5
37 user_id: 10
37 user_id: 10
38 mail_notification: false
38 mail_notification: false
39 members_007:
39 members_007:
40 id: 7
40 id: 7
41 created_on: 2006-07-19 19:35:33 +02:00
41 created_on: 2006-07-19 19:35:33 +02:00
42 project_id: 5
42 project_id: 5
43 user_id: 8
43 user_id: 8
44 mail_notification: false
44 mail_notification: false
45 No newline at end of file
45 members_008:
46 created_on: 2006-07-19 19:35:33 +02:00
47 project_id: 5
48 id: 8
49 user_id: 1
50 mail_notification: true
@@ -1,29 +1,71
1 ---
1 ---
2 versions_001:
2 versions_001:
3 created_on: 2006-07-19 21:00:07 +02:00
3 created_on: 2006-07-19 21:00:07 +02:00
4 name: "0.1"
4 name: "0.1"
5 project_id: 1
5 project_id: 1
6 updated_on: 2006-07-19 21:00:07 +02:00
6 updated_on: 2006-07-19 21:00:07 +02:00
7 id: 1
7 id: 1
8 description: Beta
8 description: Beta
9 effective_date: 2006-07-01
9 effective_date: 2006-07-01
10 status: closed
10 status: closed
11 sharing: 'none'
11 versions_002:
12 versions_002:
12 created_on: 2006-07-19 21:00:33 +02:00
13 created_on: 2006-07-19 21:00:33 +02:00
13 name: "1.0"
14 name: "1.0"
14 project_id: 1
15 project_id: 1
15 updated_on: 2006-07-19 21:00:33 +02:00
16 updated_on: 2006-07-19 21:00:33 +02:00
16 id: 2
17 id: 2
17 description: Stable release
18 description: Stable release
18 effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
19 effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
19 status: locked
20 status: locked
21 sharing: 'none'
20 versions_003:
22 versions_003:
21 created_on: 2006-07-19 21:00:33 +02:00
23 created_on: 2006-07-19 21:00:33 +02:00
22 name: "2.0"
24 name: "2.0"
23 project_id: 1
25 project_id: 1
24 updated_on: 2006-07-19 21:00:33 +02:00
26 updated_on: 2006-07-19 21:00:33 +02:00
25 id: 3
27 id: 3
26 description: Future version
28 description: Future version
27 effective_date:
29 effective_date:
28 status: open
30 status: open
29 No newline at end of file
31 sharing: 'none'
32 versions_004:
33 created_on: 2006-07-19 21:00:33 +02:00
34 name: "2.0"
35 project_id: 3
36 updated_on: 2006-07-19 21:00:33 +02:00
37 id: 4
38 description: Future version on subproject
39 effective_date:
40 status: open
41 sharing: 'tree'
42 versions_005:
43 created_on: 2006-07-19 21:00:07 +02:00
44 name: "Alpha"
45 project_id: 2
46 updated_on: 2006-07-19 21:00:07 +02:00
47 id: 5
48 description: Private Alpha
49 effective_date: 2006-07-01
50 status: open
51 sharing: 'none'
52 versions_006:
53 created_on: 2006-07-19 21:00:07 +02:00
54 name: "Private Version of public subproject"
55 project_id: 5
56 updated_on: 2006-07-19 21:00:07 +02:00
57 id: 6
58 description: "Should be done any day now..."
59 effective_date:
60 status: open
61 sharing: 'tree'
62 versions_007:
63 created_on: 2006-07-19 21:00:07 +02:00
64 name: "Systemwide visible version"
65 project_id: 2
66 updated_on: 2006-07-19 21:00:07 +02:00
67 id: 7
68 description:
69 effective_date:
70 status: open
71 sharing: 'system'
@@ -1,1218 +1,1257
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < ActionController::TestCase
24 class IssuesControllerTest < ActionController::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :member_roles,
29 :member_roles,
30 :issues,
30 :issues,
31 :issue_statuses,
31 :issue_statuses,
32 :versions,
32 :versions,
33 :trackers,
33 :trackers,
34 :projects_trackers,
34 :projects_trackers,
35 :issue_categories,
35 :issue_categories,
36 :enabled_modules,
36 :enabled_modules,
37 :enumerations,
37 :enumerations,
38 :attachments,
38 :attachments,
39 :workflows,
39 :workflows,
40 :custom_fields,
40 :custom_fields,
41 :custom_values,
41 :custom_values,
42 :custom_fields_projects,
42 :custom_fields_projects,
43 :custom_fields_trackers,
43 :custom_fields_trackers,
44 :time_entries,
44 :time_entries,
45 :journals,
45 :journals,
46 :journal_details,
46 :journal_details,
47 :queries
47 :queries
48
48
49 def setup
49 def setup
50 @controller = IssuesController.new
50 @controller = IssuesController.new
51 @request = ActionController::TestRequest.new
51 @request = ActionController::TestRequest.new
52 @response = ActionController::TestResponse.new
52 @response = ActionController::TestResponse.new
53 User.current = nil
53 User.current = nil
54 end
54 end
55
55
56 def test_index_routing
56 def test_index_routing
57 assert_routing(
57 assert_routing(
58 {:method => :get, :path => '/issues'},
58 {:method => :get, :path => '/issues'},
59 :controller => 'issues', :action => 'index'
59 :controller => 'issues', :action => 'index'
60 )
60 )
61 end
61 end
62
62
63 def test_index
63 def test_index
64 Setting.default_language = 'en'
64 Setting.default_language = 'en'
65
65
66 get :index
66 get :index
67 assert_response :success
67 assert_response :success
68 assert_template 'index.rhtml'
68 assert_template 'index.rhtml'
69 assert_not_nil assigns(:issues)
69 assert_not_nil assigns(:issues)
70 assert_nil assigns(:project)
70 assert_nil assigns(:project)
71 assert_tag :tag => 'a', :content => /Can't print recipes/
71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 assert_tag :tag => 'a', :content => /Subproject issue/
72 assert_tag :tag => 'a', :content => /Subproject issue/
73 # private projects hidden
73 # private projects hidden
74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
76 # project column
76 # project column
77 assert_tag :tag => 'th', :content => /Project/
77 assert_tag :tag => 'th', :content => /Project/
78 end
78 end
79
79
80 def test_index_should_not_list_issues_when_module_disabled
80 def test_index_should_not_list_issues_when_module_disabled
81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
82 get :index
82 get :index
83 assert_response :success
83 assert_response :success
84 assert_template 'index.rhtml'
84 assert_template 'index.rhtml'
85 assert_not_nil assigns(:issues)
85 assert_not_nil assigns(:issues)
86 assert_nil assigns(:project)
86 assert_nil assigns(:project)
87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
88 assert_tag :tag => 'a', :content => /Subproject issue/
88 assert_tag :tag => 'a', :content => /Subproject issue/
89 end
89 end
90
90
91 def test_index_with_project_routing
91 def test_index_with_project_routing
92 assert_routing(
92 assert_routing(
93 {:method => :get, :path => '/projects/23/issues'},
93 {:method => :get, :path => '/projects/23/issues'},
94 :controller => 'issues', :action => 'index', :project_id => '23'
94 :controller => 'issues', :action => 'index', :project_id => '23'
95 )
95 )
96 end
96 end
97
97
98 def test_index_should_not_list_issues_when_module_disabled
98 def test_index_should_not_list_issues_when_module_disabled
99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
100 get :index
100 get :index
101 assert_response :success
101 assert_response :success
102 assert_template 'index.rhtml'
102 assert_template 'index.rhtml'
103 assert_not_nil assigns(:issues)
103 assert_not_nil assigns(:issues)
104 assert_nil assigns(:project)
104 assert_nil assigns(:project)
105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
106 assert_tag :tag => 'a', :content => /Subproject issue/
106 assert_tag :tag => 'a', :content => /Subproject issue/
107 end
107 end
108
108
109 def test_index_with_project_routing
109 def test_index_with_project_routing
110 assert_routing(
110 assert_routing(
111 {:method => :get, :path => 'projects/23/issues'},
111 {:method => :get, :path => 'projects/23/issues'},
112 :controller => 'issues', :action => 'index', :project_id => '23'
112 :controller => 'issues', :action => 'index', :project_id => '23'
113 )
113 )
114 end
114 end
115
115
116 def test_index_with_project
116 def test_index_with_project
117 Setting.display_subprojects_issues = 0
117 Setting.display_subprojects_issues = 0
118 get :index, :project_id => 1
118 get :index, :project_id => 1
119 assert_response :success
119 assert_response :success
120 assert_template 'index.rhtml'
120 assert_template 'index.rhtml'
121 assert_not_nil assigns(:issues)
121 assert_not_nil assigns(:issues)
122 assert_tag :tag => 'a', :content => /Can't print recipes/
122 assert_tag :tag => 'a', :content => /Can't print recipes/
123 assert_no_tag :tag => 'a', :content => /Subproject issue/
123 assert_no_tag :tag => 'a', :content => /Subproject issue/
124 end
124 end
125
125
126 def test_index_with_project_and_subprojects
126 def test_index_with_project_and_subprojects
127 Setting.display_subprojects_issues = 1
127 Setting.display_subprojects_issues = 1
128 get :index, :project_id => 1
128 get :index, :project_id => 1
129 assert_response :success
129 assert_response :success
130 assert_template 'index.rhtml'
130 assert_template 'index.rhtml'
131 assert_not_nil assigns(:issues)
131 assert_not_nil assigns(:issues)
132 assert_tag :tag => 'a', :content => /Can't print recipes/
132 assert_tag :tag => 'a', :content => /Can't print recipes/
133 assert_tag :tag => 'a', :content => /Subproject issue/
133 assert_tag :tag => 'a', :content => /Subproject issue/
134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
135 end
135 end
136
136
137 def test_index_with_project_and_subprojects_should_show_private_subprojects
137 def test_index_with_project_and_subprojects_should_show_private_subprojects
138 @request.session[:user_id] = 2
138 @request.session[:user_id] = 2
139 Setting.display_subprojects_issues = 1
139 Setting.display_subprojects_issues = 1
140 get :index, :project_id => 1
140 get :index, :project_id => 1
141 assert_response :success
141 assert_response :success
142 assert_template 'index.rhtml'
142 assert_template 'index.rhtml'
143 assert_not_nil assigns(:issues)
143 assert_not_nil assigns(:issues)
144 assert_tag :tag => 'a', :content => /Can't print recipes/
144 assert_tag :tag => 'a', :content => /Can't print recipes/
145 assert_tag :tag => 'a', :content => /Subproject issue/
145 assert_tag :tag => 'a', :content => /Subproject issue/
146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
147 end
147 end
148
148
149 def test_index_with_project_routing_formatted
149 def test_index_with_project_routing_formatted
150 assert_routing(
150 assert_routing(
151 {:method => :get, :path => 'projects/23/issues.pdf'},
151 {:method => :get, :path => 'projects/23/issues.pdf'},
152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
153 )
153 )
154 assert_routing(
154 assert_routing(
155 {:method => :get, :path => 'projects/23/issues.atom'},
155 {:method => :get, :path => 'projects/23/issues.atom'},
156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
157 )
157 )
158 end
158 end
159
159
160 def test_index_with_project_and_filter
160 def test_index_with_project_and_filter
161 get :index, :project_id => 1, :set_filter => 1
161 get :index, :project_id => 1, :set_filter => 1
162 assert_response :success
162 assert_response :success
163 assert_template 'index.rhtml'
163 assert_template 'index.rhtml'
164 assert_not_nil assigns(:issues)
164 assert_not_nil assigns(:issues)
165 end
165 end
166
166
167 def test_index_with_query
167 def test_index_with_query
168 get :index, :project_id => 1, :query_id => 5
168 get :index, :project_id => 1, :query_id => 5
169 assert_response :success
169 assert_response :success
170 assert_template 'index.rhtml'
170 assert_template 'index.rhtml'
171 assert_not_nil assigns(:issues)
171 assert_not_nil assigns(:issues)
172 assert_nil assigns(:issue_count_by_group)
172 assert_nil assigns(:issue_count_by_group)
173 end
173 end
174
174
175 def test_index_with_query_grouped_by_tracker
175 def test_index_with_query_grouped_by_tracker
176 get :index, :project_id => 1, :query_id => 6
176 get :index, :project_id => 1, :query_id => 6
177 assert_response :success
177 assert_response :success
178 assert_template 'index.rhtml'
178 assert_template 'index.rhtml'
179 assert_not_nil assigns(:issues)
179 assert_not_nil assigns(:issues)
180 assert_not_nil assigns(:issue_count_by_group)
180 assert_not_nil assigns(:issue_count_by_group)
181 end
181 end
182
182
183 def test_index_with_query_grouped_by_list_custom_field
183 def test_index_with_query_grouped_by_list_custom_field
184 get :index, :project_id => 1, :query_id => 9
184 get :index, :project_id => 1, :query_id => 9
185 assert_response :success
185 assert_response :success
186 assert_template 'index.rhtml'
186 assert_template 'index.rhtml'
187 assert_not_nil assigns(:issues)
187 assert_not_nil assigns(:issues)
188 assert_not_nil assigns(:issue_count_by_group)
188 assert_not_nil assigns(:issue_count_by_group)
189 end
189 end
190
190
191 def test_index_sort_by_field_not_included_in_columns
191 def test_index_sort_by_field_not_included_in_columns
192 Setting.issue_list_default_columns = %w(subject author)
192 Setting.issue_list_default_columns = %w(subject author)
193 get :index, :sort => 'tracker'
193 get :index, :sort => 'tracker'
194 end
194 end
195
195
196 def test_index_csv_with_project
196 def test_index_csv_with_project
197 Setting.default_language = 'en'
197 Setting.default_language = 'en'
198
198
199 get :index, :format => 'csv'
199 get :index, :format => 'csv'
200 assert_response :success
200 assert_response :success
201 assert_not_nil assigns(:issues)
201 assert_not_nil assigns(:issues)
202 assert_equal 'text/csv', @response.content_type
202 assert_equal 'text/csv', @response.content_type
203 assert @response.body.starts_with?("#,")
203 assert @response.body.starts_with?("#,")
204
204
205 get :index, :project_id => 1, :format => 'csv'
205 get :index, :project_id => 1, :format => 'csv'
206 assert_response :success
206 assert_response :success
207 assert_not_nil assigns(:issues)
207 assert_not_nil assigns(:issues)
208 assert_equal 'text/csv', @response.content_type
208 assert_equal 'text/csv', @response.content_type
209 end
209 end
210
210
211 def test_index_formatted
211 def test_index_formatted
212 assert_routing(
212 assert_routing(
213 {:method => :get, :path => 'issues.pdf'},
213 {:method => :get, :path => 'issues.pdf'},
214 :controller => 'issues', :action => 'index', :format => 'pdf'
214 :controller => 'issues', :action => 'index', :format => 'pdf'
215 )
215 )
216 assert_routing(
216 assert_routing(
217 {:method => :get, :path => 'issues.atom'},
217 {:method => :get, :path => 'issues.atom'},
218 :controller => 'issues', :action => 'index', :format => 'atom'
218 :controller => 'issues', :action => 'index', :format => 'atom'
219 )
219 )
220 end
220 end
221
221
222 def test_index_pdf
222 def test_index_pdf
223 get :index, :format => 'pdf'
223 get :index, :format => 'pdf'
224 assert_response :success
224 assert_response :success
225 assert_not_nil assigns(:issues)
225 assert_not_nil assigns(:issues)
226 assert_equal 'application/pdf', @response.content_type
226 assert_equal 'application/pdf', @response.content_type
227
227
228 get :index, :project_id => 1, :format => 'pdf'
228 get :index, :project_id => 1, :format => 'pdf'
229 assert_response :success
229 assert_response :success
230 assert_not_nil assigns(:issues)
230 assert_not_nil assigns(:issues)
231 assert_equal 'application/pdf', @response.content_type
231 assert_equal 'application/pdf', @response.content_type
232
232
233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
234 assert_response :success
234 assert_response :success
235 assert_not_nil assigns(:issues)
235 assert_not_nil assigns(:issues)
236 assert_equal 'application/pdf', @response.content_type
236 assert_equal 'application/pdf', @response.content_type
237 end
237 end
238
238
239 def test_index_sort
239 def test_index_sort
240 get :index, :sort => 'tracker,id:desc'
240 get :index, :sort => 'tracker,id:desc'
241 assert_response :success
241 assert_response :success
242
242
243 sort_params = @request.session['issues_index_sort']
243 sort_params = @request.session['issues_index_sort']
244 assert sort_params.is_a?(String)
244 assert sort_params.is_a?(String)
245 assert_equal 'tracker,id:desc', sort_params
245 assert_equal 'tracker,id:desc', sort_params
246
246
247 issues = assigns(:issues)
247 issues = assigns(:issues)
248 assert_not_nil issues
248 assert_not_nil issues
249 assert !issues.empty?
249 assert !issues.empty?
250 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
250 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
251 end
251 end
252
252
253 def test_index_with_columns
253 def test_index_with_columns
254 columns = ['tracker', 'subject', 'assigned_to']
254 columns = ['tracker', 'subject', 'assigned_to']
255 get :index, :set_filter => 1, :query => { 'column_names' => columns}
255 get :index, :set_filter => 1, :query => { 'column_names' => columns}
256 assert_response :success
256 assert_response :success
257
257
258 # query should use specified columns
258 # query should use specified columns
259 query = assigns(:query)
259 query = assigns(:query)
260 assert_kind_of Query, query
260 assert_kind_of Query, query
261 assert_equal columns, query.column_names.map(&:to_s)
261 assert_equal columns, query.column_names.map(&:to_s)
262
262
263 # columns should be stored in session
263 # columns should be stored in session
264 assert_kind_of Hash, session[:query]
264 assert_kind_of Hash, session[:query]
265 assert_kind_of Array, session[:query][:column_names]
265 assert_kind_of Array, session[:query][:column_names]
266 assert_equal columns, session[:query][:column_names].map(&:to_s)
266 assert_equal columns, session[:query][:column_names].map(&:to_s)
267 end
267 end
268
268
269 def test_gantt
269 def test_gantt
270 get :gantt, :project_id => 1
270 get :gantt, :project_id => 1
271 assert_response :success
271 assert_response :success
272 assert_template 'gantt.rhtml'
272 assert_template 'gantt.rhtml'
273 assert_not_nil assigns(:gantt)
273 assert_not_nil assigns(:gantt)
274 events = assigns(:gantt).events
274 events = assigns(:gantt).events
275 assert_not_nil events
275 assert_not_nil events
276 # Issue with start and due dates
276 # Issue with start and due dates
277 i = Issue.find(1)
277 i = Issue.find(1)
278 assert_not_nil i.due_date
278 assert_not_nil i.due_date
279 assert events.include?(Issue.find(1))
279 assert events.include?(Issue.find(1))
280 # Issue with without due date but targeted to a version with date
280 # Issue with without due date but targeted to a version with date
281 i = Issue.find(2)
281 i = Issue.find(2)
282 assert_nil i.due_date
282 assert_nil i.due_date
283 assert events.include?(i)
283 assert events.include?(i)
284 end
284 end
285
285
286 def test_cross_project_gantt
286 def test_cross_project_gantt
287 get :gantt
287 get :gantt
288 assert_response :success
288 assert_response :success
289 assert_template 'gantt.rhtml'
289 assert_template 'gantt.rhtml'
290 assert_not_nil assigns(:gantt)
290 assert_not_nil assigns(:gantt)
291 events = assigns(:gantt).events
291 events = assigns(:gantt).events
292 assert_not_nil events
292 assert_not_nil events
293 end
293 end
294
294
295 def test_gantt_export_to_pdf
295 def test_gantt_export_to_pdf
296 get :gantt, :project_id => 1, :format => 'pdf'
296 get :gantt, :project_id => 1, :format => 'pdf'
297 assert_response :success
297 assert_response :success
298 assert_equal 'application/pdf', @response.content_type
298 assert_equal 'application/pdf', @response.content_type
299 assert @response.body.starts_with?('%PDF')
299 assert @response.body.starts_with?('%PDF')
300 assert_not_nil assigns(:gantt)
300 assert_not_nil assigns(:gantt)
301 end
301 end
302
302
303 def test_cross_project_gantt_export_to_pdf
303 def test_cross_project_gantt_export_to_pdf
304 get :gantt, :format => 'pdf'
304 get :gantt, :format => 'pdf'
305 assert_response :success
305 assert_response :success
306 assert_equal 'application/pdf', @response.content_type
306 assert_equal 'application/pdf', @response.content_type
307 assert @response.body.starts_with?('%PDF')
307 assert @response.body.starts_with?('%PDF')
308 assert_not_nil assigns(:gantt)
308 assert_not_nil assigns(:gantt)
309 end
309 end
310
310
311 if Object.const_defined?(:Magick)
311 if Object.const_defined?(:Magick)
312 def test_gantt_image
312 def test_gantt_image
313 get :gantt, :project_id => 1, :format => 'png'
313 get :gantt, :project_id => 1, :format => 'png'
314 assert_response :success
314 assert_response :success
315 assert_equal 'image/png', @response.content_type
315 assert_equal 'image/png', @response.content_type
316 end
316 end
317 else
317 else
318 puts "RMagick not installed. Skipping tests !!!"
318 puts "RMagick not installed. Skipping tests !!!"
319 end
319 end
320
320
321 def test_calendar
321 def test_calendar
322 get :calendar, :project_id => 1
322 get :calendar, :project_id => 1
323 assert_response :success
323 assert_response :success
324 assert_template 'calendar'
324 assert_template 'calendar'
325 assert_not_nil assigns(:calendar)
325 assert_not_nil assigns(:calendar)
326 end
326 end
327
327
328 def test_cross_project_calendar
328 def test_cross_project_calendar
329 get :calendar
329 get :calendar
330 assert_response :success
330 assert_response :success
331 assert_template 'calendar'
331 assert_template 'calendar'
332 assert_not_nil assigns(:calendar)
332 assert_not_nil assigns(:calendar)
333 end
333 end
334
334
335 def test_changes
335 def test_changes
336 get :changes, :project_id => 1
336 get :changes, :project_id => 1
337 assert_response :success
337 assert_response :success
338 assert_not_nil assigns(:journals)
338 assert_not_nil assigns(:journals)
339 assert_equal 'application/atom+xml', @response.content_type
339 assert_equal 'application/atom+xml', @response.content_type
340 end
340 end
341
341
342 def test_show_routing
342 def test_show_routing
343 assert_routing(
343 assert_routing(
344 {:method => :get, :path => '/issues/64'},
344 {:method => :get, :path => '/issues/64'},
345 :controller => 'issues', :action => 'show', :id => '64'
345 :controller => 'issues', :action => 'show', :id => '64'
346 )
346 )
347 end
347 end
348
348
349 def test_show_routing_formatted
349 def test_show_routing_formatted
350 assert_routing(
350 assert_routing(
351 {:method => :get, :path => '/issues/2332.pdf'},
351 {:method => :get, :path => '/issues/2332.pdf'},
352 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
352 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
353 )
353 )
354 assert_routing(
354 assert_routing(
355 {:method => :get, :path => '/issues/23123.atom'},
355 {:method => :get, :path => '/issues/23123.atom'},
356 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
356 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
357 )
357 )
358 end
358 end
359
359
360 def test_show_by_anonymous
360 def test_show_by_anonymous
361 get :show, :id => 1
361 get :show, :id => 1
362 assert_response :success
362 assert_response :success
363 assert_template 'show.rhtml'
363 assert_template 'show.rhtml'
364 assert_not_nil assigns(:issue)
364 assert_not_nil assigns(:issue)
365 assert_equal Issue.find(1), assigns(:issue)
365 assert_equal Issue.find(1), assigns(:issue)
366
366
367 # anonymous role is allowed to add a note
367 # anonymous role is allowed to add a note
368 assert_tag :tag => 'form',
368 assert_tag :tag => 'form',
369 :descendant => { :tag => 'fieldset',
369 :descendant => { :tag => 'fieldset',
370 :child => { :tag => 'legend',
370 :child => { :tag => 'legend',
371 :content => /Notes/ } }
371 :content => /Notes/ } }
372 end
372 end
373
373
374 def test_show_by_manager
374 def test_show_by_manager
375 @request.session[:user_id] = 2
375 @request.session[:user_id] = 2
376 get :show, :id => 1
376 get :show, :id => 1
377 assert_response :success
377 assert_response :success
378
378
379 assert_tag :tag => 'form',
379 assert_tag :tag => 'form',
380 :descendant => { :tag => 'fieldset',
380 :descendant => { :tag => 'fieldset',
381 :child => { :tag => 'legend',
381 :child => { :tag => 'legend',
382 :content => /Change properties/ } },
382 :content => /Change properties/ } },
383 :descendant => { :tag => 'fieldset',
383 :descendant => { :tag => 'fieldset',
384 :child => { :tag => 'legend',
384 :child => { :tag => 'legend',
385 :content => /Log time/ } },
385 :content => /Log time/ } },
386 :descendant => { :tag => 'fieldset',
386 :descendant => { :tag => 'fieldset',
387 :child => { :tag => 'legend',
387 :child => { :tag => 'legend',
388 :content => /Notes/ } }
388 :content => /Notes/ } }
389 end
389 end
390
390
391 def test_show_should_deny_anonymous_access_without_permission
391 def test_show_should_deny_anonymous_access_without_permission
392 Role.anonymous.remove_permission!(:view_issues)
392 Role.anonymous.remove_permission!(:view_issues)
393 get :show, :id => 1
393 get :show, :id => 1
394 assert_response :redirect
394 assert_response :redirect
395 end
395 end
396
396
397 def test_show_should_deny_non_member_access_without_permission
397 def test_show_should_deny_non_member_access_without_permission
398 Role.non_member.remove_permission!(:view_issues)
398 Role.non_member.remove_permission!(:view_issues)
399 @request.session[:user_id] = 9
399 @request.session[:user_id] = 9
400 get :show, :id => 1
400 get :show, :id => 1
401 assert_response 403
401 assert_response 403
402 end
402 end
403
403
404 def test_show_should_deny_member_access_without_permission
404 def test_show_should_deny_member_access_without_permission
405 Role.find(1).remove_permission!(:view_issues)
405 Role.find(1).remove_permission!(:view_issues)
406 @request.session[:user_id] = 2
406 @request.session[:user_id] = 2
407 get :show, :id => 1
407 get :show, :id => 1
408 assert_response 403
408 assert_response 403
409 end
409 end
410
410
411 def test_show_should_not_disclose_relations_to_invisible_issues
411 def test_show_should_not_disclose_relations_to_invisible_issues
412 Setting.cross_project_issue_relations = '1'
412 Setting.cross_project_issue_relations = '1'
413 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
413 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
414 # Relation to a private project issue
414 # Relation to a private project issue
415 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
415 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
416
416
417 get :show, :id => 1
417 get :show, :id => 1
418 assert_response :success
418 assert_response :success
419
419
420 assert_tag :div, :attributes => { :id => 'relations' },
420 assert_tag :div, :attributes => { :id => 'relations' },
421 :descendant => { :tag => 'a', :content => /#2$/ }
421 :descendant => { :tag => 'a', :content => /#2$/ }
422 assert_no_tag :div, :attributes => { :id => 'relations' },
422 assert_no_tag :div, :attributes => { :id => 'relations' },
423 :descendant => { :tag => 'a', :content => /#4$/ }
423 :descendant => { :tag => 'a', :content => /#4$/ }
424 end
424 end
425
425
426 def test_show_atom
426 def test_show_atom
427 get :show, :id => 2, :format => 'atom'
427 get :show, :id => 2, :format => 'atom'
428 assert_response :success
428 assert_response :success
429 assert_template 'changes.rxml'
429 assert_template 'changes.rxml'
430 # Inline image
430 # Inline image
431 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
431 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
432 end
432 end
433
433
434 def test_new_routing
434 def test_new_routing
435 assert_routing(
435 assert_routing(
436 {:method => :get, :path => '/projects/1/issues/new'},
436 {:method => :get, :path => '/projects/1/issues/new'},
437 :controller => 'issues', :action => 'new', :project_id => '1'
437 :controller => 'issues', :action => 'new', :project_id => '1'
438 )
438 )
439 assert_recognizes(
439 assert_recognizes(
440 {:controller => 'issues', :action => 'new', :project_id => '1'},
440 {:controller => 'issues', :action => 'new', :project_id => '1'},
441 {:method => :post, :path => '/projects/1/issues'}
441 {:method => :post, :path => '/projects/1/issues'}
442 )
442 )
443 end
443 end
444
444
445 def test_show_export_to_pdf
445 def test_show_export_to_pdf
446 get :show, :id => 3, :format => 'pdf'
446 get :show, :id => 3, :format => 'pdf'
447 assert_response :success
447 assert_response :success
448 assert_equal 'application/pdf', @response.content_type
448 assert_equal 'application/pdf', @response.content_type
449 assert @response.body.starts_with?('%PDF')
449 assert @response.body.starts_with?('%PDF')
450 assert_not_nil assigns(:issue)
450 assert_not_nil assigns(:issue)
451 end
451 end
452
452
453 def test_get_new
453 def test_get_new
454 @request.session[:user_id] = 2
454 @request.session[:user_id] = 2
455 get :new, :project_id => 1, :tracker_id => 1
455 get :new, :project_id => 1, :tracker_id => 1
456 assert_response :success
456 assert_response :success
457 assert_template 'new'
457 assert_template 'new'
458
458
459 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
459 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
460 :value => 'Default string' }
460 :value => 'Default string' }
461 end
461 end
462
462
463 def test_get_new_without_tracker_id
463 def test_get_new_without_tracker_id
464 @request.session[:user_id] = 2
464 @request.session[:user_id] = 2
465 get :new, :project_id => 1
465 get :new, :project_id => 1
466 assert_response :success
466 assert_response :success
467 assert_template 'new'
467 assert_template 'new'
468
468
469 issue = assigns(:issue)
469 issue = assigns(:issue)
470 assert_not_nil issue
470 assert_not_nil issue
471 assert_equal Project.find(1).trackers.first, issue.tracker
471 assert_equal Project.find(1).trackers.first, issue.tracker
472 end
472 end
473
473
474 def test_get_new_with_no_default_status_should_display_an_error
474 def test_get_new_with_no_default_status_should_display_an_error
475 @request.session[:user_id] = 2
475 @request.session[:user_id] = 2
476 IssueStatus.delete_all
476 IssueStatus.delete_all
477
477
478 get :new, :project_id => 1
478 get :new, :project_id => 1
479 assert_response 500
479 assert_response 500
480 assert_not_nil flash[:error]
480 assert_not_nil flash[:error]
481 assert_tag :tag => 'div', :attributes => { :class => /error/ },
481 assert_tag :tag => 'div', :attributes => { :class => /error/ },
482 :content => /No default issue/
482 :content => /No default issue/
483 end
483 end
484
484
485 def test_get_new_with_no_tracker_should_display_an_error
485 def test_get_new_with_no_tracker_should_display_an_error
486 @request.session[:user_id] = 2
486 @request.session[:user_id] = 2
487 Tracker.delete_all
487 Tracker.delete_all
488
488
489 get :new, :project_id => 1
489 get :new, :project_id => 1
490 assert_response 500
490 assert_response 500
491 assert_not_nil flash[:error]
491 assert_not_nil flash[:error]
492 assert_tag :tag => 'div', :attributes => { :class => /error/ },
492 assert_tag :tag => 'div', :attributes => { :class => /error/ },
493 :content => /No tracker/
493 :content => /No tracker/
494 end
494 end
495
495
496 def test_update_new_form
496 def test_update_new_form
497 @request.session[:user_id] = 2
497 @request.session[:user_id] = 2
498 xhr :post, :update_form, :project_id => 1,
498 xhr :post, :update_form, :project_id => 1,
499 :issue => {:tracker_id => 2,
499 :issue => {:tracker_id => 2,
500 :subject => 'This is the test_new issue',
500 :subject => 'This is the test_new issue',
501 :description => 'This is the description',
501 :description => 'This is the description',
502 :priority_id => 5}
502 :priority_id => 5}
503 assert_response :success
503 assert_response :success
504 assert_template 'attributes'
504 assert_template 'attributes'
505
505
506 issue = assigns(:issue)
506 issue = assigns(:issue)
507 assert_kind_of Issue, issue
507 assert_kind_of Issue, issue
508 assert_equal 1, issue.project_id
508 assert_equal 1, issue.project_id
509 assert_equal 2, issue.tracker_id
509 assert_equal 2, issue.tracker_id
510 assert_equal 'This is the test_new issue', issue.subject
510 assert_equal 'This is the test_new issue', issue.subject
511 end
511 end
512
512
513 def test_post_new
513 def test_post_new
514 @request.session[:user_id] = 2
514 @request.session[:user_id] = 2
515 assert_difference 'Issue.count' do
515 assert_difference 'Issue.count' do
516 post :new, :project_id => 1,
516 post :new, :project_id => 1,
517 :issue => {:tracker_id => 3,
517 :issue => {:tracker_id => 3,
518 :subject => 'This is the test_new issue',
518 :subject => 'This is the test_new issue',
519 :description => 'This is the description',
519 :description => 'This is the description',
520 :priority_id => 5,
520 :priority_id => 5,
521 :estimated_hours => '',
521 :estimated_hours => '',
522 :custom_field_values => {'2' => 'Value for field 2'}}
522 :custom_field_values => {'2' => 'Value for field 2'}}
523 end
523 end
524 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
524 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
525
525
526 issue = Issue.find_by_subject('This is the test_new issue')
526 issue = Issue.find_by_subject('This is the test_new issue')
527 assert_not_nil issue
527 assert_not_nil issue
528 assert_equal 2, issue.author_id
528 assert_equal 2, issue.author_id
529 assert_equal 3, issue.tracker_id
529 assert_equal 3, issue.tracker_id
530 assert_nil issue.estimated_hours
530 assert_nil issue.estimated_hours
531 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
531 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
532 assert_not_nil v
532 assert_not_nil v
533 assert_equal 'Value for field 2', v.value
533 assert_equal 'Value for field 2', v.value
534 end
534 end
535
535
536 def test_post_new_and_continue
536 def test_post_new_and_continue
537 @request.session[:user_id] = 2
537 @request.session[:user_id] = 2
538 post :new, :project_id => 1,
538 post :new, :project_id => 1,
539 :issue => {:tracker_id => 3,
539 :issue => {:tracker_id => 3,
540 :subject => 'This is first issue',
540 :subject => 'This is first issue',
541 :priority_id => 5},
541 :priority_id => 5},
542 :continue => ''
542 :continue => ''
543 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
543 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
544 end
544 end
545
545
546 def test_post_new_without_custom_fields_param
546 def test_post_new_without_custom_fields_param
547 @request.session[:user_id] = 2
547 @request.session[:user_id] = 2
548 assert_difference 'Issue.count' do
548 assert_difference 'Issue.count' do
549 post :new, :project_id => 1,
549 post :new, :project_id => 1,
550 :issue => {:tracker_id => 1,
550 :issue => {:tracker_id => 1,
551 :subject => 'This is the test_new issue',
551 :subject => 'This is the test_new issue',
552 :description => 'This is the description',
552 :description => 'This is the description',
553 :priority_id => 5}
553 :priority_id => 5}
554 end
554 end
555 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
555 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
556 end
556 end
557
557
558 def test_post_new_with_required_custom_field_and_without_custom_fields_param
558 def test_post_new_with_required_custom_field_and_without_custom_fields_param
559 field = IssueCustomField.find_by_name('Database')
559 field = IssueCustomField.find_by_name('Database')
560 field.update_attribute(:is_required, true)
560 field.update_attribute(:is_required, true)
561
561
562 @request.session[:user_id] = 2
562 @request.session[:user_id] = 2
563 post :new, :project_id => 1,
563 post :new, :project_id => 1,
564 :issue => {:tracker_id => 1,
564 :issue => {:tracker_id => 1,
565 :subject => 'This is the test_new issue',
565 :subject => 'This is the test_new issue',
566 :description => 'This is the description',
566 :description => 'This is the description',
567 :priority_id => 5}
567 :priority_id => 5}
568 assert_response :success
568 assert_response :success
569 assert_template 'new'
569 assert_template 'new'
570 issue = assigns(:issue)
570 issue = assigns(:issue)
571 assert_not_nil issue
571 assert_not_nil issue
572 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
572 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
573 end
573 end
574
574
575 def test_post_new_with_watchers
575 def test_post_new_with_watchers
576 @request.session[:user_id] = 2
576 @request.session[:user_id] = 2
577 ActionMailer::Base.deliveries.clear
577 ActionMailer::Base.deliveries.clear
578
578
579 assert_difference 'Watcher.count', 2 do
579 assert_difference 'Watcher.count', 2 do
580 post :new, :project_id => 1,
580 post :new, :project_id => 1,
581 :issue => {:tracker_id => 1,
581 :issue => {:tracker_id => 1,
582 :subject => 'This is a new issue with watchers',
582 :subject => 'This is a new issue with watchers',
583 :description => 'This is the description',
583 :description => 'This is the description',
584 :priority_id => 5,
584 :priority_id => 5,
585 :watcher_user_ids => ['2', '3']}
585 :watcher_user_ids => ['2', '3']}
586 end
586 end
587 issue = Issue.find_by_subject('This is a new issue with watchers')
587 issue = Issue.find_by_subject('This is a new issue with watchers')
588 assert_not_nil issue
588 assert_not_nil issue
589 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
589 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
590
590
591 # Watchers added
591 # Watchers added
592 assert_equal [2, 3], issue.watcher_user_ids.sort
592 assert_equal [2, 3], issue.watcher_user_ids.sort
593 assert issue.watched_by?(User.find(3))
593 assert issue.watched_by?(User.find(3))
594 # Watchers notified
594 # Watchers notified
595 mail = ActionMailer::Base.deliveries.last
595 mail = ActionMailer::Base.deliveries.last
596 assert_kind_of TMail::Mail, mail
596 assert_kind_of TMail::Mail, mail
597 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
597 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
598 end
598 end
599
599
600 def test_post_new_should_send_a_notification
600 def test_post_new_should_send_a_notification
601 ActionMailer::Base.deliveries.clear
601 ActionMailer::Base.deliveries.clear
602 @request.session[:user_id] = 2
602 @request.session[:user_id] = 2
603 assert_difference 'Issue.count' do
603 assert_difference 'Issue.count' do
604 post :new, :project_id => 1,
604 post :new, :project_id => 1,
605 :issue => {:tracker_id => 3,
605 :issue => {:tracker_id => 3,
606 :subject => 'This is the test_new issue',
606 :subject => 'This is the test_new issue',
607 :description => 'This is the description',
607 :description => 'This is the description',
608 :priority_id => 5,
608 :priority_id => 5,
609 :estimated_hours => '',
609 :estimated_hours => '',
610 :custom_field_values => {'2' => 'Value for field 2'}}
610 :custom_field_values => {'2' => 'Value for field 2'}}
611 end
611 end
612 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
612 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
613
613
614 assert_equal 1, ActionMailer::Base.deliveries.size
614 assert_equal 1, ActionMailer::Base.deliveries.size
615 end
615 end
616
616
617 def test_post_should_preserve_fields_values_on_validation_failure
617 def test_post_should_preserve_fields_values_on_validation_failure
618 @request.session[:user_id] = 2
618 @request.session[:user_id] = 2
619 post :new, :project_id => 1,
619 post :new, :project_id => 1,
620 :issue => {:tracker_id => 1,
620 :issue => {:tracker_id => 1,
621 # empty subject
621 # empty subject
622 :subject => '',
622 :subject => '',
623 :description => 'This is a description',
623 :description => 'This is a description',
624 :priority_id => 6,
624 :priority_id => 6,
625 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
625 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
626 assert_response :success
626 assert_response :success
627 assert_template 'new'
627 assert_template 'new'
628
628
629 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
629 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
630 :content => 'This is a description'
630 :content => 'This is a description'
631 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
631 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
632 :child => { :tag => 'option', :attributes => { :selected => 'selected',
632 :child => { :tag => 'option', :attributes => { :selected => 'selected',
633 :value => '6' },
633 :value => '6' },
634 :content => 'High' }
634 :content => 'High' }
635 # Custom fields
635 # Custom fields
636 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
636 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
637 :child => { :tag => 'option', :attributes => { :selected => 'selected',
637 :child => { :tag => 'option', :attributes => { :selected => 'selected',
638 :value => 'Oracle' },
638 :value => 'Oracle' },
639 :content => 'Oracle' }
639 :content => 'Oracle' }
640 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
640 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
641 :value => 'Value for field 2'}
641 :value => 'Value for field 2'}
642 end
642 end
643
643
644 def test_copy_routing
644 def test_copy_routing
645 assert_routing(
645 assert_routing(
646 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
646 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
647 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
647 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
648 )
648 )
649 end
649 end
650
650
651 def test_copy_issue
651 def test_copy_issue
652 @request.session[:user_id] = 2
652 @request.session[:user_id] = 2
653 get :new, :project_id => 1, :copy_from => 1
653 get :new, :project_id => 1, :copy_from => 1
654 assert_template 'new'
654 assert_template 'new'
655 assert_not_nil assigns(:issue)
655 assert_not_nil assigns(:issue)
656 orig = Issue.find(1)
656 orig = Issue.find(1)
657 assert_equal orig.subject, assigns(:issue).subject
657 assert_equal orig.subject, assigns(:issue).subject
658 end
658 end
659
659
660 def test_edit_routing
660 def test_edit_routing
661 assert_routing(
661 assert_routing(
662 {:method => :get, :path => '/issues/1/edit'},
662 {:method => :get, :path => '/issues/1/edit'},
663 :controller => 'issues', :action => 'edit', :id => '1'
663 :controller => 'issues', :action => 'edit', :id => '1'
664 )
664 )
665 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
665 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
666 {:controller => 'issues', :action => 'edit', :id => '1'},
666 {:controller => 'issues', :action => 'edit', :id => '1'},
667 {:method => :post, :path => '/issues/1/edit'}
667 {:method => :post, :path => '/issues/1/edit'}
668 )
668 )
669 end
669 end
670
670
671 def test_get_edit
671 def test_get_edit
672 @request.session[:user_id] = 2
672 @request.session[:user_id] = 2
673 get :edit, :id => 1
673 get :edit, :id => 1
674 assert_response :success
674 assert_response :success
675 assert_template 'edit'
675 assert_template 'edit'
676 assert_not_nil assigns(:issue)
676 assert_not_nil assigns(:issue)
677 assert_equal Issue.find(1), assigns(:issue)
677 assert_equal Issue.find(1), assigns(:issue)
678 end
678 end
679
679
680 def test_get_edit_with_params
680 def test_get_edit_with_params
681 @request.session[:user_id] = 2
681 @request.session[:user_id] = 2
682 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
682 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
683 assert_response :success
683 assert_response :success
684 assert_template 'edit'
684 assert_template 'edit'
685
685
686 issue = assigns(:issue)
686 issue = assigns(:issue)
687 assert_not_nil issue
687 assert_not_nil issue
688
688
689 assert_equal 5, issue.status_id
689 assert_equal 5, issue.status_id
690 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
690 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
691 :child => { :tag => 'option',
691 :child => { :tag => 'option',
692 :content => 'Closed',
692 :content => 'Closed',
693 :attributes => { :selected => 'selected' } }
693 :attributes => { :selected => 'selected' } }
694
694
695 assert_equal 7, issue.priority_id
695 assert_equal 7, issue.priority_id
696 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
696 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
697 :child => { :tag => 'option',
697 :child => { :tag => 'option',
698 :content => 'Urgent',
698 :content => 'Urgent',
699 :attributes => { :selected => 'selected' } }
699 :attributes => { :selected => 'selected' } }
700 end
700 end
701
701
702 def test_update_edit_form
702 def test_update_edit_form
703 @request.session[:user_id] = 2
703 @request.session[:user_id] = 2
704 xhr :post, :update_form, :project_id => 1,
704 xhr :post, :update_form, :project_id => 1,
705 :id => 1,
705 :id => 1,
706 :issue => {:tracker_id => 2,
706 :issue => {:tracker_id => 2,
707 :subject => 'This is the test_new issue',
707 :subject => 'This is the test_new issue',
708 :description => 'This is the description',
708 :description => 'This is the description',
709 :priority_id => 5}
709 :priority_id => 5}
710 assert_response :success
710 assert_response :success
711 assert_template 'attributes'
711 assert_template 'attributes'
712
712
713 issue = assigns(:issue)
713 issue = assigns(:issue)
714 assert_kind_of Issue, issue
714 assert_kind_of Issue, issue
715 assert_equal 1, issue.id
715 assert_equal 1, issue.id
716 assert_equal 1, issue.project_id
716 assert_equal 1, issue.project_id
717 assert_equal 2, issue.tracker_id
717 assert_equal 2, issue.tracker_id
718 assert_equal 'This is the test_new issue', issue.subject
718 assert_equal 'This is the test_new issue', issue.subject
719 end
719 end
720
720
721 def test_reply_routing
721 def test_reply_routing
722 assert_routing(
722 assert_routing(
723 {:method => :post, :path => '/issues/1/quoted'},
723 {:method => :post, :path => '/issues/1/quoted'},
724 :controller => 'issues', :action => 'reply', :id => '1'
724 :controller => 'issues', :action => 'reply', :id => '1'
725 )
725 )
726 end
726 end
727
727
728 def test_reply_to_issue
728 def test_reply_to_issue
729 @request.session[:user_id] = 2
729 @request.session[:user_id] = 2
730 get :reply, :id => 1
730 get :reply, :id => 1
731 assert_response :success
731 assert_response :success
732 assert_select_rjs :show, "update"
732 assert_select_rjs :show, "update"
733 end
733 end
734
734
735 def test_reply_to_note
735 def test_reply_to_note
736 @request.session[:user_id] = 2
736 @request.session[:user_id] = 2
737 get :reply, :id => 1, :journal_id => 2
737 get :reply, :id => 1, :journal_id => 2
738 assert_response :success
738 assert_response :success
739 assert_select_rjs :show, "update"
739 assert_select_rjs :show, "update"
740 end
740 end
741
741
742 def test_post_edit_without_custom_fields_param
742 def test_post_edit_without_custom_fields_param
743 @request.session[:user_id] = 2
743 @request.session[:user_id] = 2
744 ActionMailer::Base.deliveries.clear
744 ActionMailer::Base.deliveries.clear
745
745
746 issue = Issue.find(1)
746 issue = Issue.find(1)
747 assert_equal '125', issue.custom_value_for(2).value
747 assert_equal '125', issue.custom_value_for(2).value
748 old_subject = issue.subject
748 old_subject = issue.subject
749 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
749 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
750
750
751 assert_difference('Journal.count') do
751 assert_difference('Journal.count') do
752 assert_difference('JournalDetail.count', 2) do
752 assert_difference('JournalDetail.count', 2) do
753 post :edit, :id => 1, :issue => {:subject => new_subject,
753 post :edit, :id => 1, :issue => {:subject => new_subject,
754 :priority_id => '6',
754 :priority_id => '6',
755 :category_id => '1' # no change
755 :category_id => '1' # no change
756 }
756 }
757 end
757 end
758 end
758 end
759 assert_redirected_to :action => 'show', :id => '1'
759 assert_redirected_to :action => 'show', :id => '1'
760 issue.reload
760 issue.reload
761 assert_equal new_subject, issue.subject
761 assert_equal new_subject, issue.subject
762 # Make sure custom fields were not cleared
762 # Make sure custom fields were not cleared
763 assert_equal '125', issue.custom_value_for(2).value
763 assert_equal '125', issue.custom_value_for(2).value
764
764
765 mail = ActionMailer::Base.deliveries.last
765 mail = ActionMailer::Base.deliveries.last
766 assert_kind_of TMail::Mail, mail
766 assert_kind_of TMail::Mail, mail
767 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
767 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
768 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
768 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
769 end
769 end
770
770
771 def test_post_edit_with_custom_field_change
771 def test_post_edit_with_custom_field_change
772 @request.session[:user_id] = 2
772 @request.session[:user_id] = 2
773 issue = Issue.find(1)
773 issue = Issue.find(1)
774 assert_equal '125', issue.custom_value_for(2).value
774 assert_equal '125', issue.custom_value_for(2).value
775
775
776 assert_difference('Journal.count') do
776 assert_difference('Journal.count') do
777 assert_difference('JournalDetail.count', 3) do
777 assert_difference('JournalDetail.count', 3) do
778 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
778 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
779 :priority_id => '6',
779 :priority_id => '6',
780 :category_id => '1', # no change
780 :category_id => '1', # no change
781 :custom_field_values => { '2' => 'New custom value' }
781 :custom_field_values => { '2' => 'New custom value' }
782 }
782 }
783 end
783 end
784 end
784 end
785 assert_redirected_to :action => 'show', :id => '1'
785 assert_redirected_to :action => 'show', :id => '1'
786 issue.reload
786 issue.reload
787 assert_equal 'New custom value', issue.custom_value_for(2).value
787 assert_equal 'New custom value', issue.custom_value_for(2).value
788
788
789 mail = ActionMailer::Base.deliveries.last
789 mail = ActionMailer::Base.deliveries.last
790 assert_kind_of TMail::Mail, mail
790 assert_kind_of TMail::Mail, mail
791 assert mail.body.include?("Searchable field changed from 125 to New custom value")
791 assert mail.body.include?("Searchable field changed from 125 to New custom value")
792 end
792 end
793
793
794 def test_post_edit_with_status_and_assignee_change
794 def test_post_edit_with_status_and_assignee_change
795 issue = Issue.find(1)
795 issue = Issue.find(1)
796 assert_equal 1, issue.status_id
796 assert_equal 1, issue.status_id
797 @request.session[:user_id] = 2
797 @request.session[:user_id] = 2
798 assert_difference('TimeEntry.count', 0) do
798 assert_difference('TimeEntry.count', 0) do
799 post :edit,
799 post :edit,
800 :id => 1,
800 :id => 1,
801 :issue => { :status_id => 2, :assigned_to_id => 3 },
801 :issue => { :status_id => 2, :assigned_to_id => 3 },
802 :notes => 'Assigned to dlopper',
802 :notes => 'Assigned to dlopper',
803 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
803 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
804 end
804 end
805 assert_redirected_to :action => 'show', :id => '1'
805 assert_redirected_to :action => 'show', :id => '1'
806 issue.reload
806 issue.reload
807 assert_equal 2, issue.status_id
807 assert_equal 2, issue.status_id
808 j = issue.journals.find(:first, :order => 'id DESC')
808 j = issue.journals.find(:first, :order => 'id DESC')
809 assert_equal 'Assigned to dlopper', j.notes
809 assert_equal 'Assigned to dlopper', j.notes
810 assert_equal 2, j.details.size
810 assert_equal 2, j.details.size
811
811
812 mail = ActionMailer::Base.deliveries.last
812 mail = ActionMailer::Base.deliveries.last
813 assert mail.body.include?("Status changed from New to Assigned")
813 assert mail.body.include?("Status changed from New to Assigned")
814 # subject should contain the new status
814 # subject should contain the new status
815 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
815 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
816 end
816 end
817
817
818 def test_post_edit_with_note_only
818 def test_post_edit_with_note_only
819 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
819 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
820 # anonymous user
820 # anonymous user
821 post :edit,
821 post :edit,
822 :id => 1,
822 :id => 1,
823 :notes => notes
823 :notes => notes
824 assert_redirected_to :action => 'show', :id => '1'
824 assert_redirected_to :action => 'show', :id => '1'
825 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
825 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
826 assert_equal notes, j.notes
826 assert_equal notes, j.notes
827 assert_equal 0, j.details.size
827 assert_equal 0, j.details.size
828 assert_equal User.anonymous, j.user
828 assert_equal User.anonymous, j.user
829
829
830 mail = ActionMailer::Base.deliveries.last
830 mail = ActionMailer::Base.deliveries.last
831 assert mail.body.include?(notes)
831 assert mail.body.include?(notes)
832 end
832 end
833
833
834 def test_post_edit_with_note_and_spent_time
834 def test_post_edit_with_note_and_spent_time
835 @request.session[:user_id] = 2
835 @request.session[:user_id] = 2
836 spent_hours_before = Issue.find(1).spent_hours
836 spent_hours_before = Issue.find(1).spent_hours
837 assert_difference('TimeEntry.count') do
837 assert_difference('TimeEntry.count') do
838 post :edit,
838 post :edit,
839 :id => 1,
839 :id => 1,
840 :notes => '2.5 hours added',
840 :notes => '2.5 hours added',
841 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
841 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
842 end
842 end
843 assert_redirected_to :action => 'show', :id => '1'
843 assert_redirected_to :action => 'show', :id => '1'
844
844
845 issue = Issue.find(1)
845 issue = Issue.find(1)
846
846
847 j = issue.journals.find(:first, :order => 'id DESC')
847 j = issue.journals.find(:first, :order => 'id DESC')
848 assert_equal '2.5 hours added', j.notes
848 assert_equal '2.5 hours added', j.notes
849 assert_equal 0, j.details.size
849 assert_equal 0, j.details.size
850
850
851 t = issue.time_entries.find(:first, :order => 'id DESC')
851 t = issue.time_entries.find(:first, :order => 'id DESC')
852 assert_not_nil t
852 assert_not_nil t
853 assert_equal 2.5, t.hours
853 assert_equal 2.5, t.hours
854 assert_equal spent_hours_before + 2.5, issue.spent_hours
854 assert_equal spent_hours_before + 2.5, issue.spent_hours
855 end
855 end
856
856
857 def test_post_edit_with_attachment_only
857 def test_post_edit_with_attachment_only
858 set_tmp_attachments_directory
858 set_tmp_attachments_directory
859
859
860 # Delete all fixtured journals, a race condition can occur causing the wrong
860 # Delete all fixtured journals, a race condition can occur causing the wrong
861 # journal to get fetched in the next find.
861 # journal to get fetched in the next find.
862 Journal.delete_all
862 Journal.delete_all
863
863
864 # anonymous user
864 # anonymous user
865 post :edit,
865 post :edit,
866 :id => 1,
866 :id => 1,
867 :notes => '',
867 :notes => '',
868 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
868 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
869 assert_redirected_to :action => 'show', :id => '1'
869 assert_redirected_to :action => 'show', :id => '1'
870 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
870 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
871 assert j.notes.blank?
871 assert j.notes.blank?
872 assert_equal 1, j.details.size
872 assert_equal 1, j.details.size
873 assert_equal 'testfile.txt', j.details.first.value
873 assert_equal 'testfile.txt', j.details.first.value
874 assert_equal User.anonymous, j.user
874 assert_equal User.anonymous, j.user
875
875
876 mail = ActionMailer::Base.deliveries.last
876 mail = ActionMailer::Base.deliveries.last
877 assert mail.body.include?('testfile.txt')
877 assert mail.body.include?('testfile.txt')
878 end
878 end
879
879
880 def test_post_edit_with_no_change
880 def test_post_edit_with_no_change
881 issue = Issue.find(1)
881 issue = Issue.find(1)
882 issue.journals.clear
882 issue.journals.clear
883 ActionMailer::Base.deliveries.clear
883 ActionMailer::Base.deliveries.clear
884
884
885 post :edit,
885 post :edit,
886 :id => 1,
886 :id => 1,
887 :notes => ''
887 :notes => ''
888 assert_redirected_to :action => 'show', :id => '1'
888 assert_redirected_to :action => 'show', :id => '1'
889
889
890 issue.reload
890 issue.reload
891 assert issue.journals.empty?
891 assert issue.journals.empty?
892 # No email should be sent
892 # No email should be sent
893 assert ActionMailer::Base.deliveries.empty?
893 assert ActionMailer::Base.deliveries.empty?
894 end
894 end
895
895
896 def test_post_edit_should_send_a_notification
896 def test_post_edit_should_send_a_notification
897 @request.session[:user_id] = 2
897 @request.session[:user_id] = 2
898 ActionMailer::Base.deliveries.clear
898 ActionMailer::Base.deliveries.clear
899 issue = Issue.find(1)
899 issue = Issue.find(1)
900 old_subject = issue.subject
900 old_subject = issue.subject
901 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
901 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
902
902
903 post :edit, :id => 1, :issue => {:subject => new_subject,
903 post :edit, :id => 1, :issue => {:subject => new_subject,
904 :priority_id => '6',
904 :priority_id => '6',
905 :category_id => '1' # no change
905 :category_id => '1' # no change
906 }
906 }
907 assert_equal 1, ActionMailer::Base.deliveries.size
907 assert_equal 1, ActionMailer::Base.deliveries.size
908 end
908 end
909
909
910 def test_post_edit_with_invalid_spent_time
910 def test_post_edit_with_invalid_spent_time
911 @request.session[:user_id] = 2
911 @request.session[:user_id] = 2
912 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
912 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
913
913
914 assert_no_difference('Journal.count') do
914 assert_no_difference('Journal.count') do
915 post :edit,
915 post :edit,
916 :id => 1,
916 :id => 1,
917 :notes => notes,
917 :notes => notes,
918 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
918 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
919 end
919 end
920 assert_response :success
920 assert_response :success
921 assert_template 'edit'
921 assert_template 'edit'
922
922
923 assert_tag :textarea, :attributes => { :name => 'notes' },
923 assert_tag :textarea, :attributes => { :name => 'notes' },
924 :content => notes
924 :content => notes
925 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
925 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
926 end
926 end
927
927
928 def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
929 issue = Issue.find(2)
930 @request.session[:user_id] = 2
931
932 post :edit,
933 :id => issue.id,
934 :issue => {
935 :fixed_version_id => 4
936 }
937
938 assert_response :redirect
939 issue.reload
940 assert_equal 4, issue.fixed_version_id
941 assert_not_equal issue.project_id, issue.fixed_version.project_id
942 end
943
928 def test_get_bulk_edit
944 def test_get_bulk_edit
929 @request.session[:user_id] = 2
945 @request.session[:user_id] = 2
930 get :bulk_edit, :ids => [1, 2]
946 get :bulk_edit, :ids => [1, 2]
931 assert_response :success
947 assert_response :success
932 assert_template 'bulk_edit'
948 assert_template 'bulk_edit'
933 end
949 end
934
950
935 def test_bulk_edit
951 def test_bulk_edit
936 @request.session[:user_id] = 2
952 @request.session[:user_id] = 2
937 # update issues priority
953 # update issues priority
938 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
954 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
939 :assigned_to_id => '',
955 :assigned_to_id => '',
940 :custom_field_values => {'2' => ''},
956 :custom_field_values => {'2' => ''},
941 :notes => 'Bulk editing'
957 :notes => 'Bulk editing'
942 assert_response 302
958 assert_response 302
943 # check that the issues were updated
959 # check that the issues were updated
944 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
960 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
945
961
946 issue = Issue.find(1)
962 issue = Issue.find(1)
947 journal = issue.journals.find(:first, :order => 'created_on DESC')
963 journal = issue.journals.find(:first, :order => 'created_on DESC')
948 assert_equal '125', issue.custom_value_for(2).value
964 assert_equal '125', issue.custom_value_for(2).value
949 assert_equal 'Bulk editing', journal.notes
965 assert_equal 'Bulk editing', journal.notes
950 assert_equal 1, journal.details.size
966 assert_equal 1, journal.details.size
951 end
967 end
952
968
953 def test_bullk_edit_should_send_a_notification
969 def test_bullk_edit_should_send_a_notification
954 @request.session[:user_id] = 2
970 @request.session[:user_id] = 2
955 ActionMailer::Base.deliveries.clear
971 ActionMailer::Base.deliveries.clear
956 post(:bulk_edit,
972 post(:bulk_edit,
957 {
973 {
958 :ids => [1, 2],
974 :ids => [1, 2],
959 :priority_id => 7,
975 :priority_id => 7,
960 :assigned_to_id => '',
976 :assigned_to_id => '',
961 :custom_field_values => {'2' => ''},
977 :custom_field_values => {'2' => ''},
962 :notes => 'Bulk editing'
978 :notes => 'Bulk editing'
963 })
979 })
964
980
965 assert_response 302
981 assert_response 302
966 assert_equal 2, ActionMailer::Base.deliveries.size
982 assert_equal 2, ActionMailer::Base.deliveries.size
967 end
983 end
968
984
969 def test_bulk_edit_status
985 def test_bulk_edit_status
970 @request.session[:user_id] = 2
986 @request.session[:user_id] = 2
971 # update issues priority
987 # update issues priority
972 post :bulk_edit, :ids => [1, 2], :priority_id => '',
988 post :bulk_edit, :ids => [1, 2], :priority_id => '',
973 :assigned_to_id => '',
989 :assigned_to_id => '',
974 :status_id => '5',
990 :status_id => '5',
975 :notes => 'Bulk editing status'
991 :notes => 'Bulk editing status'
976 assert_response 302
992 assert_response 302
977 issue = Issue.find(1)
993 issue = Issue.find(1)
978 assert issue.closed?
994 assert issue.closed?
979 end
995 end
980
996
981 def test_bulk_edit_custom_field
997 def test_bulk_edit_custom_field
982 @request.session[:user_id] = 2
998 @request.session[:user_id] = 2
983 # update issues priority
999 # update issues priority
984 post :bulk_edit, :ids => [1, 2], :priority_id => '',
1000 post :bulk_edit, :ids => [1, 2], :priority_id => '',
985 :assigned_to_id => '',
1001 :assigned_to_id => '',
986 :custom_field_values => {'2' => '777'},
1002 :custom_field_values => {'2' => '777'},
987 :notes => 'Bulk editing custom field'
1003 :notes => 'Bulk editing custom field'
988 assert_response 302
1004 assert_response 302
989
1005
990 issue = Issue.find(1)
1006 issue = Issue.find(1)
991 journal = issue.journals.find(:first, :order => 'created_on DESC')
1007 journal = issue.journals.find(:first, :order => 'created_on DESC')
992 assert_equal '777', issue.custom_value_for(2).value
1008 assert_equal '777', issue.custom_value_for(2).value
993 assert_equal 1, journal.details.size
1009 assert_equal 1, journal.details.size
994 assert_equal '125', journal.details.first.old_value
1010 assert_equal '125', journal.details.first.old_value
995 assert_equal '777', journal.details.first.value
1011 assert_equal '777', journal.details.first.value
996 end
1012 end
997
1013
998 def test_bulk_unassign
1014 def test_bulk_unassign
999 assert_not_nil Issue.find(2).assigned_to
1015 assert_not_nil Issue.find(2).assigned_to
1000 @request.session[:user_id] = 2
1016 @request.session[:user_id] = 2
1001 # unassign issues
1017 # unassign issues
1002 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1018 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1003 assert_response 302
1019 assert_response 302
1004 # check that the issues were updated
1020 # check that the issues were updated
1005 assert_nil Issue.find(2).assigned_to
1021 assert_nil Issue.find(2).assigned_to
1006 end
1022 end
1007
1023
1024 def test_post_bulk_edit_should_allow_fixed_version_to_be_set_to_a_subproject
1025 @request.session[:user_id] = 2
1026
1027 post :bulk_edit,
1028 :ids => [1,2],
1029 :fixed_version_id => 4
1030
1031 assert_response :redirect
1032 issues = Issue.find([1,2])
1033 issues.each do |issue|
1034 assert_equal 4, issue.fixed_version_id
1035 assert_not_equal issue.project_id, issue.fixed_version.project_id
1036 end
1037 end
1038
1008 def test_move_routing
1039 def test_move_routing
1009 assert_routing(
1040 assert_routing(
1010 {:method => :get, :path => '/issues/1/move'},
1041 {:method => :get, :path => '/issues/1/move'},
1011 :controller => 'issues', :action => 'move', :id => '1'
1042 :controller => 'issues', :action => 'move', :id => '1'
1012 )
1043 )
1013 assert_recognizes(
1044 assert_recognizes(
1014 {:controller => 'issues', :action => 'move', :id => '1'},
1045 {:controller => 'issues', :action => 'move', :id => '1'},
1015 {:method => :post, :path => '/issues/1/move'}
1046 {:method => :post, :path => '/issues/1/move'}
1016 )
1047 )
1017 end
1048 end
1018
1049
1019 def test_move_one_issue_to_another_project
1050 def test_move_one_issue_to_another_project
1020 @request.session[:user_id] = 2
1051 @request.session[:user_id] = 2
1021 post :move, :id => 1, :new_project_id => 2
1052 post :move, :id => 1, :new_project_id => 2
1022 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1053 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1023 assert_equal 2, Issue.find(1).project_id
1054 assert_equal 2, Issue.find(1).project_id
1024 end
1055 end
1025
1056
1026 def test_move_one_issue_to_another_project_should_follow_when_needed
1057 def test_move_one_issue_to_another_project_should_follow_when_needed
1027 @request.session[:user_id] = 2
1058 @request.session[:user_id] = 2
1028 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1059 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1029 assert_redirected_to '/issues/1'
1060 assert_redirected_to '/issues/1'
1030 end
1061 end
1031
1062
1032 def test_bulk_move_to_another_project
1063 def test_bulk_move_to_another_project
1033 @request.session[:user_id] = 2
1064 @request.session[:user_id] = 2
1034 post :move, :ids => [1, 2], :new_project_id => 2
1065 post :move, :ids => [1, 2], :new_project_id => 2
1035 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1066 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1036 # Issues moved to project 2
1067 # Issues moved to project 2
1037 assert_equal 2, Issue.find(1).project_id
1068 assert_equal 2, Issue.find(1).project_id
1038 assert_equal 2, Issue.find(2).project_id
1069 assert_equal 2, Issue.find(2).project_id
1039 # No tracker change
1070 # No tracker change
1040 assert_equal 1, Issue.find(1).tracker_id
1071 assert_equal 1, Issue.find(1).tracker_id
1041 assert_equal 2, Issue.find(2).tracker_id
1072 assert_equal 2, Issue.find(2).tracker_id
1042 end
1073 end
1043
1074
1044 def test_bulk_move_to_another_tracker
1075 def test_bulk_move_to_another_tracker
1045 @request.session[:user_id] = 2
1076 @request.session[:user_id] = 2
1046 post :move, :ids => [1, 2], :new_tracker_id => 2
1077 post :move, :ids => [1, 2], :new_tracker_id => 2
1047 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1078 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1048 assert_equal 2, Issue.find(1).tracker_id
1079 assert_equal 2, Issue.find(1).tracker_id
1049 assert_equal 2, Issue.find(2).tracker_id
1080 assert_equal 2, Issue.find(2).tracker_id
1050 end
1081 end
1051
1082
1052 def test_bulk_copy_to_another_project
1083 def test_bulk_copy_to_another_project
1053 @request.session[:user_id] = 2
1084 @request.session[:user_id] = 2
1054 assert_difference 'Issue.count', 2 do
1085 assert_difference 'Issue.count', 2 do
1055 assert_no_difference 'Project.find(1).issues.count' do
1086 assert_no_difference 'Project.find(1).issues.count' do
1056 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1087 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1057 end
1088 end
1058 end
1089 end
1059 assert_redirected_to 'projects/ecookbook/issues'
1090 assert_redirected_to 'projects/ecookbook/issues'
1060 end
1091 end
1061
1092
1062 context "#move via bulk copy" do
1093 context "#move via bulk copy" do
1063 should "allow changing the issue's attributes" do
1094 should "allow changing the issue's attributes" do
1064 @request.session[:user_id] = 2
1095 @request.session[:user_id] = 2
1065 assert_difference 'Issue.count', 2 do
1096 assert_difference 'Issue.count', 2 do
1066 assert_no_difference 'Project.find(1).issues.count' do
1097 assert_no_difference 'Project.find(1).issues.count' do
1067 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1098 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}, :assigned_to_id => 4, :status_id => 3, :start_date => '2009-12-01', :due_date => '2009-12-31'
1068 end
1099 end
1069 end
1100 end
1070
1101
1071 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1102 copied_issues = Issue.all(:limit => 2, :order => 'id desc', :conditions => {:project_id => 2})
1072 assert_equal 2, copied_issues.size
1103 assert_equal 2, copied_issues.size
1073 copied_issues.each do |issue|
1104 copied_issues.each do |issue|
1074 assert_equal 2, issue.project_id, "Project is incorrect"
1105 assert_equal 2, issue.project_id, "Project is incorrect"
1075 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1106 assert_equal 4, issue.assigned_to_id, "Assigned to is incorrect"
1076 assert_equal 3, issue.status_id, "Status is incorrect"
1107 assert_equal 3, issue.status_id, "Status is incorrect"
1077 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1108 assert_equal '2009-12-01', issue.start_date.to_s, "Start date is incorrect"
1078 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1109 assert_equal '2009-12-31', issue.due_date.to_s, "Due date is incorrect"
1079 end
1110 end
1080 end
1111 end
1081 end
1112 end
1082
1113
1083 def test_copy_to_another_project_should_follow_when_needed
1114 def test_copy_to_another_project_should_follow_when_needed
1084 @request.session[:user_id] = 2
1115 @request.session[:user_id] = 2
1085 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1116 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1086 issue = Issue.first(:order => 'id DESC')
1117 issue = Issue.first(:order => 'id DESC')
1087 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1118 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1088 end
1119 end
1089
1120
1090 def test_context_menu_one_issue
1121 def test_context_menu_one_issue
1091 @request.session[:user_id] = 2
1122 @request.session[:user_id] = 2
1092 get :context_menu, :ids => [1]
1123 get :context_menu, :ids => [1]
1093 assert_response :success
1124 assert_response :success
1094 assert_template 'context_menu'
1125 assert_template 'context_menu'
1095 assert_tag :tag => 'a', :content => 'Edit',
1126 assert_tag :tag => 'a', :content => 'Edit',
1096 :attributes => { :href => '/issues/1/edit',
1127 :attributes => { :href => '/issues/1/edit',
1097 :class => 'icon-edit' }
1128 :class => 'icon-edit' }
1098 assert_tag :tag => 'a', :content => 'Closed',
1129 assert_tag :tag => 'a', :content => 'Closed',
1099 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1130 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1100 :class => '' }
1131 :class => '' }
1101 assert_tag :tag => 'a', :content => 'Immediate',
1132 assert_tag :tag => 'a', :content => 'Immediate',
1102 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1133 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1103 :class => '' }
1134 :class => '' }
1135 # Versions
1136 assert_tag :tag => 'a', :content => '2.0',
1137 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=3&amp;ids%5B%5D=1',
1138 :class => '' }
1139 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
1140 :attributes => { :href => '/issues/bulk_edit?fixed_version_id=4&amp;ids%5B%5D=1',
1141 :class => '' }
1142
1104 assert_tag :tag => 'a', :content => 'Dave Lopper',
1143 assert_tag :tag => 'a', :content => 'Dave Lopper',
1105 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1144 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1106 :class => '' }
1145 :class => '' }
1107 assert_tag :tag => 'a', :content => 'Copy',
1146 assert_tag :tag => 'a', :content => 'Copy',
1108 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1147 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1109 :class => 'icon-copy' }
1148 :class => 'icon-copy' }
1110 assert_tag :tag => 'a', :content => 'Move',
1149 assert_tag :tag => 'a', :content => 'Move',
1111 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1150 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1112 :class => 'icon-move' }
1151 :class => 'icon-move' }
1113 assert_tag :tag => 'a', :content => 'Delete',
1152 assert_tag :tag => 'a', :content => 'Delete',
1114 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1153 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1115 :class => 'icon-del' }
1154 :class => 'icon-del' }
1116 end
1155 end
1117
1156
1118 def test_context_menu_one_issue_by_anonymous
1157 def test_context_menu_one_issue_by_anonymous
1119 get :context_menu, :ids => [1]
1158 get :context_menu, :ids => [1]
1120 assert_response :success
1159 assert_response :success
1121 assert_template 'context_menu'
1160 assert_template 'context_menu'
1122 assert_tag :tag => 'a', :content => 'Delete',
1161 assert_tag :tag => 'a', :content => 'Delete',
1123 :attributes => { :href => '#',
1162 :attributes => { :href => '#',
1124 :class => 'icon-del disabled' }
1163 :class => 'icon-del disabled' }
1125 end
1164 end
1126
1165
1127 def test_context_menu_multiple_issues_of_same_project
1166 def test_context_menu_multiple_issues_of_same_project
1128 @request.session[:user_id] = 2
1167 @request.session[:user_id] = 2
1129 get :context_menu, :ids => [1, 2]
1168 get :context_menu, :ids => [1, 2]
1130 assert_response :success
1169 assert_response :success
1131 assert_template 'context_menu'
1170 assert_template 'context_menu'
1132 assert_tag :tag => 'a', :content => 'Edit',
1171 assert_tag :tag => 'a', :content => 'Edit',
1133 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1172 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1134 :class => 'icon-edit' }
1173 :class => 'icon-edit' }
1135 assert_tag :tag => 'a', :content => 'Immediate',
1174 assert_tag :tag => 'a', :content => 'Immediate',
1136 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1175 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1137 :class => '' }
1176 :class => '' }
1138 assert_tag :tag => 'a', :content => 'Dave Lopper',
1177 assert_tag :tag => 'a', :content => 'Dave Lopper',
1139 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1178 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1140 :class => '' }
1179 :class => '' }
1141 assert_tag :tag => 'a', :content => 'Copy',
1180 assert_tag :tag => 'a', :content => 'Copy',
1142 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1181 :attributes => { :href => '/issues/move?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1143 :class => 'icon-copy' }
1182 :class => 'icon-copy' }
1144 assert_tag :tag => 'a', :content => 'Move',
1183 assert_tag :tag => 'a', :content => 'Move',
1145 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1184 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1146 :class => 'icon-move' }
1185 :class => 'icon-move' }
1147 assert_tag :tag => 'a', :content => 'Delete',
1186 assert_tag :tag => 'a', :content => 'Delete',
1148 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1187 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1149 :class => 'icon-del' }
1188 :class => 'icon-del' }
1150 end
1189 end
1151
1190
1152 def test_context_menu_multiple_issues_of_different_project
1191 def test_context_menu_multiple_issues_of_different_project
1153 @request.session[:user_id] = 2
1192 @request.session[:user_id] = 2
1154 get :context_menu, :ids => [1, 2, 4]
1193 get :context_menu, :ids => [1, 2, 4]
1155 assert_response :success
1194 assert_response :success
1156 assert_template 'context_menu'
1195 assert_template 'context_menu'
1157 assert_tag :tag => 'a', :content => 'Delete',
1196 assert_tag :tag => 'a', :content => 'Delete',
1158 :attributes => { :href => '#',
1197 :attributes => { :href => '#',
1159 :class => 'icon-del disabled' }
1198 :class => 'icon-del disabled' }
1160 end
1199 end
1161
1200
1162 def test_destroy_routing
1201 def test_destroy_routing
1163 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1202 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1164 {:controller => 'issues', :action => 'destroy', :id => '1'},
1203 {:controller => 'issues', :action => 'destroy', :id => '1'},
1165 {:method => :post, :path => '/issues/1/destroy'}
1204 {:method => :post, :path => '/issues/1/destroy'}
1166 )
1205 )
1167 end
1206 end
1168
1207
1169 def test_destroy_issue_with_no_time_entries
1208 def test_destroy_issue_with_no_time_entries
1170 assert_nil TimeEntry.find_by_issue_id(2)
1209 assert_nil TimeEntry.find_by_issue_id(2)
1171 @request.session[:user_id] = 2
1210 @request.session[:user_id] = 2
1172 post :destroy, :id => 2
1211 post :destroy, :id => 2
1173 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1212 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1174 assert_nil Issue.find_by_id(2)
1213 assert_nil Issue.find_by_id(2)
1175 end
1214 end
1176
1215
1177 def test_destroy_issues_with_time_entries
1216 def test_destroy_issues_with_time_entries
1178 @request.session[:user_id] = 2
1217 @request.session[:user_id] = 2
1179 post :destroy, :ids => [1, 3]
1218 post :destroy, :ids => [1, 3]
1180 assert_response :success
1219 assert_response :success
1181 assert_template 'destroy'
1220 assert_template 'destroy'
1182 assert_not_nil assigns(:hours)
1221 assert_not_nil assigns(:hours)
1183 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1222 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1184 end
1223 end
1185
1224
1186 def test_destroy_issues_and_destroy_time_entries
1225 def test_destroy_issues_and_destroy_time_entries
1187 @request.session[:user_id] = 2
1226 @request.session[:user_id] = 2
1188 post :destroy, :ids => [1, 3], :todo => 'destroy'
1227 post :destroy, :ids => [1, 3], :todo => 'destroy'
1189 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1228 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1190 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1229 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1191 assert_nil TimeEntry.find_by_id([1, 2])
1230 assert_nil TimeEntry.find_by_id([1, 2])
1192 end
1231 end
1193
1232
1194 def test_destroy_issues_and_assign_time_entries_to_project
1233 def test_destroy_issues_and_assign_time_entries_to_project
1195 @request.session[:user_id] = 2
1234 @request.session[:user_id] = 2
1196 post :destroy, :ids => [1, 3], :todo => 'nullify'
1235 post :destroy, :ids => [1, 3], :todo => 'nullify'
1197 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1236 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1198 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1237 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1199 assert_nil TimeEntry.find(1).issue_id
1238 assert_nil TimeEntry.find(1).issue_id
1200 assert_nil TimeEntry.find(2).issue_id
1239 assert_nil TimeEntry.find(2).issue_id
1201 end
1240 end
1202
1241
1203 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1242 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1204 @request.session[:user_id] = 2
1243 @request.session[:user_id] = 2
1205 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1244 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1206 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1245 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1207 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1246 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1208 assert_equal 2, TimeEntry.find(1).issue_id
1247 assert_equal 2, TimeEntry.find(1).issue_id
1209 assert_equal 2, TimeEntry.find(2).issue_id
1248 assert_equal 2, TimeEntry.find(2).issue_id
1210 end
1249 end
1211
1250
1212 def test_default_search_scope
1251 def test_default_search_scope
1213 get :index
1252 get :index
1214 assert_tag :div, :attributes => {:id => 'quick-search'},
1253 assert_tag :div, :attributes => {:id => 'quick-search'},
1215 :child => {:tag => 'form',
1254 :child => {:tag => 'form',
1216 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1255 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1217 end
1256 end
1218 end
1257 end
@@ -1,782 +1,800
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'projects_controller'
19 require 'projects_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class ProjectsController; def rescue_action(e) raise e end; end
22 class ProjectsController; def rescue_action(e) raise e end; end
23
23
24 class ProjectsControllerTest < ActionController::TestCase
24 class ProjectsControllerTest < ActionController::TestCase
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
25 fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
26 :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
27 :attachments, :custom_fields, :custom_values, :time_entries
27 :attachments, :custom_fields, :custom_values, :time_entries
28
28
29 def setup
29 def setup
30 @controller = ProjectsController.new
30 @controller = ProjectsController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 @request.session[:user_id] = nil
33 @request.session[:user_id] = nil
34 Setting.default_language = 'en'
34 Setting.default_language = 'en'
35 end
35 end
36
36
37 def test_index_routing
37 def test_index_routing
38 assert_routing(
38 assert_routing(
39 {:method => :get, :path => '/projects'},
39 {:method => :get, :path => '/projects'},
40 :controller => 'projects', :action => 'index'
40 :controller => 'projects', :action => 'index'
41 )
41 )
42 end
42 end
43
43
44 def test_index
44 def test_index
45 get :index
45 get :index
46 assert_response :success
46 assert_response :success
47 assert_template 'index'
47 assert_template 'index'
48 assert_not_nil assigns(:projects)
48 assert_not_nil assigns(:projects)
49
49
50 assert_tag :ul, :child => {:tag => 'li',
50 assert_tag :ul, :child => {:tag => 'li',
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
51 :descendant => {:tag => 'a', :content => 'eCookbook'},
52 :child => { :tag => 'ul',
52 :child => { :tag => 'ul',
53 :descendant => { :tag => 'a',
53 :descendant => { :tag => 'a',
54 :content => 'Child of private child'
54 :content => 'Child of private child'
55 }
55 }
56 }
56 }
57 }
57 }
58
58
59 assert_no_tag :a, :content => /Private child of eCookbook/
59 assert_no_tag :a, :content => /Private child of eCookbook/
60 end
60 end
61
61
62 def test_index_atom_routing
62 def test_index_atom_routing
63 assert_routing(
63 assert_routing(
64 {:method => :get, :path => '/projects.atom'},
64 {:method => :get, :path => '/projects.atom'},
65 :controller => 'projects', :action => 'index', :format => 'atom'
65 :controller => 'projects', :action => 'index', :format => 'atom'
66 )
66 )
67 end
67 end
68
68
69 def test_index_atom
69 def test_index_atom
70 get :index, :format => 'atom'
70 get :index, :format => 'atom'
71 assert_response :success
71 assert_response :success
72 assert_template 'common/feed.atom.rxml'
72 assert_template 'common/feed.atom.rxml'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
73 assert_select 'feed>title', :text => 'Redmine: Latest projects'
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
74 assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
75 end
75 end
76
76
77 def test_add_routing
77 def test_add_routing
78 assert_routing(
78 assert_routing(
79 {:method => :get, :path => '/projects/new'},
79 {:method => :get, :path => '/projects/new'},
80 :controller => 'projects', :action => 'add'
80 :controller => 'projects', :action => 'add'
81 )
81 )
82 assert_recognizes(
82 assert_recognizes(
83 {:controller => 'projects', :action => 'add'},
83 {:controller => 'projects', :action => 'add'},
84 {:method => :post, :path => '/projects/new'}
84 {:method => :post, :path => '/projects/new'}
85 )
85 )
86 assert_recognizes(
86 assert_recognizes(
87 {:controller => 'projects', :action => 'add'},
87 {:controller => 'projects', :action => 'add'},
88 {:method => :post, :path => '/projects'}
88 {:method => :post, :path => '/projects'}
89 )
89 )
90 end
90 end
91
91
92 def test_get_add
92 def test_get_add
93 @request.session[:user_id] = 1
93 @request.session[:user_id] = 1
94 get :add
94 get :add
95 assert_response :success
95 assert_response :success
96 assert_template 'add'
96 assert_template 'add'
97 end
97 end
98
98
99 def test_get_add_by_non_admin
99 def test_get_add_by_non_admin
100 @request.session[:user_id] = 2
100 @request.session[:user_id] = 2
101 get :add
101 get :add
102 assert_response :success
102 assert_response :success
103 assert_template 'add'
103 assert_template 'add'
104 end
104 end
105
105
106 def test_post_add
106 def test_post_add
107 @request.session[:user_id] = 1
107 @request.session[:user_id] = 1
108 post :add, :project => { :name => "blog",
108 post :add, :project => { :name => "blog",
109 :description => "weblog",
109 :description => "weblog",
110 :identifier => "blog",
110 :identifier => "blog",
111 :is_public => 1,
111 :is_public => 1,
112 :custom_field_values => { '3' => 'Beta' }
112 :custom_field_values => { '3' => 'Beta' }
113 }
113 }
114 assert_redirected_to '/projects/blog/settings'
114 assert_redirected_to '/projects/blog/settings'
115
115
116 project = Project.find_by_name('blog')
116 project = Project.find_by_name('blog')
117 assert_kind_of Project, project
117 assert_kind_of Project, project
118 assert_equal 'weblog', project.description
118 assert_equal 'weblog', project.description
119 assert_equal true, project.is_public?
119 assert_equal true, project.is_public?
120 assert_nil project.parent
120 assert_nil project.parent
121 end
121 end
122
122
123 def test_post_add_subproject
123 def test_post_add_subproject
124 @request.session[:user_id] = 1
124 @request.session[:user_id] = 1
125 post :add, :project => { :name => "blog",
125 post :add, :project => { :name => "blog",
126 :description => "weblog",
126 :description => "weblog",
127 :identifier => "blog",
127 :identifier => "blog",
128 :is_public => 1,
128 :is_public => 1,
129 :custom_field_values => { '3' => 'Beta' },
129 :custom_field_values => { '3' => 'Beta' },
130 :parent_id => 1
130 :parent_id => 1
131 }
131 }
132 assert_redirected_to '/projects/blog/settings'
132 assert_redirected_to '/projects/blog/settings'
133
133
134 project = Project.find_by_name('blog')
134 project = Project.find_by_name('blog')
135 assert_kind_of Project, project
135 assert_kind_of Project, project
136 assert_equal Project.find(1), project.parent
136 assert_equal Project.find(1), project.parent
137 end
137 end
138
138
139 def test_post_add_by_non_admin
139 def test_post_add_by_non_admin
140 @request.session[:user_id] = 2
140 @request.session[:user_id] = 2
141 post :add, :project => { :name => "blog",
141 post :add, :project => { :name => "blog",
142 :description => "weblog",
142 :description => "weblog",
143 :identifier => "blog",
143 :identifier => "blog",
144 :is_public => 1,
144 :is_public => 1,
145 :custom_field_values => { '3' => 'Beta' }
145 :custom_field_values => { '3' => 'Beta' }
146 }
146 }
147 assert_redirected_to '/projects/blog/settings'
147 assert_redirected_to '/projects/blog/settings'
148
148
149 project = Project.find_by_name('blog')
149 project = Project.find_by_name('blog')
150 assert_kind_of Project, project
150 assert_kind_of Project, project
151 assert_equal 'weblog', project.description
151 assert_equal 'weblog', project.description
152 assert_equal true, project.is_public?
152 assert_equal true, project.is_public?
153
153
154 # User should be added as a project member
154 # User should be added as a project member
155 assert User.find(2).member_of?(project)
155 assert User.find(2).member_of?(project)
156 assert_equal 1, project.members.size
156 assert_equal 1, project.members.size
157 end
157 end
158
158
159 def test_show_routing
159 def test_show_routing
160 assert_routing(
160 assert_routing(
161 {:method => :get, :path => '/projects/test'},
161 {:method => :get, :path => '/projects/test'},
162 :controller => 'projects', :action => 'show', :id => 'test'
162 :controller => 'projects', :action => 'show', :id => 'test'
163 )
163 )
164 end
164 end
165
165
166 def test_show_by_id
166 def test_show_by_id
167 get :show, :id => 1
167 get :show, :id => 1
168 assert_response :success
168 assert_response :success
169 assert_template 'show'
169 assert_template 'show'
170 assert_not_nil assigns(:project)
170 assert_not_nil assigns(:project)
171 end
171 end
172
172
173 def test_show_by_identifier
173 def test_show_by_identifier
174 get :show, :id => 'ecookbook'
174 get :show, :id => 'ecookbook'
175 assert_response :success
175 assert_response :success
176 assert_template 'show'
176 assert_template 'show'
177 assert_not_nil assigns(:project)
177 assert_not_nil assigns(:project)
178 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
178 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
179 end
179 end
180
180
181 def test_show_should_not_fail_when_custom_values_are_nil
181 def test_show_should_not_fail_when_custom_values_are_nil
182 project = Project.find_by_identifier('ecookbook')
182 project = Project.find_by_identifier('ecookbook')
183 project.custom_values.first.update_attribute(:value, nil)
183 project.custom_values.first.update_attribute(:value, nil)
184 get :show, :id => 'ecookbook'
184 get :show, :id => 'ecookbook'
185 assert_response :success
185 assert_response :success
186 assert_template 'show'
186 assert_template 'show'
187 assert_not_nil assigns(:project)
187 assert_not_nil assigns(:project)
188 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
188 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
189 end
189 end
190
190
191 def test_private_subprojects_hidden
191 def test_private_subprojects_hidden
192 get :show, :id => 'ecookbook'
192 get :show, :id => 'ecookbook'
193 assert_response :success
193 assert_response :success
194 assert_template 'show'
194 assert_template 'show'
195 assert_no_tag :tag => 'a', :content => /Private child/
195 assert_no_tag :tag => 'a', :content => /Private child/
196 end
196 end
197
197
198 def test_private_subprojects_visible
198 def test_private_subprojects_visible
199 @request.session[:user_id] = 2 # manager who is a member of the private subproject
199 @request.session[:user_id] = 2 # manager who is a member of the private subproject
200 get :show, :id => 'ecookbook'
200 get :show, :id => 'ecookbook'
201 assert_response :success
201 assert_response :success
202 assert_template 'show'
202 assert_template 'show'
203 assert_tag :tag => 'a', :content => /Private child/
203 assert_tag :tag => 'a', :content => /Private child/
204 end
204 end
205
205
206 def test_settings_routing
206 def test_settings_routing
207 assert_routing(
207 assert_routing(
208 {:method => :get, :path => '/projects/4223/settings'},
208 {:method => :get, :path => '/projects/4223/settings'},
209 :controller => 'projects', :action => 'settings', :id => '4223'
209 :controller => 'projects', :action => 'settings', :id => '4223'
210 )
210 )
211 assert_routing(
211 assert_routing(
212 {:method => :get, :path => '/projects/4223/settings/members'},
212 {:method => :get, :path => '/projects/4223/settings/members'},
213 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
213 :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
214 )
214 )
215 end
215 end
216
216
217 def test_settings
217 def test_settings
218 @request.session[:user_id] = 2 # manager
218 @request.session[:user_id] = 2 # manager
219 get :settings, :id => 1
219 get :settings, :id => 1
220 assert_response :success
220 assert_response :success
221 assert_template 'settings'
221 assert_template 'settings'
222 end
222 end
223
223
224 def test_edit
224 def test_edit
225 @request.session[:user_id] = 2 # manager
225 @request.session[:user_id] = 2 # manager
226 post :edit, :id => 1, :project => {:name => 'Test changed name',
226 post :edit, :id => 1, :project => {:name => 'Test changed name',
227 :issue_custom_field_ids => ['']}
227 :issue_custom_field_ids => ['']}
228 assert_redirected_to 'projects/ecookbook/settings'
228 assert_redirected_to 'projects/ecookbook/settings'
229 project = Project.find(1)
229 project = Project.find(1)
230 assert_equal 'Test changed name', project.name
230 assert_equal 'Test changed name', project.name
231 end
231 end
232
232
233 def test_add_version_routing
233 def test_add_version_routing
234 assert_routing(
234 assert_routing(
235 {:method => :get, :path => 'projects/64/versions/new'},
235 {:method => :get, :path => 'projects/64/versions/new'},
236 :controller => 'projects', :action => 'add_version', :id => '64'
236 :controller => 'projects', :action => 'add_version', :id => '64'
237 )
237 )
238 assert_routing(
238 assert_routing(
239 #TODO: use PUT
239 #TODO: use PUT
240 {:method => :post, :path => 'projects/64/versions/new'},
240 {:method => :post, :path => 'projects/64/versions/new'},
241 :controller => 'projects', :action => 'add_version', :id => '64'
241 :controller => 'projects', :action => 'add_version', :id => '64'
242 )
242 )
243 end
243 end
244
244
245 def test_add_issue_category_routing
245 def test_add_issue_category_routing
246 assert_routing(
246 assert_routing(
247 {:method => :get, :path => 'projects/test/categories/new'},
247 {:method => :get, :path => 'projects/test/categories/new'},
248 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
248 :controller => 'projects', :action => 'add_issue_category', :id => 'test'
249 )
249 )
250 assert_routing(
250 assert_routing(
251 #TODO: use PUT and update form
251 #TODO: use PUT and update form
252 {:method => :post, :path => 'projects/64/categories/new'},
252 {:method => :post, :path => 'projects/64/categories/new'},
253 :controller => 'projects', :action => 'add_issue_category', :id => '64'
253 :controller => 'projects', :action => 'add_issue_category', :id => '64'
254 )
254 )
255 end
255 end
256
256
257 def test_destroy_routing
257 def test_destroy_routing
258 assert_routing(
258 assert_routing(
259 {:method => :get, :path => '/projects/567/destroy'},
259 {:method => :get, :path => '/projects/567/destroy'},
260 :controller => 'projects', :action => 'destroy', :id => '567'
260 :controller => 'projects', :action => 'destroy', :id => '567'
261 )
261 )
262 assert_routing(
262 assert_routing(
263 #TODO: use DELETE and update form
263 #TODO: use DELETE and update form
264 {:method => :post, :path => 'projects/64/destroy'},
264 {:method => :post, :path => 'projects/64/destroy'},
265 :controller => 'projects', :action => 'destroy', :id => '64'
265 :controller => 'projects', :action => 'destroy', :id => '64'
266 )
266 )
267 end
267 end
268
268
269 def test_get_destroy
269 def test_get_destroy
270 @request.session[:user_id] = 1 # admin
270 @request.session[:user_id] = 1 # admin
271 get :destroy, :id => 1
271 get :destroy, :id => 1
272 assert_response :success
272 assert_response :success
273 assert_template 'destroy'
273 assert_template 'destroy'
274 assert_not_nil Project.find_by_id(1)
274 assert_not_nil Project.find_by_id(1)
275 end
275 end
276
276
277 def test_post_destroy
277 def test_post_destroy
278 @request.session[:user_id] = 1 # admin
278 @request.session[:user_id] = 1 # admin
279 post :destroy, :id => 1, :confirm => 1
279 post :destroy, :id => 1, :confirm => 1
280 assert_redirected_to 'admin/projects'
280 assert_redirected_to 'admin/projects'
281 assert_nil Project.find_by_id(1)
281 assert_nil Project.find_by_id(1)
282 end
282 end
283
283
284 def test_add_file
284 def test_add_file
285 set_tmp_attachments_directory
285 set_tmp_attachments_directory
286 @request.session[:user_id] = 2
286 @request.session[:user_id] = 2
287 Setting.notified_events = ['file_added']
287 Setting.notified_events = ['file_added']
288 ActionMailer::Base.deliveries.clear
288 ActionMailer::Base.deliveries.clear
289
289
290 assert_difference 'Attachment.count' do
290 assert_difference 'Attachment.count' do
291 post :add_file, :id => 1, :version_id => '',
291 post :add_file, :id => 1, :version_id => '',
292 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
292 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
293 end
293 end
294 assert_redirected_to 'projects/ecookbook/files'
294 assert_redirected_to 'projects/ecookbook/files'
295 a = Attachment.find(:first, :order => 'created_on DESC')
295 a = Attachment.find(:first, :order => 'created_on DESC')
296 assert_equal 'testfile.txt', a.filename
296 assert_equal 'testfile.txt', a.filename
297 assert_equal Project.find(1), a.container
297 assert_equal Project.find(1), a.container
298
298
299 mail = ActionMailer::Base.deliveries.last
299 mail = ActionMailer::Base.deliveries.last
300 assert_kind_of TMail::Mail, mail
300 assert_kind_of TMail::Mail, mail
301 assert_equal "[eCookbook] New file", mail.subject
301 assert_equal "[eCookbook] New file", mail.subject
302 assert mail.body.include?('testfile.txt')
302 assert mail.body.include?('testfile.txt')
303 end
303 end
304
304
305 def test_add_file_routing
305 def test_add_file_routing
306 assert_routing(
306 assert_routing(
307 {:method => :get, :path => '/projects/33/files/new'},
307 {:method => :get, :path => '/projects/33/files/new'},
308 :controller => 'projects', :action => 'add_file', :id => '33'
308 :controller => 'projects', :action => 'add_file', :id => '33'
309 )
309 )
310 assert_routing(
310 assert_routing(
311 {:method => :post, :path => '/projects/33/files/new'},
311 {:method => :post, :path => '/projects/33/files/new'},
312 :controller => 'projects', :action => 'add_file', :id => '33'
312 :controller => 'projects', :action => 'add_file', :id => '33'
313 )
313 )
314 end
314 end
315
315
316 def test_add_version_file
316 def test_add_version_file
317 set_tmp_attachments_directory
317 set_tmp_attachments_directory
318 @request.session[:user_id] = 2
318 @request.session[:user_id] = 2
319 Setting.notified_events = ['file_added']
319 Setting.notified_events = ['file_added']
320
320
321 assert_difference 'Attachment.count' do
321 assert_difference 'Attachment.count' do
322 post :add_file, :id => 1, :version_id => '2',
322 post :add_file, :id => 1, :version_id => '2',
323 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
323 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
324 end
324 end
325 assert_redirected_to 'projects/ecookbook/files'
325 assert_redirected_to 'projects/ecookbook/files'
326 a = Attachment.find(:first, :order => 'created_on DESC')
326 a = Attachment.find(:first, :order => 'created_on DESC')
327 assert_equal 'testfile.txt', a.filename
327 assert_equal 'testfile.txt', a.filename
328 assert_equal Version.find(2), a.container
328 assert_equal Version.find(2), a.container
329 end
329 end
330
330
331 def test_list_files
331 def test_list_files
332 get :list_files, :id => 1
332 get :list_files, :id => 1
333 assert_response :success
333 assert_response :success
334 assert_template 'list_files'
334 assert_template 'list_files'
335 assert_not_nil assigns(:containers)
335 assert_not_nil assigns(:containers)
336
336
337 # file attached to the project
337 # file attached to the project
338 assert_tag :a, :content => 'project_file.zip',
338 assert_tag :a, :content => 'project_file.zip',
339 :attributes => { :href => '/attachments/download/8/project_file.zip' }
339 :attributes => { :href => '/attachments/download/8/project_file.zip' }
340
340
341 # file attached to a project's version
341 # file attached to a project's version
342 assert_tag :a, :content => 'version_file.zip',
342 assert_tag :a, :content => 'version_file.zip',
343 :attributes => { :href => '/attachments/download/9/version_file.zip' }
343 :attributes => { :href => '/attachments/download/9/version_file.zip' }
344 end
344 end
345
345
346 def test_list_files_routing
346 def test_list_files_routing
347 assert_routing(
347 assert_routing(
348 {:method => :get, :path => '/projects/33/files'},
348 {:method => :get, :path => '/projects/33/files'},
349 :controller => 'projects', :action => 'list_files', :id => '33'
349 :controller => 'projects', :action => 'list_files', :id => '33'
350 )
350 )
351 end
351 end
352
352
353 def test_changelog_routing
353 def test_changelog_routing
354 assert_routing(
354 assert_routing(
355 {:method => :get, :path => '/projects/44/changelog'},
355 {:method => :get, :path => '/projects/44/changelog'},
356 :controller => 'projects', :action => 'changelog', :id => '44'
356 :controller => 'projects', :action => 'changelog', :id => '44'
357 )
357 )
358 end
358 end
359
359
360 def test_changelog
360 def test_changelog
361 get :changelog, :id => 1
361 get :changelog, :id => 1
362 assert_response :success
362 assert_response :success
363 assert_template 'changelog'
363 assert_template 'changelog'
364 assert_not_nil assigns(:versions)
364 assert_not_nil assigns(:versions)
365 end
365 end
366
366
367 def test_changelog_showing_subprojects_versions
368 get :changelog, :id => 1, :with_subprojects => 1
369 assert_response :success
370 assert_template 'changelog'
371 assert_not_nil assigns(:versions)
372 # Version on subproject appears
373 assert assigns(:versions).include?(Version.find(4))
374 end
375
367 def test_roadmap_routing
376 def test_roadmap_routing
368 assert_routing(
377 assert_routing(
369 {:method => :get, :path => 'projects/33/roadmap'},
378 {:method => :get, :path => 'projects/33/roadmap'},
370 :controller => 'projects', :action => 'roadmap', :id => '33'
379 :controller => 'projects', :action => 'roadmap', :id => '33'
371 )
380 )
372 end
381 end
373
382
374 def test_roadmap
383 def test_roadmap
375 get :roadmap, :id => 1
384 get :roadmap, :id => 1
376 assert_response :success
385 assert_response :success
377 assert_template 'roadmap'
386 assert_template 'roadmap'
378 assert_not_nil assigns(:versions)
387 assert_not_nil assigns(:versions)
379 # Version with no date set appears
388 # Version with no date set appears
380 assert assigns(:versions).include?(Version.find(3))
389 assert assigns(:versions).include?(Version.find(3))
381 # Completed version doesn't appear
390 # Completed version doesn't appear
382 assert !assigns(:versions).include?(Version.find(1))
391 assert !assigns(:versions).include?(Version.find(1))
383 end
392 end
384
393
385 def test_roadmap_with_completed_versions
394 def test_roadmap_with_completed_versions
386 get :roadmap, :id => 1, :completed => 1
395 get :roadmap, :id => 1, :completed => 1
387 assert_response :success
396 assert_response :success
388 assert_template 'roadmap'
397 assert_template 'roadmap'
389 assert_not_nil assigns(:versions)
398 assert_not_nil assigns(:versions)
390 # Version with no date set appears
399 # Version with no date set appears
391 assert assigns(:versions).include?(Version.find(3))
400 assert assigns(:versions).include?(Version.find(3))
392 # Completed version appears
401 # Completed version appears
393 assert assigns(:versions).include?(Version.find(1))
402 assert assigns(:versions).include?(Version.find(1))
394 end
403 end
404
405 def test_roadmap_showing_subprojects_versions
406 get :roadmap, :id => 1, :with_subprojects => 1
407 assert_response :success
408 assert_template 'roadmap'
409 assert_not_nil assigns(:versions)
410 # Version on subproject appears
411 assert assigns(:versions).include?(Version.find(4))
412 end
395
413
396 def test_project_activity_routing
414 def test_project_activity_routing
397 assert_routing(
415 assert_routing(
398 {:method => :get, :path => '/projects/1/activity'},
416 {:method => :get, :path => '/projects/1/activity'},
399 :controller => 'projects', :action => 'activity', :id => '1'
417 :controller => 'projects', :action => 'activity', :id => '1'
400 )
418 )
401 end
419 end
402
420
403 def test_project_activity_atom_routing
421 def test_project_activity_atom_routing
404 assert_routing(
422 assert_routing(
405 {:method => :get, :path => '/projects/1/activity.atom'},
423 {:method => :get, :path => '/projects/1/activity.atom'},
406 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
424 :controller => 'projects', :action => 'activity', :id => '1', :format => 'atom'
407 )
425 )
408 end
426 end
409
427
410 def test_project_activity
428 def test_project_activity
411 get :activity, :id => 1, :with_subprojects => 0
429 get :activity, :id => 1, :with_subprojects => 0
412 assert_response :success
430 assert_response :success
413 assert_template 'activity'
431 assert_template 'activity'
414 assert_not_nil assigns(:events_by_day)
432 assert_not_nil assigns(:events_by_day)
415
433
416 assert_tag :tag => "h3",
434 assert_tag :tag => "h3",
417 :content => /#{2.days.ago.to_date.day}/,
435 :content => /#{2.days.ago.to_date.day}/,
418 :sibling => { :tag => "dl",
436 :sibling => { :tag => "dl",
419 :child => { :tag => "dt",
437 :child => { :tag => "dt",
420 :attributes => { :class => /issue-edit/ },
438 :attributes => { :class => /issue-edit/ },
421 :child => { :tag => "a",
439 :child => { :tag => "a",
422 :content => /(#{IssueStatus.find(2).name})/,
440 :content => /(#{IssueStatus.find(2).name})/,
423 }
441 }
424 }
442 }
425 }
443 }
426 end
444 end
427
445
428 def test_previous_project_activity
446 def test_previous_project_activity
429 get :activity, :id => 1, :from => 3.days.ago.to_date
447 get :activity, :id => 1, :from => 3.days.ago.to_date
430 assert_response :success
448 assert_response :success
431 assert_template 'activity'
449 assert_template 'activity'
432 assert_not_nil assigns(:events_by_day)
450 assert_not_nil assigns(:events_by_day)
433
451
434 assert_tag :tag => "h3",
452 assert_tag :tag => "h3",
435 :content => /#{3.day.ago.to_date.day}/,
453 :content => /#{3.day.ago.to_date.day}/,
436 :sibling => { :tag => "dl",
454 :sibling => { :tag => "dl",
437 :child => { :tag => "dt",
455 :child => { :tag => "dt",
438 :attributes => { :class => /issue/ },
456 :attributes => { :class => /issue/ },
439 :child => { :tag => "a",
457 :child => { :tag => "a",
440 :content => /#{Issue.find(1).subject}/,
458 :content => /#{Issue.find(1).subject}/,
441 }
459 }
442 }
460 }
443 }
461 }
444 end
462 end
445
463
446 def test_global_activity_routing
464 def test_global_activity_routing
447 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
465 assert_routing({:method => :get, :path => '/activity'}, :controller => 'projects', :action => 'activity', :id => nil)
448 end
466 end
449
467
450 def test_global_activity
468 def test_global_activity
451 get :activity
469 get :activity
452 assert_response :success
470 assert_response :success
453 assert_template 'activity'
471 assert_template 'activity'
454 assert_not_nil assigns(:events_by_day)
472 assert_not_nil assigns(:events_by_day)
455
473
456 assert_tag :tag => "h3",
474 assert_tag :tag => "h3",
457 :content => /#{5.day.ago.to_date.day}/,
475 :content => /#{5.day.ago.to_date.day}/,
458 :sibling => { :tag => "dl",
476 :sibling => { :tag => "dl",
459 :child => { :tag => "dt",
477 :child => { :tag => "dt",
460 :attributes => { :class => /issue/ },
478 :attributes => { :class => /issue/ },
461 :child => { :tag => "a",
479 :child => { :tag => "a",
462 :content => /#{Issue.find(5).subject}/,
480 :content => /#{Issue.find(5).subject}/,
463 }
481 }
464 }
482 }
465 }
483 }
466 end
484 end
467
485
468 def test_user_activity
486 def test_user_activity
469 get :activity, :user_id => 2
487 get :activity, :user_id => 2
470 assert_response :success
488 assert_response :success
471 assert_template 'activity'
489 assert_template 'activity'
472 assert_not_nil assigns(:events_by_day)
490 assert_not_nil assigns(:events_by_day)
473
491
474 assert_tag :tag => "h3",
492 assert_tag :tag => "h3",
475 :content => /#{3.day.ago.to_date.day}/,
493 :content => /#{3.day.ago.to_date.day}/,
476 :sibling => { :tag => "dl",
494 :sibling => { :tag => "dl",
477 :child => { :tag => "dt",
495 :child => { :tag => "dt",
478 :attributes => { :class => /issue/ },
496 :attributes => { :class => /issue/ },
479 :child => { :tag => "a",
497 :child => { :tag => "a",
480 :content => /#{Issue.find(1).subject}/,
498 :content => /#{Issue.find(1).subject}/,
481 }
499 }
482 }
500 }
483 }
501 }
484 end
502 end
485
503
486 def test_global_activity_atom_routing
504 def test_global_activity_atom_routing
487 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
505 assert_routing({:method => :get, :path => '/activity.atom'}, :controller => 'projects', :action => 'activity', :id => nil, :format => 'atom')
488 end
506 end
489
507
490 def test_activity_atom_feed
508 def test_activity_atom_feed
491 get :activity, :format => 'atom'
509 get :activity, :format => 'atom'
492 assert_response :success
510 assert_response :success
493 assert_template 'common/feed.atom.rxml'
511 assert_template 'common/feed.atom.rxml'
494 end
512 end
495
513
496 def test_archive_routing
514 def test_archive_routing
497 assert_routing(
515 assert_routing(
498 #TODO: use PUT to project path and modify form
516 #TODO: use PUT to project path and modify form
499 {:method => :post, :path => 'projects/64/archive'},
517 {:method => :post, :path => 'projects/64/archive'},
500 :controller => 'projects', :action => 'archive', :id => '64'
518 :controller => 'projects', :action => 'archive', :id => '64'
501 )
519 )
502 end
520 end
503
521
504 def test_archive
522 def test_archive
505 @request.session[:user_id] = 1 # admin
523 @request.session[:user_id] = 1 # admin
506 post :archive, :id => 1
524 post :archive, :id => 1
507 assert_redirected_to 'admin/projects'
525 assert_redirected_to 'admin/projects'
508 assert !Project.find(1).active?
526 assert !Project.find(1).active?
509 end
527 end
510
528
511 def test_unarchive_routing
529 def test_unarchive_routing
512 assert_routing(
530 assert_routing(
513 #TODO: use PUT to project path and modify form
531 #TODO: use PUT to project path and modify form
514 {:method => :post, :path => '/projects/567/unarchive'},
532 {:method => :post, :path => '/projects/567/unarchive'},
515 :controller => 'projects', :action => 'unarchive', :id => '567'
533 :controller => 'projects', :action => 'unarchive', :id => '567'
516 )
534 )
517 end
535 end
518
536
519 def test_unarchive
537 def test_unarchive
520 @request.session[:user_id] = 1 # admin
538 @request.session[:user_id] = 1 # admin
521 Project.find(1).archive
539 Project.find(1).archive
522 post :unarchive, :id => 1
540 post :unarchive, :id => 1
523 assert_redirected_to 'admin/projects'
541 assert_redirected_to 'admin/projects'
524 assert Project.find(1).active?
542 assert Project.find(1).active?
525 end
543 end
526
544
527 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
545 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
528 CustomField.delete_all
546 CustomField.delete_all
529 parent = nil
547 parent = nil
530 6.times do |i|
548 6.times do |i|
531 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
549 p = Project.create!(:name => "Breadcrumbs #{i}", :identifier => "breadcrumbs-#{i}")
532 p.set_parent!(parent)
550 p.set_parent!(parent)
533 get :show, :id => p
551 get :show, :id => p
534 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
552 assert_tag :h1, :parent => { :attributes => {:id => 'header'}},
535 :children => { :count => [i, 3].min,
553 :children => { :count => [i, 3].min,
536 :only => { :tag => 'a' } }
554 :only => { :tag => 'a' } }
537
555
538 parent = p
556 parent = p
539 end
557 end
540 end
558 end
541
559
542 def test_copy_with_project
560 def test_copy_with_project
543 @request.session[:user_id] = 1 # admin
561 @request.session[:user_id] = 1 # admin
544 get :copy, :id => 1
562 get :copy, :id => 1
545 assert_response :success
563 assert_response :success
546 assert_template 'copy'
564 assert_template 'copy'
547 assert assigns(:project)
565 assert assigns(:project)
548 assert_equal Project.find(1).description, assigns(:project).description
566 assert_equal Project.find(1).description, assigns(:project).description
549 assert_nil assigns(:project).id
567 assert_nil assigns(:project).id
550 end
568 end
551
569
552 def test_copy_without_project
570 def test_copy_without_project
553 @request.session[:user_id] = 1 # admin
571 @request.session[:user_id] = 1 # admin
554 get :copy
572 get :copy
555 assert_response :redirect
573 assert_response :redirect
556 assert_redirected_to :controller => 'admin', :action => 'projects'
574 assert_redirected_to :controller => 'admin', :action => 'projects'
557 end
575 end
558
576
559 def test_jump_should_redirect_to_active_tab
577 def test_jump_should_redirect_to_active_tab
560 get :show, :id => 1, :jump => 'issues'
578 get :show, :id => 1, :jump => 'issues'
561 assert_redirected_to 'projects/ecookbook/issues'
579 assert_redirected_to 'projects/ecookbook/issues'
562 end
580 end
563
581
564 def test_jump_should_not_redirect_to_inactive_tab
582 def test_jump_should_not_redirect_to_inactive_tab
565 get :show, :id => 3, :jump => 'documents'
583 get :show, :id => 3, :jump => 'documents'
566 assert_response :success
584 assert_response :success
567 assert_template 'show'
585 assert_template 'show'
568 end
586 end
569
587
570 def test_jump_should_not_redirect_to_unknown_tab
588 def test_jump_should_not_redirect_to_unknown_tab
571 get :show, :id => 3, :jump => 'foobar'
589 get :show, :id => 3, :jump => 'foobar'
572 assert_response :success
590 assert_response :success
573 assert_template 'show'
591 assert_template 'show'
574 end
592 end
575
593
576 def test_reset_activities_routing
594 def test_reset_activities_routing
577 assert_routing({:method => :delete, :path => 'projects/64/reset_activities'},
595 assert_routing({:method => :delete, :path => 'projects/64/reset_activities'},
578 :controller => 'projects', :action => 'reset_activities', :id => '64')
596 :controller => 'projects', :action => 'reset_activities', :id => '64')
579 end
597 end
580
598
581 def test_reset_activities
599 def test_reset_activities
582 @request.session[:user_id] = 2 # manager
600 @request.session[:user_id] = 2 # manager
583 project_activity = TimeEntryActivity.new({
601 project_activity = TimeEntryActivity.new({
584 :name => 'Project Specific',
602 :name => 'Project Specific',
585 :parent => TimeEntryActivity.find(:first),
603 :parent => TimeEntryActivity.find(:first),
586 :project => Project.find(1),
604 :project => Project.find(1),
587 :active => true
605 :active => true
588 })
606 })
589 assert project_activity.save
607 assert project_activity.save
590 project_activity_two = TimeEntryActivity.new({
608 project_activity_two = TimeEntryActivity.new({
591 :name => 'Project Specific Two',
609 :name => 'Project Specific Two',
592 :parent => TimeEntryActivity.find(:last),
610 :parent => TimeEntryActivity.find(:last),
593 :project => Project.find(1),
611 :project => Project.find(1),
594 :active => true
612 :active => true
595 })
613 })
596 assert project_activity_two.save
614 assert project_activity_two.save
597
615
598 delete :reset_activities, :id => 1
616 delete :reset_activities, :id => 1
599 assert_response :redirect
617 assert_response :redirect
600 assert_redirected_to 'projects/ecookbook/settings/activities'
618 assert_redirected_to 'projects/ecookbook/settings/activities'
601
619
602 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
620 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
603 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
621 assert_nil TimeEntryActivity.find_by_id(project_activity_two.id)
604 end
622 end
605
623
606 def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
624 def test_reset_activities_should_reassign_time_entries_back_to_the_system_activity
607 @request.session[:user_id] = 2 # manager
625 @request.session[:user_id] = 2 # manager
608 project_activity = TimeEntryActivity.new({
626 project_activity = TimeEntryActivity.new({
609 :name => 'Project Specific Design',
627 :name => 'Project Specific Design',
610 :parent => TimeEntryActivity.find(9),
628 :parent => TimeEntryActivity.find(9),
611 :project => Project.find(1),
629 :project => Project.find(1),
612 :active => true
630 :active => true
613 })
631 })
614 assert project_activity.save
632 assert project_activity.save
615 assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
633 assert TimeEntry.update_all("activity_id = '#{project_activity.id}'", ["project_id = ? AND activity_id = ?", 1, 9])
616 assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
634 assert 3, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size
617
635
618 delete :reset_activities, :id => 1
636 delete :reset_activities, :id => 1
619 assert_response :redirect
637 assert_response :redirect
620 assert_redirected_to 'projects/ecookbook/settings/activities'
638 assert_redirected_to 'projects/ecookbook/settings/activities'
621
639
622 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
640 assert_nil TimeEntryActivity.find_by_id(project_activity.id)
623 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
641 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(project_activity.id, 1).size, "TimeEntries still assigned to project specific activity"
624 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
642 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "TimeEntries still assigned to project specific activity"
625 end
643 end
626
644
627 def test_save_activities_routing
645 def test_save_activities_routing
628 assert_routing({:method => :post, :path => 'projects/64/activities/save'},
646 assert_routing({:method => :post, :path => 'projects/64/activities/save'},
629 :controller => 'projects', :action => 'save_activities', :id => '64')
647 :controller => 'projects', :action => 'save_activities', :id => '64')
630 end
648 end
631
649
632 def test_save_activities_to_override_system_activities
650 def test_save_activities_to_override_system_activities
633 @request.session[:user_id] = 2 # manager
651 @request.session[:user_id] = 2 # manager
634 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
652 billable_field = TimeEntryActivityCustomField.find_by_name("Billable")
635
653
636 post :save_activities, :id => 1, :enumerations => {
654 post :save_activities, :id => 1, :enumerations => {
637 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
655 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design, De-activate
638 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
656 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"}, # Development, Change custom value
639 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
657 "14"=>{"parent_id"=>"14", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"}, # Inactive Activity, Activate with custom value
640 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
658 "11"=>{"parent_id"=>"11", "custom_field_values"=>{"7"=>"1"}, "active"=>"1"} # QA, no changes
641 }
659 }
642
660
643 assert_response :redirect
661 assert_response :redirect
644 assert_redirected_to 'projects/ecookbook/settings/activities'
662 assert_redirected_to 'projects/ecookbook/settings/activities'
645
663
646 # Created project specific activities...
664 # Created project specific activities...
647 project = Project.find('ecookbook')
665 project = Project.find('ecookbook')
648
666
649 # ... Design
667 # ... Design
650 design = project.time_entry_activities.find_by_name("Design")
668 design = project.time_entry_activities.find_by_name("Design")
651 assert design, "Project activity not found"
669 assert design, "Project activity not found"
652
670
653 assert_equal 9, design.parent_id # Relate to the system activity
671 assert_equal 9, design.parent_id # Relate to the system activity
654 assert_not_equal design.parent.id, design.id # Different records
672 assert_not_equal design.parent.id, design.id # Different records
655 assert_equal design.parent.name, design.name # Same name
673 assert_equal design.parent.name, design.name # Same name
656 assert !design.active?
674 assert !design.active?
657
675
658 # ... Development
676 # ... Development
659 development = project.time_entry_activities.find_by_name("Development")
677 development = project.time_entry_activities.find_by_name("Development")
660 assert development, "Project activity not found"
678 assert development, "Project activity not found"
661
679
662 assert_equal 10, development.parent_id # Relate to the system activity
680 assert_equal 10, development.parent_id # Relate to the system activity
663 assert_not_equal development.parent.id, development.id # Different records
681 assert_not_equal development.parent.id, development.id # Different records
664 assert_equal development.parent.name, development.name # Same name
682 assert_equal development.parent.name, development.name # Same name
665 assert development.active?
683 assert development.active?
666 assert_equal "0", development.custom_value_for(billable_field).value
684 assert_equal "0", development.custom_value_for(billable_field).value
667
685
668 # ... Inactive Activity
686 # ... Inactive Activity
669 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
687 previously_inactive = project.time_entry_activities.find_by_name("Inactive Activity")
670 assert previously_inactive, "Project activity not found"
688 assert previously_inactive, "Project activity not found"
671
689
672 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
690 assert_equal 14, previously_inactive.parent_id # Relate to the system activity
673 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
691 assert_not_equal previously_inactive.parent.id, previously_inactive.id # Different records
674 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
692 assert_equal previously_inactive.parent.name, previously_inactive.name # Same name
675 assert previously_inactive.active?
693 assert previously_inactive.active?
676 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
694 assert_equal "1", previously_inactive.custom_value_for(billable_field).value
677
695
678 # ... QA
696 # ... QA
679 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
697 assert_equal nil, project.time_entry_activities.find_by_name("QA"), "Custom QA activity created when it wasn't modified"
680 end
698 end
681
699
682 def test_save_activities_will_update_project_specific_activities
700 def test_save_activities_will_update_project_specific_activities
683 @request.session[:user_id] = 2 # manager
701 @request.session[:user_id] = 2 # manager
684
702
685 project_activity = TimeEntryActivity.new({
703 project_activity = TimeEntryActivity.new({
686 :name => 'Project Specific',
704 :name => 'Project Specific',
687 :parent => TimeEntryActivity.find(:first),
705 :parent => TimeEntryActivity.find(:first),
688 :project => Project.find(1),
706 :project => Project.find(1),
689 :active => true
707 :active => true
690 })
708 })
691 assert project_activity.save
709 assert project_activity.save
692 project_activity_two = TimeEntryActivity.new({
710 project_activity_two = TimeEntryActivity.new({
693 :name => 'Project Specific Two',
711 :name => 'Project Specific Two',
694 :parent => TimeEntryActivity.find(:last),
712 :parent => TimeEntryActivity.find(:last),
695 :project => Project.find(1),
713 :project => Project.find(1),
696 :active => true
714 :active => true
697 })
715 })
698 assert project_activity_two.save
716 assert project_activity_two.save
699
717
700
718
701 post :save_activities, :id => 1, :enumerations => {
719 post :save_activities, :id => 1, :enumerations => {
702 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
720 project_activity.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # De-activate
703 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
721 project_activity_two.id => {"custom_field_values"=>{"7" => "1"}, "active"=>"0"} # De-activate
704 }
722 }
705
723
706 assert_response :redirect
724 assert_response :redirect
707 assert_redirected_to 'projects/ecookbook/settings/activities'
725 assert_redirected_to 'projects/ecookbook/settings/activities'
708
726
709 # Created project specific activities...
727 # Created project specific activities...
710 project = Project.find('ecookbook')
728 project = Project.find('ecookbook')
711 assert_equal 2, project.time_entry_activities.count
729 assert_equal 2, project.time_entry_activities.count
712
730
713 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
731 activity_one = project.time_entry_activities.find_by_name(project_activity.name)
714 assert activity_one, "Project activity not found"
732 assert activity_one, "Project activity not found"
715 assert_equal project_activity.id, activity_one.id
733 assert_equal project_activity.id, activity_one.id
716 assert !activity_one.active?
734 assert !activity_one.active?
717
735
718 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
736 activity_two = project.time_entry_activities.find_by_name(project_activity_two.name)
719 assert activity_two, "Project activity not found"
737 assert activity_two, "Project activity not found"
720 assert_equal project_activity_two.id, activity_two.id
738 assert_equal project_activity_two.id, activity_two.id
721 assert !activity_two.active?
739 assert !activity_two.active?
722 end
740 end
723
741
724 def test_save_activities_when_creating_new_activities_will_convert_existing_data
742 def test_save_activities_when_creating_new_activities_will_convert_existing_data
725 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
743 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
726
744
727 @request.session[:user_id] = 2 # manager
745 @request.session[:user_id] = 2 # manager
728 post :save_activities, :id => 1, :enumerations => {
746 post :save_activities, :id => 1, :enumerations => {
729 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
747 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"} # Design, De-activate
730 }
748 }
731 assert_response :redirect
749 assert_response :redirect
732
750
733 # No more TimeEntries using the system activity
751 # No more TimeEntries using the system activity
734 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
752 assert_equal 0, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries still assigned to system activities"
735 # All TimeEntries using project activity
753 # All TimeEntries using project activity
736 project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
754 project_specific_activity = TimeEntryActivity.find_by_parent_id_and_project_id(9, 1)
737 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
755 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(project_specific_activity.id, 1).size, "No Time Entries assigned to the project activity"
738 end
756 end
739
757
740 def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
758 def test_save_activities_when_creating_new_activities_will_not_convert_existing_data_if_an_exception_is_raised
741 # TODO: Need to cause an exception on create but these tests
759 # TODO: Need to cause an exception on create but these tests
742 # aren't setup for mocking. Just create a record now so the
760 # aren't setup for mocking. Just create a record now so the
743 # second one is a dupicate
761 # second one is a dupicate
744 parent = TimeEntryActivity.find(9)
762 parent = TimeEntryActivity.find(9)
745 TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
763 TimeEntryActivity.create!({:name => parent.name, :project_id => 1, :position => parent.position, :active => true})
746 TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
764 TimeEntry.create!({:project_id => 1, :hours => 1.0, :user => User.find(1), :issue_id => 3, :activity_id => 10, :spent_on => '2009-01-01'})
747
765
748 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
766 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size
749 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
767 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size
750
768
751 @request.session[:user_id] = 2 # manager
769 @request.session[:user_id] = 2 # manager
752 post :save_activities, :id => 1, :enumerations => {
770 post :save_activities, :id => 1, :enumerations => {
753 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
771 "9"=> {"parent_id"=>"9", "custom_field_values"=>{"7" => "1"}, "active"=>"0"}, # Design
754 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
772 "10"=> {"parent_id"=>"10", "custom_field_values"=>{"7"=>"0"}, "active"=>"1"} # Development, Change custom value
755 }
773 }
756 assert_response :redirect
774 assert_response :redirect
757
775
758 # TimeEntries shouldn't have been reassigned on the failed record
776 # TimeEntries shouldn't have been reassigned on the failed record
759 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
777 assert_equal 3, TimeEntry.find_all_by_activity_id_and_project_id(9, 1).size, "Time Entries are not assigned to system activities"
760 # TimeEntries shouldn't have been reassigned on the saved record either
778 # TimeEntries shouldn't have been reassigned on the saved record either
761 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
779 assert_equal 1, TimeEntry.find_all_by_activity_id_and_project_id(10, 1).size, "Time Entries are not assigned to system activities"
762 end
780 end
763
781
764 # A hook that is manually registered later
782 # A hook that is manually registered later
765 class ProjectBasedTemplate < Redmine::Hook::ViewListener
783 class ProjectBasedTemplate < Redmine::Hook::ViewListener
766 def view_layouts_base_html_head(context)
784 def view_layouts_base_html_head(context)
767 # Adds a project stylesheet
785 # Adds a project stylesheet
768 stylesheet_link_tag(context[:project].identifier) if context[:project]
786 stylesheet_link_tag(context[:project].identifier) if context[:project]
769 end
787 end
770 end
788 end
771 # Don't use this hook now
789 # Don't use this hook now
772 Redmine::Hook.clear_listeners
790 Redmine::Hook.clear_listeners
773
791
774 def test_hook_response
792 def test_hook_response
775 Redmine::Hook.add_listener(ProjectBasedTemplate)
793 Redmine::Hook.add_listener(ProjectBasedTemplate)
776 get :show, :id => 1
794 get :show, :id => 1
777 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
795 assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'},
778 :parent => {:tag => 'head'}
796 :parent => {:tag => 'head'}
779
797
780 Redmine::Hook.clear_listeners
798 Redmine::Hook.clear_listeners
781 end
799 end
782 end
800 end
@@ -1,206 +1,206
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "#{File.dirname(__FILE__)}/../test_helper"
18 require "#{File.dirname(__FILE__)}/../test_helper"
19
19
20 begin
20 begin
21 require 'mocha'
21 require 'mocha'
22 rescue
22 rescue
23 # Won't run some tests
23 # Won't run some tests
24 end
24 end
25
25
26 class AccountTest < ActionController::IntegrationTest
26 class AccountTest < ActionController::IntegrationTest
27 fixtures :users
27 fixtures :users, :roles
28
28
29 # Replace this with your real tests.
29 # Replace this with your real tests.
30 def test_login
30 def test_login
31 get "my/page"
31 get "my/page"
32 assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
32 assert_redirected_to "/login?back_url=http%3A%2F%2Fwww.example.com%2Fmy%2Fpage"
33 log_user('jsmith', 'jsmith')
33 log_user('jsmith', 'jsmith')
34
34
35 get "my/account"
35 get "my/account"
36 assert_response :success
36 assert_response :success
37 assert_template "my/account"
37 assert_template "my/account"
38 end
38 end
39
39
40 def test_autologin
40 def test_autologin
41 user = User.find(1)
41 user = User.find(1)
42 Setting.autologin = "7"
42 Setting.autologin = "7"
43 Token.delete_all
43 Token.delete_all
44
44
45 # User logs in with 'autologin' checked
45 # User logs in with 'autologin' checked
46 post '/login', :username => user.login, :password => 'admin', :autologin => 1
46 post '/login', :username => user.login, :password => 'admin', :autologin => 1
47 assert_redirected_to 'my/page'
47 assert_redirected_to 'my/page'
48 token = Token.find :first
48 token = Token.find :first
49 assert_not_nil token
49 assert_not_nil token
50 assert_equal user, token.user
50 assert_equal user, token.user
51 assert_equal 'autologin', token.action
51 assert_equal 'autologin', token.action
52 assert_equal user.id, session[:user_id]
52 assert_equal user.id, session[:user_id]
53 assert_equal token.value, cookies['autologin']
53 assert_equal token.value, cookies['autologin']
54
54
55 # Session is cleared
55 # Session is cleared
56 reset!
56 reset!
57 User.current = nil
57 User.current = nil
58 # Clears user's last login timestamp
58 # Clears user's last login timestamp
59 user.update_attribute :last_login_on, nil
59 user.update_attribute :last_login_on, nil
60 assert_nil user.reload.last_login_on
60 assert_nil user.reload.last_login_on
61
61
62 # User comes back with his autologin cookie
62 # User comes back with his autologin cookie
63 cookies[:autologin] = token.value
63 cookies[:autologin] = token.value
64 get '/my/page'
64 get '/my/page'
65 assert_response :success
65 assert_response :success
66 assert_template 'my/page'
66 assert_template 'my/page'
67 assert_equal user.id, session[:user_id]
67 assert_equal user.id, session[:user_id]
68 assert_not_nil user.reload.last_login_on
68 assert_not_nil user.reload.last_login_on
69 assert user.last_login_on.utc > 10.second.ago.utc
69 assert user.last_login_on.utc > 10.second.ago.utc
70 end
70 end
71
71
72 def test_lost_password
72 def test_lost_password
73 Token.delete_all
73 Token.delete_all
74
74
75 get "account/lost_password"
75 get "account/lost_password"
76 assert_response :success
76 assert_response :success
77 assert_template "account/lost_password"
77 assert_template "account/lost_password"
78
78
79 post "account/lost_password", :mail => 'jSmith@somenet.foo'
79 post "account/lost_password", :mail => 'jSmith@somenet.foo'
80 assert_redirected_to "/login"
80 assert_redirected_to "/login"
81
81
82 token = Token.find(:first)
82 token = Token.find(:first)
83 assert_equal 'recovery', token.action
83 assert_equal 'recovery', token.action
84 assert_equal 'jsmith@somenet.foo', token.user.mail
84 assert_equal 'jsmith@somenet.foo', token.user.mail
85 assert !token.expired?
85 assert !token.expired?
86
86
87 get "account/lost_password", :token => token.value
87 get "account/lost_password", :token => token.value
88 assert_response :success
88 assert_response :success
89 assert_template "account/password_recovery"
89 assert_template "account/password_recovery"
90
90
91 post "account/lost_password", :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'newpass'
91 post "account/lost_password", :token => token.value, :new_password => 'newpass', :new_password_confirmation => 'newpass'
92 assert_redirected_to "/login"
92 assert_redirected_to "/login"
93 assert_equal 'Password was successfully updated.', flash[:notice]
93 assert_equal 'Password was successfully updated.', flash[:notice]
94
94
95 log_user('jsmith', 'newpass')
95 log_user('jsmith', 'newpass')
96 assert_equal 0, Token.count
96 assert_equal 0, Token.count
97 end
97 end
98
98
99 def test_register_with_automatic_activation
99 def test_register_with_automatic_activation
100 Setting.self_registration = '3'
100 Setting.self_registration = '3'
101
101
102 get 'account/register'
102 get 'account/register'
103 assert_response :success
103 assert_response :success
104 assert_template 'account/register'
104 assert_template 'account/register'
105
105
106 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
106 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
107 :password => "newpass", :password_confirmation => "newpass"
107 :password => "newpass", :password_confirmation => "newpass"
108 assert_redirected_to 'my/account'
108 assert_redirected_to 'my/account'
109 follow_redirect!
109 follow_redirect!
110 assert_response :success
110 assert_response :success
111 assert_template 'my/account'
111 assert_template 'my/account'
112
112
113 user = User.find_by_login('newuser')
113 user = User.find_by_login('newuser')
114 assert_not_nil user
114 assert_not_nil user
115 assert user.active?
115 assert user.active?
116 assert_not_nil user.last_login_on
116 assert_not_nil user.last_login_on
117 end
117 end
118
118
119 def test_register_with_manual_activation
119 def test_register_with_manual_activation
120 Setting.self_registration = '2'
120 Setting.self_registration = '2'
121
121
122 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
122 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
123 :password => "newpass", :password_confirmation => "newpass"
123 :password => "newpass", :password_confirmation => "newpass"
124 assert_redirected_to '/login'
124 assert_redirected_to '/login'
125 assert !User.find_by_login('newuser').active?
125 assert !User.find_by_login('newuser').active?
126 end
126 end
127
127
128 def test_register_with_email_activation
128 def test_register_with_email_activation
129 Setting.self_registration = '1'
129 Setting.self_registration = '1'
130 Token.delete_all
130 Token.delete_all
131
131
132 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
132 post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
133 :password => "newpass", :password_confirmation => "newpass"
133 :password => "newpass", :password_confirmation => "newpass"
134 assert_redirected_to '/login'
134 assert_redirected_to '/login'
135 assert !User.find_by_login('newuser').active?
135 assert !User.find_by_login('newuser').active?
136
136
137 token = Token.find(:first)
137 token = Token.find(:first)
138 assert_equal 'register', token.action
138 assert_equal 'register', token.action
139 assert_equal 'newuser@foo.bar', token.user.mail
139 assert_equal 'newuser@foo.bar', token.user.mail
140 assert !token.expired?
140 assert !token.expired?
141
141
142 get 'account/activate', :token => token.value
142 get 'account/activate', :token => token.value
143 assert_redirected_to '/login'
143 assert_redirected_to '/login'
144 log_user('newuser', 'newpass')
144 log_user('newuser', 'newpass')
145 end
145 end
146
146
147 if Object.const_defined?(:Mocha)
147 if Object.const_defined?(:Mocha)
148
148
149 def test_onthefly_registration
149 def test_onthefly_registration
150 # disable registration
150 # disable registration
151 Setting.self_registration = '0'
151 Setting.self_registration = '0'
152 AuthSource.expects(:authenticate).returns([:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66])
152 AuthSource.expects(:authenticate).returns([:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66])
153
153
154 post 'account/login', :username => 'foo', :password => 'bar'
154 post 'account/login', :username => 'foo', :password => 'bar'
155 assert_redirected_to 'my/page'
155 assert_redirected_to 'my/page'
156
156
157 user = User.find_by_login('foo')
157 user = User.find_by_login('foo')
158 assert user.is_a?(User)
158 assert user.is_a?(User)
159 assert_equal 66, user.auth_source_id
159 assert_equal 66, user.auth_source_id
160 assert user.hashed_password.blank?
160 assert user.hashed_password.blank?
161 end
161 end
162
162
163 def test_onthefly_registration_with_invalid_attributes
163 def test_onthefly_registration_with_invalid_attributes
164 # disable registration
164 # disable registration
165 Setting.self_registration = '0'
165 Setting.self_registration = '0'
166 AuthSource.expects(:authenticate).returns([:login => 'foo', :lastname => 'Smith', :auth_source_id => 66])
166 AuthSource.expects(:authenticate).returns([:login => 'foo', :lastname => 'Smith', :auth_source_id => 66])
167
167
168 post 'account/login', :username => 'foo', :password => 'bar'
168 post 'account/login', :username => 'foo', :password => 'bar'
169 assert_response :success
169 assert_response :success
170 assert_template 'account/register'
170 assert_template 'account/register'
171 assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
171 assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
172 assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
172 assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
173 assert_no_tag :input, :attributes => { :name => 'user[login]' }
173 assert_no_tag :input, :attributes => { :name => 'user[login]' }
174 assert_no_tag :input, :attributes => { :name => 'user[password]' }
174 assert_no_tag :input, :attributes => { :name => 'user[password]' }
175
175
176 post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
176 post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
177 assert_redirected_to '/my/account'
177 assert_redirected_to '/my/account'
178
178
179 user = User.find_by_login('foo')
179 user = User.find_by_login('foo')
180 assert user.is_a?(User)
180 assert user.is_a?(User)
181 assert_equal 66, user.auth_source_id
181 assert_equal 66, user.auth_source_id
182 assert user.hashed_password.blank?
182 assert user.hashed_password.blank?
183 end
183 end
184
184
185 def test_login_and_logout_should_clear_session
185 def test_login_and_logout_should_clear_session
186 get '/login'
186 get '/login'
187 sid = session[:session_id]
187 sid = session[:session_id]
188
188
189 post '/login', :username => 'admin', :password => 'admin'
189 post '/login', :username => 'admin', :password => 'admin'
190 assert_redirected_to 'my/page'
190 assert_redirected_to 'my/page'
191 assert_not_equal sid, session[:session_id], "login should reset session"
191 assert_not_equal sid, session[:session_id], "login should reset session"
192 assert_equal 1, session[:user_id]
192 assert_equal 1, session[:user_id]
193 sid = session[:session_id]
193 sid = session[:session_id]
194
194
195 get '/'
195 get '/'
196 assert_equal sid, session[:session_id]
196 assert_equal sid, session[:session_id]
197
197
198 get '/logout'
198 get '/logout'
199 assert_not_equal sid, session[:session_id], "logout should reset session"
199 assert_not_equal sid, session[:session_id], "logout should reset session"
200 assert_nil session[:user_id]
200 assert_nil session[:user_id]
201 end
201 end
202
202
203 else
203 else
204 puts 'Mocha is missing. Skipping tests.'
204 puts 'Mocha is missing. Skipping tests.'
205 end
205 end
206 end
206 end
@@ -1,77 +1,78
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 ENV["RAILS_ENV"] ||= "test"
18 ENV["RAILS_ENV"] ||= "test"
19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 require 'test_help'
20 require 'test_help'
21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
22 require File.join(RAILS_ROOT,'test', 'mocks', 'open_id_authentication_mock.rb')
22 require File.join(RAILS_ROOT,'test', 'mocks', 'open_id_authentication_mock.rb')
23
23
24 require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
24 require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
25 include ObjectDaddyHelpers
25 include ObjectDaddyHelpers
26
26
27 class ActiveSupport::TestCase
27 class ActiveSupport::TestCase
28 # Transactional fixtures accelerate your tests by wrapping each test method
28 # Transactional fixtures accelerate your tests by wrapping each test method
29 # in a transaction that's rolled back on completion. This ensures that the
29 # in a transaction that's rolled back on completion. This ensures that the
30 # test database remains unchanged so your fixtures don't have to be reloaded
30 # test database remains unchanged so your fixtures don't have to be reloaded
31 # between every test method. Fewer database queries means faster tests.
31 # between every test method. Fewer database queries means faster tests.
32 #
32 #
33 # Read Mike Clark's excellent walkthrough at
33 # Read Mike Clark's excellent walkthrough at
34 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
34 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
35 #
35 #
36 # Every Active Record database supports transactions except MyISAM tables
36 # Every Active Record database supports transactions except MyISAM tables
37 # in MySQL. Turn off transactional fixtures in this case; however, if you
37 # in MySQL. Turn off transactional fixtures in this case; however, if you
38 # don't care one way or the other, switching from MyISAM to InnoDB tables
38 # don't care one way or the other, switching from MyISAM to InnoDB tables
39 # is recommended.
39 # is recommended.
40 self.use_transactional_fixtures = true
40 self.use_transactional_fixtures = true
41
41
42 # Instantiated fixtures are slow, but give you @david where otherwise you
42 # Instantiated fixtures are slow, but give you @david where otherwise you
43 # would need people(:david). If you don't want to migrate your existing
43 # would need people(:david). If you don't want to migrate your existing
44 # test cases which use the @david style and don't mind the speed hit (each
44 # test cases which use the @david style and don't mind the speed hit (each
45 # instantiated fixtures translates to a database query per test method),
45 # instantiated fixtures translates to a database query per test method),
46 # then set this back to true.
46 # then set this back to true.
47 self.use_instantiated_fixtures = false
47 self.use_instantiated_fixtures = false
48
48
49 # Add more helper methods to be used by all tests here...
49 # Add more helper methods to be used by all tests here...
50
50
51 def log_user(login, password)
51 def log_user(login, password)
52 User.anonymous
52 get "/login"
53 get "/login"
53 assert_equal nil, session[:user_id]
54 assert_equal nil, session[:user_id]
54 assert_response :success
55 assert_response :success
55 assert_template "account/login"
56 assert_template "account/login"
56 post "/login", :username => login, :password => password
57 post "/login", :username => login, :password => password
57 assert_equal login, User.find(session[:user_id]).login
58 assert_equal login, User.find(session[:user_id]).login
58 end
59 end
59
60
60 def uploaded_test_file(name, mime)
61 def uploaded_test_file(name, mime)
61 ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime)
62 ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime)
62 end
63 end
63
64
64 # Use a temporary directory for attachment related tests
65 # Use a temporary directory for attachment related tests
65 def set_tmp_attachments_directory
66 def set_tmp_attachments_directory
66 Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test")
67 Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test")
67 Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
68 Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
68 Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
69 Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
69 end
70 end
70
71
71 def with_settings(options, &block)
72 def with_settings(options, &block)
72 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
73 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
73 options.each {|k, v| Setting[k] = v}
74 options.each {|k, v| Setting[k] = v}
74 yield
75 yield
75 saved_settings.each {|k, v| Setting[k] = v}
76 saved_settings.each {|k, v| Setting[k] = v}
76 end
77 end
77 end
78 end
@@ -1,111 +1,111
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class EnumerationTest < ActiveSupport::TestCase
20 class EnumerationTest < ActiveSupport::TestCase
21 fixtures :enumerations, :issues, :custom_fields, :custom_values
21 fixtures :enumerations, :issues, :custom_fields, :custom_values
22
22
23 def setup
23 def setup
24 end
24 end
25
25
26 def test_objects_count
26 def test_objects_count
27 # low priority
27 # low priority
28 assert_equal 5, Enumeration.find(4).objects_count
28 assert_equal 6, Enumeration.find(4).objects_count
29 # urgent
29 # urgent
30 assert_equal 0, Enumeration.find(7).objects_count
30 assert_equal 0, Enumeration.find(7).objects_count
31 end
31 end
32
32
33 def test_in_use
33 def test_in_use
34 # low priority
34 # low priority
35 assert Enumeration.find(4).in_use?
35 assert Enumeration.find(4).in_use?
36 # urgent
36 # urgent
37 assert !Enumeration.find(7).in_use?
37 assert !Enumeration.find(7).in_use?
38 end
38 end
39
39
40 def test_default
40 def test_default
41 e = Enumeration.default
41 e = Enumeration.default
42 assert e.is_a?(Enumeration)
42 assert e.is_a?(Enumeration)
43 assert e.is_default?
43 assert e.is_default?
44 assert_equal 'Default Enumeration', e.name
44 assert_equal 'Default Enumeration', e.name
45 end
45 end
46
46
47 def test_create
47 def test_create
48 e = Enumeration.new(:name => 'Not default', :is_default => false)
48 e = Enumeration.new(:name => 'Not default', :is_default => false)
49 e.type = 'Enumeration'
49 e.type = 'Enumeration'
50 assert e.save
50 assert e.save
51 assert_equal 'Default Enumeration', Enumeration.default.name
51 assert_equal 'Default Enumeration', Enumeration.default.name
52 end
52 end
53
53
54 def test_create_as_default
54 def test_create_as_default
55 e = Enumeration.new(:name => 'Very urgent', :is_default => true)
55 e = Enumeration.new(:name => 'Very urgent', :is_default => true)
56 e.type = 'Enumeration'
56 e.type = 'Enumeration'
57 assert e.save
57 assert e.save
58 assert_equal e, Enumeration.default
58 assert_equal e, Enumeration.default
59 end
59 end
60
60
61 def test_update_default
61 def test_update_default
62 e = Enumeration.default
62 e = Enumeration.default
63 e.update_attributes(:name => 'Changed', :is_default => true)
63 e.update_attributes(:name => 'Changed', :is_default => true)
64 assert_equal e, Enumeration.default
64 assert_equal e, Enumeration.default
65 end
65 end
66
66
67 def test_update_default_to_non_default
67 def test_update_default_to_non_default
68 e = Enumeration.default
68 e = Enumeration.default
69 e.update_attributes(:name => 'Changed', :is_default => false)
69 e.update_attributes(:name => 'Changed', :is_default => false)
70 assert_nil Enumeration.default
70 assert_nil Enumeration.default
71 end
71 end
72
72
73 def test_change_default
73 def test_change_default
74 e = Enumeration.find_by_name('Default Enumeration')
74 e = Enumeration.find_by_name('Default Enumeration')
75 e.update_attributes(:name => 'Changed Enumeration', :is_default => true)
75 e.update_attributes(:name => 'Changed Enumeration', :is_default => true)
76 assert_equal e, Enumeration.default
76 assert_equal e, Enumeration.default
77 end
77 end
78
78
79 def test_destroy_with_reassign
79 def test_destroy_with_reassign
80 Enumeration.find(4).destroy(Enumeration.find(6))
80 Enumeration.find(4).destroy(Enumeration.find(6))
81 assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
81 assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
82 assert_equal 5, Enumeration.find(6).objects_count
82 assert_equal 6, Enumeration.find(6).objects_count
83 end
83 end
84
84
85 def test_should_be_customizable
85 def test_should_be_customizable
86 assert Enumeration.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
86 assert Enumeration.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
87 end
87 end
88
88
89 def test_should_belong_to_a_project
89 def test_should_belong_to_a_project
90 association = Enumeration.reflect_on_association(:project)
90 association = Enumeration.reflect_on_association(:project)
91 assert association, "No Project association found"
91 assert association, "No Project association found"
92 assert_equal :belongs_to, association.macro
92 assert_equal :belongs_to, association.macro
93 end
93 end
94
94
95 def test_should_act_as_tree
95 def test_should_act_as_tree
96 enumeration = Enumeration.find(4)
96 enumeration = Enumeration.find(4)
97
97
98 assert enumeration.respond_to?(:parent)
98 assert enumeration.respond_to?(:parent)
99 assert enumeration.respond_to?(:children)
99 assert enumeration.respond_to?(:children)
100 end
100 end
101
101
102 def test_is_override
102 def test_is_override
103 # Defaults to off
103 # Defaults to off
104 enumeration = Enumeration.find(4)
104 enumeration = Enumeration.find(4)
105 assert !enumeration.is_override?
105 assert !enumeration.is_override?
106
106
107 # Setup as an override
107 # Setup as an override
108 enumeration.parent = Enumeration.find(5)
108 enumeration.parent = Enumeration.find(5)
109 assert enumeration.is_override?
109 assert enumeration.is_override?
110 end
110 end
111 end
111 end
@@ -1,38 +1,38
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssuePriorityTest < ActiveSupport::TestCase
20 class IssuePriorityTest < ActiveSupport::TestCase
21 fixtures :enumerations, :issues
21 fixtures :enumerations, :issues
22
22
23 def test_should_be_an_enumeration
23 def test_should_be_an_enumeration
24 assert IssuePriority.ancestors.include?(Enumeration)
24 assert IssuePriority.ancestors.include?(Enumeration)
25 end
25 end
26
26
27 def test_objects_count
27 def test_objects_count
28 # low priority
28 # low priority
29 assert_equal 5, IssuePriority.find(4).objects_count
29 assert_equal 6, IssuePriority.find(4).objects_count
30 # urgent
30 # urgent
31 assert_equal 0, IssuePriority.find(7).objects_count
31 assert_equal 0, IssuePriority.find(7).objects_count
32 end
32 end
33
33
34 def test_option_name
34 def test_option_name
35 assert_equal :enumeration_issue_priorities, IssuePriority.new.option_name
35 assert_equal :enumeration_issue_priorities, IssuePriority.new.option_name
36 end
36 end
37 end
37 end
38
38
@@ -1,475 +1,515
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :versions,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
25 :enumerations,
26 :issues,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
28 :time_entries
29
29
30 def test_create
30 def test_create
31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 assert issue.save
32 assert issue.save
33 issue.reload
33 issue.reload
34 assert_equal 1.5, issue.estimated_hours
34 assert_equal 1.5, issue.estimated_hours
35 end
35 end
36
36
37 def test_create_minimal
37 def test_create_minimal
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 assert issue.save
39 assert issue.save
40 assert issue.description.nil?
40 assert issue.description.nil?
41 end
41 end
42
42
43 def test_create_with_required_custom_field
43 def test_create_with_required_custom_field
44 field = IssueCustomField.find_by_name('Database')
44 field = IssueCustomField.find_by_name('Database')
45 field.update_attribute(:is_required, true)
45 field.update_attribute(:is_required, true)
46
46
47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 assert issue.available_custom_fields.include?(field)
48 assert issue.available_custom_fields.include?(field)
49 # No value for the custom field
49 # No value for the custom field
50 assert !issue.save
50 assert !issue.save
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 # Blank value
52 # Blank value
53 issue.custom_field_values = { field.id => '' }
53 issue.custom_field_values = { field.id => '' }
54 assert !issue.save
54 assert !issue.save
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 # Invalid value
56 # Invalid value
57 issue.custom_field_values = { field.id => 'SQLServer' }
57 issue.custom_field_values = { field.id => 'SQLServer' }
58 assert !issue.save
58 assert !issue.save
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 # Valid value
60 # Valid value
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 assert issue.save
62 assert issue.save
63 issue.reload
63 issue.reload
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 end
65 end
66
66
67 def test_visible_scope_for_anonymous
67 def test_visible_scope_for_anonymous
68 # Anonymous user should see issues of public projects only
68 # Anonymous user should see issues of public projects only
69 issues = Issue.visible(User.anonymous).all
69 issues = Issue.visible(User.anonymous).all
70 assert issues.any?
70 assert issues.any?
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 # Anonymous user should not see issues without permission
72 # Anonymous user should not see issues without permission
73 Role.anonymous.remove_permission!(:view_issues)
73 Role.anonymous.remove_permission!(:view_issues)
74 issues = Issue.visible(User.anonymous).all
74 issues = Issue.visible(User.anonymous).all
75 assert issues.empty?
75 assert issues.empty?
76 end
76 end
77
77
78 def test_visible_scope_for_user
78 def test_visible_scope_for_user
79 user = User.find(9)
79 user = User.find(9)
80 assert user.projects.empty?
80 assert user.projects.empty?
81 # Non member user should see issues of public projects only
81 # Non member user should see issues of public projects only
82 issues = Issue.visible(user).all
82 issues = Issue.visible(user).all
83 assert issues.any?
83 assert issues.any?
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 # Non member user should not see issues without permission
85 # Non member user should not see issues without permission
86 Role.non_member.remove_permission!(:view_issues)
86 Role.non_member.remove_permission!(:view_issues)
87 user.reload
87 user.reload
88 issues = Issue.visible(user).all
88 issues = Issue.visible(user).all
89 assert issues.empty?
89 assert issues.empty?
90 # User should see issues of projects for which he has view_issues permissions only
90 # User should see issues of projects for which he has view_issues permissions only
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 user.reload
92 user.reload
93 issues = Issue.visible(user).all
93 issues = Issue.visible(user).all
94 assert issues.any?
94 assert issues.any?
95 assert_nil issues.detect {|issue| issue.project_id != 2}
95 assert_nil issues.detect {|issue| issue.project_id != 2}
96 end
96 end
97
97
98 def test_visible_scope_for_admin
98 def test_visible_scope_for_admin
99 user = User.find(1)
99 user = User.find(1)
100 user.members.each(&:destroy)
100 user.members.each(&:destroy)
101 assert user.projects.empty?
101 assert user.projects.empty?
102 issues = Issue.visible(user).all
102 issues = Issue.visible(user).all
103 assert issues.any?
103 assert issues.any?
104 # Admin should see issues on private projects that he does not belong to
104 # Admin should see issues on private projects that he does not belong to
105 assert issues.detect {|issue| !issue.project.is_public?}
105 assert issues.detect {|issue| !issue.project.is_public?}
106 end
106 end
107
107
108 def test_errors_full_messages_should_include_custom_fields_errors
108 def test_errors_full_messages_should_include_custom_fields_errors
109 field = IssueCustomField.find_by_name('Database')
109 field = IssueCustomField.find_by_name('Database')
110
110
111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
112 assert issue.available_custom_fields.include?(field)
112 assert issue.available_custom_fields.include?(field)
113 # Invalid value
113 # Invalid value
114 issue.custom_field_values = { field.id => 'SQLServer' }
114 issue.custom_field_values = { field.id => 'SQLServer' }
115
115
116 assert !issue.valid?
116 assert !issue.valid?
117 assert_equal 1, issue.errors.full_messages.size
117 assert_equal 1, issue.errors.full_messages.size
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 end
119 end
120
120
121 def test_update_issue_with_required_custom_field
121 def test_update_issue_with_required_custom_field
122 field = IssueCustomField.find_by_name('Database')
122 field = IssueCustomField.find_by_name('Database')
123 field.update_attribute(:is_required, true)
123 field.update_attribute(:is_required, true)
124
124
125 issue = Issue.find(1)
125 issue = Issue.find(1)
126 assert_nil issue.custom_value_for(field)
126 assert_nil issue.custom_value_for(field)
127 assert issue.available_custom_fields.include?(field)
127 assert issue.available_custom_fields.include?(field)
128 # No change to custom values, issue can be saved
128 # No change to custom values, issue can be saved
129 assert issue.save
129 assert issue.save
130 # Blank value
130 # Blank value
131 issue.custom_field_values = { field.id => '' }
131 issue.custom_field_values = { field.id => '' }
132 assert !issue.save
132 assert !issue.save
133 # Valid value
133 # Valid value
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 assert issue.save
135 assert issue.save
136 issue.reload
136 issue.reload
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 end
138 end
139
139
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 issue = Issue.find(1)
141 issue = Issue.find(1)
142 field = IssueCustomField.find_by_name('Database')
142 field = IssueCustomField.find_by_name('Database')
143 assert issue.available_custom_fields.include?(field)
143 assert issue.available_custom_fields.include?(field)
144
144
145 issue.custom_field_values = { field.id => 'Invalid' }
145 issue.custom_field_values = { field.id => 'Invalid' }
146 issue.subject = 'Should be not be saved'
146 issue.subject = 'Should be not be saved'
147 assert !issue.save
147 assert !issue.save
148
148
149 issue.reload
149 issue.reload
150 assert_equal "Can't print recipes", issue.subject
150 assert_equal "Can't print recipes", issue.subject
151 end
151 end
152
152
153 def test_should_not_recreate_custom_values_objects_on_update
153 def test_should_not_recreate_custom_values_objects_on_update
154 field = IssueCustomField.find_by_name('Database')
154 field = IssueCustomField.find_by_name('Database')
155
155
156 issue = Issue.find(1)
156 issue = Issue.find(1)
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 assert issue.save
158 assert issue.save
159 custom_value = issue.custom_value_for(field)
159 custom_value = issue.custom_value_for(field)
160 issue.reload
160 issue.reload
161 issue.custom_field_values = { field.id => 'MySQL' }
161 issue.custom_field_values = { field.id => 'MySQL' }
162 assert issue.save
162 assert issue.save
163 issue.reload
163 issue.reload
164 assert_equal custom_value.id, issue.custom_value_for(field).id
164 assert_equal custom_value.id, issue.custom_value_for(field).id
165 end
165 end
166
166
167 def test_should_update_issue_with_disabled_tracker
167 def test_should_update_issue_with_disabled_tracker
168 p = Project.find(1)
168 p = Project.find(1)
169 issue = Issue.find(1)
169 issue = Issue.find(1)
170
170
171 p.trackers.delete(issue.tracker)
171 p.trackers.delete(issue.tracker)
172 assert !p.trackers.include?(issue.tracker)
172 assert !p.trackers.include?(issue.tracker)
173
173
174 issue.reload
174 issue.reload
175 issue.subject = 'New subject'
175 issue.subject = 'New subject'
176 assert issue.save
176 assert issue.save
177 end
177 end
178
178
179 def test_should_not_set_a_disabled_tracker
179 def test_should_not_set_a_disabled_tracker
180 p = Project.find(1)
180 p = Project.find(1)
181 p.trackers.delete(Tracker.find(2))
181 p.trackers.delete(Tracker.find(2))
182
182
183 issue = Issue.find(1)
183 issue = Issue.find(1)
184 issue.tracker_id = 2
184 issue.tracker_id = 2
185 issue.subject = 'New subject'
185 issue.subject = 'New subject'
186 assert !issue.save
186 assert !issue.save
187 assert_not_nil issue.errors.on(:tracker_id)
187 assert_not_nil issue.errors.on(:tracker_id)
188 end
188 end
189
189
190 def test_category_based_assignment
190 def test_category_based_assignment
191 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
191 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
192 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
192 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
193 end
193 end
194
194
195 def test_copy
195 def test_copy
196 issue = Issue.new.copy_from(1)
196 issue = Issue.new.copy_from(1)
197 assert issue.save
197 assert issue.save
198 issue.reload
198 issue.reload
199 orig = Issue.find(1)
199 orig = Issue.find(1)
200 assert_equal orig.subject, issue.subject
200 assert_equal orig.subject, issue.subject
201 assert_equal orig.tracker, issue.tracker
201 assert_equal orig.tracker, issue.tracker
202 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
202 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
203 end
203 end
204
204
205 def test_copy_should_copy_status
205 def test_copy_should_copy_status
206 orig = Issue.find(8)
206 orig = Issue.find(8)
207 assert orig.status != IssueStatus.default
207 assert orig.status != IssueStatus.default
208
208
209 issue = Issue.new.copy_from(orig)
209 issue = Issue.new.copy_from(orig)
210 assert issue.save
210 assert issue.save
211 issue.reload
211 issue.reload
212 assert_equal orig.status, issue.status
212 assert_equal orig.status, issue.status
213 end
213 end
214
214
215 def test_should_close_duplicates
215 def test_should_close_duplicates
216 # Create 3 issues
216 # Create 3 issues
217 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
217 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
218 assert issue1.save
218 assert issue1.save
219 issue2 = issue1.clone
219 issue2 = issue1.clone
220 assert issue2.save
220 assert issue2.save
221 issue3 = issue1.clone
221 issue3 = issue1.clone
222 assert issue3.save
222 assert issue3.save
223
223
224 # 2 is a dupe of 1
224 # 2 is a dupe of 1
225 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
225 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
226 # And 3 is a dupe of 2
226 # And 3 is a dupe of 2
227 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
227 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
228 # And 3 is a dupe of 1 (circular duplicates)
228 # And 3 is a dupe of 1 (circular duplicates)
229 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
229 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
230
230
231 assert issue1.reload.duplicates.include?(issue2)
231 assert issue1.reload.duplicates.include?(issue2)
232
232
233 # Closing issue 1
233 # Closing issue 1
234 issue1.init_journal(User.find(:first), "Closing issue1")
234 issue1.init_journal(User.find(:first), "Closing issue1")
235 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
235 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
236 assert issue1.save
236 assert issue1.save
237 # 2 and 3 should be also closed
237 # 2 and 3 should be also closed
238 assert issue2.reload.closed?
238 assert issue2.reload.closed?
239 assert issue3.reload.closed?
239 assert issue3.reload.closed?
240 end
240 end
241
241
242 def test_should_not_close_duplicated_issue
242 def test_should_not_close_duplicated_issue
243 # Create 3 issues
243 # Create 3 issues
244 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
244 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
245 assert issue1.save
245 assert issue1.save
246 issue2 = issue1.clone
246 issue2 = issue1.clone
247 assert issue2.save
247 assert issue2.save
248
248
249 # 2 is a dupe of 1
249 # 2 is a dupe of 1
250 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
250 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
251 # 2 is a dup of 1 but 1 is not a duplicate of 2
251 # 2 is a dup of 1 but 1 is not a duplicate of 2
252 assert !issue2.reload.duplicates.include?(issue1)
252 assert !issue2.reload.duplicates.include?(issue1)
253
253
254 # Closing issue 2
254 # Closing issue 2
255 issue2.init_journal(User.find(:first), "Closing issue2")
255 issue2.init_journal(User.find(:first), "Closing issue2")
256 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
256 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
257 assert issue2.save
257 assert issue2.save
258 # 1 should not be also closed
258 # 1 should not be also closed
259 assert !issue1.reload.closed?
259 assert !issue1.reload.closed?
260 end
260 end
261
261
262 def test_assignable_versions
262 def test_assignable_versions
263 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
263 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
264 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
264 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
265 end
265 end
266
266
267 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
267 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
268 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
268 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
269 assert !issue.save
269 assert !issue.save
270 assert_not_nil issue.errors.on(:fixed_version_id)
270 assert_not_nil issue.errors.on(:fixed_version_id)
271 end
271 end
272
272
273 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
273 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
274 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
274 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
275 assert !issue.save
275 assert !issue.save
276 assert_not_nil issue.errors.on(:fixed_version_id)
276 assert_not_nil issue.errors.on(:fixed_version_id)
277 end
277 end
278
278
279 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
279 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
280 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
280 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
281 assert issue.save
281 assert issue.save
282 end
282 end
283
283
284 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
284 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
285 issue = Issue.find(11)
285 issue = Issue.find(11)
286 assert_equal 'closed', issue.fixed_version.status
286 assert_equal 'closed', issue.fixed_version.status
287 issue.subject = 'Subject changed'
287 issue.subject = 'Subject changed'
288 assert issue.save
288 assert issue.save
289 end
289 end
290
290
291 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
291 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
292 issue = Issue.find(11)
292 issue = Issue.find(11)
293 issue.status_id = 1
293 issue.status_id = 1
294 assert !issue.save
294 assert !issue.save
295 assert_not_nil issue.errors.on_base
295 assert_not_nil issue.errors.on_base
296 end
296 end
297
297
298 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
298 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
299 issue = Issue.find(11)
299 issue = Issue.find(11)
300 issue.status_id = 1
300 issue.status_id = 1
301 issue.fixed_version_id = 3
301 issue.fixed_version_id = 3
302 assert issue.save
302 assert issue.save
303 end
303 end
304
304
305 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
305 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
306 issue = Issue.find(12)
306 issue = Issue.find(12)
307 assert_equal 'locked', issue.fixed_version.status
307 assert_equal 'locked', issue.fixed_version.status
308 issue.status_id = 1
308 issue.status_id = 1
309 assert issue.save
309 assert issue.save
310 end
310 end
311
311
312 def test_move_to_another_project_with_same_category
312 def test_move_to_another_project_with_same_category
313 issue = Issue.find(1)
313 issue = Issue.find(1)
314 assert issue.move_to(Project.find(2))
314 assert issue.move_to(Project.find(2))
315 issue.reload
315 issue.reload
316 assert_equal 2, issue.project_id
316 assert_equal 2, issue.project_id
317 # Category changes
317 # Category changes
318 assert_equal 4, issue.category_id
318 assert_equal 4, issue.category_id
319 # Make sure time entries were move to the target project
319 # Make sure time entries were move to the target project
320 assert_equal 2, issue.time_entries.first.project_id
320 assert_equal 2, issue.time_entries.first.project_id
321 end
321 end
322
322
323 def test_move_to_another_project_without_same_category
323 def test_move_to_another_project_without_same_category
324 issue = Issue.find(2)
324 issue = Issue.find(2)
325 assert issue.move_to(Project.find(2))
325 assert issue.move_to(Project.find(2))
326 issue.reload
326 issue.reload
327 assert_equal 2, issue.project_id
327 assert_equal 2, issue.project_id
328 # Category cleared
328 # Category cleared
329 assert_nil issue.category_id
329 assert_nil issue.category_id
330 end
330 end
331
331
332 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
333 issue = Issue.find(1)
334 issue.update_attribute(:fixed_version_id, 1)
335 assert issue.move_to(Project.find(2))
336 issue.reload
337 assert_equal 2, issue.project_id
338 # Cleared fixed_version
339 assert_equal nil, issue.fixed_version
340 end
341
342 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
343 issue = Issue.find(1)
344 issue.update_attribute(:fixed_version_id, 4)
345 assert issue.move_to(Project.find(5))
346 issue.reload
347 assert_equal 5, issue.project_id
348 # Keep fixed_version
349 assert_equal 4, issue.fixed_version_id
350 end
351
352 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
353 issue = Issue.find(1)
354 issue.update_attribute(:fixed_version_id, 1)
355 assert issue.move_to(Project.find(5))
356 issue.reload
357 assert_equal 5, issue.project_id
358 # Cleared fixed_version
359 assert_equal nil, issue.fixed_version
360 end
361
362 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
363 issue = Issue.find(1)
364 issue.update_attribute(:fixed_version_id, 7)
365 assert issue.move_to(Project.find(2))
366 issue.reload
367 assert_equal 2, issue.project_id
368 # Keep fixed_version
369 assert_equal 7, issue.fixed_version_id
370 end
371
332 def test_copy_to_the_same_project
372 def test_copy_to_the_same_project
333 issue = Issue.find(1)
373 issue = Issue.find(1)
334 copy = nil
374 copy = nil
335 assert_difference 'Issue.count' do
375 assert_difference 'Issue.count' do
336 copy = issue.move_to(issue.project, nil, :copy => true)
376 copy = issue.move_to(issue.project, nil, :copy => true)
337 end
377 end
338 assert_kind_of Issue, copy
378 assert_kind_of Issue, copy
339 assert_equal issue.project, copy.project
379 assert_equal issue.project, copy.project
340 assert_equal "125", copy.custom_value_for(2).value
380 assert_equal "125", copy.custom_value_for(2).value
341 end
381 end
342
382
343 def test_copy_to_another_project_and_tracker
383 def test_copy_to_another_project_and_tracker
344 issue = Issue.find(1)
384 issue = Issue.find(1)
345 copy = nil
385 copy = nil
346 assert_difference 'Issue.count' do
386 assert_difference 'Issue.count' do
347 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
387 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
348 end
388 end
349 assert_kind_of Issue, copy
389 assert_kind_of Issue, copy
350 assert_equal Project.find(3), copy.project
390 assert_equal Project.find(3), copy.project
351 assert_equal Tracker.find(2), copy.tracker
391 assert_equal Tracker.find(2), copy.tracker
352 # Custom field #2 is not associated with target tracker
392 # Custom field #2 is not associated with target tracker
353 assert_nil copy.custom_value_for(2)
393 assert_nil copy.custom_value_for(2)
354 end
394 end
355
395
356 context "#move_to" do
396 context "#move_to" do
357 context "as a copy" do
397 context "as a copy" do
358 setup do
398 setup do
359 @issue = Issue.find(1)
399 @issue = Issue.find(1)
360 @copy = nil
400 @copy = nil
361 end
401 end
362
402
363 should "allow assigned_to changes" do
403 should "allow assigned_to changes" do
364 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
404 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
365 assert_equal 3, @copy.assigned_to_id
405 assert_equal 3, @copy.assigned_to_id
366 end
406 end
367
407
368 should "allow status changes" do
408 should "allow status changes" do
369 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
409 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
370 assert_equal 2, @copy.status_id
410 assert_equal 2, @copy.status_id
371 end
411 end
372
412
373 should "allow start date changes" do
413 should "allow start date changes" do
374 date = Date.today
414 date = Date.today
375 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
415 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
376 assert_equal date, @copy.start_date
416 assert_equal date, @copy.start_date
377 end
417 end
378
418
379 should "allow due date changes" do
419 should "allow due date changes" do
380 date = Date.today
420 date = Date.today
381 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
421 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
382
422
383 assert_equal date, @copy.due_date
423 assert_equal date, @copy.due_date
384 end
424 end
385 end
425 end
386 end
426 end
387
427
388 def test_recipients_should_not_include_users_that_cannot_view_the_issue
428 def test_recipients_should_not_include_users_that_cannot_view_the_issue
389 issue = Issue.find(12)
429 issue = Issue.find(12)
390 assert issue.recipients.include?(issue.author.mail)
430 assert issue.recipients.include?(issue.author.mail)
391 # move the issue to a private project
431 # move the issue to a private project
392 copy = issue.move_to(Project.find(5), Tracker.find(2), :copy => true)
432 copy = issue.move_to(Project.find(5), Tracker.find(2), :copy => true)
393 # author is not a member of project anymore
433 # author is not a member of project anymore
394 assert !copy.recipients.include?(copy.author.mail)
434 assert !copy.recipients.include?(copy.author.mail)
395 end
435 end
396
436
397 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
437 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
398 user = User.find(3)
438 user = User.find(3)
399 issue = Issue.find(9)
439 issue = Issue.find(9)
400 Watcher.create!(:user => user, :watchable => issue)
440 Watcher.create!(:user => user, :watchable => issue)
401 assert issue.watched_by?(user)
441 assert issue.watched_by?(user)
402 assert !issue.watcher_recipients.include?(user.mail)
442 assert !issue.watcher_recipients.include?(user.mail)
403 end
443 end
404
444
405 def test_issue_destroy
445 def test_issue_destroy
406 Issue.find(1).destroy
446 Issue.find(1).destroy
407 assert_nil Issue.find_by_id(1)
447 assert_nil Issue.find_by_id(1)
408 assert_nil TimeEntry.find_by_issue_id(1)
448 assert_nil TimeEntry.find_by_issue_id(1)
409 end
449 end
410
450
411 def test_blocked
451 def test_blocked
412 blocked_issue = Issue.find(9)
452 blocked_issue = Issue.find(9)
413 blocking_issue = Issue.find(10)
453 blocking_issue = Issue.find(10)
414
454
415 assert blocked_issue.blocked?
455 assert blocked_issue.blocked?
416 assert !blocking_issue.blocked?
456 assert !blocking_issue.blocked?
417 end
457 end
418
458
419 def test_blocked_issues_dont_allow_closed_statuses
459 def test_blocked_issues_dont_allow_closed_statuses
420 blocked_issue = Issue.find(9)
460 blocked_issue = Issue.find(9)
421
461
422 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
462 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
423 assert !allowed_statuses.empty?
463 assert !allowed_statuses.empty?
424 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
464 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
425 assert closed_statuses.empty?
465 assert closed_statuses.empty?
426 end
466 end
427
467
428 def test_unblocked_issues_allow_closed_statuses
468 def test_unblocked_issues_allow_closed_statuses
429 blocking_issue = Issue.find(10)
469 blocking_issue = Issue.find(10)
430
470
431 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
471 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
432 assert !allowed_statuses.empty?
472 assert !allowed_statuses.empty?
433 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
473 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
434 assert !closed_statuses.empty?
474 assert !closed_statuses.empty?
435 end
475 end
436
476
437 def test_overdue
477 def test_overdue
438 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
478 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
439 assert !Issue.new(:due_date => Date.today).overdue?
479 assert !Issue.new(:due_date => Date.today).overdue?
440 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
480 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
441 assert !Issue.new(:due_date => nil).overdue?
481 assert !Issue.new(:due_date => nil).overdue?
442 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
482 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
443 end
483 end
444
484
445 def test_assignable_users
485 def test_assignable_users
446 assert_kind_of User, Issue.find(1).assignable_users.first
486 assert_kind_of User, Issue.find(1).assignable_users.first
447 end
487 end
448
488
449 def test_create_should_send_email_notification
489 def test_create_should_send_email_notification
450 ActionMailer::Base.deliveries.clear
490 ActionMailer::Base.deliveries.clear
451 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
491 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
452
492
453 assert issue.save
493 assert issue.save
454 assert_equal 1, ActionMailer::Base.deliveries.size
494 assert_equal 1, ActionMailer::Base.deliveries.size
455 end
495 end
456
496
457 def test_stale_issue_should_not_send_email_notification
497 def test_stale_issue_should_not_send_email_notification
458 ActionMailer::Base.deliveries.clear
498 ActionMailer::Base.deliveries.clear
459 issue = Issue.find(1)
499 issue = Issue.find(1)
460 stale = Issue.find(1)
500 stale = Issue.find(1)
461
501
462 issue.init_journal(User.find(1))
502 issue.init_journal(User.find(1))
463 issue.subject = 'Subjet update'
503 issue.subject = 'Subjet update'
464 assert issue.save
504 assert issue.save
465 assert_equal 1, ActionMailer::Base.deliveries.size
505 assert_equal 1, ActionMailer::Base.deliveries.size
466 ActionMailer::Base.deliveries.clear
506 ActionMailer::Base.deliveries.clear
467
507
468 stale.init_journal(User.find(1))
508 stale.init_journal(User.find(1))
469 stale.subject = 'Another subjet update'
509 stale.subject = 'Another subjet update'
470 assert_raise ActiveRecord::StaleObjectError do
510 assert_raise ActiveRecord::StaleObjectError do
471 stale.save
511 stale.save
472 end
512 end
473 assert ActionMailer::Base.deliveries.empty?
513 assert ActionMailer::Base.deliveries.empty?
474 end
514 end
475 end
515 end
@@ -1,560 +1,650
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules,
21 fixtures :all
22 :issues, :issue_statuses, :journals, :journal_details,
23 :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,
24 :queries
25
22
26 def setup
23 def setup
27 @ecookbook = Project.find(1)
24 @ecookbook = Project.find(1)
28 @ecookbook_sub1 = Project.find(3)
25 @ecookbook_sub1 = Project.find(3)
29 User.current = nil
26 User.current = nil
30 end
27 end
31
28
32 should_validate_presence_of :name
29 should_validate_presence_of :name
33 should_validate_presence_of :identifier
30 should_validate_presence_of :identifier
34
31
35 should_validate_uniqueness_of :name
32 should_validate_uniqueness_of :name
36 should_validate_uniqueness_of :identifier
33 should_validate_uniqueness_of :identifier
37
34
38 context "associations" do
35 context "associations" do
39 should_have_many :members
36 should_have_many :members
40 should_have_many :users, :through => :members
37 should_have_many :users, :through => :members
41 should_have_many :member_principals
38 should_have_many :member_principals
42 should_have_many :principals, :through => :member_principals
39 should_have_many :principals, :through => :member_principals
43 should_have_many :enabled_modules
40 should_have_many :enabled_modules
44 should_have_many :issues
41 should_have_many :issues
45 should_have_many :issue_changes, :through => :issues
42 should_have_many :issue_changes, :through => :issues
46 should_have_many :versions
43 should_have_many :versions
47 should_have_many :time_entries
44 should_have_many :time_entries
48 should_have_many :queries
45 should_have_many :queries
49 should_have_many :documents
46 should_have_many :documents
50 should_have_many :news
47 should_have_many :news
51 should_have_many :issue_categories
48 should_have_many :issue_categories
52 should_have_many :boards
49 should_have_many :boards
53 should_have_many :changesets, :through => :repository
50 should_have_many :changesets, :through => :repository
54
51
55 should_have_one :repository
52 should_have_one :repository
56 should_have_one :wiki
53 should_have_one :wiki
57
54
58 should_have_and_belong_to_many :trackers
55 should_have_and_belong_to_many :trackers
59 should_have_and_belong_to_many :issue_custom_fields
56 should_have_and_belong_to_many :issue_custom_fields
60 end
57 end
61
58
62 def test_truth
59 def test_truth
63 assert_kind_of Project, @ecookbook
60 assert_kind_of Project, @ecookbook
64 assert_equal "eCookbook", @ecookbook.name
61 assert_equal "eCookbook", @ecookbook.name
65 end
62 end
66
63
67 def test_update
64 def test_update
68 assert_equal "eCookbook", @ecookbook.name
65 assert_equal "eCookbook", @ecookbook.name
69 @ecookbook.name = "eCook"
66 @ecookbook.name = "eCook"
70 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
67 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
71 @ecookbook.reload
68 @ecookbook.reload
72 assert_equal "eCook", @ecookbook.name
69 assert_equal "eCook", @ecookbook.name
73 end
70 end
74
71
75 def test_validate_identifier
72 def test_validate_identifier
76 to_test = {"abc" => true,
73 to_test = {"abc" => true,
77 "ab12" => true,
74 "ab12" => true,
78 "ab-12" => true,
75 "ab-12" => true,
79 "12" => false,
76 "12" => false,
80 "new" => false}
77 "new" => false}
81
78
82 to_test.each do |identifier, valid|
79 to_test.each do |identifier, valid|
83 p = Project.new
80 p = Project.new
84 p.identifier = identifier
81 p.identifier = identifier
85 p.valid?
82 p.valid?
86 assert_equal valid, p.errors.on('identifier').nil?
83 assert_equal valid, p.errors.on('identifier').nil?
87 end
84 end
88 end
85 end
89
86
90 def test_members_should_be_active_users
87 def test_members_should_be_active_users
91 Project.all.each do |project|
88 Project.all.each do |project|
92 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
89 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
93 end
90 end
94 end
91 end
95
92
96 def test_users_should_be_active_users
93 def test_users_should_be_active_users
97 Project.all.each do |project|
94 Project.all.each do |project|
98 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
95 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
99 end
96 end
100 end
97 end
101
98
102 def test_archive
99 def test_archive
103 user = @ecookbook.members.first.user
100 user = @ecookbook.members.first.user
104 @ecookbook.archive
101 @ecookbook.archive
105 @ecookbook.reload
102 @ecookbook.reload
106
103
107 assert !@ecookbook.active?
104 assert !@ecookbook.active?
108 assert !user.projects.include?(@ecookbook)
105 assert !user.projects.include?(@ecookbook)
109 # Subproject are also archived
106 # Subproject are also archived
110 assert !@ecookbook.children.empty?
107 assert !@ecookbook.children.empty?
111 assert @ecookbook.descendants.active.empty?
108 assert @ecookbook.descendants.active.empty?
112 end
109 end
113
110
111 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
112 # Assign an issue of a project to a version of a child project
113 Issue.find(4).update_attribute :fixed_version_id, 4
114
115 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
116 assert_equal false, @ecookbook.archive
117 end
118 @ecookbook.reload
119 assert @ecookbook.active?
120 end
121
114 def test_unarchive
122 def test_unarchive
115 user = @ecookbook.members.first.user
123 user = @ecookbook.members.first.user
116 @ecookbook.archive
124 @ecookbook.archive
117 # A subproject of an archived project can not be unarchived
125 # A subproject of an archived project can not be unarchived
118 assert !@ecookbook_sub1.unarchive
126 assert !@ecookbook_sub1.unarchive
119
127
120 # Unarchive project
128 # Unarchive project
121 assert @ecookbook.unarchive
129 assert @ecookbook.unarchive
122 @ecookbook.reload
130 @ecookbook.reload
123 assert @ecookbook.active?
131 assert @ecookbook.active?
124 assert user.projects.include?(@ecookbook)
132 assert user.projects.include?(@ecookbook)
125 # Subproject can now be unarchived
133 # Subproject can now be unarchived
126 @ecookbook_sub1.reload
134 @ecookbook_sub1.reload
127 assert @ecookbook_sub1.unarchive
135 assert @ecookbook_sub1.unarchive
128 end
136 end
129
137
130 def test_destroy
138 def test_destroy
131 # 2 active members
139 # 2 active members
132 assert_equal 2, @ecookbook.members.size
140 assert_equal 2, @ecookbook.members.size
133 # and 1 is locked
141 # and 1 is locked
134 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
142 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
135 # some boards
143 # some boards
136 assert @ecookbook.boards.any?
144 assert @ecookbook.boards.any?
137
145
138 @ecookbook.destroy
146 @ecookbook.destroy
139 # make sure that the project non longer exists
147 # make sure that the project non longer exists
140 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
148 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
141 # make sure related data was removed
149 # make sure related data was removed
142 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
150 assert Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
143 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
151 assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
144 end
152 end
145
153
146 def test_move_an_orphan_project_to_a_root_project
154 def test_move_an_orphan_project_to_a_root_project
147 sub = Project.find(2)
155 sub = Project.find(2)
148 sub.set_parent! @ecookbook
156 sub.set_parent! @ecookbook
149 assert_equal @ecookbook.id, sub.parent.id
157 assert_equal @ecookbook.id, sub.parent.id
150 @ecookbook.reload
158 @ecookbook.reload
151 assert_equal 4, @ecookbook.children.size
159 assert_equal 4, @ecookbook.children.size
152 end
160 end
153
161
154 def test_move_an_orphan_project_to_a_subproject
162 def test_move_an_orphan_project_to_a_subproject
155 sub = Project.find(2)
163 sub = Project.find(2)
156 assert sub.set_parent!(@ecookbook_sub1)
164 assert sub.set_parent!(@ecookbook_sub1)
157 end
165 end
158
166
159 def test_move_a_root_project_to_a_project
167 def test_move_a_root_project_to_a_project
160 sub = @ecookbook
168 sub = @ecookbook
161 assert sub.set_parent!(Project.find(2))
169 assert sub.set_parent!(Project.find(2))
162 end
170 end
163
171
164 def test_should_not_move_a_project_to_its_children
172 def test_should_not_move_a_project_to_its_children
165 sub = @ecookbook
173 sub = @ecookbook
166 assert !(sub.set_parent!(Project.find(3)))
174 assert !(sub.set_parent!(Project.find(3)))
167 end
175 end
168
176
169 def test_set_parent_should_add_roots_in_alphabetical_order
177 def test_set_parent_should_add_roots_in_alphabetical_order
170 ProjectCustomField.delete_all
178 ProjectCustomField.delete_all
171 Project.delete_all
179 Project.delete_all
172 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
180 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
173 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
181 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
174 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
182 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
175 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
183 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
176
184
177 assert_equal 4, Project.count
185 assert_equal 4, Project.count
178 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
186 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
179 end
187 end
180
188
181 def test_set_parent_should_add_children_in_alphabetical_order
189 def test_set_parent_should_add_children_in_alphabetical_order
182 ProjectCustomField.delete_all
190 ProjectCustomField.delete_all
183 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
191 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
184 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
192 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
185 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
193 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
186 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
194 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
187 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
195 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
188
196
189 parent.reload
197 parent.reload
190 assert_equal 4, parent.children.size
198 assert_equal 4, parent.children.size
191 assert_equal parent.children.sort_by(&:name), parent.children
199 assert_equal parent.children.sort_by(&:name), parent.children
192 end
200 end
193
201
194 def test_rebuild_should_sort_children_alphabetically
202 def test_rebuild_should_sort_children_alphabetically
195 ProjectCustomField.delete_all
203 ProjectCustomField.delete_all
196 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
204 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
197 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
205 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
198 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
206 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
199 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
207 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
200 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
208 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
201
209
202 Project.update_all("lft = NULL, rgt = NULL")
210 Project.update_all("lft = NULL, rgt = NULL")
203 Project.rebuild!
211 Project.rebuild!
204
212
205 parent.reload
213 parent.reload
206 assert_equal 4, parent.children.size
214 assert_equal 4, parent.children.size
207 assert_equal parent.children.sort_by(&:name), parent.children
215 assert_equal parent.children.sort_by(&:name), parent.children
208 end
216 end
217
218
219 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
220 # Parent issue with a hierarchy project's fixed version
221 parent_issue = Issue.find(1)
222 parent_issue.update_attribute(:fixed_version_id, 4)
223 parent_issue.reload
224 assert_equal 4, parent_issue.fixed_version_id
225
226 # Should keep fixed versions for the issues
227 issue_with_local_fixed_version = Issue.find(5)
228 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
229 issue_with_local_fixed_version.reload
230 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
231
232 # Local issue with hierarchy fixed_version
233 issue_with_hierarchy_fixed_version = Issue.find(13)
234 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
235 issue_with_hierarchy_fixed_version.reload
236 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
237
238 # Move project out of the issue's hierarchy
239 moved_project = Project.find(3)
240 moved_project.set_parent!(Project.find(2))
241 parent_issue.reload
242 issue_with_local_fixed_version.reload
243 issue_with_hierarchy_fixed_version.reload
244
245 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
246 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
247 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
248 end
209
249
210 def test_parent
250 def test_parent
211 p = Project.find(6).parent
251 p = Project.find(6).parent
212 assert p.is_a?(Project)
252 assert p.is_a?(Project)
213 assert_equal 5, p.id
253 assert_equal 5, p.id
214 end
254 end
215
255
216 def test_ancestors
256 def test_ancestors
217 a = Project.find(6).ancestors
257 a = Project.find(6).ancestors
218 assert a.first.is_a?(Project)
258 assert a.first.is_a?(Project)
219 assert_equal [1, 5], a.collect(&:id)
259 assert_equal [1, 5], a.collect(&:id)
220 end
260 end
221
261
222 def test_root
262 def test_root
223 r = Project.find(6).root
263 r = Project.find(6).root
224 assert r.is_a?(Project)
264 assert r.is_a?(Project)
225 assert_equal 1, r.id
265 assert_equal 1, r.id
226 end
266 end
227
267
228 def test_children
268 def test_children
229 c = Project.find(1).children
269 c = Project.find(1).children
230 assert c.first.is_a?(Project)
270 assert c.first.is_a?(Project)
231 assert_equal [5, 3, 4], c.collect(&:id)
271 assert_equal [5, 3, 4], c.collect(&:id)
232 end
272 end
233
273
234 def test_descendants
274 def test_descendants
235 d = Project.find(1).descendants
275 d = Project.find(1).descendants
236 assert d.first.is_a?(Project)
276 assert d.first.is_a?(Project)
237 assert_equal [5, 6, 3, 4], d.collect(&:id)
277 assert_equal [5, 6, 3, 4], d.collect(&:id)
238 end
278 end
239
279
240 def test_allowed_parents_should_be_empty_for_non_member_user
280 def test_allowed_parents_should_be_empty_for_non_member_user
241 Role.non_member.add_permission!(:add_project)
281 Role.non_member.add_permission!(:add_project)
242 user = User.find(9)
282 user = User.find(9)
243 assert user.memberships.empty?
283 assert user.memberships.empty?
244 User.current = user
284 User.current = user
245 assert Project.new.allowed_parents.empty?
285 assert Project.new.allowed_parents.empty?
246 end
286 end
247
287
248 def test_users_by_role
288 def test_users_by_role
249 users_by_role = Project.find(1).users_by_role
289 users_by_role = Project.find(1).users_by_role
250 assert_kind_of Hash, users_by_role
290 assert_kind_of Hash, users_by_role
251 role = Role.find(1)
291 role = Role.find(1)
252 assert_kind_of Array, users_by_role[role]
292 assert_kind_of Array, users_by_role[role]
253 assert users_by_role[role].include?(User.find(2))
293 assert users_by_role[role].include?(User.find(2))
254 end
294 end
255
295
256 def test_rolled_up_trackers
296 def test_rolled_up_trackers
257 parent = Project.find(1)
297 parent = Project.find(1)
258 parent.trackers = Tracker.find([1,2])
298 parent.trackers = Tracker.find([1,2])
259 child = parent.children.find(3)
299 child = parent.children.find(3)
260
300
261 assert_equal [1, 2], parent.tracker_ids
301 assert_equal [1, 2], parent.tracker_ids
262 assert_equal [2, 3], child.trackers.collect(&:id)
302 assert_equal [2, 3], child.trackers.collect(&:id)
263
303
264 assert_kind_of Tracker, parent.rolled_up_trackers.first
304 assert_kind_of Tracker, parent.rolled_up_trackers.first
265 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
305 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
266
306
267 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
307 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
268 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
308 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
269 end
309 end
270
310
271 def test_rolled_up_trackers_should_ignore_archived_subprojects
311 def test_rolled_up_trackers_should_ignore_archived_subprojects
272 parent = Project.find(1)
312 parent = Project.find(1)
273 parent.trackers = Tracker.find([1,2])
313 parent.trackers = Tracker.find([1,2])
274 child = parent.children.find(3)
314 child = parent.children.find(3)
275 child.trackers = Tracker.find([1,3])
315 child.trackers = Tracker.find([1,3])
276 parent.children.each(&:archive)
316 parent.children.each(&:archive)
277
317
278 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
318 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
279 end
319 end
320
321 def test_shared_versions
322 parent = Project.find(1)
323 child = parent.children.find(3)
324 private_child = parent.children.find(5)
325
326 assert_equal [1,2,3], parent.version_ids.sort
327 assert_equal [4], child.version_ids
328 assert_equal [6], private_child.version_ids
329 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
330
331 assert_equal 6, parent.shared_versions.size
332 parent.shared_versions.each do |version|
333 assert_kind_of Version, version
334 end
335
336 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
337 end
338
339 def test_shared_versions_should_ignore_archived_subprojects
340 parent = Project.find(1)
341 child = parent.children.find(3)
342 child.archive
343 parent.reload
344
345 assert_equal [1,2,3], parent.version_ids.sort
346 assert_equal [4], child.version_ids
347 assert !parent.shared_versions.collect(&:id).include?(4)
348 end
349
350 def test_shared_versions_visible_to_user
351 user = User.find(3)
352 parent = Project.find(1)
353 child = parent.children.find(5)
354
355 assert_equal [1,2,3], parent.version_ids.sort
356 assert_equal [6], child.version_ids
357
358 versions = parent.shared_versions.visible(user)
359
360 assert_equal 4, versions.size
361 versions.each do |version|
362 assert_kind_of Version, version
363 end
364
365 assert !versions.collect(&:id).include?(6)
366 end
367
280
368
281 def test_next_identifier
369 def test_next_identifier
282 ProjectCustomField.delete_all
370 ProjectCustomField.delete_all
283 Project.create!(:name => 'last', :identifier => 'p2008040')
371 Project.create!(:name => 'last', :identifier => 'p2008040')
284 assert_equal 'p2008041', Project.next_identifier
372 assert_equal 'p2008041', Project.next_identifier
285 end
373 end
286
374
287 def test_next_identifier_first_project
375 def test_next_identifier_first_project
288 Project.delete_all
376 Project.delete_all
289 assert_nil Project.next_identifier
377 assert_nil Project.next_identifier
290 end
378 end
291
379
292
380
293 def test_enabled_module_names_should_not_recreate_enabled_modules
381 def test_enabled_module_names_should_not_recreate_enabled_modules
294 project = Project.find(1)
382 project = Project.find(1)
295 # Remove one module
383 # Remove one module
296 modules = project.enabled_modules.slice(0..-2)
384 modules = project.enabled_modules.slice(0..-2)
297 assert modules.any?
385 assert modules.any?
298 assert_difference 'EnabledModule.count', -1 do
386 assert_difference 'EnabledModule.count', -1 do
299 project.enabled_module_names = modules.collect(&:name)
387 project.enabled_module_names = modules.collect(&:name)
300 end
388 end
301 project.reload
389 project.reload
302 # Ids should be preserved
390 # Ids should be preserved
303 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
391 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
304 end
392 end
305
393
306 def test_copy_from_existing_project
394 def test_copy_from_existing_project
307 source_project = Project.find(1)
395 source_project = Project.find(1)
308 copied_project = Project.copy_from(1)
396 copied_project = Project.copy_from(1)
309
397
310 assert copied_project
398 assert copied_project
311 # Cleared attributes
399 # Cleared attributes
312 assert copied_project.id.blank?
400 assert copied_project.id.blank?
313 assert copied_project.name.blank?
401 assert copied_project.name.blank?
314 assert copied_project.identifier.blank?
402 assert copied_project.identifier.blank?
315
403
316 # Duplicated attributes
404 # Duplicated attributes
317 assert_equal source_project.description, copied_project.description
405 assert_equal source_project.description, copied_project.description
318 assert_equal source_project.enabled_modules, copied_project.enabled_modules
406 assert_equal source_project.enabled_modules, copied_project.enabled_modules
319 assert_equal source_project.trackers, copied_project.trackers
407 assert_equal source_project.trackers, copied_project.trackers
320
408
321 # Default attributes
409 # Default attributes
322 assert_equal 1, copied_project.status
410 assert_equal 1, copied_project.status
323 end
411 end
324
412
325 def test_activities_should_use_the_system_activities
413 def test_activities_should_use_the_system_activities
326 project = Project.find(1)
414 project = Project.find(1)
327 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
415 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
328 end
416 end
329
417
330
418
331 def test_activities_should_use_the_project_specific_activities
419 def test_activities_should_use_the_project_specific_activities
332 project = Project.find(1)
420 project = Project.find(1)
333 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
421 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
334 assert overridden_activity.save!
422 assert overridden_activity.save!
335
423
336 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
424 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
337 end
425 end
338
426
339 def test_activities_should_not_include_the_inactive_project_specific_activities
427 def test_activities_should_not_include_the_inactive_project_specific_activities
340 project = Project.find(1)
428 project = Project.find(1)
341 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
429 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
342 assert overridden_activity.save!
430 assert overridden_activity.save!
343
431
344 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
432 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
345 end
433 end
346
434
347 def test_activities_should_not_include_project_specific_activities_from_other_projects
435 def test_activities_should_not_include_project_specific_activities_from_other_projects
348 project = Project.find(1)
436 project = Project.find(1)
349 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
437 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
350 assert overridden_activity.save!
438 assert overridden_activity.save!
351
439
352 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
440 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
353 end
441 end
354
442
355 def test_activities_should_handle_nils
443 def test_activities_should_handle_nils
356 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
444 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
357 TimeEntryActivity.delete_all
445 TimeEntryActivity.delete_all
358
446
359 # No activities
447 # No activities
360 project = Project.find(1)
448 project = Project.find(1)
361 assert project.activities.empty?
449 assert project.activities.empty?
362
450
363 # No system, one overridden
451 # No system, one overridden
364 assert overridden_activity.save!
452 assert overridden_activity.save!
365 project.reload
453 project.reload
366 assert_equal [overridden_activity], project.activities
454 assert_equal [overridden_activity], project.activities
367 end
455 end
368
456
369 def test_activities_should_override_system_activities_with_project_activities
457 def test_activities_should_override_system_activities_with_project_activities
370 project = Project.find(1)
458 project = Project.find(1)
371 parent_activity = TimeEntryActivity.find(:first)
459 parent_activity = TimeEntryActivity.find(:first)
372 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
460 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
373 assert overridden_activity.save!
461 assert overridden_activity.save!
374
462
375 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
463 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
376 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
464 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
377 end
465 end
378
466
379 def test_activities_should_include_inactive_activities_if_specified
467 def test_activities_should_include_inactive_activities_if_specified
380 project = Project.find(1)
468 project = Project.find(1)
381 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
469 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
382 assert overridden_activity.save!
470 assert overridden_activity.save!
383
471
384 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
472 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
385 end
473 end
386
474
387 def test_close_completed_versions
475 def test_close_completed_versions
388 Version.update_all("status = 'open'")
476 Version.update_all("status = 'open'")
389 project = Project.find(1)
477 project = Project.find(1)
390 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
478 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
391 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
479 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
392 project.close_completed_versions
480 project.close_completed_versions
393 project.reload
481 project.reload
394 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
482 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
395 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
483 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
396 end
484 end
397
485
398 context "Project#copy" do
486 context "Project#copy" do
399 setup do
487 setup do
400 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
488 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
401 Project.destroy_all :identifier => "copy-test"
489 Project.destroy_all :identifier => "copy-test"
402 @source_project = Project.find(2)
490 @source_project = Project.find(2)
403 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
491 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
404 @project.trackers = @source_project.trackers
492 @project.trackers = @source_project.trackers
405 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
493 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
406 end
494 end
407
495
408 should "copy issues" do
496 should "copy issues" do
409 @source_project.issues << Issue.generate!(:status_id => 5,
497 @source_project.issues << Issue.generate!(:status_id => 5,
410 :subject => "copy issue status",
498 :subject => "copy issue status",
411 :tracker_id => 1,
499 :tracker_id => 1,
412 :assigned_to_id => 2,
500 :assigned_to_id => 2,
413 :project_id => @source_project.id)
501 :project_id => @source_project.id)
414 assert @project.valid?
502 assert @project.valid?
415 assert @project.issues.empty?
503 assert @project.issues.empty?
416 assert @project.copy(@source_project)
504 assert @project.copy(@source_project)
417
505
418 assert_equal @source_project.issues.size, @project.issues.size
506 assert_equal @source_project.issues.size, @project.issues.size
419 @project.issues.each do |issue|
507 @project.issues.each do |issue|
420 assert issue.valid?
508 assert issue.valid?
421 assert ! issue.assigned_to.blank?
509 assert ! issue.assigned_to.blank?
422 assert_equal @project, issue.project
510 assert_equal @project, issue.project
423 end
511 end
424
512
425 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
513 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
426 assert copied_issue
514 assert copied_issue
427 assert copied_issue.status
515 assert copied_issue.status
428 assert_equal "Closed", copied_issue.status.name
516 assert_equal "Closed", copied_issue.status.name
429 end
517 end
430
518
431 should "change the new issues to use the copied version" do
519 should "change the new issues to use the copied version" do
432 assigned_version = Version.generate!(:name => "Assigned Issues")
520 User.current = User.find(1)
521 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
433 @source_project.versions << assigned_version
522 @source_project.versions << assigned_version
434 assert_equal 1, @source_project.versions.size
523 assert_equal 3, @source_project.versions.size
435 @source_project.issues << Issue.generate!(:fixed_version_id => assigned_version.id,
524 Issue.generate_for_project!(@source_project,
436 :subject => "change the new issues to use the copied version",
525 :fixed_version_id => assigned_version.id,
437 :tracker_id => 1,
526 :subject => "change the new issues to use the copied version",
438 :project_id => @source_project.id)
527 :tracker_id => 1,
528 :project_id => @source_project.id)
439
529
440 assert @project.copy(@source_project)
530 assert @project.copy(@source_project)
441 @project.reload
531 @project.reload
442 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
532 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
443
533
444 assert copied_issue
534 assert copied_issue
445 assert copied_issue.fixed_version
535 assert copied_issue.fixed_version
446 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
536 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
447 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
537 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
448 end
538 end
449
539
450 should "copy members" do
540 should "copy members" do
451 assert @project.valid?
541 assert @project.valid?
452 assert @project.members.empty?
542 assert @project.members.empty?
453 assert @project.copy(@source_project)
543 assert @project.copy(@source_project)
454
544
455 assert_equal @source_project.members.size, @project.members.size
545 assert_equal @source_project.members.size, @project.members.size
456 @project.members.each do |member|
546 @project.members.each do |member|
457 assert member
547 assert member
458 assert_equal @project, member.project
548 assert_equal @project, member.project
459 end
549 end
460 end
550 end
461
551
462 should "copy project specific queries" do
552 should "copy project specific queries" do
463 assert @project.valid?
553 assert @project.valid?
464 assert @project.queries.empty?
554 assert @project.queries.empty?
465 assert @project.copy(@source_project)
555 assert @project.copy(@source_project)
466
556
467 assert_equal @source_project.queries.size, @project.queries.size
557 assert_equal @source_project.queries.size, @project.queries.size
468 @project.queries.each do |query|
558 @project.queries.each do |query|
469 assert query
559 assert query
470 assert_equal @project, query.project
560 assert_equal @project, query.project
471 end
561 end
472 end
562 end
473
563
474 should "copy versions" do
564 should "copy versions" do
475 @source_project.versions << Version.generate!
565 @source_project.versions << Version.generate!
476 @source_project.versions << Version.generate!
566 @source_project.versions << Version.generate!
477
567
478 assert @project.versions.empty?
568 assert @project.versions.empty?
479 assert @project.copy(@source_project)
569 assert @project.copy(@source_project)
480
570
481 assert_equal @source_project.versions.size, @project.versions.size
571 assert_equal @source_project.versions.size, @project.versions.size
482 @project.versions.each do |version|
572 @project.versions.each do |version|
483 assert version
573 assert version
484 assert_equal @project, version.project
574 assert_equal @project, version.project
485 end
575 end
486 end
576 end
487
577
488 should "copy wiki" do
578 should "copy wiki" do
489 assert_difference 'Wiki.count' do
579 assert_difference 'Wiki.count' do
490 assert @project.copy(@source_project)
580 assert @project.copy(@source_project)
491 end
581 end
492
582
493 assert @project.wiki
583 assert @project.wiki
494 assert_not_equal @source_project.wiki, @project.wiki
584 assert_not_equal @source_project.wiki, @project.wiki
495 assert_equal "Start page", @project.wiki.start_page
585 assert_equal "Start page", @project.wiki.start_page
496 end
586 end
497
587
498 should "copy wiki pages and content" do
588 should "copy wiki pages and content" do
499 assert @project.copy(@source_project)
589 assert @project.copy(@source_project)
500
590
501 assert @project.wiki
591 assert @project.wiki
502 assert_equal 1, @project.wiki.pages.length
592 assert_equal 1, @project.wiki.pages.length
503
593
504 @project.wiki.pages.each do |wiki_page|
594 @project.wiki.pages.each do |wiki_page|
505 assert wiki_page.content
595 assert wiki_page.content
506 assert !@source_project.wiki.pages.include?(wiki_page)
596 assert !@source_project.wiki.pages.include?(wiki_page)
507 end
597 end
508 end
598 end
509
599
510 should "copy custom fields"
600 should "copy custom fields"
511
601
512 should "copy issue categories" do
602 should "copy issue categories" do
513 assert @project.copy(@source_project)
603 assert @project.copy(@source_project)
514
604
515 assert_equal 2, @project.issue_categories.size
605 assert_equal 2, @project.issue_categories.size
516 @project.issue_categories.each do |issue_category|
606 @project.issue_categories.each do |issue_category|
517 assert !@source_project.issue_categories.include?(issue_category)
607 assert !@source_project.issue_categories.include?(issue_category)
518 end
608 end
519 end
609 end
520
610
521 should "copy boards" do
611 should "copy boards" do
522 assert @project.copy(@source_project)
612 assert @project.copy(@source_project)
523
613
524 assert_equal 1, @project.boards.size
614 assert_equal 1, @project.boards.size
525 @project.boards.each do |board|
615 @project.boards.each do |board|
526 assert !@source_project.boards.include?(board)
616 assert !@source_project.boards.include?(board)
527 end
617 end
528 end
618 end
529
619
530 should "change the new issues to use the copied issue categories" do
620 should "change the new issues to use the copied issue categories" do
531 issue = Issue.find(4)
621 issue = Issue.find(4)
532 issue.update_attribute(:category_id, 3)
622 issue.update_attribute(:category_id, 3)
533
623
534 assert @project.copy(@source_project)
624 assert @project.copy(@source_project)
535
625
536 @project.issues.each do |issue|
626 @project.issues.each do |issue|
537 assert issue.category
627 assert issue.category
538 assert_equal "Stock management", issue.category.name # Same name
628 assert_equal "Stock management", issue.category.name # Same name
539 assert_not_equal IssueCategory.find(3), issue.category # Different record
629 assert_not_equal IssueCategory.find(3), issue.category # Different record
540 end
630 end
541 end
631 end
542
632
543 should "limit copy with :only option" do
633 should "limit copy with :only option" do
544 assert @project.members.empty?
634 assert @project.members.empty?
545 assert @project.issue_categories.empty?
635 assert @project.issue_categories.empty?
546 assert @source_project.issues.any?
636 assert @source_project.issues.any?
547
637
548 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
638 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
549
639
550 assert @project.members.any?
640 assert @project.members.any?
551 assert @project.issue_categories.any?
641 assert @project.issue_categories.any?
552 assert @project.issues.empty?
642 assert @project.issues.empty?
553 end
643 end
554
644
555 should "copy issue relations"
645 should "copy issue relations"
556 should "link issue relations if cross project issue relations are valid"
646 should "link issue relations if cross project issue relations are valid"
557
647
558 end
648 end
559
649
560 end
650 end
@@ -1,339 +1,347
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def find_issues_with_query(query)
29 def find_issues_with_query(query)
30 Issue.find :all,
30 Issue.find :all,
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
32 :conditions => query.statement
32 :conditions => query.statement
33 end
33 end
34
35 def test_query_should_allow_shared_versions_for_a_project_query
36 subproject_version = Version.find(4)
37 query = Query.new(:project => Project.find(1), :name => '_')
38 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
39
40 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
41 end
34
42
35 def test_query_with_multiple_custom_fields
43 def test_query_with_multiple_custom_fields
36 query = Query.find(1)
44 query = Query.find(1)
37 assert query.valid?
45 assert query.valid?
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
46 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
39 issues = find_issues_with_query(query)
47 issues = find_issues_with_query(query)
40 assert_equal 1, issues.length
48 assert_equal 1, issues.length
41 assert_equal Issue.find(3), issues.first
49 assert_equal Issue.find(3), issues.first
42 end
50 end
43
51
44 def test_operator_none
52 def test_operator_none
45 query = Query.new(:project => Project.find(1), :name => '_')
53 query = Query.new(:project => Project.find(1), :name => '_')
46 query.add_filter('fixed_version_id', '!*', [''])
54 query.add_filter('fixed_version_id', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
55 query.add_filter('cf_1', '!*', [''])
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
56 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
57 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
50 find_issues_with_query(query)
58 find_issues_with_query(query)
51 end
59 end
52
60
53 def test_operator_none_for_integer
61 def test_operator_none_for_integer
54 query = Query.new(:project => Project.find(1), :name => '_')
62 query = Query.new(:project => Project.find(1), :name => '_')
55 query.add_filter('estimated_hours', '!*', [''])
63 query.add_filter('estimated_hours', '!*', [''])
56 issues = find_issues_with_query(query)
64 issues = find_issues_with_query(query)
57 assert !issues.empty?
65 assert !issues.empty?
58 assert issues.all? {|i| !i.estimated_hours}
66 assert issues.all? {|i| !i.estimated_hours}
59 end
67 end
60
68
61 def test_operator_all
69 def test_operator_all
62 query = Query.new(:project => Project.find(1), :name => '_')
70 query = Query.new(:project => Project.find(1), :name => '_')
63 query.add_filter('fixed_version_id', '*', [''])
71 query.add_filter('fixed_version_id', '*', [''])
64 query.add_filter('cf_1', '*', [''])
72 query.add_filter('cf_1', '*', [''])
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
73 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
74 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
67 find_issues_with_query(query)
75 find_issues_with_query(query)
68 end
76 end
69
77
70 def test_operator_greater_than
78 def test_operator_greater_than
71 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
72 query.add_filter('done_ratio', '>=', ['40'])
80 query.add_filter('done_ratio', '>=', ['40'])
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
81 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
74 find_issues_with_query(query)
82 find_issues_with_query(query)
75 end
83 end
76
84
77 def test_operator_in_more_than
85 def test_operator_in_more_than
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
86 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
79 query = Query.new(:project => Project.find(1), :name => '_')
87 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('due_date', '>t+', ['15'])
88 query.add_filter('due_date', '>t+', ['15'])
81 issues = find_issues_with_query(query)
89 issues = find_issues_with_query(query)
82 assert !issues.empty?
90 assert !issues.empty?
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
91 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
84 end
92 end
85
93
86 def test_operator_in_less_than
94 def test_operator_in_less_than
87 query = Query.new(:project => Project.find(1), :name => '_')
95 query = Query.new(:project => Project.find(1), :name => '_')
88 query.add_filter('due_date', '<t+', ['15'])
96 query.add_filter('due_date', '<t+', ['15'])
89 issues = find_issues_with_query(query)
97 issues = find_issues_with_query(query)
90 assert !issues.empty?
98 assert !issues.empty?
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
99 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
92 end
100 end
93
101
94 def test_operator_less_than_ago
102 def test_operator_less_than_ago
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
103 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
96 query = Query.new(:project => Project.find(1), :name => '_')
104 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('due_date', '>t-', ['3'])
105 query.add_filter('due_date', '>t-', ['3'])
98 issues = find_issues_with_query(query)
106 issues = find_issues_with_query(query)
99 assert !issues.empty?
107 assert !issues.empty?
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
108 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
101 end
109 end
102
110
103 def test_operator_more_than_ago
111 def test_operator_more_than_ago
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
112 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
105 query = Query.new(:project => Project.find(1), :name => '_')
113 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('due_date', '<t-', ['10'])
114 query.add_filter('due_date', '<t-', ['10'])
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
115 assert query.statement.include?("#{Issue.table_name}.due_date <=")
108 issues = find_issues_with_query(query)
116 issues = find_issues_with_query(query)
109 assert !issues.empty?
117 assert !issues.empty?
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
118 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
111 end
119 end
112
120
113 def test_operator_in
121 def test_operator_in
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
122 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
115 query = Query.new(:project => Project.find(1), :name => '_')
123 query = Query.new(:project => Project.find(1), :name => '_')
116 query.add_filter('due_date', 't+', ['2'])
124 query.add_filter('due_date', 't+', ['2'])
117 issues = find_issues_with_query(query)
125 issues = find_issues_with_query(query)
118 assert !issues.empty?
126 assert !issues.empty?
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
127 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
120 end
128 end
121
129
122 def test_operator_ago
130 def test_operator_ago
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
131 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
124 query = Query.new(:project => Project.find(1), :name => '_')
132 query = Query.new(:project => Project.find(1), :name => '_')
125 query.add_filter('due_date', 't-', ['3'])
133 query.add_filter('due_date', 't-', ['3'])
126 issues = find_issues_with_query(query)
134 issues = find_issues_with_query(query)
127 assert !issues.empty?
135 assert !issues.empty?
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
136 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
129 end
137 end
130
138
131 def test_operator_today
139 def test_operator_today
132 query = Query.new(:project => Project.find(1), :name => '_')
140 query = Query.new(:project => Project.find(1), :name => '_')
133 query.add_filter('due_date', 't', [''])
141 query.add_filter('due_date', 't', [''])
134 issues = find_issues_with_query(query)
142 issues = find_issues_with_query(query)
135 assert !issues.empty?
143 assert !issues.empty?
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
144 issues.each {|issue| assert_equal Date.today, issue.due_date}
137 end
145 end
138
146
139 def test_operator_this_week_on_date
147 def test_operator_this_week_on_date
140 query = Query.new(:project => Project.find(1), :name => '_')
148 query = Query.new(:project => Project.find(1), :name => '_')
141 query.add_filter('due_date', 'w', [''])
149 query.add_filter('due_date', 'w', [''])
142 find_issues_with_query(query)
150 find_issues_with_query(query)
143 end
151 end
144
152
145 def test_operator_this_week_on_datetime
153 def test_operator_this_week_on_datetime
146 query = Query.new(:project => Project.find(1), :name => '_')
154 query = Query.new(:project => Project.find(1), :name => '_')
147 query.add_filter('created_on', 'w', [''])
155 query.add_filter('created_on', 'w', [''])
148 find_issues_with_query(query)
156 find_issues_with_query(query)
149 end
157 end
150
158
151 def test_operator_contains
159 def test_operator_contains
152 query = Query.new(:project => Project.find(1), :name => '_')
160 query = Query.new(:project => Project.find(1), :name => '_')
153 query.add_filter('subject', '~', ['uNable'])
161 query.add_filter('subject', '~', ['uNable'])
154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
162 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
155 result = find_issues_with_query(query)
163 result = find_issues_with_query(query)
156 assert result.empty?
164 assert result.empty?
157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
165 result.each {|issue| assert issue.subject.downcase.include?('unable') }
158 end
166 end
159
167
160 def test_operator_does_not_contains
168 def test_operator_does_not_contains
161 query = Query.new(:project => Project.find(1), :name => '_')
169 query = Query.new(:project => Project.find(1), :name => '_')
162 query.add_filter('subject', '!~', ['uNable'])
170 query.add_filter('subject', '!~', ['uNable'])
163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
171 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
164 find_issues_with_query(query)
172 find_issues_with_query(query)
165 end
173 end
166
174
167 def test_filter_watched_issues
175 def test_filter_watched_issues
168 User.current = User.find(1)
176 User.current = User.find(1)
169 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
177 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
170 result = find_issues_with_query(query)
178 result = find_issues_with_query(query)
171 assert_not_nil result
179 assert_not_nil result
172 assert !result.empty?
180 assert !result.empty?
173 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
181 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
174 User.current = nil
182 User.current = nil
175 end
183 end
176
184
177 def test_filter_unwatched_issues
185 def test_filter_unwatched_issues
178 User.current = User.find(1)
186 User.current = User.find(1)
179 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
187 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
180 result = find_issues_with_query(query)
188 result = find_issues_with_query(query)
181 assert_not_nil result
189 assert_not_nil result
182 assert !result.empty?
190 assert !result.empty?
183 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
191 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
184 User.current = nil
192 User.current = nil
185 end
193 end
186
194
187 def test_default_columns
195 def test_default_columns
188 q = Query.new
196 q = Query.new
189 assert !q.columns.empty?
197 assert !q.columns.empty?
190 end
198 end
191
199
192 def test_set_column_names
200 def test_set_column_names
193 q = Query.new
201 q = Query.new
194 q.column_names = ['tracker', :subject, '', 'unknonw_column']
202 q.column_names = ['tracker', :subject, '', 'unknonw_column']
195 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
203 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
196 c = q.columns.first
204 c = q.columns.first
197 assert q.has_column?(c)
205 assert q.has_column?(c)
198 end
206 end
199
207
200 def test_groupable_columns_should_include_custom_fields
208 def test_groupable_columns_should_include_custom_fields
201 q = Query.new
209 q = Query.new
202 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
210 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
203 end
211 end
204
212
205 def test_default_sort
213 def test_default_sort
206 q = Query.new
214 q = Query.new
207 assert_equal [], q.sort_criteria
215 assert_equal [], q.sort_criteria
208 end
216 end
209
217
210 def test_set_sort_criteria_with_hash
218 def test_set_sort_criteria_with_hash
211 q = Query.new
219 q = Query.new
212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
220 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
221 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
214 end
222 end
215
223
216 def test_set_sort_criteria_with_array
224 def test_set_sort_criteria_with_array
217 q = Query.new
225 q = Query.new
218 q.sort_criteria = [['priority', 'desc'], 'tracker']
226 q.sort_criteria = [['priority', 'desc'], 'tracker']
219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
220 end
228 end
221
229
222 def test_create_query_with_sort
230 def test_create_query_with_sort
223 q = Query.new(:name => 'Sorted')
231 q = Query.new(:name => 'Sorted')
224 q.sort_criteria = [['priority', 'desc'], 'tracker']
232 q.sort_criteria = [['priority', 'desc'], 'tracker']
225 assert q.save
233 assert q.save
226 q.reload
234 q.reload
227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
235 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
228 end
236 end
229
237
230 def test_sort_by_string_custom_field_asc
238 def test_sort_by_string_custom_field_asc
231 q = Query.new
239 q = Query.new
232 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
240 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
233 assert c
241 assert c
234 assert c.sortable
242 assert c.sortable
235 issues = Issue.find :all,
243 issues = Issue.find :all,
236 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
244 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
237 :conditions => q.statement,
245 :conditions => q.statement,
238 :order => "#{c.sortable} ASC"
246 :order => "#{c.sortable} ASC"
239 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
247 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
240 assert !values.empty?
248 assert !values.empty?
241 assert_equal values.sort, values
249 assert_equal values.sort, values
242 end
250 end
243
251
244 def test_sort_by_string_custom_field_desc
252 def test_sort_by_string_custom_field_desc
245 q = Query.new
253 q = Query.new
246 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
254 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
247 assert c
255 assert c
248 assert c.sortable
256 assert c.sortable
249 issues = Issue.find :all,
257 issues = Issue.find :all,
250 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
258 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
251 :conditions => q.statement,
259 :conditions => q.statement,
252 :order => "#{c.sortable} DESC"
260 :order => "#{c.sortable} DESC"
253 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
261 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
254 assert !values.empty?
262 assert !values.empty?
255 assert_equal values.sort.reverse, values
263 assert_equal values.sort.reverse, values
256 end
264 end
257
265
258 def test_sort_by_float_custom_field_asc
266 def test_sort_by_float_custom_field_asc
259 q = Query.new
267 q = Query.new
260 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
268 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
261 assert c
269 assert c
262 assert c.sortable
270 assert c.sortable
263 issues = Issue.find :all,
271 issues = Issue.find :all,
264 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
272 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
265 :conditions => q.statement,
273 :conditions => q.statement,
266 :order => "#{c.sortable} ASC"
274 :order => "#{c.sortable} ASC"
267 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
275 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
268 assert !values.empty?
276 assert !values.empty?
269 assert_equal values.sort, values
277 assert_equal values.sort, values
270 end
278 end
271
279
272 def test_invalid_query_should_raise_query_statement_invalid_error
280 def test_invalid_query_should_raise_query_statement_invalid_error
273 q = Query.new
281 q = Query.new
274 assert_raise Query::StatementInvalid do
282 assert_raise Query::StatementInvalid do
275 q.issues(:conditions => "foo = 1")
283 q.issues(:conditions => "foo = 1")
276 end
284 end
277 end
285 end
278
286
279 def test_issue_count_by_association_group
287 def test_issue_count_by_association_group
280 q = Query.new(:name => '_', :group_by => 'assigned_to')
288 q = Query.new(:name => '_', :group_by => 'assigned_to')
281 count_by_group = q.issue_count_by_group
289 count_by_group = q.issue_count_by_group
282 assert_kind_of Hash, count_by_group
290 assert_kind_of Hash, count_by_group
283 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
291 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
284 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
292 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
285 assert count_by_group.has_key?(User.find(3))
293 assert count_by_group.has_key?(User.find(3))
286 end
294 end
287
295
288 def test_issue_count_by_list_custom_field_group
296 def test_issue_count_by_list_custom_field_group
289 q = Query.new(:name => '_', :group_by => 'cf_1')
297 q = Query.new(:name => '_', :group_by => 'cf_1')
290 count_by_group = q.issue_count_by_group
298 count_by_group = q.issue_count_by_group
291 assert_kind_of Hash, count_by_group
299 assert_kind_of Hash, count_by_group
292 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
300 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
293 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
301 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
294 assert count_by_group.has_key?('MySQL')
302 assert count_by_group.has_key?('MySQL')
295 end
303 end
296
304
297 def test_issue_count_by_date_custom_field_group
305 def test_issue_count_by_date_custom_field_group
298 q = Query.new(:name => '_', :group_by => 'cf_8')
306 q = Query.new(:name => '_', :group_by => 'cf_8')
299 count_by_group = q.issue_count_by_group
307 count_by_group = q.issue_count_by_group
300 assert_kind_of Hash, count_by_group
308 assert_kind_of Hash, count_by_group
301 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
309 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
302 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
310 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
303 end
311 end
304
312
305 def test_label_for
313 def test_label_for
306 q = Query.new
314 q = Query.new
307 assert_equal 'assigned_to', q.label_for('assigned_to_id')
315 assert_equal 'assigned_to', q.label_for('assigned_to_id')
308 end
316 end
309
317
310 def test_editable_by
318 def test_editable_by
311 admin = User.find(1)
319 admin = User.find(1)
312 manager = User.find(2)
320 manager = User.find(2)
313 developer = User.find(3)
321 developer = User.find(3)
314
322
315 # Public query on project 1
323 # Public query on project 1
316 q = Query.find(1)
324 q = Query.find(1)
317 assert q.editable_by?(admin)
325 assert q.editable_by?(admin)
318 assert q.editable_by?(manager)
326 assert q.editable_by?(manager)
319 assert !q.editable_by?(developer)
327 assert !q.editable_by?(developer)
320
328
321 # Private query on project 1
329 # Private query on project 1
322 q = Query.find(2)
330 q = Query.find(2)
323 assert q.editable_by?(admin)
331 assert q.editable_by?(admin)
324 assert !q.editable_by?(manager)
332 assert !q.editable_by?(manager)
325 assert q.editable_by?(developer)
333 assert q.editable_by?(developer)
326
334
327 # Private query for all projects
335 # Private query for all projects
328 q = Query.find(3)
336 q = Query.find(3)
329 assert q.editable_by?(admin)
337 assert q.editable_by?(admin)
330 assert !q.editable_by?(manager)
338 assert !q.editable_by?(manager)
331 assert q.editable_by?(developer)
339 assert q.editable_by?(developer)
332
340
333 # Public query for all projects
341 # Public query for all projects
334 q = Query.find(4)
342 q = Query.find(4)
335 assert q.editable_by?(admin)
343 assert q.editable_by?(admin)
336 assert !q.editable_by?(manager)
344 assert !q.editable_by?(manager)
337 assert !q.editable_by?(developer)
345 assert !q.editable_by?(developer)
338 end
346 end
339 end
347 end
@@ -1,121 +1,156
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class VersionTest < ActiveSupport::TestCase
20 class VersionTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :issues, :issue_statuses, :trackers, :enumerations, :versions
21 fixtures :projects, :users, :issues, :issue_statuses, :trackers, :enumerations, :versions
22
22
23 def setup
23 def setup
24 end
24 end
25
25
26 def test_create
26 def test_create
27 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '2011-03-25')
27 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '2011-03-25')
28 assert v.save
28 assert v.save
29 assert_equal 'open', v.status
29 assert_equal 'open', v.status
30 end
30 end
31
31
32 def test_invalid_effective_date_validation
32 def test_invalid_effective_date_validation
33 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '99999-01-01')
33 v = Version.new(:project => Project.find(1), :name => '1.1', :effective_date => '99999-01-01')
34 assert !v.save
34 assert !v.save
35 assert_equal I18n.translate('activerecord.errors.messages.not_a_date'), v.errors.on(:effective_date)
35 assert_equal I18n.translate('activerecord.errors.messages.not_a_date'), v.errors.on(:effective_date)
36 end
36 end
37
37
38 def test_progress_should_be_0_with_no_assigned_issues
38 def test_progress_should_be_0_with_no_assigned_issues
39 project = Project.find(1)
39 project = Project.find(1)
40 v = Version.create!(:project => project, :name => 'Progress')
40 v = Version.create!(:project => project, :name => 'Progress')
41 assert_equal 0, v.completed_pourcent
41 assert_equal 0, v.completed_pourcent
42 assert_equal 0, v.closed_pourcent
42 assert_equal 0, v.closed_pourcent
43 end
43 end
44
44
45 def test_progress_should_be_0_with_unbegun_assigned_issues
45 def test_progress_should_be_0_with_unbegun_assigned_issues
46 project = Project.find(1)
46 project = Project.find(1)
47 v = Version.create!(:project => project, :name => 'Progress')
47 v = Version.create!(:project => project, :name => 'Progress')
48 add_issue(v)
48 add_issue(v)
49 add_issue(v, :done_ratio => 0)
49 add_issue(v, :done_ratio => 0)
50 assert_progress_equal 0, v.completed_pourcent
50 assert_progress_equal 0, v.completed_pourcent
51 assert_progress_equal 0, v.closed_pourcent
51 assert_progress_equal 0, v.closed_pourcent
52 end
52 end
53
53
54 def test_progress_should_be_100_with_closed_assigned_issues
54 def test_progress_should_be_100_with_closed_assigned_issues
55 project = Project.find(1)
55 project = Project.find(1)
56 status = IssueStatus.find(:first, :conditions => {:is_closed => true})
56 status = IssueStatus.find(:first, :conditions => {:is_closed => true})
57 v = Version.create!(:project => project, :name => 'Progress')
57 v = Version.create!(:project => project, :name => 'Progress')
58 add_issue(v, :status => status)
58 add_issue(v, :status => status)
59 add_issue(v, :status => status, :done_ratio => 20)
59 add_issue(v, :status => status, :done_ratio => 20)
60 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
60 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
61 add_issue(v, :status => status, :estimated_hours => 15)
61 add_issue(v, :status => status, :estimated_hours => 15)
62 assert_progress_equal 100.0, v.completed_pourcent
62 assert_progress_equal 100.0, v.completed_pourcent
63 assert_progress_equal 100.0, v.closed_pourcent
63 assert_progress_equal 100.0, v.closed_pourcent
64 end
64 end
65
65
66 def test_progress_should_consider_done_ratio_of_open_assigned_issues
66 def test_progress_should_consider_done_ratio_of_open_assigned_issues
67 project = Project.find(1)
67 project = Project.find(1)
68 v = Version.create!(:project => project, :name => 'Progress')
68 v = Version.create!(:project => project, :name => 'Progress')
69 add_issue(v)
69 add_issue(v)
70 add_issue(v, :done_ratio => 20)
70 add_issue(v, :done_ratio => 20)
71 add_issue(v, :done_ratio => 70)
71 add_issue(v, :done_ratio => 70)
72 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_pourcent
72 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_pourcent
73 assert_progress_equal 0, v.closed_pourcent
73 assert_progress_equal 0, v.closed_pourcent
74 end
74 end
75
75
76 def test_progress_should_consider_closed_issues_as_completed
76 def test_progress_should_consider_closed_issues_as_completed
77 project = Project.find(1)
77 project = Project.find(1)
78 v = Version.create!(:project => project, :name => 'Progress')
78 v = Version.create!(:project => project, :name => 'Progress')
79 add_issue(v)
79 add_issue(v)
80 add_issue(v, :done_ratio => 20)
80 add_issue(v, :done_ratio => 20)
81 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
81 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
82 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_pourcent
82 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_pourcent
83 assert_progress_equal (100.0)/3, v.closed_pourcent
83 assert_progress_equal (100.0)/3, v.closed_pourcent
84 end
84 end
85
85
86 def test_progress_should_consider_estimated_hours_to_weigth_issues
86 def test_progress_should_consider_estimated_hours_to_weigth_issues
87 project = Project.find(1)
87 project = Project.find(1)
88 v = Version.create!(:project => project, :name => 'Progress')
88 v = Version.create!(:project => project, :name => 'Progress')
89 add_issue(v, :estimated_hours => 10)
89 add_issue(v, :estimated_hours => 10)
90 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
90 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
91 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
91 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
92 add_issue(v, :estimated_hours => 25, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
92 add_issue(v, :estimated_hours => 25, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
93 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_pourcent
93 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_pourcent
94 assert_progress_equal 25.0/95.0*100, v.closed_pourcent
94 assert_progress_equal 25.0/95.0*100, v.closed_pourcent
95 end
95 end
96
96
97 def test_progress_should_consider_average_estimated_hours_to_weigth_unestimated_issues
97 def test_progress_should_consider_average_estimated_hours_to_weigth_unestimated_issues
98 project = Project.find(1)
98 project = Project.find(1)
99 v = Version.create!(:project => project, :name => 'Progress')
99 v = Version.create!(:project => project, :name => 'Progress')
100 add_issue(v, :done_ratio => 20)
100 add_issue(v, :done_ratio => 20)
101 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
101 add_issue(v, :status => IssueStatus.find(:first, :conditions => {:is_closed => true}))
102 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
102 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
103 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
103 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
104 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent
104 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_pourcent
105 assert_progress_equal 25.0/100.0*100, v.closed_pourcent
105 assert_progress_equal 25.0/100.0*100, v.closed_pourcent
106 end
106 end
107
108 test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
109 User.current = User.find(1) # Need the admin's permissions
110
111 @version = Version.find(7)
112 # Separate hierarchy
113 project_1_issue = Issue.find(1)
114 project_1_issue.fixed_version = @version
115 assert project_1_issue.save, project_1_issue.errors.full_messages
116
117 project_5_issue = Issue.find(6)
118 project_5_issue.fixed_version = @version
119 assert project_5_issue.save
120
121 # Project
122 project_2_issue = Issue.find(4)
123 project_2_issue.fixed_version = @version
124 assert project_2_issue.save
125
126 # Update the sharing
127 @version.sharing = 'none'
128 assert @version.save
129
130 # Project 1 now out of the shared scope
131 project_1_issue.reload
132 assert_equal nil, project_1_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
133
134 # Project 5 now out of the shared scope
135 project_5_issue.reload
136 assert_equal nil, project_5_issue.fixed_version, "Fixed version is still set after changing the Version's sharing"
137
138 # Project 2 issue remains
139 project_2_issue.reload
140 assert_equal @version, project_2_issue.fixed_version
141 end
107
142
108 private
143 private
109
144
110 def add_issue(version, attributes={})
145 def add_issue(version, attributes={})
111 Issue.create!({:project => version.project,
146 Issue.create!({:project => version.project,
112 :fixed_version => version,
147 :fixed_version => version,
113 :subject => 'Test',
148 :subject => 'Test',
114 :author => User.find(:first),
149 :author => User.find(:first),
115 :tracker => version.project.trackers.find(:first)}.merge(attributes))
150 :tracker => version.project.trackers.find(:first)}.merge(attributes))
116 end
151 end
117
152
118 def assert_progress_equal(expected_float, actual_float, message="")
153 def assert_progress_equal(expected_float, actual_float, message="")
119 assert_in_delta(expected_float, actual_float, 0.000001, message="")
154 assert_in_delta(expected_float, actual_float, 0.000001, message="")
120 end
155 end
121 end
156 end
General Comments 0
You need to be logged in to leave comments. Login now