##// END OF EJS Templates
Allow additional workflow transitions for issue author and assignee (#2732)....
Jean-Philippe Lang -
r4775:4b096e9a5676
parent child
Show More
@@ -0,0 +1,40
1 <table class="list transitions-<%= name %>">
2 <thead>
3 <tr>
4 <th align="left">
5 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input')",
6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
7 <%=l(:label_current_status)%>
8 </th>
9 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
10 </tr>
11 <tr>
12 <td></td>
13 <% for new_status in @statuses %>
14 <td width="<%= 75 / @statuses.size %>%" align="center">
15 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.new-status-#{new_status.id}')",
16 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
17 <%=h new_status.name %>
18 </td>
19 <% end %>
20 </tr>
21 </thead>
22 <tbody>
23 <% for old_status in @statuses %>
24 <tr class="<%= cycle("odd", "even") %>">
25 <td>
26 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('table.transitions-#{name} input.old-status-#{old_status.id}')",
27 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
28
29 <%=h old_status.name %>
30 </td>
31 <% for new_status in @statuses -%>
32 <td align="center">
33 <%= check_box_tag "issue_status[#{ old_status.id }][#{new_status.id}][]", name, workflows.detect {|w| w.old_status_id == old_status.id && w.new_status_id == new_status.id},
34 :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
35 </td>
36 <% end -%>
37 </tr>
38 <% end %>
39 </tbody>
40 </table> No newline at end of file
@@ -0,0 +1,13
1 class AddWorkflowsAssigneeAndAuthor < ActiveRecord::Migration
2 def self.up
3 add_column :workflows, :assignee, :boolean, :null => false, :default => false
4 add_column :workflows, :author, :boolean, :null => false, :default => false
5 Workflow.update_all("assignee = #{Workflow.connection.quoted_false}")
6 Workflow.update_all("author = #{Workflow.connection.quoted_false}")
7 end
8
9 def self.down
10 remove_column :workflows, :assignee
11 remove_column :workflows, :author
12 end
13 end
@@ -1,91 +1,102
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 WorkflowsController < ApplicationController
18 class WorkflowsController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin
21 before_filter :require_admin
22 before_filter :find_roles
22 before_filter :find_roles
23 before_filter :find_trackers
23 before_filter :find_trackers
24
24
25 def index
25 def index
26 @workflow_counts = Workflow.count_by_tracker_and_role
26 @workflow_counts = Workflow.count_by_tracker_and_role
27 end
27 end
28
28
29 def edit
29 def edit
30 @role = Role.find_by_id(params[:role_id])
30 @role = Role.find_by_id(params[:role_id])
31 @tracker = Tracker.find_by_id(params[:tracker_id])
31 @tracker = Tracker.find_by_id(params[:tracker_id])
32
32
33 if request.post?
33 if request.post?
34 Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
34 Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
35 (params[:issue_status] || []).each { |old, news|
35 (params[:issue_status] || []).each { |status_id, transitions|
36 news.each { |new|
36 transitions.each { |new_status_id, options|
37 @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => old, :new_status_id => new)
37 author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
38 assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
39 @role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
38 }
40 }
39 }
41 }
40 if @role.save
42 if @role.save
41 flash[:notice] = l(:notice_successful_update)
43 flash[:notice] = l(:notice_successful_update)
42 redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
44 redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
45 return
43 end
46 end
44 end
47 end
45
48
46 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
49 @used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
47 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
50 if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
48 @statuses = @tracker.issue_statuses
51 @statuses = @tracker.issue_statuses
49 end
52 end
50 @statuses ||= IssueStatus.find(:all, :order => 'position')
53 @statuses ||= IssueStatus.find(:all, :order => 'position')
54
55 if @tracker && @role && @statuses.any?
56 workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
57 @workflows = {}
58 @workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
59 @workflows['author'] = workflows.select {|w| w.author}
60 @workflows['assignee'] = workflows.select {|w| w.assignee}
61 end
51 end
62 end
52
63
53 def copy
64 def copy
54
65
55 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
66 if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
56 @source_tracker = nil
67 @source_tracker = nil
57 else
68 else
58 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
69 @source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
59 end
70 end
60 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
71 if params[:source_role_id].blank? || params[:source_role_id] == 'any'
61 @source_role = nil
72 @source_role = nil
62 else
73 else
63 @source_role = Role.find_by_id(params[:source_role_id].to_i)
74 @source_role = Role.find_by_id(params[:source_role_id].to_i)
64 end
75 end
65
76
66 @target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
77 @target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
67 @target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
78 @target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
68
79
69 if request.post?
80 if request.post?
70 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
81 if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
71 flash.now[:error] = l(:error_workflow_copy_source)
82 flash.now[:error] = l(:error_workflow_copy_source)
72 elsif @target_trackers.nil? || @target_roles.nil?
83 elsif @target_trackers.nil? || @target_roles.nil?
73 flash.now[:error] = l(:error_workflow_copy_target)
84 flash.now[:error] = l(:error_workflow_copy_target)
74 else
85 else
75 Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
86 Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
76 flash[:notice] = l(:notice_successful_update)
87 flash[:notice] = l(:notice_successful_update)
77 redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
88 redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
78 end
89 end
79 end
90 end
80 end
91 end
81
92
82 private
93 private
83
94
84 def find_roles
95 def find_roles
85 @roles = Role.find(:all, :order => 'builtin, position')
96 @roles = Role.find(:all, :order => 'builtin, position')
86 end
97 end
87
98
88 def find_trackers
99 def find_trackers
89 @trackers = Tracker.find(:all, :order => 'position')
100 @trackers = Tracker.find(:all, :order => 'position')
90 end
101 end
91 end
102 end
@@ -1,876 +1,881
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 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_remove => :attachment_removed
38 acts_as_attachable :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61
61
62 named_scope :visible, lambda {|*args| { :include => :project,
62 named_scope :visible, lambda {|*args| { :include => :project,
63 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
63 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
64
64
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
65 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66
66
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
67 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
68 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
69 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
70 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71 named_scope :for_gantt, lambda {
71 named_scope :for_gantt, lambda {
72 {
72 {
73 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version]
73 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version]
74 }
74 }
75 }
75 }
76
76
77 named_scope :without_version, lambda {
77 named_scope :without_version, lambda {
78 {
78 {
79 :conditions => { :fixed_version_id => nil}
79 :conditions => { :fixed_version_id => nil}
80 }
80 }
81 }
81 }
82
82
83 named_scope :with_query, lambda {|query|
83 named_scope :with_query, lambda {|query|
84 {
84 {
85 :conditions => Query.merge_conditions(query.statement)
85 :conditions => Query.merge_conditions(query.statement)
86 }
86 }
87 }
87 }
88
88
89 before_create :default_assign
89 before_create :default_assign
90 before_save :close_duplicates, :update_done_ratio_from_issue_status
90 before_save :close_duplicates, :update_done_ratio_from_issue_status
91 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
91 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
92 after_destroy :update_parent_attributes
92 after_destroy :update_parent_attributes
93
93
94 # Returns true if usr or current user is allowed to view the issue
94 # Returns true if usr or current user is allowed to view the issue
95 def visible?(usr=nil)
95 def visible?(usr=nil)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
96 (usr || User.current).allowed_to?(:view_issues, self.project)
97 end
97 end
98
98
99 def after_initialize
99 def after_initialize
100 if new_record?
100 if new_record?
101 # set default values for new records only
101 # set default values for new records only
102 self.status ||= IssueStatus.default
102 self.status ||= IssueStatus.default
103 self.priority ||= IssuePriority.default
103 self.priority ||= IssuePriority.default
104 end
104 end
105 end
105 end
106
106
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
107 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
108 def available_custom_fields
108 def available_custom_fields
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
109 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
110 end
110 end
111
111
112 def copy_from(arg)
112 def copy_from(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
113 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
114 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
115 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
116 self.status = issue.status
116 self.status = issue.status
117 self
117 self
118 end
118 end
119
119
120 # Moves/copies an issue to a new project and tracker
120 # Moves/copies an issue to a new project and tracker
121 # Returns the moved/copied issue on success, false on failure
121 # Returns the moved/copied issue on success, false on failure
122 def move_to_project(*args)
122 def move_to_project(*args)
123 ret = Issue.transaction do
123 ret = Issue.transaction do
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
124 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
125 end || false
125 end || false
126 end
126 end
127
127
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
128 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
129 options ||= {}
129 options ||= {}
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
130 issue = options[:copy] ? self.class.new.copy_from(self) : self
131
131
132 if new_project && issue.project_id != new_project.id
132 if new_project && issue.project_id != new_project.id
133 # delete issue relations
133 # delete issue relations
134 unless Setting.cross_project_issue_relations?
134 unless Setting.cross_project_issue_relations?
135 issue.relations_from.clear
135 issue.relations_from.clear
136 issue.relations_to.clear
136 issue.relations_to.clear
137 end
137 end
138 # issue is moved to another project
138 # issue is moved to another project
139 # reassign to the category with same name if any
139 # reassign to the category with same name if any
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
140 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
141 issue.category = new_category
141 issue.category = new_category
142 # Keep the fixed_version if it's still valid in the new_project
142 # Keep the fixed_version if it's still valid in the new_project
143 unless new_project.shared_versions.include?(issue.fixed_version)
143 unless new_project.shared_versions.include?(issue.fixed_version)
144 issue.fixed_version = nil
144 issue.fixed_version = nil
145 end
145 end
146 issue.project = new_project
146 issue.project = new_project
147 if issue.parent && issue.parent.project_id != issue.project_id
147 if issue.parent && issue.parent.project_id != issue.project_id
148 issue.parent_issue_id = nil
148 issue.parent_issue_id = nil
149 end
149 end
150 end
150 end
151 if new_tracker
151 if new_tracker
152 issue.tracker = new_tracker
152 issue.tracker = new_tracker
153 issue.reset_custom_values!
153 issue.reset_custom_values!
154 end
154 end
155 if options[:copy]
155 if options[:copy]
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
156 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
157 issue.status = if options[:attributes] && options[:attributes][:status_id]
158 IssueStatus.find_by_id(options[:attributes][:status_id])
158 IssueStatus.find_by_id(options[:attributes][:status_id])
159 else
159 else
160 self.status
160 self.status
161 end
161 end
162 end
162 end
163 # Allow bulk setting of attributes on the issue
163 # Allow bulk setting of attributes on the issue
164 if options[:attributes]
164 if options[:attributes]
165 issue.attributes = options[:attributes]
165 issue.attributes = options[:attributes]
166 end
166 end
167 if issue.save
167 if issue.save
168 unless options[:copy]
168 unless options[:copy]
169 # Manually update project_id on related time entries
169 # Manually update project_id on related time entries
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
170 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
171
171
172 issue.children.each do |child|
172 issue.children.each do |child|
173 unless child.move_to_project_without_transaction(new_project)
173 unless child.move_to_project_without_transaction(new_project)
174 # Move failed and transaction was rollback'd
174 # Move failed and transaction was rollback'd
175 return false
175 return false
176 end
176 end
177 end
177 end
178 end
178 end
179 else
179 else
180 return false
180 return false
181 end
181 end
182 issue
182 issue
183 end
183 end
184
184
185 def status_id=(sid)
185 def status_id=(sid)
186 self.status = nil
186 self.status = nil
187 write_attribute(:status_id, sid)
187 write_attribute(:status_id, sid)
188 end
188 end
189
189
190 def priority_id=(pid)
190 def priority_id=(pid)
191 self.priority = nil
191 self.priority = nil
192 write_attribute(:priority_id, pid)
192 write_attribute(:priority_id, pid)
193 end
193 end
194
194
195 def tracker_id=(tid)
195 def tracker_id=(tid)
196 self.tracker = nil
196 self.tracker = nil
197 result = write_attribute(:tracker_id, tid)
197 result = write_attribute(:tracker_id, tid)
198 @custom_field_values = nil
198 @custom_field_values = nil
199 result
199 result
200 end
200 end
201
201
202 # Overrides attributes= so that tracker_id gets assigned first
202 # Overrides attributes= so that tracker_id gets assigned first
203 def attributes_with_tracker_first=(new_attributes, *args)
203 def attributes_with_tracker_first=(new_attributes, *args)
204 return if new_attributes.nil?
204 return if new_attributes.nil?
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
205 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
206 if new_tracker_id
206 if new_tracker_id
207 self.tracker_id = new_tracker_id
207 self.tracker_id = new_tracker_id
208 end
208 end
209 send :attributes_without_tracker_first=, new_attributes, *args
209 send :attributes_without_tracker_first=, new_attributes, *args
210 end
210 end
211 # Do not redefine alias chain on reload (see #4838)
211 # Do not redefine alias chain on reload (see #4838)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
212 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
213
213
214 def estimated_hours=(h)
214 def estimated_hours=(h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
215 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
216 end
216 end
217
217
218 safe_attributes 'tracker_id',
218 safe_attributes 'tracker_id',
219 'status_id',
219 'status_id',
220 'parent_issue_id',
220 'parent_issue_id',
221 'category_id',
221 'category_id',
222 'assigned_to_id',
222 'assigned_to_id',
223 'priority_id',
223 'priority_id',
224 'fixed_version_id',
224 'fixed_version_id',
225 'subject',
225 'subject',
226 'description',
226 'description',
227 'start_date',
227 'start_date',
228 'due_date',
228 'due_date',
229 'done_ratio',
229 'done_ratio',
230 'estimated_hours',
230 'estimated_hours',
231 'custom_field_values',
231 'custom_field_values',
232 'custom_fields',
232 'custom_fields',
233 'lock_version',
233 'lock_version',
234 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
234 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
235
235
236 safe_attributes 'status_id',
236 safe_attributes 'status_id',
237 'assigned_to_id',
237 'assigned_to_id',
238 'fixed_version_id',
238 'fixed_version_id',
239 'done_ratio',
239 'done_ratio',
240 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
240 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
241
241
242 # Safely sets attributes
242 # Safely sets attributes
243 # Should be called from controllers instead of #attributes=
243 # Should be called from controllers instead of #attributes=
244 # attr_accessible is too rough because we still want things like
244 # attr_accessible is too rough because we still want things like
245 # Issue.new(:project => foo) to work
245 # Issue.new(:project => foo) to work
246 # TODO: move workflow/permission checks from controllers to here
246 # TODO: move workflow/permission checks from controllers to here
247 def safe_attributes=(attrs, user=User.current)
247 def safe_attributes=(attrs, user=User.current)
248 return unless attrs.is_a?(Hash)
248 return unless attrs.is_a?(Hash)
249
249
250 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
250 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
251 attrs = delete_unsafe_attributes(attrs, user)
251 attrs = delete_unsafe_attributes(attrs, user)
252 return if attrs.empty?
252 return if attrs.empty?
253
253
254 # Tracker must be set before since new_statuses_allowed_to depends on it.
254 # Tracker must be set before since new_statuses_allowed_to depends on it.
255 if t = attrs.delete('tracker_id')
255 if t = attrs.delete('tracker_id')
256 self.tracker_id = t
256 self.tracker_id = t
257 end
257 end
258
258
259 if attrs['status_id']
259 if attrs['status_id']
260 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
260 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
261 attrs.delete('status_id')
261 attrs.delete('status_id')
262 end
262 end
263 end
263 end
264
264
265 unless leaf?
265 unless leaf?
266 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
266 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
267 end
267 end
268
268
269 if attrs.has_key?('parent_issue_id')
269 if attrs.has_key?('parent_issue_id')
270 if !user.allowed_to?(:manage_subtasks, project)
270 if !user.allowed_to?(:manage_subtasks, project)
271 attrs.delete('parent_issue_id')
271 attrs.delete('parent_issue_id')
272 elsif !attrs['parent_issue_id'].blank?
272 elsif !attrs['parent_issue_id'].blank?
273 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
273 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
274 end
274 end
275 end
275 end
276
276
277 self.attributes = attrs
277 self.attributes = attrs
278 end
278 end
279
279
280 def done_ratio
280 def done_ratio
281 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
281 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
282 status.default_done_ratio
282 status.default_done_ratio
283 else
283 else
284 read_attribute(:done_ratio)
284 read_attribute(:done_ratio)
285 end
285 end
286 end
286 end
287
287
288 def self.use_status_for_done_ratio?
288 def self.use_status_for_done_ratio?
289 Setting.issue_done_ratio == 'issue_status'
289 Setting.issue_done_ratio == 'issue_status'
290 end
290 end
291
291
292 def self.use_field_for_done_ratio?
292 def self.use_field_for_done_ratio?
293 Setting.issue_done_ratio == 'issue_field'
293 Setting.issue_done_ratio == 'issue_field'
294 end
294 end
295
295
296 def validate
296 def validate
297 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
297 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
298 errors.add :due_date, :not_a_date
298 errors.add :due_date, :not_a_date
299 end
299 end
300
300
301 if self.due_date and self.start_date and self.due_date < self.start_date
301 if self.due_date and self.start_date and self.due_date < self.start_date
302 errors.add :due_date, :greater_than_start_date
302 errors.add :due_date, :greater_than_start_date
303 end
303 end
304
304
305 if start_date && soonest_start && start_date < soonest_start
305 if start_date && soonest_start && start_date < soonest_start
306 errors.add :start_date, :invalid
306 errors.add :start_date, :invalid
307 end
307 end
308
308
309 if fixed_version
309 if fixed_version
310 if !assignable_versions.include?(fixed_version)
310 if !assignable_versions.include?(fixed_version)
311 errors.add :fixed_version_id, :inclusion
311 errors.add :fixed_version_id, :inclusion
312 elsif reopened? && fixed_version.closed?
312 elsif reopened? && fixed_version.closed?
313 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
313 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
314 end
314 end
315 end
315 end
316
316
317 # Checks that the issue can not be added/moved to a disabled tracker
317 # Checks that the issue can not be added/moved to a disabled tracker
318 if project && (tracker_id_changed? || project_id_changed?)
318 if project && (tracker_id_changed? || project_id_changed?)
319 unless project.trackers.include?(tracker)
319 unless project.trackers.include?(tracker)
320 errors.add :tracker_id, :inclusion
320 errors.add :tracker_id, :inclusion
321 end
321 end
322 end
322 end
323
323
324 # Checks parent issue assignment
324 # Checks parent issue assignment
325 if @parent_issue
325 if @parent_issue
326 if @parent_issue.project_id != project_id
326 if @parent_issue.project_id != project_id
327 errors.add :parent_issue_id, :not_same_project
327 errors.add :parent_issue_id, :not_same_project
328 elsif !new_record?
328 elsif !new_record?
329 # moving an existing issue
329 # moving an existing issue
330 if @parent_issue.root_id != root_id
330 if @parent_issue.root_id != root_id
331 # we can always move to another tree
331 # we can always move to another tree
332 elsif move_possible?(@parent_issue)
332 elsif move_possible?(@parent_issue)
333 # move accepted inside tree
333 # move accepted inside tree
334 else
334 else
335 errors.add :parent_issue_id, :not_a_valid_parent
335 errors.add :parent_issue_id, :not_a_valid_parent
336 end
336 end
337 end
337 end
338 end
338 end
339 end
339 end
340
340
341 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
341 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
342 # even if the user turns off the setting later
342 # even if the user turns off the setting later
343 def update_done_ratio_from_issue_status
343 def update_done_ratio_from_issue_status
344 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
344 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
345 self.done_ratio = status.default_done_ratio
345 self.done_ratio = status.default_done_ratio
346 end
346 end
347 end
347 end
348
348
349 def init_journal(user, notes = "")
349 def init_journal(user, notes = "")
350 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
350 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
351 @issue_before_change = self.clone
351 @issue_before_change = self.clone
352 @issue_before_change.status = self.status
352 @issue_before_change.status = self.status
353 @custom_values_before_change = {}
353 @custom_values_before_change = {}
354 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
354 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
355 # Make sure updated_on is updated when adding a note.
355 # Make sure updated_on is updated when adding a note.
356 updated_on_will_change!
356 updated_on_will_change!
357 @current_journal
357 @current_journal
358 end
358 end
359
359
360 # Return true if the issue is closed, otherwise false
360 # Return true if the issue is closed, otherwise false
361 def closed?
361 def closed?
362 self.status.is_closed?
362 self.status.is_closed?
363 end
363 end
364
364
365 # Return true if the issue is being reopened
365 # Return true if the issue is being reopened
366 def reopened?
366 def reopened?
367 if !new_record? && status_id_changed?
367 if !new_record? && status_id_changed?
368 status_was = IssueStatus.find_by_id(status_id_was)
368 status_was = IssueStatus.find_by_id(status_id_was)
369 status_new = IssueStatus.find_by_id(status_id)
369 status_new = IssueStatus.find_by_id(status_id)
370 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
370 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
371 return true
371 return true
372 end
372 end
373 end
373 end
374 false
374 false
375 end
375 end
376
376
377 # Return true if the issue is being closed
377 # Return true if the issue is being closed
378 def closing?
378 def closing?
379 if !new_record? && status_id_changed?
379 if !new_record? && status_id_changed?
380 status_was = IssueStatus.find_by_id(status_id_was)
380 status_was = IssueStatus.find_by_id(status_id_was)
381 status_new = IssueStatus.find_by_id(status_id)
381 status_new = IssueStatus.find_by_id(status_id)
382 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
382 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
383 return true
383 return true
384 end
384 end
385 end
385 end
386 false
386 false
387 end
387 end
388
388
389 # Returns true if the issue is overdue
389 # Returns true if the issue is overdue
390 def overdue?
390 def overdue?
391 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
391 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
392 end
392 end
393
393
394 # Is the amount of work done less than it should for the due date
394 # Is the amount of work done less than it should for the due date
395 def behind_schedule?
395 def behind_schedule?
396 return false if start_date.nil? || due_date.nil?
396 return false if start_date.nil? || due_date.nil?
397 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
397 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
398 return done_date <= Date.today
398 return done_date <= Date.today
399 end
399 end
400
400
401 # Does this issue have children?
401 # Does this issue have children?
402 def children?
402 def children?
403 !leaf?
403 !leaf?
404 end
404 end
405
405
406 # Users the issue can be assigned to
406 # Users the issue can be assigned to
407 def assignable_users
407 def assignable_users
408 users = project.assignable_users
408 users = project.assignable_users
409 users << author if author
409 users << author if author
410 users.uniq.sort
410 users.uniq.sort
411 end
411 end
412
412
413 # Versions that the issue can be assigned to
413 # Versions that the issue can be assigned to
414 def assignable_versions
414 def assignable_versions
415 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
415 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
416 end
416 end
417
417
418 # Returns true if this issue is blocked by another issue that is still open
418 # Returns true if this issue is blocked by another issue that is still open
419 def blocked?
419 def blocked?
420 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
420 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
421 end
421 end
422
422
423 # Returns an array of status that user is able to apply
423 # Returns an array of status that user is able to apply
424 def new_statuses_allowed_to(user, include_default=false)
424 def new_statuses_allowed_to(user, include_default=false)
425 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
425 statuses = status.find_new_statuses_allowed_to(
426 user.roles_for_project(project),
427 tracker,
428 author == user,
429 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
430 )
426 statuses << status unless statuses.empty?
431 statuses << status unless statuses.empty?
427 statuses << IssueStatus.default if include_default
432 statuses << IssueStatus.default if include_default
428 statuses = statuses.uniq.sort
433 statuses = statuses.uniq.sort
429 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
434 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
430 end
435 end
431
436
432 # Returns the mail adresses of users that should be notified
437 # Returns the mail adresses of users that should be notified
433 def recipients
438 def recipients
434 notified = project.notified_users
439 notified = project.notified_users
435 # Author and assignee are always notified unless they have been
440 # Author and assignee are always notified unless they have been
436 # locked or don't want to be notified
441 # locked or don't want to be notified
437 notified << author if author && author.active? && author.notify_about?(self)
442 notified << author if author && author.active? && author.notify_about?(self)
438 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
443 notified << assigned_to if assigned_to && assigned_to.active? && assigned_to.notify_about?(self)
439 notified.uniq!
444 notified.uniq!
440 # Remove users that can not view the issue
445 # Remove users that can not view the issue
441 notified.reject! {|user| !visible?(user)}
446 notified.reject! {|user| !visible?(user)}
442 notified.collect(&:mail)
447 notified.collect(&:mail)
443 end
448 end
444
449
445 # Returns the total number of hours spent on this issue and its descendants
450 # Returns the total number of hours spent on this issue and its descendants
446 #
451 #
447 # Example:
452 # Example:
448 # spent_hours => 0.0
453 # spent_hours => 0.0
449 # spent_hours => 50.2
454 # spent_hours => 50.2
450 def spent_hours
455 def spent_hours
451 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
456 @spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
452 end
457 end
453
458
454 def relations
459 def relations
455 (relations_from + relations_to).sort
460 (relations_from + relations_to).sort
456 end
461 end
457
462
458 def all_dependent_issues(except=nil)
463 def all_dependent_issues(except=nil)
459 except ||= self
464 except ||= self
460 dependencies = []
465 dependencies = []
461 relations_from.each do |relation|
466 relations_from.each do |relation|
462 if relation.issue_to && relation.issue_to != except
467 if relation.issue_to && relation.issue_to != except
463 dependencies << relation.issue_to
468 dependencies << relation.issue_to
464 dependencies += relation.issue_to.all_dependent_issues(except)
469 dependencies += relation.issue_to.all_dependent_issues(except)
465 end
470 end
466 end
471 end
467 dependencies
472 dependencies
468 end
473 end
469
474
470 # Returns an array of issues that duplicate this one
475 # Returns an array of issues that duplicate this one
471 def duplicates
476 def duplicates
472 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
477 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
473 end
478 end
474
479
475 # Returns the due date or the target due date if any
480 # Returns the due date or the target due date if any
476 # Used on gantt chart
481 # Used on gantt chart
477 def due_before
482 def due_before
478 due_date || (fixed_version ? fixed_version.effective_date : nil)
483 due_date || (fixed_version ? fixed_version.effective_date : nil)
479 end
484 end
480
485
481 # Returns the time scheduled for this issue.
486 # Returns the time scheduled for this issue.
482 #
487 #
483 # Example:
488 # Example:
484 # Start Date: 2/26/09, End Date: 3/04/09
489 # Start Date: 2/26/09, End Date: 3/04/09
485 # duration => 6
490 # duration => 6
486 def duration
491 def duration
487 (start_date && due_date) ? due_date - start_date : 0
492 (start_date && due_date) ? due_date - start_date : 0
488 end
493 end
489
494
490 def soonest_start
495 def soonest_start
491 @soonest_start ||= (
496 @soonest_start ||= (
492 relations_to.collect{|relation| relation.successor_soonest_start} +
497 relations_to.collect{|relation| relation.successor_soonest_start} +
493 ancestors.collect(&:soonest_start)
498 ancestors.collect(&:soonest_start)
494 ).compact.max
499 ).compact.max
495 end
500 end
496
501
497 def reschedule_after(date)
502 def reschedule_after(date)
498 return if date.nil?
503 return if date.nil?
499 if leaf?
504 if leaf?
500 if start_date.nil? || start_date < date
505 if start_date.nil? || start_date < date
501 self.start_date, self.due_date = date, date + duration
506 self.start_date, self.due_date = date, date + duration
502 save
507 save
503 end
508 end
504 else
509 else
505 leaves.each do |leaf|
510 leaves.each do |leaf|
506 leaf.reschedule_after(date)
511 leaf.reschedule_after(date)
507 end
512 end
508 end
513 end
509 end
514 end
510
515
511 def <=>(issue)
516 def <=>(issue)
512 if issue.nil?
517 if issue.nil?
513 -1
518 -1
514 elsif root_id != issue.root_id
519 elsif root_id != issue.root_id
515 (root_id || 0) <=> (issue.root_id || 0)
520 (root_id || 0) <=> (issue.root_id || 0)
516 else
521 else
517 (lft || 0) <=> (issue.lft || 0)
522 (lft || 0) <=> (issue.lft || 0)
518 end
523 end
519 end
524 end
520
525
521 def to_s
526 def to_s
522 "#{tracker} ##{id}: #{subject}"
527 "#{tracker} ##{id}: #{subject}"
523 end
528 end
524
529
525 # Returns a string of css classes that apply to the issue
530 # Returns a string of css classes that apply to the issue
526 def css_classes
531 def css_classes
527 s = "issue status-#{status.position} priority-#{priority.position}"
532 s = "issue status-#{status.position} priority-#{priority.position}"
528 s << ' closed' if closed?
533 s << ' closed' if closed?
529 s << ' overdue' if overdue?
534 s << ' overdue' if overdue?
530 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
535 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
531 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
536 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
532 s
537 s
533 end
538 end
534
539
535 # Saves an issue, time_entry, attachments, and a journal from the parameters
540 # Saves an issue, time_entry, attachments, and a journal from the parameters
536 # Returns false if save fails
541 # Returns false if save fails
537 def save_issue_with_child_records(params, existing_time_entry=nil)
542 def save_issue_with_child_records(params, existing_time_entry=nil)
538 Issue.transaction do
543 Issue.transaction do
539 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
544 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
540 @time_entry = existing_time_entry || TimeEntry.new
545 @time_entry = existing_time_entry || TimeEntry.new
541 @time_entry.project = project
546 @time_entry.project = project
542 @time_entry.issue = self
547 @time_entry.issue = self
543 @time_entry.user = User.current
548 @time_entry.user = User.current
544 @time_entry.spent_on = Date.today
549 @time_entry.spent_on = Date.today
545 @time_entry.attributes = params[:time_entry]
550 @time_entry.attributes = params[:time_entry]
546 self.time_entries << @time_entry
551 self.time_entries << @time_entry
547 end
552 end
548
553
549 if valid?
554 if valid?
550 attachments = Attachment.attach_files(self, params[:attachments])
555 attachments = Attachment.attach_files(self, params[:attachments])
551
556
552 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
557 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
553 # TODO: Rename hook
558 # TODO: Rename hook
554 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
559 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
555 begin
560 begin
556 if save
561 if save
557 # TODO: Rename hook
562 # TODO: Rename hook
558 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
563 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
559 else
564 else
560 raise ActiveRecord::Rollback
565 raise ActiveRecord::Rollback
561 end
566 end
562 rescue ActiveRecord::StaleObjectError
567 rescue ActiveRecord::StaleObjectError
563 attachments[:files].each(&:destroy)
568 attachments[:files].each(&:destroy)
564 errors.add_to_base l(:notice_locking_conflict)
569 errors.add_to_base l(:notice_locking_conflict)
565 raise ActiveRecord::Rollback
570 raise ActiveRecord::Rollback
566 end
571 end
567 end
572 end
568 end
573 end
569 end
574 end
570
575
571 # Unassigns issues from +version+ if it's no longer shared with issue's project
576 # Unassigns issues from +version+ if it's no longer shared with issue's project
572 def self.update_versions_from_sharing_change(version)
577 def self.update_versions_from_sharing_change(version)
573 # Update issues assigned to the version
578 # Update issues assigned to the version
574 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
579 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
575 end
580 end
576
581
577 # Unassigns issues from versions that are no longer shared
582 # Unassigns issues from versions that are no longer shared
578 # after +project+ was moved
583 # after +project+ was moved
579 def self.update_versions_from_hierarchy_change(project)
584 def self.update_versions_from_hierarchy_change(project)
580 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
585 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
581 # Update issues of the moved projects and issues assigned to a version of a moved project
586 # Update issues of the moved projects and issues assigned to a version of a moved project
582 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
587 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
583 end
588 end
584
589
585 def parent_issue_id=(arg)
590 def parent_issue_id=(arg)
586 parent_issue_id = arg.blank? ? nil : arg.to_i
591 parent_issue_id = arg.blank? ? nil : arg.to_i
587 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
592 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
588 @parent_issue.id
593 @parent_issue.id
589 else
594 else
590 @parent_issue = nil
595 @parent_issue = nil
591 nil
596 nil
592 end
597 end
593 end
598 end
594
599
595 def parent_issue_id
600 def parent_issue_id
596 if instance_variable_defined? :@parent_issue
601 if instance_variable_defined? :@parent_issue
597 @parent_issue.nil? ? nil : @parent_issue.id
602 @parent_issue.nil? ? nil : @parent_issue.id
598 else
603 else
599 parent_id
604 parent_id
600 end
605 end
601 end
606 end
602
607
603 # Extracted from the ReportsController.
608 # Extracted from the ReportsController.
604 def self.by_tracker(project)
609 def self.by_tracker(project)
605 count_and_group_by(:project => project,
610 count_and_group_by(:project => project,
606 :field => 'tracker_id',
611 :field => 'tracker_id',
607 :joins => Tracker.table_name)
612 :joins => Tracker.table_name)
608 end
613 end
609
614
610 def self.by_version(project)
615 def self.by_version(project)
611 count_and_group_by(:project => project,
616 count_and_group_by(:project => project,
612 :field => 'fixed_version_id',
617 :field => 'fixed_version_id',
613 :joins => Version.table_name)
618 :joins => Version.table_name)
614 end
619 end
615
620
616 def self.by_priority(project)
621 def self.by_priority(project)
617 count_and_group_by(:project => project,
622 count_and_group_by(:project => project,
618 :field => 'priority_id',
623 :field => 'priority_id',
619 :joins => IssuePriority.table_name)
624 :joins => IssuePriority.table_name)
620 end
625 end
621
626
622 def self.by_category(project)
627 def self.by_category(project)
623 count_and_group_by(:project => project,
628 count_and_group_by(:project => project,
624 :field => 'category_id',
629 :field => 'category_id',
625 :joins => IssueCategory.table_name)
630 :joins => IssueCategory.table_name)
626 end
631 end
627
632
628 def self.by_assigned_to(project)
633 def self.by_assigned_to(project)
629 count_and_group_by(:project => project,
634 count_and_group_by(:project => project,
630 :field => 'assigned_to_id',
635 :field => 'assigned_to_id',
631 :joins => User.table_name)
636 :joins => User.table_name)
632 end
637 end
633
638
634 def self.by_author(project)
639 def self.by_author(project)
635 count_and_group_by(:project => project,
640 count_and_group_by(:project => project,
636 :field => 'author_id',
641 :field => 'author_id',
637 :joins => User.table_name)
642 :joins => User.table_name)
638 end
643 end
639
644
640 def self.by_subproject(project)
645 def self.by_subproject(project)
641 ActiveRecord::Base.connection.select_all("select s.id as status_id,
646 ActiveRecord::Base.connection.select_all("select s.id as status_id,
642 s.is_closed as closed,
647 s.is_closed as closed,
643 i.project_id as project_id,
648 i.project_id as project_id,
644 count(i.id) as total
649 count(i.id) as total
645 from
650 from
646 #{Issue.table_name} i, #{IssueStatus.table_name} s
651 #{Issue.table_name} i, #{IssueStatus.table_name} s
647 where
652 where
648 i.status_id=s.id
653 i.status_id=s.id
649 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
654 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
650 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
655 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
651 end
656 end
652 # End ReportsController extraction
657 # End ReportsController extraction
653
658
654 # Returns an array of projects that current user can move issues to
659 # Returns an array of projects that current user can move issues to
655 def self.allowed_target_projects_on_move
660 def self.allowed_target_projects_on_move
656 projects = []
661 projects = []
657 if User.current.admin?
662 if User.current.admin?
658 # admin is allowed to move issues to any active (visible) project
663 # admin is allowed to move issues to any active (visible) project
659 projects = Project.visible.all
664 projects = Project.visible.all
660 elsif User.current.logged?
665 elsif User.current.logged?
661 if Role.non_member.allowed_to?(:move_issues)
666 if Role.non_member.allowed_to?(:move_issues)
662 projects = Project.visible.all
667 projects = Project.visible.all
663 else
668 else
664 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
669 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
665 end
670 end
666 end
671 end
667 projects
672 projects
668 end
673 end
669
674
670 private
675 private
671
676
672 def update_nested_set_attributes
677 def update_nested_set_attributes
673 if root_id.nil?
678 if root_id.nil?
674 # issue was just created
679 # issue was just created
675 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
680 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
676 set_default_left_and_right
681 set_default_left_and_right
677 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
682 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
678 if @parent_issue
683 if @parent_issue
679 move_to_child_of(@parent_issue)
684 move_to_child_of(@parent_issue)
680 end
685 end
681 reload
686 reload
682 elsif parent_issue_id != parent_id
687 elsif parent_issue_id != parent_id
683 former_parent_id = parent_id
688 former_parent_id = parent_id
684 # moving an existing issue
689 # moving an existing issue
685 if @parent_issue && @parent_issue.root_id == root_id
690 if @parent_issue && @parent_issue.root_id == root_id
686 # inside the same tree
691 # inside the same tree
687 move_to_child_of(@parent_issue)
692 move_to_child_of(@parent_issue)
688 else
693 else
689 # to another tree
694 # to another tree
690 unless root?
695 unless root?
691 move_to_right_of(root)
696 move_to_right_of(root)
692 reload
697 reload
693 end
698 end
694 old_root_id = root_id
699 old_root_id = root_id
695 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
700 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
696 target_maxright = nested_set_scope.maximum(right_column_name) || 0
701 target_maxright = nested_set_scope.maximum(right_column_name) || 0
697 offset = target_maxright + 1 - lft
702 offset = target_maxright + 1 - lft
698 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
703 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
699 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
704 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
700 self[left_column_name] = lft + offset
705 self[left_column_name] = lft + offset
701 self[right_column_name] = rgt + offset
706 self[right_column_name] = rgt + offset
702 if @parent_issue
707 if @parent_issue
703 move_to_child_of(@parent_issue)
708 move_to_child_of(@parent_issue)
704 end
709 end
705 end
710 end
706 reload
711 reload
707 # delete invalid relations of all descendants
712 # delete invalid relations of all descendants
708 self_and_descendants.each do |issue|
713 self_and_descendants.each do |issue|
709 issue.relations.each do |relation|
714 issue.relations.each do |relation|
710 relation.destroy unless relation.valid?
715 relation.destroy unless relation.valid?
711 end
716 end
712 end
717 end
713 # update former parent
718 # update former parent
714 recalculate_attributes_for(former_parent_id) if former_parent_id
719 recalculate_attributes_for(former_parent_id) if former_parent_id
715 end
720 end
716 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
721 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
717 end
722 end
718
723
719 def update_parent_attributes
724 def update_parent_attributes
720 recalculate_attributes_for(parent_id) if parent_id
725 recalculate_attributes_for(parent_id) if parent_id
721 end
726 end
722
727
723 def recalculate_attributes_for(issue_id)
728 def recalculate_attributes_for(issue_id)
724 if issue_id && p = Issue.find_by_id(issue_id)
729 if issue_id && p = Issue.find_by_id(issue_id)
725 # priority = highest priority of children
730 # priority = highest priority of children
726 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
731 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
727 p.priority = IssuePriority.find_by_position(priority_position)
732 p.priority = IssuePriority.find_by_position(priority_position)
728 end
733 end
729
734
730 # start/due dates = lowest/highest dates of children
735 # start/due dates = lowest/highest dates of children
731 p.start_date = p.children.minimum(:start_date)
736 p.start_date = p.children.minimum(:start_date)
732 p.due_date = p.children.maximum(:due_date)
737 p.due_date = p.children.maximum(:due_date)
733 if p.start_date && p.due_date && p.due_date < p.start_date
738 if p.start_date && p.due_date && p.due_date < p.start_date
734 p.start_date, p.due_date = p.due_date, p.start_date
739 p.start_date, p.due_date = p.due_date, p.start_date
735 end
740 end
736
741
737 # done ratio = weighted average ratio of leaves
742 # done ratio = weighted average ratio of leaves
738 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
743 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
739 leaves_count = p.leaves.count
744 leaves_count = p.leaves.count
740 if leaves_count > 0
745 if leaves_count > 0
741 average = p.leaves.average(:estimated_hours).to_f
746 average = p.leaves.average(:estimated_hours).to_f
742 if average == 0
747 if average == 0
743 average = 1
748 average = 1
744 end
749 end
745 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
750 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
746 progress = done / (average * leaves_count)
751 progress = done / (average * leaves_count)
747 p.done_ratio = progress.round
752 p.done_ratio = progress.round
748 end
753 end
749 end
754 end
750
755
751 # estimate = sum of leaves estimates
756 # estimate = sum of leaves estimates
752 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
757 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
753 p.estimated_hours = nil if p.estimated_hours == 0.0
758 p.estimated_hours = nil if p.estimated_hours == 0.0
754
759
755 # ancestors will be recursively updated
760 # ancestors will be recursively updated
756 p.save(false)
761 p.save(false)
757 end
762 end
758 end
763 end
759
764
760 # Update issues so their versions are not pointing to a
765 # Update issues so their versions are not pointing to a
761 # fixed_version that is not shared with the issue's project
766 # fixed_version that is not shared with the issue's project
762 def self.update_versions(conditions=nil)
767 def self.update_versions(conditions=nil)
763 # Only need to update issues with a fixed_version from
768 # Only need to update issues with a fixed_version from
764 # a different project and that is not systemwide shared
769 # a different project and that is not systemwide shared
765 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
770 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
766 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
771 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
767 " AND #{Version.table_name}.sharing <> 'system'",
772 " AND #{Version.table_name}.sharing <> 'system'",
768 conditions),
773 conditions),
769 :include => [:project, :fixed_version]
774 :include => [:project, :fixed_version]
770 ).each do |issue|
775 ).each do |issue|
771 next if issue.project.nil? || issue.fixed_version.nil?
776 next if issue.project.nil? || issue.fixed_version.nil?
772 unless issue.project.shared_versions.include?(issue.fixed_version)
777 unless issue.project.shared_versions.include?(issue.fixed_version)
773 issue.init_journal(User.current)
778 issue.init_journal(User.current)
774 issue.fixed_version = nil
779 issue.fixed_version = nil
775 issue.save
780 issue.save
776 end
781 end
777 end
782 end
778 end
783 end
779
784
780 # Callback on attachment deletion
785 # Callback on attachment deletion
781 def attachment_removed(obj)
786 def attachment_removed(obj)
782 journal = init_journal(User.current)
787 journal = init_journal(User.current)
783 journal.details << JournalDetail.new(:property => 'attachment',
788 journal.details << JournalDetail.new(:property => 'attachment',
784 :prop_key => obj.id,
789 :prop_key => obj.id,
785 :old_value => obj.filename)
790 :old_value => obj.filename)
786 journal.save
791 journal.save
787 end
792 end
788
793
789 # Default assignment based on category
794 # Default assignment based on category
790 def default_assign
795 def default_assign
791 if assigned_to.nil? && category && category.assigned_to
796 if assigned_to.nil? && category && category.assigned_to
792 self.assigned_to = category.assigned_to
797 self.assigned_to = category.assigned_to
793 end
798 end
794 end
799 end
795
800
796 # Updates start/due dates of following issues
801 # Updates start/due dates of following issues
797 def reschedule_following_issues
802 def reschedule_following_issues
798 if start_date_changed? || due_date_changed?
803 if start_date_changed? || due_date_changed?
799 relations_from.each do |relation|
804 relations_from.each do |relation|
800 relation.set_issue_to_dates
805 relation.set_issue_to_dates
801 end
806 end
802 end
807 end
803 end
808 end
804
809
805 # Closes duplicates if the issue is being closed
810 # Closes duplicates if the issue is being closed
806 def close_duplicates
811 def close_duplicates
807 if closing?
812 if closing?
808 duplicates.each do |duplicate|
813 duplicates.each do |duplicate|
809 # Reload is need in case the duplicate was updated by a previous duplicate
814 # Reload is need in case the duplicate was updated by a previous duplicate
810 duplicate.reload
815 duplicate.reload
811 # Don't re-close it if it's already closed
816 # Don't re-close it if it's already closed
812 next if duplicate.closed?
817 next if duplicate.closed?
813 # Same user and notes
818 # Same user and notes
814 if @current_journal
819 if @current_journal
815 duplicate.init_journal(@current_journal.user, @current_journal.notes)
820 duplicate.init_journal(@current_journal.user, @current_journal.notes)
816 end
821 end
817 duplicate.update_attribute :status, self.status
822 duplicate.update_attribute :status, self.status
818 end
823 end
819 end
824 end
820 end
825 end
821
826
822 # Saves the changes in a Journal
827 # Saves the changes in a Journal
823 # Called after_save
828 # Called after_save
824 def create_journal
829 def create_journal
825 if @current_journal
830 if @current_journal
826 # attributes changes
831 # attributes changes
827 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
832 (Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
828 @current_journal.details << JournalDetail.new(:property => 'attr',
833 @current_journal.details << JournalDetail.new(:property => 'attr',
829 :prop_key => c,
834 :prop_key => c,
830 :old_value => @issue_before_change.send(c),
835 :old_value => @issue_before_change.send(c),
831 :value => send(c)) unless send(c)==@issue_before_change.send(c)
836 :value => send(c)) unless send(c)==@issue_before_change.send(c)
832 }
837 }
833 # custom fields changes
838 # custom fields changes
834 custom_values.each {|c|
839 custom_values.each {|c|
835 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
840 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
836 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
841 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
837 @current_journal.details << JournalDetail.new(:property => 'cf',
842 @current_journal.details << JournalDetail.new(:property => 'cf',
838 :prop_key => c.custom_field_id,
843 :prop_key => c.custom_field_id,
839 :old_value => @custom_values_before_change[c.custom_field_id],
844 :old_value => @custom_values_before_change[c.custom_field_id],
840 :value => c.value)
845 :value => c.value)
841 }
846 }
842 @current_journal.save
847 @current_journal.save
843 # reset current journal
848 # reset current journal
844 init_journal @current_journal.user, @current_journal.notes
849 init_journal @current_journal.user, @current_journal.notes
845 end
850 end
846 end
851 end
847
852
848 # Query generator for selecting groups of issue counts for a project
853 # Query generator for selecting groups of issue counts for a project
849 # based on specific criteria
854 # based on specific criteria
850 #
855 #
851 # Options
856 # Options
852 # * project - Project to search in.
857 # * project - Project to search in.
853 # * field - String. Issue field to key off of in the grouping.
858 # * field - String. Issue field to key off of in the grouping.
854 # * joins - String. The table name to join against.
859 # * joins - String. The table name to join against.
855 def self.count_and_group_by(options)
860 def self.count_and_group_by(options)
856 project = options.delete(:project)
861 project = options.delete(:project)
857 select_field = options.delete(:field)
862 select_field = options.delete(:field)
858 joins = options.delete(:joins)
863 joins = options.delete(:joins)
859
864
860 where = "i.#{select_field}=j.id"
865 where = "i.#{select_field}=j.id"
861
866
862 ActiveRecord::Base.connection.select_all("select s.id as status_id,
867 ActiveRecord::Base.connection.select_all("select s.id as status_id,
863 s.is_closed as closed,
868 s.is_closed as closed,
864 j.id as #{select_field},
869 j.id as #{select_field},
865 count(i.id) as total
870 count(i.id) as total
866 from
871 from
867 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
872 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} j
868 where
873 where
869 i.status_id=s.id
874 i.status_id=s.id
870 and #{where}
875 and #{where}
871 and i.project_id=#{project.id}
876 and i.project_id=#{project.id}
872 group by s.id, s.is_closed, j.id")
877 group by s.id, s.is_closed, j.id")
873 end
878 end
874
879
875
880
876 end
881 end
@@ -1,98 +1,99
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 IssueStatus < ActiveRecord::Base
18 class IssueStatus < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 has_many :workflows, :foreign_key => "old_status_id"
20 has_many :workflows, :foreign_key => "old_status_id"
21 acts_as_list
21 acts_as_list
22
22
23 before_destroy :delete_workflows
23 before_destroy :delete_workflows
24
24
25 validates_presence_of :name
25 validates_presence_of :name
26 validates_uniqueness_of :name
26 validates_uniqueness_of :name
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
28 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
29
29
30 def after_save
30 def after_save
31 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
31 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
32 end
32 end
33
33
34 # Returns the default status for new issues
34 # Returns the default status for new issues
35 def self.default
35 def self.default
36 find(:first, :conditions =>["is_default=?", true])
36 find(:first, :conditions =>["is_default=?", true])
37 end
37 end
38
38
39 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
39 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
40 def self.update_issue_done_ratios
40 def self.update_issue_done_ratios
41 if Issue.use_status_for_done_ratio?
41 if Issue.use_status_for_done_ratio?
42 IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
42 IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
43 Issue.update_all(["done_ratio = ?", status.default_done_ratio],
43 Issue.update_all(["done_ratio = ?", status.default_done_ratio],
44 ["status_id = ?", status.id])
44 ["status_id = ?", status.id])
45 end
45 end
46 end
46 end
47
47
48 return Issue.use_status_for_done_ratio?
48 return Issue.use_status_for_done_ratio?
49 end
49 end
50
50
51 # Returns an array of all statuses the given role can switch to
51 # Returns an array of all statuses the given role can switch to
52 # Uses association cache when called more than one time
52 # Uses association cache when called more than one time
53 def new_statuses_allowed_to(roles, tracker)
53 def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
54 if roles && tracker
54 if roles && tracker
55 role_ids = roles.collect(&:id)
55 role_ids = roles.collect(&:id)
56 new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
56 transitions = workflows.select do |w|
57 role_ids.include?(w.role_id) &&
58 w.tracker_id == tracker.id &&
59 (author || !w.author) &&
60 (assignee || !w.assignee)
61 end
62 transitions.collect{|w| w.new_status}.compact.sort
57 else
63 else
58 []
64 []
59 end
65 end
60 end
66 end
61
67
62 # Same thing as above but uses a database query
68 # Same thing as above but uses a database query
63 # More efficient than the previous method if called just once
69 # More efficient than the previous method if called just once
64 def find_new_statuses_allowed_to(roles, tracker)
70 def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
65 if roles && tracker
71 if roles && tracker
72 conditions = {:role_id => roles.collect(&:id), :tracker_id => tracker.id}
73 conditions[:author] = false unless author
74 conditions[:assignee] = false unless assignee
75
66 workflows.find(:all,
76 workflows.find(:all,
67 :include => :new_status,
77 :include => :new_status,
68 :conditions => { :role_id => roles.collect(&:id),
78 :conditions => conditions).collect{|w| w.new_status}.compact.sort
69 :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
70 else
79 else
71 []
80 []
72 end
81 end
73 end
82 end
74
75 def new_status_allowed_to?(status, roles, tracker)
76 if status && roles && tracker
77 !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
78 else
79 false
80 end
81 end
82
83
83 def <=>(status)
84 def <=>(status)
84 position <=> status.position
85 position <=> status.position
85 end
86 end
86
87
87 def to_s; name end
88 def to_s; name end
88
89
89 private
90 private
90 def check_integrity
91 def check_integrity
91 raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
92 raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
92 end
93 end
93
94
94 # Deletes associated workflows
95 # Deletes associated workflows
95 def delete_workflows
96 def delete_workflows
96 Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
97 Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
97 end
98 end
98 end
99 end
@@ -1,73 +1,50
1 <%= render :partial => 'action_menu' %>
1 <%= render :partial => 'action_menu' %>
2
2
3 <h2><%=l(:label_workflow)%></h2>
3 <h2><%=l(:label_workflow)%></h2>
4
4
5 <p><%=l(:text_workflow_edit)%>:</p>
5 <p><%=l(:text_workflow_edit)%>:</p>
6
6
7 <% form_tag({}, :method => 'get') do %>
7 <% form_tag({}, :method => 'get') do %>
8 <p>
8 <p>
9 <label><%=l(:label_role)%>:</label>
9 <label><%=l(:label_role)%>:</label>
10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %>
10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %>
11
11
12 <label><%=l(:label_tracker)%>:</label>
12 <label><%=l(:label_tracker)%>:</label>
13 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %>
13 <%= select_tag 'tracker_id', options_from_collection_for_select(@trackers, "id", "name", @tracker && @tracker.id) %>
14
14
15 <%= hidden_field_tag 'used_statuses_only', '0' %>
15 <%= hidden_field_tag 'used_statuses_only', '0' %>
16 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
16 <label><%= check_box_tag 'used_statuses_only', '1', @used_statuses_only %> <%= l(:label_display_used_statuses_only) %></label>
17 </p>
17 </p>
18 <p>
18 <p>
19 <%= submit_tag l(:button_edit), :name => nil %>
19 <%= submit_tag l(:button_edit), :name => nil %>
20 </p>
20 </p>
21 <% end %>
21 <% end %>
22
22
23
24 <% if @tracker && @role && @statuses.any? %>
23 <% if @tracker && @role && @statuses.any? %>
25 <% form_tag({}, :id => 'workflow_form' ) do %>
24 <% form_tag({}, :id => 'workflow_form' ) do %>
26 <%= hidden_field_tag 'tracker_id', @tracker.id %>
25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
27 <%= hidden_field_tag 'role_id', @role.id %>
26 <%= hidden_field_tag 'role_id', @role.id %>
28 <div class="autoscroll">
27 <div class="autoscroll">
29 <table class="list">
28 <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
30 <thead>
29
31 <tr>
30 <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
32 <th align="left"><%=l(:label_current_status)%></th>
31 <legend onclick="toggleFieldset(this);">Autorisations supplémentaires lorsque l'utilisateur a créé la demande</legend>
33 <th align="center" colspan="<%= @statuses.length %>"><%=l(:label_new_statuses_allowed)%></th>
32 <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
34 </tr>
33 <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
35 <tr>
34 </div>
36 <td></td>
35 </fieldset>
37 <% for new_status in @statuses %>
36 <%= javascript_tag "hideFieldset($('author_workflows'))" unless @workflows['author'].present? %>
38 <td width="<%= 75 / @statuses.size %>%" align="center">
37
39 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.new-status-#{new_status.id}')",
38 <fieldset class="collapsible" style="padding: 0;">
40 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
39 <legend onclick="toggleFieldset(this);">Autorisations supplΓ©mentaires lorsque la demande est assignΓ©e Γ  l'utilisateur</legend>
41 <%= new_status.name %>
40 <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
42 </td>
41 <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
43 <% end %>
42 </div>
44 </tr>
43 </fieldset>
45 </thead>
44 <%= javascript_tag "hideFieldset($('assignee_workflows'))" unless @workflows['assignee'].present? %>
46 <tbody>
45 </div>
47 <% for old_status in @statuses %>
46 <%= submit_tag l(:button_save) %>
48 <tr class="<%= cycle("odd", "even") %>">
47 <% end %>
49 <td>
50 <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.old-status-#{old_status.id}')",
51 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
52
53 <%= old_status.name %>
54 </td>
55 <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to([@role], @tracker).collect(&:id) -%>
56 <% for new_status in @statuses -%>
57 <td align="center">
58 <%= check_box_tag "issue_status[#{ old_status.id }][]", new_status.id, new_status_ids_allowed.include?(new_status.id),
59 :class => "old-status-#{old_status.id} new-status-#{new_status.id}" %>
60 </td>
61 <% end -%>
62 </tr>
63 <% end %>
64 </tbody>
65 </table>
66 </div>
67 <p><%= check_all_links 'workflow_form' %></p>
68
69 <%= submit_tag l(:button_save) %>
70 <% end %>
71 <% end %>
48 <% end %>
72
49
73 <% html_title(l(:label_workflow)) -%>
50 <% html_title(l(:label_workflow)) -%>
@@ -1,273 +1,279
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 function checkAll (id, checked) {
4 function checkAll (id, checked) {
5 var els = Element.descendants(id);
5 var els = Element.descendants(id);
6 for (var i = 0; i < els.length; i++) {
6 for (var i = 0; i < els.length; i++) {
7 if (els[i].disabled==false) {
7 if (els[i].disabled==false) {
8 els[i].checked = checked;
8 els[i].checked = checked;
9 }
9 }
10 }
10 }
11 }
11 }
12
12
13 function toggleCheckboxesBySelector(selector) {
13 function toggleCheckboxesBySelector(selector) {
14 boxes = $$(selector);
14 boxes = $$(selector);
15 var all_checked = true;
15 var all_checked = true;
16 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
16 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
17 for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }
17 for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }
18 }
18 }
19
19
20 function setCheckboxesBySelector(checked, selector) {
20 function setCheckboxesBySelector(checked, selector) {
21 var boxes = $$(selector);
21 var boxes = $$(selector);
22 boxes.each(function(ele) {
22 boxes.each(function(ele) {
23 ele.checked = checked;
23 ele.checked = checked;
24 });
24 });
25 }
25 }
26
26
27 function showAndScrollTo(id, focus) {
27 function showAndScrollTo(id, focus) {
28 Element.show(id);
28 Element.show(id);
29 if (focus!=null) { Form.Element.focus(focus); }
29 if (focus!=null) { Form.Element.focus(focus); }
30 Element.scrollTo(id);
30 Element.scrollTo(id);
31 }
31 }
32
32
33 function toggleRowGroup(el) {
33 function toggleRowGroup(el) {
34 var tr = Element.up(el, 'tr');
34 var tr = Element.up(el, 'tr');
35 var n = Element.next(tr);
35 var n = Element.next(tr);
36 tr.toggleClassName('open');
36 tr.toggleClassName('open');
37 while (n != undefined && !n.hasClassName('group')) {
37 while (n != undefined && !n.hasClassName('group')) {
38 Element.toggle(n);
38 Element.toggle(n);
39 n = Element.next(n);
39 n = Element.next(n);
40 }
40 }
41 }
41 }
42
42
43 function toggleFieldset(el) {
43 function toggleFieldset(el) {
44 var fieldset = Element.up(el, 'fieldset');
44 var fieldset = Element.up(el, 'fieldset');
45 fieldset.toggleClassName('collapsed');
45 fieldset.toggleClassName('collapsed');
46 Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
46 Effect.toggle(fieldset.down('div'), 'slide', {duration:0.2});
47 }
47 }
48
48
49 function hideFieldset(el) {
50 var fieldset = Element.up(el, 'fieldset');
51 fieldset.toggleClassName('collapsed');
52 fieldset.down('div').hide();
53 }
54
49 var fileFieldCount = 1;
55 var fileFieldCount = 1;
50
56
51 function addFileField() {
57 function addFileField() {
52 if (fileFieldCount >= 10) return false
58 if (fileFieldCount >= 10) return false
53 fileFieldCount++;
59 fileFieldCount++;
54 var f = document.createElement("input");
60 var f = document.createElement("input");
55 f.type = "file";
61 f.type = "file";
56 f.name = "attachments[" + fileFieldCount + "][file]";
62 f.name = "attachments[" + fileFieldCount + "][file]";
57 f.size = 30;
63 f.size = 30;
58 var d = document.createElement("input");
64 var d = document.createElement("input");
59 d.type = "text";
65 d.type = "text";
60 d.name = "attachments[" + fileFieldCount + "][description]";
66 d.name = "attachments[" + fileFieldCount + "][description]";
61 d.size = 60;
67 d.size = 60;
62 var dLabel = new Element('label');
68 var dLabel = new Element('label');
63 dLabel.addClassName('inline');
69 dLabel.addClassName('inline');
64 // Pulls the languge value used for Optional Description
70 // Pulls the languge value used for Optional Description
65 dLabel.update($('attachment_description_label_content').innerHTML)
71 dLabel.update($('attachment_description_label_content').innerHTML)
66 p = document.getElementById("attachments_fields");
72 p = document.getElementById("attachments_fields");
67 p.appendChild(document.createElement("br"));
73 p.appendChild(document.createElement("br"));
68 p.appendChild(f);
74 p.appendChild(f);
69 p.appendChild(dLabel);
75 p.appendChild(dLabel);
70 dLabel.appendChild(d);
76 dLabel.appendChild(d);
71
77
72 }
78 }
73
79
74 function showTab(name) {
80 function showTab(name) {
75 var f = $$('div#content .tab-content');
81 var f = $$('div#content .tab-content');
76 for(var i=0; i<f.length; i++){
82 for(var i=0; i<f.length; i++){
77 Element.hide(f[i]);
83 Element.hide(f[i]);
78 }
84 }
79 var f = $$('div.tabs a');
85 var f = $$('div.tabs a');
80 for(var i=0; i<f.length; i++){
86 for(var i=0; i<f.length; i++){
81 Element.removeClassName(f[i], "selected");
87 Element.removeClassName(f[i], "selected");
82 }
88 }
83 Element.show('tab-content-' + name);
89 Element.show('tab-content-' + name);
84 Element.addClassName('tab-' + name, "selected");
90 Element.addClassName('tab-' + name, "selected");
85 return false;
91 return false;
86 }
92 }
87
93
88 function moveTabRight(el) {
94 function moveTabRight(el) {
89 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
95 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
90 var tabsWidth = 0;
96 var tabsWidth = 0;
91 var i;
97 var i;
92 for (i=0; i<lis.length; i++) {
98 for (i=0; i<lis.length; i++) {
93 if (lis[i].visible()) {
99 if (lis[i].visible()) {
94 tabsWidth += lis[i].getWidth() + 6;
100 tabsWidth += lis[i].getWidth() + 6;
95 }
101 }
96 }
102 }
97 if (tabsWidth < Element.up(el, 'div.tabs').getWidth() - 60) {
103 if (tabsWidth < Element.up(el, 'div.tabs').getWidth() - 60) {
98 return;
104 return;
99 }
105 }
100 i=0;
106 i=0;
101 while (i<lis.length && !lis[i].visible()) {
107 while (i<lis.length && !lis[i].visible()) {
102 i++;
108 i++;
103 }
109 }
104 lis[i].hide();
110 lis[i].hide();
105 }
111 }
106
112
107 function moveTabLeft(el) {
113 function moveTabLeft(el) {
108 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
114 var lis = Element.up(el, 'div.tabs').down('ul').childElements();
109 var i = 0;
115 var i = 0;
110 while (i<lis.length && !lis[i].visible()) {
116 while (i<lis.length && !lis[i].visible()) {
111 i++;
117 i++;
112 }
118 }
113 if (i>0) {
119 if (i>0) {
114 lis[i-1].show();
120 lis[i-1].show();
115 }
121 }
116 }
122 }
117
123
118 function displayTabsButtons() {
124 function displayTabsButtons() {
119 var lis;
125 var lis;
120 var tabsWidth = 0;
126 var tabsWidth = 0;
121 var i;
127 var i;
122 $$('div.tabs').each(function(el) {
128 $$('div.tabs').each(function(el) {
123 lis = el.down('ul').childElements();
129 lis = el.down('ul').childElements();
124 for (i=0; i<lis.length; i++) {
130 for (i=0; i<lis.length; i++) {
125 if (lis[i].visible()) {
131 if (lis[i].visible()) {
126 tabsWidth += lis[i].getWidth() + 6;
132 tabsWidth += lis[i].getWidth() + 6;
127 }
133 }
128 }
134 }
129 if ((tabsWidth < el.getWidth() - 60) && (lis[0].visible())) {
135 if ((tabsWidth < el.getWidth() - 60) && (lis[0].visible())) {
130 el.down('div.tabs-buttons').hide();
136 el.down('div.tabs-buttons').hide();
131 } else {
137 } else {
132 el.down('div.tabs-buttons').show();
138 el.down('div.tabs-buttons').show();
133 }
139 }
134 });
140 });
135 }
141 }
136
142
137 function setPredecessorFieldsVisibility() {
143 function setPredecessorFieldsVisibility() {
138 relationType = $('relation_relation_type');
144 relationType = $('relation_relation_type');
139 if (relationType && (relationType.value == "precedes" || relationType.value == "follows")) {
145 if (relationType && (relationType.value == "precedes" || relationType.value == "follows")) {
140 Element.show('predecessor_fields');
146 Element.show('predecessor_fields');
141 } else {
147 } else {
142 Element.hide('predecessor_fields');
148 Element.hide('predecessor_fields');
143 }
149 }
144 }
150 }
145
151
146 function promptToRemote(text, param, url) {
152 function promptToRemote(text, param, url) {
147 value = prompt(text + ':');
153 value = prompt(text + ':');
148 if (value) {
154 if (value) {
149 new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true});
155 new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true});
150 return false;
156 return false;
151 }
157 }
152 }
158 }
153
159
154 function collapseScmEntry(id) {
160 function collapseScmEntry(id) {
155 var els = document.getElementsByClassName(id, 'browser');
161 var els = document.getElementsByClassName(id, 'browser');
156 for (var i = 0; i < els.length; i++) {
162 for (var i = 0; i < els.length; i++) {
157 if (els[i].hasClassName('open')) {
163 if (els[i].hasClassName('open')) {
158 collapseScmEntry(els[i].id);
164 collapseScmEntry(els[i].id);
159 }
165 }
160 Element.hide(els[i]);
166 Element.hide(els[i]);
161 }
167 }
162 $(id).removeClassName('open');
168 $(id).removeClassName('open');
163 }
169 }
164
170
165 function expandScmEntry(id) {
171 function expandScmEntry(id) {
166 var els = document.getElementsByClassName(id, 'browser');
172 var els = document.getElementsByClassName(id, 'browser');
167 for (var i = 0; i < els.length; i++) {
173 for (var i = 0; i < els.length; i++) {
168 Element.show(els[i]);
174 Element.show(els[i]);
169 if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) {
175 if (els[i].hasClassName('loaded') && !els[i].hasClassName('collapsed')) {
170 expandScmEntry(els[i].id);
176 expandScmEntry(els[i].id);
171 }
177 }
172 }
178 }
173 $(id).addClassName('open');
179 $(id).addClassName('open');
174 }
180 }
175
181
176 function scmEntryClick(id) {
182 function scmEntryClick(id) {
177 el = $(id);
183 el = $(id);
178 if (el.hasClassName('open')) {
184 if (el.hasClassName('open')) {
179 collapseScmEntry(id);
185 collapseScmEntry(id);
180 el.addClassName('collapsed');
186 el.addClassName('collapsed');
181 return false;
187 return false;
182 } else if (el.hasClassName('loaded')) {
188 } else if (el.hasClassName('loaded')) {
183 expandScmEntry(id);
189 expandScmEntry(id);
184 el.removeClassName('collapsed');
190 el.removeClassName('collapsed');
185 return false;
191 return false;
186 }
192 }
187 if (el.hasClassName('loading')) {
193 if (el.hasClassName('loading')) {
188 return false;
194 return false;
189 }
195 }
190 el.addClassName('loading');
196 el.addClassName('loading');
191 return true;
197 return true;
192 }
198 }
193
199
194 function scmEntryLoaded(id) {
200 function scmEntryLoaded(id) {
195 Element.addClassName(id, 'open');
201 Element.addClassName(id, 'open');
196 Element.addClassName(id, 'loaded');
202 Element.addClassName(id, 'loaded');
197 Element.removeClassName(id, 'loading');
203 Element.removeClassName(id, 'loading');
198 }
204 }
199
205
200 function randomKey(size) {
206 function randomKey(size) {
201 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
207 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
202 var key = '';
208 var key = '';
203 for (i = 0; i < size; i++) {
209 for (i = 0; i < size; i++) {
204 key += chars[Math.floor(Math.random() * chars.length)];
210 key += chars[Math.floor(Math.random() * chars.length)];
205 }
211 }
206 return key;
212 return key;
207 }
213 }
208
214
209 function observeParentIssueField(url) {
215 function observeParentIssueField(url) {
210 new Ajax.Autocompleter('issue_parent_issue_id',
216 new Ajax.Autocompleter('issue_parent_issue_id',
211 'parent_issue_candidates',
217 'parent_issue_candidates',
212 url,
218 url,
213 { minChars: 3,
219 { minChars: 3,
214 frequency: 0.5,
220 frequency: 0.5,
215 paramName: 'q',
221 paramName: 'q',
216 updateElement: function(value) {
222 updateElement: function(value) {
217 document.getElementById('issue_parent_issue_id').value = value.id;
223 document.getElementById('issue_parent_issue_id').value = value.id;
218 }});
224 }});
219 }
225 }
220
226
221 function observeRelatedIssueField(url) {
227 function observeRelatedIssueField(url) {
222 new Ajax.Autocompleter('relation_issue_to_id',
228 new Ajax.Autocompleter('relation_issue_to_id',
223 'related_issue_candidates',
229 'related_issue_candidates',
224 url,
230 url,
225 { minChars: 3,
231 { minChars: 3,
226 frequency: 0.5,
232 frequency: 0.5,
227 paramName: 'q',
233 paramName: 'q',
228 updateElement: function(value) {
234 updateElement: function(value) {
229 document.getElementById('relation_issue_to_id').value = value.id;
235 document.getElementById('relation_issue_to_id').value = value.id;
230 },
236 },
231 parameters: 'scope=all'
237 parameters: 'scope=all'
232 });
238 });
233 }
239 }
234
240
235 function setVisible(id, visible) {
241 function setVisible(id, visible) {
236 var el = $(id);
242 var el = $(id);
237 if (el) {if (visible) {el.show();} else {el.hide();}}
243 if (el) {if (visible) {el.show();} else {el.hide();}}
238 }
244 }
239
245
240 function observeProjectModules() {
246 function observeProjectModules() {
241 var f = function() {
247 var f = function() {
242 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
248 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
243 var c = ($('project_enabled_module_names_issue_tracking').checked == true);
249 var c = ($('project_enabled_module_names_issue_tracking').checked == true);
244 setVisible('project_trackers', c);
250 setVisible('project_trackers', c);
245 setVisible('project_issue_custom_fields', c);
251 setVisible('project_issue_custom_fields', c);
246 };
252 };
247
253
248 Event.observe(window, 'load', f);
254 Event.observe(window, 'load', f);
249 Event.observe('project_enabled_module_names_issue_tracking', 'change', f);
255 Event.observe('project_enabled_module_names_issue_tracking', 'change', f);
250 }
256 }
251
257
252
258
253 /* shows and hides ajax indicator */
259 /* shows and hides ajax indicator */
254 Ajax.Responders.register({
260 Ajax.Responders.register({
255 onCreate: function(){
261 onCreate: function(){
256 if ($('ajax-indicator') && Ajax.activeRequestCount > 0) {
262 if ($('ajax-indicator') && Ajax.activeRequestCount > 0) {
257 Element.show('ajax-indicator');
263 Element.show('ajax-indicator');
258 }
264 }
259 },
265 },
260 onComplete: function(){
266 onComplete: function(){
261 if ($('ajax-indicator') && Ajax.activeRequestCount == 0) {
267 if ($('ajax-indicator') && Ajax.activeRequestCount == 0) {
262 Element.hide('ajax-indicator');
268 Element.hide('ajax-indicator');
263 }
269 }
264 }
270 }
265 });
271 });
266
272
267 function hideOnLoad() {
273 function hideOnLoad() {
268 $$('.hol').each(function(el) {
274 $$('.hol').each(function(el) {
269 el.hide();
275 el.hide();
270 });
276 });
271 }
277 }
272
278
273 Event.observe(window, 'load', hideOnLoad);
279 Event.observe(window, 'load', hideOnLoad);
@@ -1,31 +1,37
1 ---
1 ---
2 issue_statuses_006:
3 name: Rejected
4 is_default: false
5 is_closed: true
6 id: 6
7 issue_statuses_001:
2 issue_statuses_001:
3 id: 1
8 name: New
4 name: New
9 is_default: true
5 is_default: true
10 is_closed: false
6 is_closed: false
11 id: 1
7 position: 1
12 issue_statuses_002:
8 issue_statuses_002:
9 id: 2
13 name: Assigned
10 name: Assigned
14 is_default: false
11 is_default: false
15 is_closed: false
12 is_closed: false
16 id: 2
13 position: 2
17 issue_statuses_003:
14 issue_statuses_003:
15 id: 3
18 name: Resolved
16 name: Resolved
19 is_default: false
17 is_default: false
20 is_closed: false
18 is_closed: false
21 id: 3
19 position: 3
22 issue_statuses_004:
20 issue_statuses_004:
23 name: Feedback
21 name: Feedback
22 id: 4
24 is_default: false
23 is_default: false
25 is_closed: false
24 is_closed: false
26 id: 4
25 position: 4
27 issue_statuses_005:
26 issue_statuses_005:
27 id: 5
28 name: Closed
28 name: Closed
29 is_default: false
29 is_default: false
30 is_closed: true
30 is_closed: true
31 id: 5
31 position: 5
32 issue_statuses_006:
33 id: 6
34 name: Rejected
35 is_default: false
36 is_closed: true
37 position: 6
@@ -1,158 +1,186
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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'workflows_controller'
19 require 'workflows_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class WorkflowsController; def rescue_action(e) raise e end; end
22 class WorkflowsController; def rescue_action(e) raise e end; end
23
23
24 class WorkflowsControllerTest < ActionController::TestCase
24 class WorkflowsControllerTest < ActionController::TestCase
25 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
25 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
26
26
27 def setup
27 def setup
28 @controller = WorkflowsController.new
28 @controller = WorkflowsController.new
29 @request = ActionController::TestRequest.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
31 User.current = nil
32 @request.session[:user_id] = 1 # admin
32 @request.session[:user_id] = 1 # admin
33 end
33 end
34
34
35 def test_index
35 def test_index
36 get :index
36 get :index
37 assert_response :success
37 assert_response :success
38 assert_template 'index'
38 assert_template 'index'
39
39
40 count = Workflow.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
40 count = Workflow.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
41 assert_tag :tag => 'a', :content => count.to_s,
41 assert_tag :tag => 'a', :content => count.to_s,
42 :attributes => { :href => '/workflows/edit?role_id=1&amp;tracker_id=2' }
42 :attributes => { :href => '/workflows/edit?role_id=1&amp;tracker_id=2' }
43 end
43 end
44
44
45 def test_get_edit
45 def test_get_edit
46 get :edit
46 get :edit
47 assert_response :success
47 assert_response :success
48 assert_template 'edit'
48 assert_template 'edit'
49 assert_not_nil assigns(:roles)
49 assert_not_nil assigns(:roles)
50 assert_not_nil assigns(:trackers)
50 assert_not_nil assigns(:trackers)
51 end
51 end
52
52
53 def test_get_edit_with_role_and_tracker
53 def test_get_edit_with_role_and_tracker
54 Workflow.delete_all
54 Workflow.delete_all
55 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
55 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
56 Workflow.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
56 Workflow.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
57
57
58 get :edit, :role_id => 2, :tracker_id => 1
58 get :edit, :role_id => 2, :tracker_id => 1
59 assert_response :success
59 assert_response :success
60 assert_template 'edit'
60 assert_template 'edit'
61
61
62 # used status only
62 # used status only
63 assert_not_nil assigns(:statuses)
63 assert_not_nil assigns(:statuses)
64 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
64 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
65
65
66 # allowed transitions
66 # allowed transitions
67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
68 :name => 'issue_status[3][]',
68 :name => 'issue_status[3][5][]',
69 :value => '5',
69 :value => 'always',
70 :checked => 'checked' }
70 :checked => 'checked' }
71 # not allowed
71 # not allowed
72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
73 :name => 'issue_status[3][]',
73 :name => 'issue_status[3][2][]',
74 :value => '2',
74 :value => 'always',
75 :checked => nil }
75 :checked => nil }
76 # unused
76 # unused
77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
78 :name => 'issue_status[4][]' }
78 :name => 'issue_status[1][1][]' }
79 end
79 end
80
80
81 def test_get_edit_with_role_and_tracker_and_all_statuses
81 def test_get_edit_with_role_and_tracker_and_all_statuses
82 Workflow.delete_all
82 Workflow.delete_all
83
83
84 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
84 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
85 assert_response :success
85 assert_response :success
86 assert_template 'edit'
86 assert_template 'edit'
87
87
88 assert_not_nil assigns(:statuses)
88 assert_not_nil assigns(:statuses)
89 assert_equal IssueStatus.count, assigns(:statuses).size
89 assert_equal IssueStatus.count, assigns(:statuses).size
90
90
91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
92 :name => 'issue_status[1][]',
92 :name => 'issue_status[1][1][]',
93 :value => '1',
93 :value => 'always',
94 :checked => nil }
94 :checked => nil }
95 end
95 end
96
96
97 def test_post_edit
97 def test_post_edit
98 post :edit, :role_id => 2, :tracker_id => 1, :issue_status => {'4' => ['5'], '3' => ['1', '2']}
98 post :edit, :role_id => 2, :tracker_id => 1,
99 :issue_status => {
100 '4' => {'5' => ['always']},
101 '3' => {'1' => ['always'], '2' => ['always']}
102 }
99 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
103 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
100
104
101 assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
105 assert_equal 3, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
102 assert_not_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
106 assert_not_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
103 assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
107 assert_nil Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
104 end
108 end
105
109
110 def test_post_edit_with_additional_transitions
111 post :edit, :role_id => 2, :tracker_id => 1,
112 :issue_status => {
113 '4' => {'5' => ['always']},
114 '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
115 }
116 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
117
118 assert_equal 4, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
119
120 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5})
121 assert ! w.author
122 assert ! w.assignee
123 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1})
124 assert w.author
125 assert ! w.assignee
126 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
127 assert ! w.author
128 assert w.assignee
129 w = Workflow.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4})
130 assert w.author
131 assert w.assignee
132 end
133
106 def test_clear_workflow
134 def test_clear_workflow
107 assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
135 assert Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
108
136
109 post :edit, :role_id => 2, :tracker_id => 1
137 post :edit, :role_id => 2, :tracker_id => 1
110 assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
138 assert_equal 0, Workflow.count(:conditions => {:tracker_id => 1, :role_id => 2})
111 end
139 end
112
140
113 def test_get_copy
141 def test_get_copy
114 get :copy
142 get :copy
115 assert_response :success
143 assert_response :success
116 assert_template 'copy'
144 assert_template 'copy'
117 end
145 end
118
146
119 def test_post_copy_one_to_one
147 def test_post_copy_one_to_one
120 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
148 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
121
149
122 post :copy, :source_tracker_id => '1', :source_role_id => '2',
150 post :copy, :source_tracker_id => '1', :source_role_id => '2',
123 :target_tracker_ids => ['3'], :target_role_ids => ['1']
151 :target_tracker_ids => ['3'], :target_role_ids => ['1']
124 assert_response 302
152 assert_response 302
125 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
153 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
126 end
154 end
127
155
128 def test_post_copy_one_to_many
156 def test_post_copy_one_to_many
129 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
157 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
130
158
131 post :copy, :source_tracker_id => '1', :source_role_id => '2',
159 post :copy, :source_tracker_id => '1', :source_role_id => '2',
132 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
160 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
133 assert_response 302
161 assert_response 302
134 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
162 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
135 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
163 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
136 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
164 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
137 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
165 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
138 end
166 end
139
167
140 def test_post_copy_many_to_many
168 def test_post_copy_many_to_many
141 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
169 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
142 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
170 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
143
171
144 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
172 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
145 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
173 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
146 assert_response 302
174 assert_response 302
147 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
175 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
148 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
176 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
149 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
177 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
150 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
178 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
151 end
179 end
152
180
153 # Returns an array of status transitions that can be compared
181 # Returns an array of status transitions that can be compared
154 def status_transitions(conditions)
182 def status_transitions(conditions)
155 Workflow.find(:all, :conditions => conditions,
183 Workflow.find(:all, :conditions => conditions,
156 :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]}
184 :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]}
157 end
185 end
158 end
186 end
@@ -1,107 +1,131
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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueStatusTest < ActiveSupport::TestCase
20 class IssueStatusTest < ActiveSupport::TestCase
21 fixtures :issue_statuses, :issues
21 fixtures :issue_statuses, :issues, :roles, :trackers
22
22
23 def test_create
23 def test_create
24 status = IssueStatus.new :name => "Assigned"
24 status = IssueStatus.new :name => "Assigned"
25 assert !status.save
25 assert !status.save
26 # status name uniqueness
26 # status name uniqueness
27 assert_equal 1, status.errors.count
27 assert_equal 1, status.errors.count
28
28
29 status.name = "Test Status"
29 status.name = "Test Status"
30 assert status.save
30 assert status.save
31 assert !status.is_default
31 assert !status.is_default
32 end
32 end
33
33
34 def test_destroy
34 def test_destroy
35 status = IssueStatus.find(3)
35 status = IssueStatus.find(3)
36 assert_difference 'IssueStatus.count', -1 do
36 assert_difference 'IssueStatus.count', -1 do
37 assert status.destroy
37 assert status.destroy
38 end
38 end
39 assert_nil Workflow.first(:conditions => {:old_status_id => status.id})
39 assert_nil Workflow.first(:conditions => {:old_status_id => status.id})
40 assert_nil Workflow.first(:conditions => {:new_status_id => status.id})
40 assert_nil Workflow.first(:conditions => {:new_status_id => status.id})
41 end
41 end
42
42
43 def test_destroy_status_in_use
43 def test_destroy_status_in_use
44 # Status assigned to an Issue
44 # Status assigned to an Issue
45 status = Issue.find(1).status
45 status = Issue.find(1).status
46 assert_raise(RuntimeError, "Can't delete status") { status.destroy }
46 assert_raise(RuntimeError, "Can't delete status") { status.destroy }
47 end
47 end
48
48
49 def test_default
49 def test_default
50 status = IssueStatus.default
50 status = IssueStatus.default
51 assert_kind_of IssueStatus, status
51 assert_kind_of IssueStatus, status
52 end
52 end
53
53
54 def test_change_default
54 def test_change_default
55 status = IssueStatus.find(2)
55 status = IssueStatus.find(2)
56 assert !status.is_default
56 assert !status.is_default
57 status.is_default = true
57 status.is_default = true
58 assert status.save
58 assert status.save
59 status.reload
59 status.reload
60
60
61 assert_equal status, IssueStatus.default
61 assert_equal status, IssueStatus.default
62 assert !IssueStatus.find(1).is_default
62 assert !IssueStatus.find(1).is_default
63 end
63 end
64
64
65 def test_reorder_should_not_clear_default_status
65 def test_reorder_should_not_clear_default_status
66 status = IssueStatus.default
66 status = IssueStatus.default
67 status.move_to_bottom
67 status.move_to_bottom
68 status.reload
68 status.reload
69 assert status.is_default?
69 assert status.is_default?
70 end
70 end
71
72 def test_new_statuses_allowed_to
73 Workflow.delete_all
74
75 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
76 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
77 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
78 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
79 status = IssueStatus.find(1)
80 role = Role.find(1)
81 tracker = Tracker.find(1)
82
83 assert_equal [2], status.new_statuses_allowed_to([role], tracker, false, false).map(&:id)
84 assert_equal [2], status.find_new_statuses_allowed_to([role], tracker, false, false).map(&:id)
85
86 assert_equal [2, 3], status.new_statuses_allowed_to([role], tracker, true, false).map(&:id)
87 assert_equal [2, 3], status.find_new_statuses_allowed_to([role], tracker, true, false).map(&:id)
88
89 assert_equal [2, 4], status.new_statuses_allowed_to([role], tracker, false, true).map(&:id)
90 assert_equal [2, 4], status.find_new_statuses_allowed_to([role], tracker, false, true).map(&:id)
91
92 assert_equal [2, 3, 4, 5], status.new_statuses_allowed_to([role], tracker, true, true).map(&:id)
93 assert_equal [2, 3, 4, 5], status.find_new_statuses_allowed_to([role], tracker, true, true).map(&:id)
94 end
71
95
72 context "#update_done_ratios" do
96 context "#update_done_ratios" do
73 setup do
97 setup do
74 @issue = Issue.find(1)
98 @issue = Issue.find(1)
75 @issue_status = IssueStatus.find(1)
99 @issue_status = IssueStatus.find(1)
76 @issue_status.update_attribute(:default_done_ratio, 50)
100 @issue_status.update_attribute(:default_done_ratio, 50)
77 end
101 end
78
102
79 context "with Setting.issue_done_ratio using the issue_field" do
103 context "with Setting.issue_done_ratio using the issue_field" do
80 setup do
104 setup do
81 Setting.issue_done_ratio = 'issue_field'
105 Setting.issue_done_ratio = 'issue_field'
82 end
106 end
83
107
84 should "change nothing" do
108 should "change nothing" do
85 IssueStatus.update_issue_done_ratios
109 IssueStatus.update_issue_done_ratios
86
110
87 assert_equal 0, Issue.count(:conditions => {:done_ratio => 50})
111 assert_equal 0, Issue.count(:conditions => {:done_ratio => 50})
88 end
112 end
89 end
113 end
90
114
91 context "with Setting.issue_done_ratio using the issue_status" do
115 context "with Setting.issue_done_ratio using the issue_status" do
92 setup do
116 setup do
93 Setting.issue_done_ratio = 'issue_status'
117 Setting.issue_done_ratio = 'issue_status'
94 end
118 end
95
119
96 should "update all of the issue's done_ratios to match their Issue Status" do
120 should "update all of the issue's done_ratios to match their Issue Status" do
97 IssueStatus.update_issue_done_ratios
121 IssueStatus.update_issue_done_ratios
98
122
99 issues = Issue.find([1,3,4,5,6,7,9,10])
123 issues = Issue.find([1,3,4,5,6,7,9,10])
100 issues.each do |issue|
124 issues.each do |issue|
101 assert_equal @issue_status, issue.status
125 assert_equal @issue_status, issue.status
102 assert_equal 50, issue.read_attribute(:done_ratio)
126 assert_equal 50, issue.read_attribute(:done_ratio)
103 end
127 end
104 end
128 end
105 end
129 end
106 end
130 end
107 end
131 end
@@ -1,835 +1,862
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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
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 :enabled_modules,
23 :enabled_modules,
24 :versions,
24 :versions,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :enumerations,
26 :enumerations,
27 :issues,
27 :issues,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :time_entries
29 :time_entries
30
30
31 def test_create
31 def test_create
32 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 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')
33 assert issue.save
33 assert issue.save
34 issue.reload
34 issue.reload
35 assert_equal 1.5, issue.estimated_hours
35 assert_equal 1.5, issue.estimated_hours
36 end
36 end
37
37
38 def test_create_minimal
38 def test_create_minimal
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 assert issue.save
40 assert issue.save
41 assert issue.description.nil?
41 assert issue.description.nil?
42 end
42 end
43
43
44 def test_create_with_required_custom_field
44 def test_create_with_required_custom_field
45 field = IssueCustomField.find_by_name('Database')
45 field = IssueCustomField.find_by_name('Database')
46 field.update_attribute(:is_required, true)
46 field.update_attribute(:is_required, true)
47
47
48 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 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')
49 assert issue.available_custom_fields.include?(field)
49 assert issue.available_custom_fields.include?(field)
50 # No value for the custom field
50 # No value for the custom field
51 assert !issue.save
51 assert !issue.save
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 # Blank value
53 # Blank value
54 issue.custom_field_values = { field.id => '' }
54 issue.custom_field_values = { field.id => '' }
55 assert !issue.save
55 assert !issue.save
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 # Invalid value
57 # Invalid value
58 issue.custom_field_values = { field.id => 'SQLServer' }
58 issue.custom_field_values = { field.id => 'SQLServer' }
59 assert !issue.save
59 assert !issue.save
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 # Valid value
61 # Valid value
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 assert issue.save
63 assert issue.save
64 issue.reload
64 issue.reload
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 end
66 end
67
67
68 def test_visible_scope_for_anonymous
68 def test_visible_scope_for_anonymous
69 # Anonymous user should see issues of public projects only
69 # Anonymous user should see issues of public projects only
70 issues = Issue.visible(User.anonymous).all
70 issues = Issue.visible(User.anonymous).all
71 assert issues.any?
71 assert issues.any?
72 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 assert_nil issues.detect {|issue| !issue.project.is_public?}
73 # Anonymous user should not see issues without permission
73 # Anonymous user should not see issues without permission
74 Role.anonymous.remove_permission!(:view_issues)
74 Role.anonymous.remove_permission!(:view_issues)
75 issues = Issue.visible(User.anonymous).all
75 issues = Issue.visible(User.anonymous).all
76 assert issues.empty?
76 assert issues.empty?
77 end
77 end
78
78
79 def test_visible_scope_for_user
79 def test_visible_scope_for_user
80 user = User.find(9)
80 user = User.find(9)
81 assert user.projects.empty?
81 assert user.projects.empty?
82 # Non member user should see issues of public projects only
82 # Non member user should see issues of public projects only
83 issues = Issue.visible(user).all
83 issues = Issue.visible(user).all
84 assert issues.any?
84 assert issues.any?
85 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 assert_nil issues.detect {|issue| !issue.project.is_public?}
86 # Non member user should not see issues without permission
86 # Non member user should not see issues without permission
87 Role.non_member.remove_permission!(:view_issues)
87 Role.non_member.remove_permission!(:view_issues)
88 user.reload
88 user.reload
89 issues = Issue.visible(user).all
89 issues = Issue.visible(user).all
90 assert issues.empty?
90 assert issues.empty?
91 # User should see issues of projects for which he has view_issues permissions only
91 # User should see issues of projects for which he has view_issues permissions only
92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
93 user.reload
93 user.reload
94 issues = Issue.visible(user).all
94 issues = Issue.visible(user).all
95 assert issues.any?
95 assert issues.any?
96 assert_nil issues.detect {|issue| issue.project_id != 2}
96 assert_nil issues.detect {|issue| issue.project_id != 2}
97 end
97 end
98
98
99 def test_visible_scope_for_admin
99 def test_visible_scope_for_admin
100 user = User.find(1)
100 user = User.find(1)
101 user.members.each(&:destroy)
101 user.members.each(&:destroy)
102 assert user.projects.empty?
102 assert user.projects.empty?
103 issues = Issue.visible(user).all
103 issues = Issue.visible(user).all
104 assert issues.any?
104 assert issues.any?
105 # Admin should see issues on private projects that he does not belong to
105 # Admin should see issues on private projects that he does not belong to
106 assert issues.detect {|issue| !issue.project.is_public?}
106 assert issues.detect {|issue| !issue.project.is_public?}
107 end
107 end
108
108
109 def test_errors_full_messages_should_include_custom_fields_errors
109 def test_errors_full_messages_should_include_custom_fields_errors
110 field = IssueCustomField.find_by_name('Database')
110 field = IssueCustomField.find_by_name('Database')
111
111
112 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 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')
113 assert issue.available_custom_fields.include?(field)
113 assert issue.available_custom_fields.include?(field)
114 # Invalid value
114 # Invalid value
115 issue.custom_field_values = { field.id => 'SQLServer' }
115 issue.custom_field_values = { field.id => 'SQLServer' }
116
116
117 assert !issue.valid?
117 assert !issue.valid?
118 assert_equal 1, issue.errors.full_messages.size
118 assert_equal 1, issue.errors.full_messages.size
119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
120 end
120 end
121
121
122 def test_update_issue_with_required_custom_field
122 def test_update_issue_with_required_custom_field
123 field = IssueCustomField.find_by_name('Database')
123 field = IssueCustomField.find_by_name('Database')
124 field.update_attribute(:is_required, true)
124 field.update_attribute(:is_required, true)
125
125
126 issue = Issue.find(1)
126 issue = Issue.find(1)
127 assert_nil issue.custom_value_for(field)
127 assert_nil issue.custom_value_for(field)
128 assert issue.available_custom_fields.include?(field)
128 assert issue.available_custom_fields.include?(field)
129 # No change to custom values, issue can be saved
129 # No change to custom values, issue can be saved
130 assert issue.save
130 assert issue.save
131 # Blank value
131 # Blank value
132 issue.custom_field_values = { field.id => '' }
132 issue.custom_field_values = { field.id => '' }
133 assert !issue.save
133 assert !issue.save
134 # Valid value
134 # Valid value
135 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 issue.custom_field_values = { field.id => 'PostgreSQL' }
136 assert issue.save
136 assert issue.save
137 issue.reload
137 issue.reload
138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
139 end
139 end
140
140
141 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 def test_should_not_update_attributes_if_custom_fields_validation_fails
142 issue = Issue.find(1)
142 issue = Issue.find(1)
143 field = IssueCustomField.find_by_name('Database')
143 field = IssueCustomField.find_by_name('Database')
144 assert issue.available_custom_fields.include?(field)
144 assert issue.available_custom_fields.include?(field)
145
145
146 issue.custom_field_values = { field.id => 'Invalid' }
146 issue.custom_field_values = { field.id => 'Invalid' }
147 issue.subject = 'Should be not be saved'
147 issue.subject = 'Should be not be saved'
148 assert !issue.save
148 assert !issue.save
149
149
150 issue.reload
150 issue.reload
151 assert_equal "Can't print recipes", issue.subject
151 assert_equal "Can't print recipes", issue.subject
152 end
152 end
153
153
154 def test_should_not_recreate_custom_values_objects_on_update
154 def test_should_not_recreate_custom_values_objects_on_update
155 field = IssueCustomField.find_by_name('Database')
155 field = IssueCustomField.find_by_name('Database')
156
156
157 issue = Issue.find(1)
157 issue = Issue.find(1)
158 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 assert issue.save
159 assert issue.save
160 custom_value = issue.custom_value_for(field)
160 custom_value = issue.custom_value_for(field)
161 issue.reload
161 issue.reload
162 issue.custom_field_values = { field.id => 'MySQL' }
162 issue.custom_field_values = { field.id => 'MySQL' }
163 assert issue.save
163 assert issue.save
164 issue.reload
164 issue.reload
165 assert_equal custom_value.id, issue.custom_value_for(field).id
165 assert_equal custom_value.id, issue.custom_value_for(field).id
166 end
166 end
167
167
168 def test_assigning_tracker_id_should_reload_custom_fields_values
168 def test_assigning_tracker_id_should_reload_custom_fields_values
169 issue = Issue.new(:project => Project.find(1))
169 issue = Issue.new(:project => Project.find(1))
170 assert issue.custom_field_values.empty?
170 assert issue.custom_field_values.empty?
171 issue.tracker_id = 1
171 issue.tracker_id = 1
172 assert issue.custom_field_values.any?
172 assert issue.custom_field_values.any?
173 end
173 end
174
174
175 def test_assigning_attributes_should_assign_tracker_id_first
175 def test_assigning_attributes_should_assign_tracker_id_first
176 attributes = ActiveSupport::OrderedHash.new
176 attributes = ActiveSupport::OrderedHash.new
177 attributes['custom_field_values'] = { '1' => 'MySQL' }
177 attributes['custom_field_values'] = { '1' => 'MySQL' }
178 attributes['tracker_id'] = '1'
178 attributes['tracker_id'] = '1'
179 issue = Issue.new(:project => Project.find(1))
179 issue = Issue.new(:project => Project.find(1))
180 issue.attributes = attributes
180 issue.attributes = attributes
181 assert_not_nil issue.custom_value_for(1)
181 assert_not_nil issue.custom_value_for(1)
182 assert_equal 'MySQL', issue.custom_value_for(1).value
182 assert_equal 'MySQL', issue.custom_value_for(1).value
183 end
183 end
184
184
185 def test_should_update_issue_with_disabled_tracker
185 def test_should_update_issue_with_disabled_tracker
186 p = Project.find(1)
186 p = Project.find(1)
187 issue = Issue.find(1)
187 issue = Issue.find(1)
188
188
189 p.trackers.delete(issue.tracker)
189 p.trackers.delete(issue.tracker)
190 assert !p.trackers.include?(issue.tracker)
190 assert !p.trackers.include?(issue.tracker)
191
191
192 issue.reload
192 issue.reload
193 issue.subject = 'New subject'
193 issue.subject = 'New subject'
194 assert issue.save
194 assert issue.save
195 end
195 end
196
196
197 def test_should_not_set_a_disabled_tracker
197 def test_should_not_set_a_disabled_tracker
198 p = Project.find(1)
198 p = Project.find(1)
199 p.trackers.delete(Tracker.find(2))
199 p.trackers.delete(Tracker.find(2))
200
200
201 issue = Issue.find(1)
201 issue = Issue.find(1)
202 issue.tracker_id = 2
202 issue.tracker_id = 2
203 issue.subject = 'New subject'
203 issue.subject = 'New subject'
204 assert !issue.save
204 assert !issue.save
205 assert_not_nil issue.errors.on(:tracker_id)
205 assert_not_nil issue.errors.on(:tracker_id)
206 end
206 end
207
207
208 def test_category_based_assignment
208 def test_category_based_assignment
209 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)
209 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)
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
211 end
211 end
212
212
213
214
215 def test_new_statuses_allowed_to
216 Workflow.delete_all
217
218 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
219 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
220 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
221 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
222 status = IssueStatus.find(1)
223 role = Role.find(1)
224 tracker = Tracker.find(1)
225 user = User.find(2)
226
227 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
228 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
229
230 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
231 assert_equal [1, 2, 3], issue.new_statuses_allowed_to(user).map(&:id)
232
233 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
234 assert_equal [1, 2, 4], issue.new_statuses_allowed_to(user).map(&:id)
235
236 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
237 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
238 end
239
213 def test_copy
240 def test_copy
214 issue = Issue.new.copy_from(1)
241 issue = Issue.new.copy_from(1)
215 assert issue.save
242 assert issue.save
216 issue.reload
243 issue.reload
217 orig = Issue.find(1)
244 orig = Issue.find(1)
218 assert_equal orig.subject, issue.subject
245 assert_equal orig.subject, issue.subject
219 assert_equal orig.tracker, issue.tracker
246 assert_equal orig.tracker, issue.tracker
220 assert_equal "125", issue.custom_value_for(2).value
247 assert_equal "125", issue.custom_value_for(2).value
221 end
248 end
222
249
223 def test_copy_should_copy_status
250 def test_copy_should_copy_status
224 orig = Issue.find(8)
251 orig = Issue.find(8)
225 assert orig.status != IssueStatus.default
252 assert orig.status != IssueStatus.default
226
253
227 issue = Issue.new.copy_from(orig)
254 issue = Issue.new.copy_from(orig)
228 assert issue.save
255 assert issue.save
229 issue.reload
256 issue.reload
230 assert_equal orig.status, issue.status
257 assert_equal orig.status, issue.status
231 end
258 end
232
259
233 def test_should_close_duplicates
260 def test_should_close_duplicates
234 # Create 3 issues
261 # Create 3 issues
235 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')
262 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')
236 assert issue1.save
263 assert issue1.save
237 issue2 = issue1.clone
264 issue2 = issue1.clone
238 assert issue2.save
265 assert issue2.save
239 issue3 = issue1.clone
266 issue3 = issue1.clone
240 assert issue3.save
267 assert issue3.save
241
268
242 # 2 is a dupe of 1
269 # 2 is a dupe of 1
243 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
270 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
244 # And 3 is a dupe of 2
271 # And 3 is a dupe of 2
245 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
272 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
246 # And 3 is a dupe of 1 (circular duplicates)
273 # And 3 is a dupe of 1 (circular duplicates)
247 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
274 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
248
275
249 assert issue1.reload.duplicates.include?(issue2)
276 assert issue1.reload.duplicates.include?(issue2)
250
277
251 # Closing issue 1
278 # Closing issue 1
252 issue1.init_journal(User.find(:first), "Closing issue1")
279 issue1.init_journal(User.find(:first), "Closing issue1")
253 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
280 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
254 assert issue1.save
281 assert issue1.save
255 # 2 and 3 should be also closed
282 # 2 and 3 should be also closed
256 assert issue2.reload.closed?
283 assert issue2.reload.closed?
257 assert issue3.reload.closed?
284 assert issue3.reload.closed?
258 end
285 end
259
286
260 def test_should_not_close_duplicated_issue
287 def test_should_not_close_duplicated_issue
261 # Create 3 issues
288 # Create 3 issues
262 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')
289 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')
263 assert issue1.save
290 assert issue1.save
264 issue2 = issue1.clone
291 issue2 = issue1.clone
265 assert issue2.save
292 assert issue2.save
266
293
267 # 2 is a dupe of 1
294 # 2 is a dupe of 1
268 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
295 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
269 # 2 is a dup of 1 but 1 is not a duplicate of 2
296 # 2 is a dup of 1 but 1 is not a duplicate of 2
270 assert !issue2.reload.duplicates.include?(issue1)
297 assert !issue2.reload.duplicates.include?(issue1)
271
298
272 # Closing issue 2
299 # Closing issue 2
273 issue2.init_journal(User.find(:first), "Closing issue2")
300 issue2.init_journal(User.find(:first), "Closing issue2")
274 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
301 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
275 assert issue2.save
302 assert issue2.save
276 # 1 should not be also closed
303 # 1 should not be also closed
277 assert !issue1.reload.closed?
304 assert !issue1.reload.closed?
278 end
305 end
279
306
280 def test_assignable_versions
307 def test_assignable_versions
281 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
308 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
282 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
309 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
283 end
310 end
284
311
285 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
312 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
286 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
313 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
287 assert !issue.save
314 assert !issue.save
288 assert_not_nil issue.errors.on(:fixed_version_id)
315 assert_not_nil issue.errors.on(:fixed_version_id)
289 end
316 end
290
317
291 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
318 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
292 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
319 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
293 assert !issue.save
320 assert !issue.save
294 assert_not_nil issue.errors.on(:fixed_version_id)
321 assert_not_nil issue.errors.on(:fixed_version_id)
295 end
322 end
296
323
297 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
324 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
298 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
325 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
299 assert issue.save
326 assert issue.save
300 end
327 end
301
328
302 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
329 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
303 issue = Issue.find(11)
330 issue = Issue.find(11)
304 assert_equal 'closed', issue.fixed_version.status
331 assert_equal 'closed', issue.fixed_version.status
305 issue.subject = 'Subject changed'
332 issue.subject = 'Subject changed'
306 assert issue.save
333 assert issue.save
307 end
334 end
308
335
309 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
336 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
310 issue = Issue.find(11)
337 issue = Issue.find(11)
311 issue.status_id = 1
338 issue.status_id = 1
312 assert !issue.save
339 assert !issue.save
313 assert_not_nil issue.errors.on_base
340 assert_not_nil issue.errors.on_base
314 end
341 end
315
342
316 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
343 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
317 issue = Issue.find(11)
344 issue = Issue.find(11)
318 issue.status_id = 1
345 issue.status_id = 1
319 issue.fixed_version_id = 3
346 issue.fixed_version_id = 3
320 assert issue.save
347 assert issue.save
321 end
348 end
322
349
323 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
350 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
324 issue = Issue.find(12)
351 issue = Issue.find(12)
325 assert_equal 'locked', issue.fixed_version.status
352 assert_equal 'locked', issue.fixed_version.status
326 issue.status_id = 1
353 issue.status_id = 1
327 assert issue.save
354 assert issue.save
328 end
355 end
329
356
330 def test_move_to_another_project_with_same_category
357 def test_move_to_another_project_with_same_category
331 issue = Issue.find(1)
358 issue = Issue.find(1)
332 assert issue.move_to_project(Project.find(2))
359 assert issue.move_to_project(Project.find(2))
333 issue.reload
360 issue.reload
334 assert_equal 2, issue.project_id
361 assert_equal 2, issue.project_id
335 # Category changes
362 # Category changes
336 assert_equal 4, issue.category_id
363 assert_equal 4, issue.category_id
337 # Make sure time entries were move to the target project
364 # Make sure time entries were move to the target project
338 assert_equal 2, issue.time_entries.first.project_id
365 assert_equal 2, issue.time_entries.first.project_id
339 end
366 end
340
367
341 def test_move_to_another_project_without_same_category
368 def test_move_to_another_project_without_same_category
342 issue = Issue.find(2)
369 issue = Issue.find(2)
343 assert issue.move_to_project(Project.find(2))
370 assert issue.move_to_project(Project.find(2))
344 issue.reload
371 issue.reload
345 assert_equal 2, issue.project_id
372 assert_equal 2, issue.project_id
346 # Category cleared
373 # Category cleared
347 assert_nil issue.category_id
374 assert_nil issue.category_id
348 end
375 end
349
376
350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
377 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
351 issue = Issue.find(1)
378 issue = Issue.find(1)
352 issue.update_attribute(:fixed_version_id, 1)
379 issue.update_attribute(:fixed_version_id, 1)
353 assert issue.move_to_project(Project.find(2))
380 assert issue.move_to_project(Project.find(2))
354 issue.reload
381 issue.reload
355 assert_equal 2, issue.project_id
382 assert_equal 2, issue.project_id
356 # Cleared fixed_version
383 # Cleared fixed_version
357 assert_equal nil, issue.fixed_version
384 assert_equal nil, issue.fixed_version
358 end
385 end
359
386
360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
387 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
361 issue = Issue.find(1)
388 issue = Issue.find(1)
362 issue.update_attribute(:fixed_version_id, 4)
389 issue.update_attribute(:fixed_version_id, 4)
363 assert issue.move_to_project(Project.find(5))
390 assert issue.move_to_project(Project.find(5))
364 issue.reload
391 issue.reload
365 assert_equal 5, issue.project_id
392 assert_equal 5, issue.project_id
366 # Keep fixed_version
393 # Keep fixed_version
367 assert_equal 4, issue.fixed_version_id
394 assert_equal 4, issue.fixed_version_id
368 end
395 end
369
396
370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
397 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
371 issue = Issue.find(1)
398 issue = Issue.find(1)
372 issue.update_attribute(:fixed_version_id, 1)
399 issue.update_attribute(:fixed_version_id, 1)
373 assert issue.move_to_project(Project.find(5))
400 assert issue.move_to_project(Project.find(5))
374 issue.reload
401 issue.reload
375 assert_equal 5, issue.project_id
402 assert_equal 5, issue.project_id
376 # Cleared fixed_version
403 # Cleared fixed_version
377 assert_equal nil, issue.fixed_version
404 assert_equal nil, issue.fixed_version
378 end
405 end
379
406
380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
407 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
381 issue = Issue.find(1)
408 issue = Issue.find(1)
382 issue.update_attribute(:fixed_version_id, 7)
409 issue.update_attribute(:fixed_version_id, 7)
383 assert issue.move_to_project(Project.find(2))
410 assert issue.move_to_project(Project.find(2))
384 issue.reload
411 issue.reload
385 assert_equal 2, issue.project_id
412 assert_equal 2, issue.project_id
386 # Keep fixed_version
413 # Keep fixed_version
387 assert_equal 7, issue.fixed_version_id
414 assert_equal 7, issue.fixed_version_id
388 end
415 end
389
416
390 def test_move_to_another_project_with_disabled_tracker
417 def test_move_to_another_project_with_disabled_tracker
391 issue = Issue.find(1)
418 issue = Issue.find(1)
392 target = Project.find(2)
419 target = Project.find(2)
393 target.tracker_ids = [3]
420 target.tracker_ids = [3]
394 target.save
421 target.save
395 assert_equal false, issue.move_to_project(target)
422 assert_equal false, issue.move_to_project(target)
396 issue.reload
423 issue.reload
397 assert_equal 1, issue.project_id
424 assert_equal 1, issue.project_id
398 end
425 end
399
426
400 def test_copy_to_the_same_project
427 def test_copy_to_the_same_project
401 issue = Issue.find(1)
428 issue = Issue.find(1)
402 copy = nil
429 copy = nil
403 assert_difference 'Issue.count' do
430 assert_difference 'Issue.count' do
404 copy = issue.move_to_project(issue.project, nil, :copy => true)
431 copy = issue.move_to_project(issue.project, nil, :copy => true)
405 end
432 end
406 assert_kind_of Issue, copy
433 assert_kind_of Issue, copy
407 assert_equal issue.project, copy.project
434 assert_equal issue.project, copy.project
408 assert_equal "125", copy.custom_value_for(2).value
435 assert_equal "125", copy.custom_value_for(2).value
409 end
436 end
410
437
411 def test_copy_to_another_project_and_tracker
438 def test_copy_to_another_project_and_tracker
412 issue = Issue.find(1)
439 issue = Issue.find(1)
413 copy = nil
440 copy = nil
414 assert_difference 'Issue.count' do
441 assert_difference 'Issue.count' do
415 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
442 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
416 end
443 end
417 copy.reload
444 copy.reload
418 assert_kind_of Issue, copy
445 assert_kind_of Issue, copy
419 assert_equal Project.find(3), copy.project
446 assert_equal Project.find(3), copy.project
420 assert_equal Tracker.find(2), copy.tracker
447 assert_equal Tracker.find(2), copy.tracker
421 # Custom field #2 is not associated with target tracker
448 # Custom field #2 is not associated with target tracker
422 assert_nil copy.custom_value_for(2)
449 assert_nil copy.custom_value_for(2)
423 end
450 end
424
451
425 context "#move_to_project" do
452 context "#move_to_project" do
426 context "as a copy" do
453 context "as a copy" do
427 setup do
454 setup do
428 @issue = Issue.find(1)
455 @issue = Issue.find(1)
429 @copy = nil
456 @copy = nil
430 end
457 end
431
458
432 should "allow assigned_to changes" do
459 should "allow assigned_to changes" do
433 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
460 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
434 assert_equal 3, @copy.assigned_to_id
461 assert_equal 3, @copy.assigned_to_id
435 end
462 end
436
463
437 should "allow status changes" do
464 should "allow status changes" do
438 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
465 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
439 assert_equal 2, @copy.status_id
466 assert_equal 2, @copy.status_id
440 end
467 end
441
468
442 should "allow start date changes" do
469 should "allow start date changes" do
443 date = Date.today
470 date = Date.today
444 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
471 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
445 assert_equal date, @copy.start_date
472 assert_equal date, @copy.start_date
446 end
473 end
447
474
448 should "allow due date changes" do
475 should "allow due date changes" do
449 date = Date.today
476 date = Date.today
450 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
477 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
451
478
452 assert_equal date, @copy.due_date
479 assert_equal date, @copy.due_date
453 end
480 end
454 end
481 end
455 end
482 end
456
483
457 def test_recipients_should_not_include_users_that_cannot_view_the_issue
484 def test_recipients_should_not_include_users_that_cannot_view_the_issue
458 issue = Issue.find(12)
485 issue = Issue.find(12)
459 assert issue.recipients.include?(issue.author.mail)
486 assert issue.recipients.include?(issue.author.mail)
460 # move the issue to a private project
487 # move the issue to a private project
461 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
488 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
462 # author is not a member of project anymore
489 # author is not a member of project anymore
463 assert !copy.recipients.include?(copy.author.mail)
490 assert !copy.recipients.include?(copy.author.mail)
464 end
491 end
465
492
466 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
493 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
467 user = User.find(3)
494 user = User.find(3)
468 issue = Issue.find(9)
495 issue = Issue.find(9)
469 Watcher.create!(:user => user, :watchable => issue)
496 Watcher.create!(:user => user, :watchable => issue)
470 assert issue.watched_by?(user)
497 assert issue.watched_by?(user)
471 assert !issue.watcher_recipients.include?(user.mail)
498 assert !issue.watcher_recipients.include?(user.mail)
472 end
499 end
473
500
474 def test_issue_destroy
501 def test_issue_destroy
475 Issue.find(1).destroy
502 Issue.find(1).destroy
476 assert_nil Issue.find_by_id(1)
503 assert_nil Issue.find_by_id(1)
477 assert_nil TimeEntry.find_by_issue_id(1)
504 assert_nil TimeEntry.find_by_issue_id(1)
478 end
505 end
479
506
480 def test_blocked
507 def test_blocked
481 blocked_issue = Issue.find(9)
508 blocked_issue = Issue.find(9)
482 blocking_issue = Issue.find(10)
509 blocking_issue = Issue.find(10)
483
510
484 assert blocked_issue.blocked?
511 assert blocked_issue.blocked?
485 assert !blocking_issue.blocked?
512 assert !blocking_issue.blocked?
486 end
513 end
487
514
488 def test_blocked_issues_dont_allow_closed_statuses
515 def test_blocked_issues_dont_allow_closed_statuses
489 blocked_issue = Issue.find(9)
516 blocked_issue = Issue.find(9)
490
517
491 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
518 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
492 assert !allowed_statuses.empty?
519 assert !allowed_statuses.empty?
493 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
520 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
494 assert closed_statuses.empty?
521 assert closed_statuses.empty?
495 end
522 end
496
523
497 def test_unblocked_issues_allow_closed_statuses
524 def test_unblocked_issues_allow_closed_statuses
498 blocking_issue = Issue.find(10)
525 blocking_issue = Issue.find(10)
499
526
500 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
527 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
501 assert !allowed_statuses.empty?
528 assert !allowed_statuses.empty?
502 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
529 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
503 assert !closed_statuses.empty?
530 assert !closed_statuses.empty?
504 end
531 end
505
532
506 def test_rescheduling_an_issue_should_reschedule_following_issue
533 def test_rescheduling_an_issue_should_reschedule_following_issue
507 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
534 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
508 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
535 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
509 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
536 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
510 assert_equal issue1.due_date + 1, issue2.reload.start_date
537 assert_equal issue1.due_date + 1, issue2.reload.start_date
511
538
512 issue1.due_date = Date.today + 5
539 issue1.due_date = Date.today + 5
513 issue1.save!
540 issue1.save!
514 assert_equal issue1.due_date + 1, issue2.reload.start_date
541 assert_equal issue1.due_date + 1, issue2.reload.start_date
515 end
542 end
516
543
517 def test_overdue
544 def test_overdue
518 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
545 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
519 assert !Issue.new(:due_date => Date.today).overdue?
546 assert !Issue.new(:due_date => Date.today).overdue?
520 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
547 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
521 assert !Issue.new(:due_date => nil).overdue?
548 assert !Issue.new(:due_date => nil).overdue?
522 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
549 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
523 end
550 end
524
551
525 context "#behind_schedule?" do
552 context "#behind_schedule?" do
526 should "be false if the issue has no start_date" do
553 should "be false if the issue has no start_date" do
527 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
554 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
528 end
555 end
529
556
530 should "be false if the issue has no end_date" do
557 should "be false if the issue has no end_date" do
531 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
558 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
532 end
559 end
533
560
534 should "be false if the issue has more done than it's calendar time" do
561 should "be false if the issue has more done than it's calendar time" do
535 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
562 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
536 end
563 end
537
564
538 should "be true if the issue hasn't been started at all" do
565 should "be true if the issue hasn't been started at all" do
539 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
566 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
540 end
567 end
541
568
542 should "be true if the issue has used more calendar time than it's done ratio" do
569 should "be true if the issue has used more calendar time than it's done ratio" do
543 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
570 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
544 end
571 end
545 end
572 end
546
573
547 context "#assignable_users" do
574 context "#assignable_users" do
548 should "be Users" do
575 should "be Users" do
549 assert_kind_of User, Issue.find(1).assignable_users.first
576 assert_kind_of User, Issue.find(1).assignable_users.first
550 end
577 end
551
578
552 should "include the issue author" do
579 should "include the issue author" do
553 project = Project.find(1)
580 project = Project.find(1)
554 non_project_member = User.generate!
581 non_project_member = User.generate!
555 issue = Issue.generate_for_project!(project, :author => non_project_member)
582 issue = Issue.generate_for_project!(project, :author => non_project_member)
556
583
557 assert issue.assignable_users.include?(non_project_member)
584 assert issue.assignable_users.include?(non_project_member)
558 end
585 end
559
586
560 should "not show the issue author twice" do
587 should "not show the issue author twice" do
561 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
588 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
562 assert_equal 2, assignable_user_ids.length
589 assert_equal 2, assignable_user_ids.length
563
590
564 assignable_user_ids.each do |user_id|
591 assignable_user_ids.each do |user_id|
565 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
592 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
566 end
593 end
567 end
594 end
568 end
595 end
569
596
570 def test_create_should_send_email_notification
597 def test_create_should_send_email_notification
571 ActionMailer::Base.deliveries.clear
598 ActionMailer::Base.deliveries.clear
572 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')
599 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')
573
600
574 assert issue.save
601 assert issue.save
575 assert_equal 1, ActionMailer::Base.deliveries.size
602 assert_equal 1, ActionMailer::Base.deliveries.size
576 end
603 end
577
604
578 def test_stale_issue_should_not_send_email_notification
605 def test_stale_issue_should_not_send_email_notification
579 ActionMailer::Base.deliveries.clear
606 ActionMailer::Base.deliveries.clear
580 issue = Issue.find(1)
607 issue = Issue.find(1)
581 stale = Issue.find(1)
608 stale = Issue.find(1)
582
609
583 issue.init_journal(User.find(1))
610 issue.init_journal(User.find(1))
584 issue.subject = 'Subjet update'
611 issue.subject = 'Subjet update'
585 assert issue.save
612 assert issue.save
586 assert_equal 1, ActionMailer::Base.deliveries.size
613 assert_equal 1, ActionMailer::Base.deliveries.size
587 ActionMailer::Base.deliveries.clear
614 ActionMailer::Base.deliveries.clear
588
615
589 stale.init_journal(User.find(1))
616 stale.init_journal(User.find(1))
590 stale.subject = 'Another subjet update'
617 stale.subject = 'Another subjet update'
591 assert_raise ActiveRecord::StaleObjectError do
618 assert_raise ActiveRecord::StaleObjectError do
592 stale.save
619 stale.save
593 end
620 end
594 assert ActionMailer::Base.deliveries.empty?
621 assert ActionMailer::Base.deliveries.empty?
595 end
622 end
596
623
597 def test_saving_twice_should_not_duplicate_journal_details
624 def test_saving_twice_should_not_duplicate_journal_details
598 i = Issue.find(:first)
625 i = Issue.find(:first)
599 i.init_journal(User.find(2), 'Some notes')
626 i.init_journal(User.find(2), 'Some notes')
600 # initial changes
627 # initial changes
601 i.subject = 'New subject'
628 i.subject = 'New subject'
602 i.done_ratio = i.done_ratio + 10
629 i.done_ratio = i.done_ratio + 10
603 assert_difference 'Journal.count' do
630 assert_difference 'Journal.count' do
604 assert i.save
631 assert i.save
605 end
632 end
606 # 1 more change
633 # 1 more change
607 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
634 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
608 assert_no_difference 'Journal.count' do
635 assert_no_difference 'Journal.count' do
609 assert_difference 'JournalDetail.count', 1 do
636 assert_difference 'JournalDetail.count', 1 do
610 i.save
637 i.save
611 end
638 end
612 end
639 end
613 # no more change
640 # no more change
614 assert_no_difference 'Journal.count' do
641 assert_no_difference 'Journal.count' do
615 assert_no_difference 'JournalDetail.count' do
642 assert_no_difference 'JournalDetail.count' do
616 i.save
643 i.save
617 end
644 end
618 end
645 end
619 end
646 end
620
647
621 def test_all_dependent_issues
648 def test_all_dependent_issues
622 IssueRelation.delete_all
649 IssueRelation.delete_all
623 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
650 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
624 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
651 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
625 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
652 assert IssueRelation.create!(:issue_from => Issue.find(3), :issue_to => Issue.find(8), :relation_type => IssueRelation::TYPE_PRECEDES)
626
653
627 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
654 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
628 end
655 end
629
656
630 def test_all_dependent_issues_with_persistent_circular_dependency
657 def test_all_dependent_issues_with_persistent_circular_dependency
631 IssueRelation.delete_all
658 IssueRelation.delete_all
632 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
659 assert IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => IssueRelation::TYPE_PRECEDES)
633 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
660 assert IssueRelation.create!(:issue_from => Issue.find(2), :issue_to => Issue.find(3), :relation_type => IssueRelation::TYPE_PRECEDES)
634 # Validation skipping
661 # Validation skipping
635 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
662 assert IssueRelation.new(:issue_from => Issue.find(3), :issue_to => Issue.find(1), :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
636
663
637 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
664 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
638 end
665 end
639
666
640 context "#done_ratio" do
667 context "#done_ratio" do
641 setup do
668 setup do
642 @issue = Issue.find(1)
669 @issue = Issue.find(1)
643 @issue_status = IssueStatus.find(1)
670 @issue_status = IssueStatus.find(1)
644 @issue_status.update_attribute(:default_done_ratio, 50)
671 @issue_status.update_attribute(:default_done_ratio, 50)
645 @issue2 = Issue.find(2)
672 @issue2 = Issue.find(2)
646 @issue_status2 = IssueStatus.find(2)
673 @issue_status2 = IssueStatus.find(2)
647 @issue_status2.update_attribute(:default_done_ratio, 0)
674 @issue_status2.update_attribute(:default_done_ratio, 0)
648 end
675 end
649
676
650 context "with Setting.issue_done_ratio using the issue_field" do
677 context "with Setting.issue_done_ratio using the issue_field" do
651 setup do
678 setup do
652 Setting.issue_done_ratio = 'issue_field'
679 Setting.issue_done_ratio = 'issue_field'
653 end
680 end
654
681
655 should "read the issue's field" do
682 should "read the issue's field" do
656 assert_equal 0, @issue.done_ratio
683 assert_equal 0, @issue.done_ratio
657 assert_equal 30, @issue2.done_ratio
684 assert_equal 30, @issue2.done_ratio
658 end
685 end
659 end
686 end
660
687
661 context "with Setting.issue_done_ratio using the issue_status" do
688 context "with Setting.issue_done_ratio using the issue_status" do
662 setup do
689 setup do
663 Setting.issue_done_ratio = 'issue_status'
690 Setting.issue_done_ratio = 'issue_status'
664 end
691 end
665
692
666 should "read the Issue Status's default done ratio" do
693 should "read the Issue Status's default done ratio" do
667 assert_equal 50, @issue.done_ratio
694 assert_equal 50, @issue.done_ratio
668 assert_equal 0, @issue2.done_ratio
695 assert_equal 0, @issue2.done_ratio
669 end
696 end
670 end
697 end
671 end
698 end
672
699
673 context "#update_done_ratio_from_issue_status" do
700 context "#update_done_ratio_from_issue_status" do
674 setup do
701 setup do
675 @issue = Issue.find(1)
702 @issue = Issue.find(1)
676 @issue_status = IssueStatus.find(1)
703 @issue_status = IssueStatus.find(1)
677 @issue_status.update_attribute(:default_done_ratio, 50)
704 @issue_status.update_attribute(:default_done_ratio, 50)
678 @issue2 = Issue.find(2)
705 @issue2 = Issue.find(2)
679 @issue_status2 = IssueStatus.find(2)
706 @issue_status2 = IssueStatus.find(2)
680 @issue_status2.update_attribute(:default_done_ratio, 0)
707 @issue_status2.update_attribute(:default_done_ratio, 0)
681 end
708 end
682
709
683 context "with Setting.issue_done_ratio using the issue_field" do
710 context "with Setting.issue_done_ratio using the issue_field" do
684 setup do
711 setup do
685 Setting.issue_done_ratio = 'issue_field'
712 Setting.issue_done_ratio = 'issue_field'
686 end
713 end
687
714
688 should "not change the issue" do
715 should "not change the issue" do
689 @issue.update_done_ratio_from_issue_status
716 @issue.update_done_ratio_from_issue_status
690 @issue2.update_done_ratio_from_issue_status
717 @issue2.update_done_ratio_from_issue_status
691
718
692 assert_equal 0, @issue.read_attribute(:done_ratio)
719 assert_equal 0, @issue.read_attribute(:done_ratio)
693 assert_equal 30, @issue2.read_attribute(:done_ratio)
720 assert_equal 30, @issue2.read_attribute(:done_ratio)
694 end
721 end
695 end
722 end
696
723
697 context "with Setting.issue_done_ratio using the issue_status" do
724 context "with Setting.issue_done_ratio using the issue_status" do
698 setup do
725 setup do
699 Setting.issue_done_ratio = 'issue_status'
726 Setting.issue_done_ratio = 'issue_status'
700 end
727 end
701
728
702 should "change the issue's done ratio" do
729 should "change the issue's done ratio" do
703 @issue.update_done_ratio_from_issue_status
730 @issue.update_done_ratio_from_issue_status
704 @issue2.update_done_ratio_from_issue_status
731 @issue2.update_done_ratio_from_issue_status
705
732
706 assert_equal 50, @issue.read_attribute(:done_ratio)
733 assert_equal 50, @issue.read_attribute(:done_ratio)
707 assert_equal 0, @issue2.read_attribute(:done_ratio)
734 assert_equal 0, @issue2.read_attribute(:done_ratio)
708 end
735 end
709 end
736 end
710 end
737 end
711
738
712 test "#by_tracker" do
739 test "#by_tracker" do
713 groups = Issue.by_tracker(Project.find(1))
740 groups = Issue.by_tracker(Project.find(1))
714 assert_equal 3, groups.size
741 assert_equal 3, groups.size
715 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
742 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
716 end
743 end
717
744
718 test "#by_version" do
745 test "#by_version" do
719 groups = Issue.by_version(Project.find(1))
746 groups = Issue.by_version(Project.find(1))
720 assert_equal 3, groups.size
747 assert_equal 3, groups.size
721 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
748 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
722 end
749 end
723
750
724 test "#by_priority" do
751 test "#by_priority" do
725 groups = Issue.by_priority(Project.find(1))
752 groups = Issue.by_priority(Project.find(1))
726 assert_equal 4, groups.size
753 assert_equal 4, groups.size
727 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
754 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
728 end
755 end
729
756
730 test "#by_category" do
757 test "#by_category" do
731 groups = Issue.by_category(Project.find(1))
758 groups = Issue.by_category(Project.find(1))
732 assert_equal 2, groups.size
759 assert_equal 2, groups.size
733 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
760 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
734 end
761 end
735
762
736 test "#by_assigned_to" do
763 test "#by_assigned_to" do
737 groups = Issue.by_assigned_to(Project.find(1))
764 groups = Issue.by_assigned_to(Project.find(1))
738 assert_equal 2, groups.size
765 assert_equal 2, groups.size
739 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
766 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
740 end
767 end
741
768
742 test "#by_author" do
769 test "#by_author" do
743 groups = Issue.by_author(Project.find(1))
770 groups = Issue.by_author(Project.find(1))
744 assert_equal 4, groups.size
771 assert_equal 4, groups.size
745 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
772 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
746 end
773 end
747
774
748 test "#by_subproject" do
775 test "#by_subproject" do
749 groups = Issue.by_subproject(Project.find(1))
776 groups = Issue.by_subproject(Project.find(1))
750 assert_equal 2, groups.size
777 assert_equal 2, groups.size
751 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
778 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
752 end
779 end
753
780
754
781
755 context ".allowed_target_projects_on_move" do
782 context ".allowed_target_projects_on_move" do
756 should "return all active projects for admin users" do
783 should "return all active projects for admin users" do
757 User.current = User.find(1)
784 User.current = User.find(1)
758 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
785 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
759 end
786 end
760
787
761 should "return allowed projects for non admin users" do
788 should "return allowed projects for non admin users" do
762 User.current = User.find(2)
789 User.current = User.find(2)
763 Role.non_member.remove_permission! :move_issues
790 Role.non_member.remove_permission! :move_issues
764 assert_equal 3, Issue.allowed_target_projects_on_move.size
791 assert_equal 3, Issue.allowed_target_projects_on_move.size
765
792
766 Role.non_member.add_permission! :move_issues
793 Role.non_member.add_permission! :move_issues
767 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
794 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
768 end
795 end
769 end
796 end
770
797
771 def test_recently_updated_with_limit_scopes
798 def test_recently_updated_with_limit_scopes
772 #should return the last updated issue
799 #should return the last updated issue
773 assert_equal 1, Issue.recently_updated.with_limit(1).length
800 assert_equal 1, Issue.recently_updated.with_limit(1).length
774 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
801 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
775 end
802 end
776
803
777 def test_on_active_projects_scope
804 def test_on_active_projects_scope
778 assert Project.find(2).archive
805 assert Project.find(2).archive
779
806
780 before = Issue.on_active_project.length
807 before = Issue.on_active_project.length
781 # test inclusion to results
808 # test inclusion to results
782 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
809 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
783 assert_equal before + 1, Issue.on_active_project.length
810 assert_equal before + 1, Issue.on_active_project.length
784
811
785 # Move to an archived project
812 # Move to an archived project
786 issue.project = Project.find(2)
813 issue.project = Project.find(2)
787 assert issue.save
814 assert issue.save
788 assert_equal before, Issue.on_active_project.length
815 assert_equal before, Issue.on_active_project.length
789 end
816 end
790
817
791 context "Issue#recipients" do
818 context "Issue#recipients" do
792 setup do
819 setup do
793 @project = Project.find(1)
820 @project = Project.find(1)
794 @author = User.generate_with_protected!
821 @author = User.generate_with_protected!
795 @assignee = User.generate_with_protected!
822 @assignee = User.generate_with_protected!
796 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
823 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
797 end
824 end
798
825
799 should "include project recipients" do
826 should "include project recipients" do
800 assert @project.recipients.present?
827 assert @project.recipients.present?
801 @project.recipients.each do |project_recipient|
828 @project.recipients.each do |project_recipient|
802 assert @issue.recipients.include?(project_recipient)
829 assert @issue.recipients.include?(project_recipient)
803 end
830 end
804 end
831 end
805
832
806 should "include the author if the author is active" do
833 should "include the author if the author is active" do
807 assert @issue.author, "No author set for Issue"
834 assert @issue.author, "No author set for Issue"
808 assert @issue.recipients.include?(@issue.author.mail)
835 assert @issue.recipients.include?(@issue.author.mail)
809 end
836 end
810
837
811 should "include the assigned to user if the assigned to user is active" do
838 should "include the assigned to user if the assigned to user is active" do
812 assert @issue.assigned_to, "No assigned_to set for Issue"
839 assert @issue.assigned_to, "No assigned_to set for Issue"
813 assert @issue.recipients.include?(@issue.assigned_to.mail)
840 assert @issue.recipients.include?(@issue.assigned_to.mail)
814 end
841 end
815
842
816 should "not include users who opt out of all email" do
843 should "not include users who opt out of all email" do
817 @author.update_attribute(:mail_notification, :none)
844 @author.update_attribute(:mail_notification, :none)
818
845
819 assert !@issue.recipients.include?(@issue.author.mail)
846 assert !@issue.recipients.include?(@issue.author.mail)
820 end
847 end
821
848
822 should "not include the issue author if they are only notified of assigned issues" do
849 should "not include the issue author if they are only notified of assigned issues" do
823 @author.update_attribute(:mail_notification, :only_assigned)
850 @author.update_attribute(:mail_notification, :only_assigned)
824
851
825 assert !@issue.recipients.include?(@issue.author.mail)
852 assert !@issue.recipients.include?(@issue.author.mail)
826 end
853 end
827
854
828 should "not include the assigned user if they are only notified of owned issues" do
855 should "not include the assigned user if they are only notified of owned issues" do
829 @assignee.update_attribute(:mail_notification, :only_owner)
856 @assignee.update_attribute(:mail_notification, :only_owner)
830
857
831 assert !@issue.recipients.include?(@issue.assigned_to.mail)
858 assert !@issue.recipients.include?(@issue.assigned_to.mail)
832 end
859 end
833
860
834 end
861 end
835 end
862 end
General Comments 0
You need to be logged in to leave comments. Login now