##// END OF EJS Templates
Assignable users should not include users that cannot view the tracker (#23172)....
Jean-Philippe Lang -
r15204:83777f727a42
parent child
Show More
@@ -1,97 +1,89
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 ContextMenusController < ApplicationController
18 class ContextMenusController < ApplicationController
19 helper :watchers
19 helper :watchers
20 helper :issues
20 helper :issues
21
21
22 before_filter :find_issues, :only => :issues
22 before_filter :find_issues, :only => :issues
23
23
24 def issues
24 def issues
25 if (@issues.size == 1)
25 if (@issues.size == 1)
26 @issue = @issues.first
26 @issue = @issues.first
27 end
27 end
28 @issue_ids = @issues.map(&:id).sort
28 @issue_ids = @issues.map(&:id).sort
29
29
30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
31
31
32 @can = {:edit => @issues.all?(&:attributes_editable?),
32 @can = {:edit => @issues.all?(&:attributes_editable?),
33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
34 :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?,
34 :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?,
35 :add_watchers => User.current.allowed_to?(:add_issue_watchers, @projects),
35 :add_watchers => User.current.allowed_to?(:add_issue_watchers, @projects),
36 :delete => @issues.all?(&:deletable?)
36 :delete => @issues.all?(&:deletable?)
37 }
37 }
38 if @project
38
39 if @issue
39 @assignables = @issues.map(&:assignable_users).reduce(:&)
40 @assignables = @issue.assignable_users
41 else
42 @assignables = @project.assignable_users
43 end
44 else
45 #when multiple projects, we only keep the intersection of each set
46 @assignables = @projects.map(&:assignable_users).reduce(:&)
47 end
48 @trackers = @projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
40 @trackers = @projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
49 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
41 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
50
42
51 @priorities = IssuePriority.active.reverse
43 @priorities = IssuePriority.active.reverse
52 @back = back_url
44 @back = back_url
53
45
54 @options_by_custom_field = {}
46 @options_by_custom_field = {}
55 if @can[:edit]
47 if @can[:edit]
56 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
48 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
57 custom_fields.each do |field|
49 custom_fields.each do |field|
58 values = field.possible_values_options(@projects)
50 values = field.possible_values_options(@projects)
59 if values.present?
51 if values.present?
60 @options_by_custom_field[field] = values
52 @options_by_custom_field[field] = values
61 end
53 end
62 end
54 end
63 end
55 end
64
56
65 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
57 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
66 render :layout => false
58 render :layout => false
67 end
59 end
68
60
69 def time_entries
61 def time_entries
70 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
62 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
71 (render_404; return) unless @time_entries.present?
63 (render_404; return) unless @time_entries.present?
72 if (@time_entries.size == 1)
64 if (@time_entries.size == 1)
73 @time_entry = @time_entries.first
65 @time_entry = @time_entries.first
74 end
66 end
75
67
76 @projects = @time_entries.collect(&:project).compact.uniq
68 @projects = @time_entries.collect(&:project).compact.uniq
77 @project = @projects.first if @projects.size == 1
69 @project = @projects.first if @projects.size == 1
78 @activities = TimeEntryActivity.shared.active
70 @activities = TimeEntryActivity.shared.active
79
71
80 edit_allowed = @time_entries.all? {|t| t.editable_by?(User.current)}
72 edit_allowed = @time_entries.all? {|t| t.editable_by?(User.current)}
81 @can = {:edit => edit_allowed, :delete => edit_allowed}
73 @can = {:edit => edit_allowed, :delete => edit_allowed}
82 @back = back_url
74 @back = back_url
83
75
84 @options_by_custom_field = {}
76 @options_by_custom_field = {}
85 if @can[:edit]
77 if @can[:edit]
86 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
78 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
87 custom_fields.each do |field|
79 custom_fields.each do |field|
88 values = field.possible_values_options(@projects)
80 values = field.possible_values_options(@projects)
89 if values.present?
81 if values.present?
90 @options_by_custom_field[field] = values
82 @options_by_custom_field[field] = values
91 end
83 end
92 end
84 end
93 end
85 end
94
86
95 render :layout => false
87 render :layout => false
96 end
88 end
97 end
89 end
@@ -1,1708 +1,1708
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21 include Redmine::I18n
21 include Redmine::I18n
22 before_save :set_parent_id
22 before_save :set_parent_id
23 include Redmine::NestedSet::IssueNestedSet
23 include Redmine::NestedSet::IssueNestedSet
24
24
25 belongs_to :project
25 belongs_to :project
26 belongs_to :tracker
26 belongs_to :tracker
27 belongs_to :status, :class_name => 'IssueStatus'
27 belongs_to :status, :class_name => 'IssueStatus'
28 belongs_to :author, :class_name => 'User'
28 belongs_to :author, :class_name => 'User'
29 belongs_to :assigned_to, :class_name => 'Principal'
29 belongs_to :assigned_to, :class_name => 'Principal'
30 belongs_to :fixed_version, :class_name => 'Version'
30 belongs_to :fixed_version, :class_name => 'Version'
31 belongs_to :priority, :class_name => 'IssuePriority'
31 belongs_to :priority, :class_name => 'IssuePriority'
32 belongs_to :category, :class_name => 'IssueCategory'
32 belongs_to :category, :class_name => 'IssueCategory'
33
33
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 has_many :visible_journals,
35 has_many :visible_journals,
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 :class_name => 'Journal',
37 :class_name => 'Journal',
38 :as => :journalized
38 :as => :journalized
39
39
40 has_many :time_entries, :dependent => :destroy
40 has_many :time_entries, :dependent => :destroy
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42
42
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45
45
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 acts_as_customizable
47 acts_as_customizable
48 acts_as_watchable
48 acts_as_watchable
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 :preload => [:project, :status, :tracker],
50 :preload => [:project, :status, :tracker],
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52
52
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56
56
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker, :status),
58 :author_key => :author_id
58 :author_key => :author_id
59
59
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61
61
62 attr_reader :current_journal
62 attr_reader :current_journal
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64
64
65 validates_presence_of :subject, :project, :tracker
65 validates_presence_of :subject, :project, :tracker
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69
69
70 validates_length_of :subject, :maximum => 255
70 validates_length_of :subject, :maximum => 255
71 validates_inclusion_of :done_ratio, :in => 0..100
71 validates_inclusion_of :done_ratio, :in => 0..100
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 validates :start_date, :date => true
73 validates :start_date, :date => true
74 validates :due_date, :date => true
74 validates :due_date, :date => true
75 validate :validate_issue, :validate_required_fields
75 validate :validate_issue, :validate_required_fields
76 attr_protected :id
76 attr_protected :id
77
77
78 scope :visible, lambda {|*args|
78 scope :visible, lambda {|*args|
79 joins(:project).
79 joins(:project).
80 where(Issue.visible_condition(args.shift || User.current, *args))
80 where(Issue.visible_condition(args.shift || User.current, *args))
81 }
81 }
82
82
83 scope :open, lambda {|*args|
83 scope :open, lambda {|*args|
84 is_closed = args.size > 0 ? !args.first : false
84 is_closed = args.size > 0 ? !args.first : false
85 joins(:status).
85 joins(:status).
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 }
87 }
88
88
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 scope :on_active_project, lambda {
90 scope :on_active_project, lambda {
91 joins(:project).
91 joins(:project).
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 }
93 }
94 scope :fixed_version, lambda {|versions|
94 scope :fixed_version, lambda {|versions|
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 }
97 }
98 scope :assigned_to, lambda {|arg|
98 scope :assigned_to, lambda {|arg|
99 arg = Array(arg).uniq
99 arg = Array(arg).uniq
100 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
100 ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
101 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
101 ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
102 ids.compact!
102 ids.compact!
103 ids.any? ? where(:assigned_to_id => ids) : none
103 ids.any? ? where(:assigned_to_id => ids) : none
104 }
104 }
105
105
106 before_validation :clear_disabled_fields
106 before_validation :clear_disabled_fields
107 before_create :default_assign
107 before_create :default_assign
108 before_save :close_duplicates, :update_done_ratio_from_issue_status,
108 before_save :close_duplicates, :update_done_ratio_from_issue_status,
109 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
109 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
110 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
110 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
111 after_save :reschedule_following_issues, :update_nested_set_attributes,
111 after_save :reschedule_following_issues, :update_nested_set_attributes,
112 :update_parent_attributes, :create_journal
112 :update_parent_attributes, :create_journal
113 # Should be after_create but would be called before previous after_save callbacks
113 # Should be after_create but would be called before previous after_save callbacks
114 after_save :after_create_from_copy
114 after_save :after_create_from_copy
115 after_destroy :update_parent_attributes
115 after_destroy :update_parent_attributes
116 after_create :send_notification
116 after_create :send_notification
117 # Keep it at the end of after_save callbacks
117 # Keep it at the end of after_save callbacks
118 after_save :clear_assigned_to_was
118 after_save :clear_assigned_to_was
119
119
120 # Returns a SQL conditions string used to find all issues visible by the specified user
120 # Returns a SQL conditions string used to find all issues visible by the specified user
121 def self.visible_condition(user, options={})
121 def self.visible_condition(user, options={})
122 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
122 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
123 sql = if user.id && user.logged?
123 sql = if user.id && user.logged?
124 case role.issues_visibility
124 case role.issues_visibility
125 when 'all'
125 when 'all'
126 '1=1'
126 '1=1'
127 when 'default'
127 when 'default'
128 user_ids = [user.id] + user.groups.map(&:id).compact
128 user_ids = [user.id] + user.groups.map(&:id).compact
129 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
129 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
130 when 'own'
130 when 'own'
131 user_ids = [user.id] + user.groups.map(&:id).compact
131 user_ids = [user.id] + user.groups.map(&:id).compact
132 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
132 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
133 else
133 else
134 '1=0'
134 '1=0'
135 end
135 end
136 else
136 else
137 "(#{table_name}.is_private = #{connection.quoted_false})"
137 "(#{table_name}.is_private = #{connection.quoted_false})"
138 end
138 end
139 unless role.permissions_all_trackers?(:view_issues)
139 unless role.permissions_all_trackers?(:view_issues)
140 tracker_ids = role.permissions_tracker_ids(:view_issues)
140 tracker_ids = role.permissions_tracker_ids(:view_issues)
141 if tracker_ids.any?
141 if tracker_ids.any?
142 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
142 sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
143 else
143 else
144 sql = '1=0'
144 sql = '1=0'
145 end
145 end
146 end
146 end
147 sql
147 sql
148 end
148 end
149 end
149 end
150
150
151 # Returns true if usr or current user is allowed to view the issue
151 # Returns true if usr or current user is allowed to view the issue
152 def visible?(usr=nil)
152 def visible?(usr=nil)
153 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
153 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
154 visible = if user.logged?
154 visible = if user.logged?
155 case role.issues_visibility
155 case role.issues_visibility
156 when 'all'
156 when 'all'
157 true
157 true
158 when 'default'
158 when 'default'
159 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
159 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
160 when 'own'
160 when 'own'
161 self.author == user || user.is_or_belongs_to?(assigned_to)
161 self.author == user || user.is_or_belongs_to?(assigned_to)
162 else
162 else
163 false
163 false
164 end
164 end
165 else
165 else
166 !self.is_private?
166 !self.is_private?
167 end
167 end
168 unless role.permissions_all_trackers?(:view_issues)
168 unless role.permissions_all_trackers?(:view_issues)
169 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
169 visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
170 end
170 end
171 visible
171 visible
172 end
172 end
173 end
173 end
174
174
175 # Returns true if user or current user is allowed to edit or add notes to the issue
175 # Returns true if user or current user is allowed to edit or add notes to the issue
176 def editable?(user=User.current)
176 def editable?(user=User.current)
177 attributes_editable?(user) || notes_addable?(user)
177 attributes_editable?(user) || notes_addable?(user)
178 end
178 end
179
179
180 # Returns true if user or current user is allowed to edit the issue
180 # Returns true if user or current user is allowed to edit the issue
181 def attributes_editable?(user=User.current)
181 def attributes_editable?(user=User.current)
182 user_tracker_permission?(user, :edit_issues)
182 user_tracker_permission?(user, :edit_issues)
183 end
183 end
184
184
185 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
185 # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
186 def attachments_editable?(user=User.current)
186 def attachments_editable?(user=User.current)
187 attributes_editable?(user)
187 attributes_editable?(user)
188 end
188 end
189
189
190 # Returns true if user or current user is allowed to add notes to the issue
190 # Returns true if user or current user is allowed to add notes to the issue
191 def notes_addable?(user=User.current)
191 def notes_addable?(user=User.current)
192 user_tracker_permission?(user, :add_issue_notes)
192 user_tracker_permission?(user, :add_issue_notes)
193 end
193 end
194
194
195 # Returns true if user or current user is allowed to delete the issue
195 # Returns true if user or current user is allowed to delete the issue
196 def deletable?(user=User.current)
196 def deletable?(user=User.current)
197 user_tracker_permission?(user, :delete_issues)
197 user_tracker_permission?(user, :delete_issues)
198 end
198 end
199
199
200 def initialize(attributes=nil, *args)
200 def initialize(attributes=nil, *args)
201 super
201 super
202 if new_record?
202 if new_record?
203 # set default values for new records only
203 # set default values for new records only
204 self.priority ||= IssuePriority.default
204 self.priority ||= IssuePriority.default
205 self.watcher_user_ids = []
205 self.watcher_user_ids = []
206 end
206 end
207 end
207 end
208
208
209 def create_or_update
209 def create_or_update
210 super
210 super
211 ensure
211 ensure
212 @status_was = nil
212 @status_was = nil
213 end
213 end
214 private :create_or_update
214 private :create_or_update
215
215
216 # AR#Persistence#destroy would raise and RecordNotFound exception
216 # AR#Persistence#destroy would raise and RecordNotFound exception
217 # if the issue was already deleted or updated (non matching lock_version).
217 # if the issue was already deleted or updated (non matching lock_version).
218 # This is a problem when bulk deleting issues or deleting a project
218 # This is a problem when bulk deleting issues or deleting a project
219 # (because an issue may already be deleted if its parent was deleted
219 # (because an issue may already be deleted if its parent was deleted
220 # first).
220 # first).
221 # The issue is reloaded by the nested_set before being deleted so
221 # The issue is reloaded by the nested_set before being deleted so
222 # the lock_version condition should not be an issue but we handle it.
222 # the lock_version condition should not be an issue but we handle it.
223 def destroy
223 def destroy
224 super
224 super
225 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
225 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
226 # Stale or already deleted
226 # Stale or already deleted
227 begin
227 begin
228 reload
228 reload
229 rescue ActiveRecord::RecordNotFound
229 rescue ActiveRecord::RecordNotFound
230 # The issue was actually already deleted
230 # The issue was actually already deleted
231 @destroyed = true
231 @destroyed = true
232 return freeze
232 return freeze
233 end
233 end
234 # The issue was stale, retry to destroy
234 # The issue was stale, retry to destroy
235 super
235 super
236 end
236 end
237
237
238 alias :base_reload :reload
238 alias :base_reload :reload
239 def reload(*args)
239 def reload(*args)
240 @workflow_rule_by_attribute = nil
240 @workflow_rule_by_attribute = nil
241 @assignable_versions = nil
241 @assignable_versions = nil
242 @relations = nil
242 @relations = nil
243 @spent_hours = nil
243 @spent_hours = nil
244 @total_spent_hours = nil
244 @total_spent_hours = nil
245 @total_estimated_hours = nil
245 @total_estimated_hours = nil
246 base_reload(*args)
246 base_reload(*args)
247 end
247 end
248
248
249 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
249 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
250 def available_custom_fields
250 def available_custom_fields
251 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
251 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
252 end
252 end
253
253
254 def visible_custom_field_values(user=nil)
254 def visible_custom_field_values(user=nil)
255 user_real = user || User.current
255 user_real = user || User.current
256 custom_field_values.select do |value|
256 custom_field_values.select do |value|
257 value.custom_field.visible_by?(project, user_real)
257 value.custom_field.visible_by?(project, user_real)
258 end
258 end
259 end
259 end
260
260
261 # Copies attributes from another issue, arg can be an id or an Issue
261 # Copies attributes from another issue, arg can be an id or an Issue
262 def copy_from(arg, options={})
262 def copy_from(arg, options={})
263 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
263 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
264 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
264 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
265 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
265 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
266 self.status = issue.status
266 self.status = issue.status
267 self.author = User.current
267 self.author = User.current
268 unless options[:attachments] == false
268 unless options[:attachments] == false
269 self.attachments = issue.attachments.map do |attachement|
269 self.attachments = issue.attachments.map do |attachement|
270 attachement.copy(:container => self)
270 attachement.copy(:container => self)
271 end
271 end
272 end
272 end
273 @copied_from = issue
273 @copied_from = issue
274 @copy_options = options
274 @copy_options = options
275 self
275 self
276 end
276 end
277
277
278 # Returns an unsaved copy of the issue
278 # Returns an unsaved copy of the issue
279 def copy(attributes=nil, copy_options={})
279 def copy(attributes=nil, copy_options={})
280 copy = self.class.new.copy_from(self, copy_options)
280 copy = self.class.new.copy_from(self, copy_options)
281 copy.attributes = attributes if attributes
281 copy.attributes = attributes if attributes
282 copy
282 copy
283 end
283 end
284
284
285 # Returns true if the issue is a copy
285 # Returns true if the issue is a copy
286 def copy?
286 def copy?
287 @copied_from.present?
287 @copied_from.present?
288 end
288 end
289
289
290 def status_id=(status_id)
290 def status_id=(status_id)
291 if status_id.to_s != self.status_id.to_s
291 if status_id.to_s != self.status_id.to_s
292 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
292 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
293 end
293 end
294 self.status_id
294 self.status_id
295 end
295 end
296
296
297 # Sets the status.
297 # Sets the status.
298 def status=(status)
298 def status=(status)
299 if status != self.status
299 if status != self.status
300 @workflow_rule_by_attribute = nil
300 @workflow_rule_by_attribute = nil
301 end
301 end
302 association(:status).writer(status)
302 association(:status).writer(status)
303 end
303 end
304
304
305 def priority_id=(pid)
305 def priority_id=(pid)
306 self.priority = nil
306 self.priority = nil
307 write_attribute(:priority_id, pid)
307 write_attribute(:priority_id, pid)
308 end
308 end
309
309
310 def category_id=(cid)
310 def category_id=(cid)
311 self.category = nil
311 self.category = nil
312 write_attribute(:category_id, cid)
312 write_attribute(:category_id, cid)
313 end
313 end
314
314
315 def fixed_version_id=(vid)
315 def fixed_version_id=(vid)
316 self.fixed_version = nil
316 self.fixed_version = nil
317 write_attribute(:fixed_version_id, vid)
317 write_attribute(:fixed_version_id, vid)
318 end
318 end
319
319
320 def tracker_id=(tracker_id)
320 def tracker_id=(tracker_id)
321 if tracker_id.to_s != self.tracker_id.to_s
321 if tracker_id.to_s != self.tracker_id.to_s
322 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
322 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
323 end
323 end
324 self.tracker_id
324 self.tracker_id
325 end
325 end
326
326
327 # Sets the tracker.
327 # Sets the tracker.
328 # This will set the status to the default status of the new tracker if:
328 # This will set the status to the default status of the new tracker if:
329 # * the status was the default for the previous tracker
329 # * the status was the default for the previous tracker
330 # * or if the status was not part of the new tracker statuses
330 # * or if the status was not part of the new tracker statuses
331 # * or the status was nil
331 # * or the status was nil
332 def tracker=(tracker)
332 def tracker=(tracker)
333 tracker_was = self.tracker
333 tracker_was = self.tracker
334 association(:tracker).writer(tracker)
334 association(:tracker).writer(tracker)
335 if tracker != tracker_was
335 if tracker != tracker_was
336 if status == tracker_was.try(:default_status)
336 if status == tracker_was.try(:default_status)
337 self.status = nil
337 self.status = nil
338 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
338 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
339 self.status = nil
339 self.status = nil
340 end
340 end
341 reassign_custom_field_values
341 reassign_custom_field_values
342 @workflow_rule_by_attribute = nil
342 @workflow_rule_by_attribute = nil
343 end
343 end
344 self.status ||= default_status
344 self.status ||= default_status
345 self.tracker
345 self.tracker
346 end
346 end
347
347
348 def project_id=(project_id)
348 def project_id=(project_id)
349 if project_id.to_s != self.project_id.to_s
349 if project_id.to_s != self.project_id.to_s
350 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
350 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
351 end
351 end
352 self.project_id
352 self.project_id
353 end
353 end
354
354
355 # Sets the project.
355 # Sets the project.
356 # Unless keep_tracker argument is set to true, this will change the tracker
356 # Unless keep_tracker argument is set to true, this will change the tracker
357 # to the first tracker of the new project if the previous tracker is not part
357 # to the first tracker of the new project if the previous tracker is not part
358 # of the new project trackers.
358 # of the new project trackers.
359 # This will:
359 # This will:
360 # * clear the fixed_version is it's no longer valid for the new project.
360 # * clear the fixed_version is it's no longer valid for the new project.
361 # * clear the parent issue if it's no longer valid for the new project.
361 # * clear the parent issue if it's no longer valid for the new project.
362 # * set the category to the category with the same name in the new
362 # * set the category to the category with the same name in the new
363 # project if it exists, or clear it if it doesn't.
363 # project if it exists, or clear it if it doesn't.
364 # * for new issue, set the fixed_version to the project default version
364 # * for new issue, set the fixed_version to the project default version
365 # if it's a valid fixed_version.
365 # if it's a valid fixed_version.
366 def project=(project, keep_tracker=false)
366 def project=(project, keep_tracker=false)
367 project_was = self.project
367 project_was = self.project
368 association(:project).writer(project)
368 association(:project).writer(project)
369 if project_was && project && project_was != project
369 if project_was && project && project_was != project
370 @assignable_versions = nil
370 @assignable_versions = nil
371
371
372 unless keep_tracker || project.trackers.include?(tracker)
372 unless keep_tracker || project.trackers.include?(tracker)
373 self.tracker = project.trackers.first
373 self.tracker = project.trackers.first
374 end
374 end
375 # Reassign to the category with same name if any
375 # Reassign to the category with same name if any
376 if category
376 if category
377 self.category = project.issue_categories.find_by_name(category.name)
377 self.category = project.issue_categories.find_by_name(category.name)
378 end
378 end
379 # Keep the fixed_version if it's still valid in the new_project
379 # Keep the fixed_version if it's still valid in the new_project
380 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
380 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
381 self.fixed_version = nil
381 self.fixed_version = nil
382 end
382 end
383 # Clear the parent task if it's no longer valid
383 # Clear the parent task if it's no longer valid
384 unless valid_parent_project?
384 unless valid_parent_project?
385 self.parent_issue_id = nil
385 self.parent_issue_id = nil
386 end
386 end
387 reassign_custom_field_values
387 reassign_custom_field_values
388 @workflow_rule_by_attribute = nil
388 @workflow_rule_by_attribute = nil
389 end
389 end
390 # Set fixed_version to the project default version if it's valid
390 # Set fixed_version to the project default version if it's valid
391 if new_record? && fixed_version.nil? && project && project.default_version_id?
391 if new_record? && fixed_version.nil? && project && project.default_version_id?
392 if project.shared_versions.open.exists?(project.default_version_id)
392 if project.shared_versions.open.exists?(project.default_version_id)
393 self.fixed_version_id = project.default_version_id
393 self.fixed_version_id = project.default_version_id
394 end
394 end
395 end
395 end
396 self.project
396 self.project
397 end
397 end
398
398
399 def description=(arg)
399 def description=(arg)
400 if arg.is_a?(String)
400 if arg.is_a?(String)
401 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
401 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
402 end
402 end
403 write_attribute(:description, arg)
403 write_attribute(:description, arg)
404 end
404 end
405
405
406 # Overrides assign_attributes so that project and tracker get assigned first
406 # Overrides assign_attributes so that project and tracker get assigned first
407 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
407 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
408 return if new_attributes.nil?
408 return if new_attributes.nil?
409 attrs = new_attributes.dup
409 attrs = new_attributes.dup
410 attrs.stringify_keys!
410 attrs.stringify_keys!
411
411
412 %w(project project_id tracker tracker_id).each do |attr|
412 %w(project project_id tracker tracker_id).each do |attr|
413 if attrs.has_key?(attr)
413 if attrs.has_key?(attr)
414 send "#{attr}=", attrs.delete(attr)
414 send "#{attr}=", attrs.delete(attr)
415 end
415 end
416 end
416 end
417 send :assign_attributes_without_project_and_tracker_first, attrs, *args
417 send :assign_attributes_without_project_and_tracker_first, attrs, *args
418 end
418 end
419 # Do not redefine alias chain on reload (see #4838)
419 # Do not redefine alias chain on reload (see #4838)
420 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
420 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
421
421
422 def attributes=(new_attributes)
422 def attributes=(new_attributes)
423 assign_attributes new_attributes
423 assign_attributes new_attributes
424 end
424 end
425
425
426 def estimated_hours=(h)
426 def estimated_hours=(h)
427 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
427 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
428 end
428 end
429
429
430 safe_attributes 'project_id',
430 safe_attributes 'project_id',
431 'tracker_id',
431 'tracker_id',
432 'status_id',
432 'status_id',
433 'category_id',
433 'category_id',
434 'assigned_to_id',
434 'assigned_to_id',
435 'priority_id',
435 'priority_id',
436 'fixed_version_id',
436 'fixed_version_id',
437 'subject',
437 'subject',
438 'description',
438 'description',
439 'start_date',
439 'start_date',
440 'due_date',
440 'due_date',
441 'done_ratio',
441 'done_ratio',
442 'estimated_hours',
442 'estimated_hours',
443 'custom_field_values',
443 'custom_field_values',
444 'custom_fields',
444 'custom_fields',
445 'lock_version',
445 'lock_version',
446 'notes',
446 'notes',
447 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
447 :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user) }
448
448
449 safe_attributes 'notes',
449 safe_attributes 'notes',
450 :if => lambda {|issue, user| issue.notes_addable?(user)}
450 :if => lambda {|issue, user| issue.notes_addable?(user)}
451
451
452 safe_attributes 'private_notes',
452 safe_attributes 'private_notes',
453 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
453 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
454
454
455 safe_attributes 'watcher_user_ids',
455 safe_attributes 'watcher_user_ids',
456 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
456 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
457
457
458 safe_attributes 'is_private',
458 safe_attributes 'is_private',
459 :if => lambda {|issue, user|
459 :if => lambda {|issue, user|
460 user.allowed_to?(:set_issues_private, issue.project) ||
460 user.allowed_to?(:set_issues_private, issue.project) ||
461 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
461 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
462 }
462 }
463
463
464 safe_attributes 'parent_issue_id',
464 safe_attributes 'parent_issue_id',
465 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
465 :if => lambda {|issue, user| (issue.new_record? || issue.attributes_editable?(user)) &&
466 user.allowed_to?(:manage_subtasks, issue.project)}
466 user.allowed_to?(:manage_subtasks, issue.project)}
467
467
468 def safe_attribute_names(user=nil)
468 def safe_attribute_names(user=nil)
469 names = super
469 names = super
470 names -= disabled_core_fields
470 names -= disabled_core_fields
471 names -= read_only_attribute_names(user)
471 names -= read_only_attribute_names(user)
472 if new_record?
472 if new_record?
473 # Make sure that project_id can always be set for new issues
473 # Make sure that project_id can always be set for new issues
474 names |= %w(project_id)
474 names |= %w(project_id)
475 end
475 end
476 if dates_derived?
476 if dates_derived?
477 names -= %w(start_date due_date)
477 names -= %w(start_date due_date)
478 end
478 end
479 if priority_derived?
479 if priority_derived?
480 names -= %w(priority_id)
480 names -= %w(priority_id)
481 end
481 end
482 if done_ratio_derived?
482 if done_ratio_derived?
483 names -= %w(done_ratio)
483 names -= %w(done_ratio)
484 end
484 end
485 names
485 names
486 end
486 end
487
487
488 # Safely sets attributes
488 # Safely sets attributes
489 # Should be called from controllers instead of #attributes=
489 # Should be called from controllers instead of #attributes=
490 # attr_accessible is too rough because we still want things like
490 # attr_accessible is too rough because we still want things like
491 # Issue.new(:project => foo) to work
491 # Issue.new(:project => foo) to work
492 def safe_attributes=(attrs, user=User.current)
492 def safe_attributes=(attrs, user=User.current)
493 return unless attrs.is_a?(Hash)
493 return unless attrs.is_a?(Hash)
494
494
495 attrs = attrs.deep_dup
495 attrs = attrs.deep_dup
496
496
497 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
497 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
498 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
498 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
499 if allowed_target_projects(user).where(:id => p.to_i).exists?
499 if allowed_target_projects(user).where(:id => p.to_i).exists?
500 self.project_id = p
500 self.project_id = p
501 end
501 end
502
502
503 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
503 if project_id_changed? && attrs['category_id'].to_s == category_id_was.to_s
504 # Discard submitted category on previous project
504 # Discard submitted category on previous project
505 attrs.delete('category_id')
505 attrs.delete('category_id')
506 end
506 end
507 end
507 end
508
508
509 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
509 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
510 if allowed_target_trackers(user).where(:id => t.to_i).exists?
510 if allowed_target_trackers(user).where(:id => t.to_i).exists?
511 self.tracker_id = t
511 self.tracker_id = t
512 end
512 end
513 end
513 end
514 if project
514 if project
515 # Set a default tracker to accept custom field values
515 # Set a default tracker to accept custom field values
516 # even if tracker is not specified
516 # even if tracker is not specified
517 self.tracker ||= allowed_target_trackers(user).first
517 self.tracker ||= allowed_target_trackers(user).first
518 end
518 end
519
519
520 statuses_allowed = new_statuses_allowed_to(user)
520 statuses_allowed = new_statuses_allowed_to(user)
521 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
521 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
522 if statuses_allowed.collect(&:id).include?(s.to_i)
522 if statuses_allowed.collect(&:id).include?(s.to_i)
523 self.status_id = s
523 self.status_id = s
524 end
524 end
525 end
525 end
526 if new_record? && !statuses_allowed.include?(status)
526 if new_record? && !statuses_allowed.include?(status)
527 self.status = statuses_allowed.first || default_status
527 self.status = statuses_allowed.first || default_status
528 end
528 end
529 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
529 if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
530 if u.blank?
530 if u.blank?
531 self.assigned_to_id = nil
531 self.assigned_to_id = nil
532 else
532 else
533 u = u.to_i
533 u = u.to_i
534 if assignable_users.any?{|assignable_user| assignable_user.id == u}
534 if assignable_users.any?{|assignable_user| assignable_user.id == u}
535 self.assigned_to_id = u
535 self.assigned_to_id = u
536 end
536 end
537 end
537 end
538 end
538 end
539
539
540
540
541 attrs = delete_unsafe_attributes(attrs, user)
541 attrs = delete_unsafe_attributes(attrs, user)
542 return if attrs.empty?
542 return if attrs.empty?
543
543
544 if attrs['parent_issue_id'].present?
544 if attrs['parent_issue_id'].present?
545 s = attrs['parent_issue_id'].to_s
545 s = attrs['parent_issue_id'].to_s
546 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
546 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
547 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
547 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
548 end
548 end
549 end
549 end
550
550
551 if attrs['custom_field_values'].present?
551 if attrs['custom_field_values'].present?
552 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
552 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
553 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
553 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
554 end
554 end
555
555
556 if attrs['custom_fields'].present?
556 if attrs['custom_fields'].present?
557 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
557 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
558 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
558 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
559 end
559 end
560
560
561 # mass-assignment security bypass
561 # mass-assignment security bypass
562 assign_attributes attrs, :without_protection => true
562 assign_attributes attrs, :without_protection => true
563 end
563 end
564
564
565 def disabled_core_fields
565 def disabled_core_fields
566 tracker ? tracker.disabled_core_fields : []
566 tracker ? tracker.disabled_core_fields : []
567 end
567 end
568
568
569 # Returns the custom_field_values that can be edited by the given user
569 # Returns the custom_field_values that can be edited by the given user
570 def editable_custom_field_values(user=nil)
570 def editable_custom_field_values(user=nil)
571 visible_custom_field_values(user).reject do |value|
571 visible_custom_field_values(user).reject do |value|
572 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
572 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
573 end
573 end
574 end
574 end
575
575
576 # Returns the custom fields that can be edited by the given user
576 # Returns the custom fields that can be edited by the given user
577 def editable_custom_fields(user=nil)
577 def editable_custom_fields(user=nil)
578 editable_custom_field_values(user).map(&:custom_field).uniq
578 editable_custom_field_values(user).map(&:custom_field).uniq
579 end
579 end
580
580
581 # Returns the names of attributes that are read-only for user or the current user
581 # Returns the names of attributes that are read-only for user or the current user
582 # For users with multiple roles, the read-only fields are the intersection of
582 # For users with multiple roles, the read-only fields are the intersection of
583 # read-only fields of each role
583 # read-only fields of each role
584 # The result is an array of strings where sustom fields are represented with their ids
584 # The result is an array of strings where sustom fields are represented with their ids
585 #
585 #
586 # Examples:
586 # Examples:
587 # issue.read_only_attribute_names # => ['due_date', '2']
587 # issue.read_only_attribute_names # => ['due_date', '2']
588 # issue.read_only_attribute_names(user) # => []
588 # issue.read_only_attribute_names(user) # => []
589 def read_only_attribute_names(user=nil)
589 def read_only_attribute_names(user=nil)
590 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
590 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
591 end
591 end
592
592
593 # Returns the names of required attributes for user or the current user
593 # Returns the names of required attributes for user or the current user
594 # For users with multiple roles, the required fields are the intersection of
594 # For users with multiple roles, the required fields are the intersection of
595 # required fields of each role
595 # required fields of each role
596 # The result is an array of strings where sustom fields are represented with their ids
596 # The result is an array of strings where sustom fields are represented with their ids
597 #
597 #
598 # Examples:
598 # Examples:
599 # issue.required_attribute_names # => ['due_date', '2']
599 # issue.required_attribute_names # => ['due_date', '2']
600 # issue.required_attribute_names(user) # => []
600 # issue.required_attribute_names(user) # => []
601 def required_attribute_names(user=nil)
601 def required_attribute_names(user=nil)
602 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
602 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
603 end
603 end
604
604
605 # Returns true if the attribute is required for user
605 # Returns true if the attribute is required for user
606 def required_attribute?(name, user=nil)
606 def required_attribute?(name, user=nil)
607 required_attribute_names(user).include?(name.to_s)
607 required_attribute_names(user).include?(name.to_s)
608 end
608 end
609
609
610 # Returns a hash of the workflow rule by attribute for the given user
610 # Returns a hash of the workflow rule by attribute for the given user
611 #
611 #
612 # Examples:
612 # Examples:
613 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
613 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
614 def workflow_rule_by_attribute(user=nil)
614 def workflow_rule_by_attribute(user=nil)
615 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
615 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
616
616
617 user_real = user || User.current
617 user_real = user || User.current
618 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
618 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
619 roles = roles.select(&:consider_workflow?)
619 roles = roles.select(&:consider_workflow?)
620 return {} if roles.empty?
620 return {} if roles.empty?
621
621
622 result = {}
622 result = {}
623 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
623 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
624 if workflow_permissions.any?
624 if workflow_permissions.any?
625 workflow_rules = workflow_permissions.inject({}) do |h, wp|
625 workflow_rules = workflow_permissions.inject({}) do |h, wp|
626 h[wp.field_name] ||= {}
626 h[wp.field_name] ||= {}
627 h[wp.field_name][wp.role_id] = wp.rule
627 h[wp.field_name][wp.role_id] = wp.rule
628 h
628 h
629 end
629 end
630 fields_with_roles = {}
630 fields_with_roles = {}
631 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
631 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
632 fields_with_roles[field_id] ||= []
632 fields_with_roles[field_id] ||= []
633 fields_with_roles[field_id] << role_id
633 fields_with_roles[field_id] << role_id
634 end
634 end
635 roles.each do |role|
635 roles.each do |role|
636 fields_with_roles.each do |field_id, role_ids|
636 fields_with_roles.each do |field_id, role_ids|
637 unless role_ids.include?(role.id)
637 unless role_ids.include?(role.id)
638 field_name = field_id.to_s
638 field_name = field_id.to_s
639 workflow_rules[field_name] ||= {}
639 workflow_rules[field_name] ||= {}
640 workflow_rules[field_name][role.id] = 'readonly'
640 workflow_rules[field_name][role.id] = 'readonly'
641 end
641 end
642 end
642 end
643 end
643 end
644 workflow_rules.each do |attr, rules|
644 workflow_rules.each do |attr, rules|
645 next if rules.size < roles.size
645 next if rules.size < roles.size
646 uniq_rules = rules.values.uniq
646 uniq_rules = rules.values.uniq
647 if uniq_rules.size == 1
647 if uniq_rules.size == 1
648 result[attr] = uniq_rules.first
648 result[attr] = uniq_rules.first
649 else
649 else
650 result[attr] = 'required'
650 result[attr] = 'required'
651 end
651 end
652 end
652 end
653 end
653 end
654 @workflow_rule_by_attribute = result if user.nil?
654 @workflow_rule_by_attribute = result if user.nil?
655 result
655 result
656 end
656 end
657 private :workflow_rule_by_attribute
657 private :workflow_rule_by_attribute
658
658
659 def done_ratio
659 def done_ratio
660 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
660 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
661 status.default_done_ratio
661 status.default_done_ratio
662 else
662 else
663 read_attribute(:done_ratio)
663 read_attribute(:done_ratio)
664 end
664 end
665 end
665 end
666
666
667 def self.use_status_for_done_ratio?
667 def self.use_status_for_done_ratio?
668 Setting.issue_done_ratio == 'issue_status'
668 Setting.issue_done_ratio == 'issue_status'
669 end
669 end
670
670
671 def self.use_field_for_done_ratio?
671 def self.use_field_for_done_ratio?
672 Setting.issue_done_ratio == 'issue_field'
672 Setting.issue_done_ratio == 'issue_field'
673 end
673 end
674
674
675 def validate_issue
675 def validate_issue
676 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
676 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
677 errors.add :due_date, :greater_than_start_date
677 errors.add :due_date, :greater_than_start_date
678 end
678 end
679
679
680 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
680 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
681 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
681 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
682 end
682 end
683
683
684 if fixed_version
684 if fixed_version
685 if !assignable_versions.include?(fixed_version)
685 if !assignable_versions.include?(fixed_version)
686 errors.add :fixed_version_id, :inclusion
686 errors.add :fixed_version_id, :inclusion
687 elsif reopening? && fixed_version.closed?
687 elsif reopening? && fixed_version.closed?
688 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
688 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
689 end
689 end
690 end
690 end
691
691
692 # Checks that the issue can not be added/moved to a disabled tracker
692 # Checks that the issue can not be added/moved to a disabled tracker
693 if project && (tracker_id_changed? || project_id_changed?)
693 if project && (tracker_id_changed? || project_id_changed?)
694 if tracker && !project.trackers.include?(tracker)
694 if tracker && !project.trackers.include?(tracker)
695 errors.add :tracker_id, :inclusion
695 errors.add :tracker_id, :inclusion
696 end
696 end
697 end
697 end
698
698
699 # Checks parent issue assignment
699 # Checks parent issue assignment
700 if @invalid_parent_issue_id.present?
700 if @invalid_parent_issue_id.present?
701 errors.add :parent_issue_id, :invalid
701 errors.add :parent_issue_id, :invalid
702 elsif @parent_issue
702 elsif @parent_issue
703 if !valid_parent_project?(@parent_issue)
703 if !valid_parent_project?(@parent_issue)
704 errors.add :parent_issue_id, :invalid
704 errors.add :parent_issue_id, :invalid
705 elsif (@parent_issue != parent) && (
705 elsif (@parent_issue != parent) && (
706 self.would_reschedule?(@parent_issue) ||
706 self.would_reschedule?(@parent_issue) ||
707 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
707 @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
708 )
708 )
709 errors.add :parent_issue_id, :invalid
709 errors.add :parent_issue_id, :invalid
710 elsif !new_record?
710 elsif !new_record?
711 # moving an existing issue
711 # moving an existing issue
712 if move_possible?(@parent_issue)
712 if move_possible?(@parent_issue)
713 # move accepted
713 # move accepted
714 else
714 else
715 errors.add :parent_issue_id, :invalid
715 errors.add :parent_issue_id, :invalid
716 end
716 end
717 end
717 end
718 end
718 end
719 end
719 end
720
720
721 # Validates the issue against additional workflow requirements
721 # Validates the issue against additional workflow requirements
722 def validate_required_fields
722 def validate_required_fields
723 user = new_record? ? author : current_journal.try(:user)
723 user = new_record? ? author : current_journal.try(:user)
724
724
725 required_attribute_names(user).each do |attribute|
725 required_attribute_names(user).each do |attribute|
726 if attribute =~ /^\d+$/
726 if attribute =~ /^\d+$/
727 attribute = attribute.to_i
727 attribute = attribute.to_i
728 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
728 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
729 if v && Array(v.value).detect(&:present?).nil?
729 if v && Array(v.value).detect(&:present?).nil?
730 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
730 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
731 end
731 end
732 else
732 else
733 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
733 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
734 next if attribute == 'category_id' && project.try(:issue_categories).blank?
734 next if attribute == 'category_id' && project.try(:issue_categories).blank?
735 next if attribute == 'fixed_version_id' && assignable_versions.blank?
735 next if attribute == 'fixed_version_id' && assignable_versions.blank?
736 errors.add attribute, :blank
736 errors.add attribute, :blank
737 end
737 end
738 end
738 end
739 end
739 end
740 end
740 end
741
741
742 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
742 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
743 # so that custom values that are not editable are not validated (eg. a custom field that
743 # so that custom values that are not editable are not validated (eg. a custom field that
744 # is marked as required should not trigger a validation error if the user is not allowed
744 # is marked as required should not trigger a validation error if the user is not allowed
745 # to edit this field).
745 # to edit this field).
746 def validate_custom_field_values
746 def validate_custom_field_values
747 user = new_record? ? author : current_journal.try(:user)
747 user = new_record? ? author : current_journal.try(:user)
748 if new_record? || custom_field_values_changed?
748 if new_record? || custom_field_values_changed?
749 editable_custom_field_values(user).each(&:validate_value)
749 editable_custom_field_values(user).each(&:validate_value)
750 end
750 end
751 end
751 end
752
752
753 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
753 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
754 # even if the user turns off the setting later
754 # even if the user turns off the setting later
755 def update_done_ratio_from_issue_status
755 def update_done_ratio_from_issue_status
756 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
756 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
757 self.done_ratio = status.default_done_ratio
757 self.done_ratio = status.default_done_ratio
758 end
758 end
759 end
759 end
760
760
761 def init_journal(user, notes = "")
761 def init_journal(user, notes = "")
762 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
762 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
763 end
763 end
764
764
765 # Returns the current journal or nil if it's not initialized
765 # Returns the current journal or nil if it's not initialized
766 def current_journal
766 def current_journal
767 @current_journal
767 @current_journal
768 end
768 end
769
769
770 # Returns the names of attributes that are journalized when updating the issue
770 # Returns the names of attributes that are journalized when updating the issue
771 def journalized_attribute_names
771 def journalized_attribute_names
772 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
772 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
773 if tracker
773 if tracker
774 names -= tracker.disabled_core_fields
774 names -= tracker.disabled_core_fields
775 end
775 end
776 names
776 names
777 end
777 end
778
778
779 # Returns the id of the last journal or nil
779 # Returns the id of the last journal or nil
780 def last_journal_id
780 def last_journal_id
781 if new_record?
781 if new_record?
782 nil
782 nil
783 else
783 else
784 journals.maximum(:id)
784 journals.maximum(:id)
785 end
785 end
786 end
786 end
787
787
788 # Returns a scope for journals that have an id greater than journal_id
788 # Returns a scope for journals that have an id greater than journal_id
789 def journals_after(journal_id)
789 def journals_after(journal_id)
790 scope = journals.reorder("#{Journal.table_name}.id ASC")
790 scope = journals.reorder("#{Journal.table_name}.id ASC")
791 if journal_id.present?
791 if journal_id.present?
792 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
792 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
793 end
793 end
794 scope
794 scope
795 end
795 end
796
796
797 # Returns the initial status of the issue
797 # Returns the initial status of the issue
798 # Returns nil for a new issue
798 # Returns nil for a new issue
799 def status_was
799 def status_was
800 if status_id_changed?
800 if status_id_changed?
801 if status_id_was.to_i > 0
801 if status_id_was.to_i > 0
802 @status_was ||= IssueStatus.find_by_id(status_id_was)
802 @status_was ||= IssueStatus.find_by_id(status_id_was)
803 end
803 end
804 else
804 else
805 @status_was ||= status
805 @status_was ||= status
806 end
806 end
807 end
807 end
808
808
809 # Return true if the issue is closed, otherwise false
809 # Return true if the issue is closed, otherwise false
810 def closed?
810 def closed?
811 status.present? && status.is_closed?
811 status.present? && status.is_closed?
812 end
812 end
813
813
814 # Returns true if the issue was closed when loaded
814 # Returns true if the issue was closed when loaded
815 def was_closed?
815 def was_closed?
816 status_was.present? && status_was.is_closed?
816 status_was.present? && status_was.is_closed?
817 end
817 end
818
818
819 # Return true if the issue is being reopened
819 # Return true if the issue is being reopened
820 def reopening?
820 def reopening?
821 if new_record?
821 if new_record?
822 false
822 false
823 else
823 else
824 status_id_changed? && !closed? && was_closed?
824 status_id_changed? && !closed? && was_closed?
825 end
825 end
826 end
826 end
827 alias :reopened? :reopening?
827 alias :reopened? :reopening?
828
828
829 # Return true if the issue is being closed
829 # Return true if the issue is being closed
830 def closing?
830 def closing?
831 if new_record?
831 if new_record?
832 closed?
832 closed?
833 else
833 else
834 status_id_changed? && closed? && !was_closed?
834 status_id_changed? && closed? && !was_closed?
835 end
835 end
836 end
836 end
837
837
838 # Returns true if the issue is overdue
838 # Returns true if the issue is overdue
839 def overdue?
839 def overdue?
840 due_date.present? && (due_date < User.current.today) && !closed?
840 due_date.present? && (due_date < User.current.today) && !closed?
841 end
841 end
842
842
843 # Is the amount of work done less than it should for the due date
843 # Is the amount of work done less than it should for the due date
844 def behind_schedule?
844 def behind_schedule?
845 return false if start_date.nil? || due_date.nil?
845 return false if start_date.nil? || due_date.nil?
846 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
846 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
847 return done_date <= User.current.today
847 return done_date <= User.current.today
848 end
848 end
849
849
850 # Does this issue have children?
850 # Does this issue have children?
851 def children?
851 def children?
852 !leaf?
852 !leaf?
853 end
853 end
854
854
855 # Users the issue can be assigned to
855 # Users the issue can be assigned to
856 def assignable_users
856 def assignable_users
857 users = project.assignable_users.to_a
857 users = project.assignable_users(tracker).to_a
858 users << author if author && author.active?
858 users << author if author && author.active?
859 users << assigned_to if assigned_to
859 users << assigned_to if assigned_to
860 users.uniq.sort
860 users.uniq.sort
861 end
861 end
862
862
863 # Versions that the issue can be assigned to
863 # Versions that the issue can be assigned to
864 def assignable_versions
864 def assignable_versions
865 return @assignable_versions if @assignable_versions
865 return @assignable_versions if @assignable_versions
866
866
867 versions = project.shared_versions.open.to_a
867 versions = project.shared_versions.open.to_a
868 if fixed_version
868 if fixed_version
869 if fixed_version_id_changed?
869 if fixed_version_id_changed?
870 # nothing to do
870 # nothing to do
871 elsif project_id_changed?
871 elsif project_id_changed?
872 if project.shared_versions.include?(fixed_version)
872 if project.shared_versions.include?(fixed_version)
873 versions << fixed_version
873 versions << fixed_version
874 end
874 end
875 else
875 else
876 versions << fixed_version
876 versions << fixed_version
877 end
877 end
878 end
878 end
879 @assignable_versions = versions.uniq.sort
879 @assignable_versions = versions.uniq.sort
880 end
880 end
881
881
882 # Returns true if this issue is blocked by another issue that is still open
882 # Returns true if this issue is blocked by another issue that is still open
883 def blocked?
883 def blocked?
884 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
884 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
885 end
885 end
886
886
887 # Returns the default status of the issue based on its tracker
887 # Returns the default status of the issue based on its tracker
888 # Returns nil if tracker is nil
888 # Returns nil if tracker is nil
889 def default_status
889 def default_status
890 tracker.try(:default_status)
890 tracker.try(:default_status)
891 end
891 end
892
892
893 # Returns an array of statuses that user is able to apply
893 # Returns an array of statuses that user is able to apply
894 def new_statuses_allowed_to(user=User.current, include_default=false)
894 def new_statuses_allowed_to(user=User.current, include_default=false)
895 if new_record? && @copied_from
895 if new_record? && @copied_from
896 [default_status, @copied_from.status].compact.uniq.sort
896 [default_status, @copied_from.status].compact.uniq.sort
897 else
897 else
898 initial_status = nil
898 initial_status = nil
899 if new_record?
899 if new_record?
900 # nop
900 # nop
901 elsif tracker_id_changed?
901 elsif tracker_id_changed?
902 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
902 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
903 initial_status = default_status
903 initial_status = default_status
904 elsif tracker.issue_status_ids.include?(status_id_was)
904 elsif tracker.issue_status_ids.include?(status_id_was)
905 initial_status = IssueStatus.find_by_id(status_id_was)
905 initial_status = IssueStatus.find_by_id(status_id_was)
906 else
906 else
907 initial_status = default_status
907 initial_status = default_status
908 end
908 end
909 else
909 else
910 initial_status = status_was
910 initial_status = status_was
911 end
911 end
912
912
913 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
913 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
914 assignee_transitions_allowed = initial_assigned_to_id.present? &&
914 assignee_transitions_allowed = initial_assigned_to_id.present? &&
915 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
915 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
916
916
917 statuses = []
917 statuses = []
918 statuses += IssueStatus.new_statuses_allowed(
918 statuses += IssueStatus.new_statuses_allowed(
919 initial_status,
919 initial_status,
920 user.admin ? Role.all.to_a : user.roles_for_project(project),
920 user.admin ? Role.all.to_a : user.roles_for_project(project),
921 tracker,
921 tracker,
922 author == user,
922 author == user,
923 assignee_transitions_allowed
923 assignee_transitions_allowed
924 )
924 )
925 statuses << initial_status unless statuses.empty?
925 statuses << initial_status unless statuses.empty?
926 statuses << default_status if include_default || (new_record? && statuses.empty?)
926 statuses << default_status if include_default || (new_record? && statuses.empty?)
927 statuses = statuses.compact.uniq.sort
927 statuses = statuses.compact.uniq.sort
928 if blocked?
928 if blocked?
929 statuses.reject!(&:is_closed?)
929 statuses.reject!(&:is_closed?)
930 end
930 end
931 statuses
931 statuses
932 end
932 end
933 end
933 end
934
934
935 # Returns the previous assignee (user or group) if changed
935 # Returns the previous assignee (user or group) if changed
936 def assigned_to_was
936 def assigned_to_was
937 # assigned_to_id_was is reset before after_save callbacks
937 # assigned_to_id_was is reset before after_save callbacks
938 user_id = @previous_assigned_to_id || assigned_to_id_was
938 user_id = @previous_assigned_to_id || assigned_to_id_was
939 if user_id && user_id != assigned_to_id
939 if user_id && user_id != assigned_to_id
940 @assigned_to_was ||= Principal.find_by_id(user_id)
940 @assigned_to_was ||= Principal.find_by_id(user_id)
941 end
941 end
942 end
942 end
943
943
944 # Returns the original tracker
944 # Returns the original tracker
945 def tracker_was
945 def tracker_was
946 Tracker.find_by_id(tracker_id_was)
946 Tracker.find_by_id(tracker_id_was)
947 end
947 end
948
948
949 # Returns the users that should be notified
949 # Returns the users that should be notified
950 def notified_users
950 def notified_users
951 notified = []
951 notified = []
952 # Author and assignee are always notified unless they have been
952 # Author and assignee are always notified unless they have been
953 # locked or don't want to be notified
953 # locked or don't want to be notified
954 notified << author if author
954 notified << author if author
955 if assigned_to
955 if assigned_to
956 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
956 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
957 end
957 end
958 if assigned_to_was
958 if assigned_to_was
959 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
959 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
960 end
960 end
961 notified = notified.select {|u| u.active? && u.notify_about?(self)}
961 notified = notified.select {|u| u.active? && u.notify_about?(self)}
962
962
963 notified += project.notified_users
963 notified += project.notified_users
964 notified.uniq!
964 notified.uniq!
965 # Remove users that can not view the issue
965 # Remove users that can not view the issue
966 notified.reject! {|user| !visible?(user)}
966 notified.reject! {|user| !visible?(user)}
967 notified
967 notified
968 end
968 end
969
969
970 # Returns the email addresses that should be notified
970 # Returns the email addresses that should be notified
971 def recipients
971 def recipients
972 notified_users.collect(&:mail)
972 notified_users.collect(&:mail)
973 end
973 end
974
974
975 def each_notification(users, &block)
975 def each_notification(users, &block)
976 if users.any?
976 if users.any?
977 if custom_field_values.detect {|value| !value.custom_field.visible?}
977 if custom_field_values.detect {|value| !value.custom_field.visible?}
978 users_by_custom_field_visibility = users.group_by do |user|
978 users_by_custom_field_visibility = users.group_by do |user|
979 visible_custom_field_values(user).map(&:custom_field_id).sort
979 visible_custom_field_values(user).map(&:custom_field_id).sort
980 end
980 end
981 users_by_custom_field_visibility.values.each do |users|
981 users_by_custom_field_visibility.values.each do |users|
982 yield(users)
982 yield(users)
983 end
983 end
984 else
984 else
985 yield(users)
985 yield(users)
986 end
986 end
987 end
987 end
988 end
988 end
989
989
990 def notify?
990 def notify?
991 @notify != false
991 @notify != false
992 end
992 end
993
993
994 def notify=(arg)
994 def notify=(arg)
995 @notify = arg
995 @notify = arg
996 end
996 end
997
997
998 # Returns the number of hours spent on this issue
998 # Returns the number of hours spent on this issue
999 def spent_hours
999 def spent_hours
1000 @spent_hours ||= time_entries.sum(:hours) || 0
1000 @spent_hours ||= time_entries.sum(:hours) || 0
1001 end
1001 end
1002
1002
1003 # Returns the total number of hours spent on this issue and its descendants
1003 # Returns the total number of hours spent on this issue and its descendants
1004 def total_spent_hours
1004 def total_spent_hours
1005 @total_spent_hours ||= if leaf?
1005 @total_spent_hours ||= if leaf?
1006 spent_hours
1006 spent_hours
1007 else
1007 else
1008 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1008 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1009 end
1009 end
1010 end
1010 end
1011
1011
1012 def total_estimated_hours
1012 def total_estimated_hours
1013 if leaf?
1013 if leaf?
1014 estimated_hours
1014 estimated_hours
1015 else
1015 else
1016 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1016 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
1017 end
1017 end
1018 end
1018 end
1019
1019
1020 def relations
1020 def relations
1021 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1021 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1022 end
1022 end
1023
1023
1024 # Preloads relations for a collection of issues
1024 # Preloads relations for a collection of issues
1025 def self.load_relations(issues)
1025 def self.load_relations(issues)
1026 if issues.any?
1026 if issues.any?
1027 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1027 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
1028 issues.each do |issue|
1028 issues.each do |issue|
1029 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1029 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1030 end
1030 end
1031 end
1031 end
1032 end
1032 end
1033
1033
1034 # Preloads visible spent time for a collection of issues
1034 # Preloads visible spent time for a collection of issues
1035 def self.load_visible_spent_hours(issues, user=User.current)
1035 def self.load_visible_spent_hours(issues, user=User.current)
1036 if issues.any?
1036 if issues.any?
1037 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1037 hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1038 issues.each do |issue|
1038 issues.each do |issue|
1039 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1039 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
1040 end
1040 end
1041 end
1041 end
1042 end
1042 end
1043
1043
1044 # Preloads visible total spent time for a collection of issues
1044 # Preloads visible total spent time for a collection of issues
1045 def self.load_visible_total_spent_hours(issues, user=User.current)
1045 def self.load_visible_total_spent_hours(issues, user=User.current)
1046 if issues.any?
1046 if issues.any?
1047 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1047 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1048 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1048 joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1049 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1049 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1050 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1050 where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1051 issues.each do |issue|
1051 issues.each do |issue|
1052 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1052 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
1053 end
1053 end
1054 end
1054 end
1055 end
1055 end
1056
1056
1057 # Preloads visible relations for a collection of issues
1057 # Preloads visible relations for a collection of issues
1058 def self.load_visible_relations(issues, user=User.current)
1058 def self.load_visible_relations(issues, user=User.current)
1059 if issues.any?
1059 if issues.any?
1060 issue_ids = issues.map(&:id)
1060 issue_ids = issues.map(&:id)
1061 # Relations with issue_from in given issues and visible issue_to
1061 # Relations with issue_from in given issues and visible issue_to
1062 relations_from = IssueRelation.joins(:issue_to => :project).
1062 relations_from = IssueRelation.joins(:issue_to => :project).
1063 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1063 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1064 # Relations with issue_to in given issues and visible issue_from
1064 # Relations with issue_to in given issues and visible issue_from
1065 relations_to = IssueRelation.joins(:issue_from => :project).
1065 relations_to = IssueRelation.joins(:issue_from => :project).
1066 where(visible_condition(user)).
1066 where(visible_condition(user)).
1067 where(:issue_to_id => issue_ids).to_a
1067 where(:issue_to_id => issue_ids).to_a
1068 issues.each do |issue|
1068 issues.each do |issue|
1069 relations =
1069 relations =
1070 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1070 relations_from.select {|relation| relation.issue_from_id == issue.id} +
1071 relations_to.select {|relation| relation.issue_to_id == issue.id}
1071 relations_to.select {|relation| relation.issue_to_id == issue.id}
1072
1072
1073 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1073 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
1074 end
1074 end
1075 end
1075 end
1076 end
1076 end
1077
1077
1078 # Finds an issue relation given its id.
1078 # Finds an issue relation given its id.
1079 def find_relation(relation_id)
1079 def find_relation(relation_id)
1080 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1080 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1081 end
1081 end
1082
1082
1083 # Returns true if this issue blocks the other issue, otherwise returns false
1083 # Returns true if this issue blocks the other issue, otherwise returns false
1084 def blocks?(other)
1084 def blocks?(other)
1085 all = [self]
1085 all = [self]
1086 last = [self]
1086 last = [self]
1087 while last.any?
1087 while last.any?
1088 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1088 current = last.map {|i| i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)}.flatten.uniq
1089 current -= last
1089 current -= last
1090 current -= all
1090 current -= all
1091 return true if current.include?(other)
1091 return true if current.include?(other)
1092 last = current
1092 last = current
1093 all += last
1093 all += last
1094 end
1094 end
1095 false
1095 false
1096 end
1096 end
1097
1097
1098 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1098 # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1099 def would_reschedule?(other)
1099 def would_reschedule?(other)
1100 all = [self]
1100 all = [self]
1101 last = [self]
1101 last = [self]
1102 while last.any?
1102 while last.any?
1103 current = last.map {|i|
1103 current = last.map {|i|
1104 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1104 i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1105 i.leaves.to_a +
1105 i.leaves.to_a +
1106 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1106 i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1107 }.flatten.uniq
1107 }.flatten.uniq
1108 current -= last
1108 current -= last
1109 current -= all
1109 current -= all
1110 return true if current.include?(other)
1110 return true if current.include?(other)
1111 last = current
1111 last = current
1112 all += last
1112 all += last
1113 end
1113 end
1114 false
1114 false
1115 end
1115 end
1116
1116
1117 # Returns an array of issues that duplicate this one
1117 # Returns an array of issues that duplicate this one
1118 def duplicates
1118 def duplicates
1119 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1119 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1120 end
1120 end
1121
1121
1122 # Returns the due date or the target due date if any
1122 # Returns the due date or the target due date if any
1123 # Used on gantt chart
1123 # Used on gantt chart
1124 def due_before
1124 def due_before
1125 due_date || (fixed_version ? fixed_version.effective_date : nil)
1125 due_date || (fixed_version ? fixed_version.effective_date : nil)
1126 end
1126 end
1127
1127
1128 # Returns the time scheduled for this issue.
1128 # Returns the time scheduled for this issue.
1129 #
1129 #
1130 # Example:
1130 # Example:
1131 # Start Date: 2/26/09, End Date: 3/04/09
1131 # Start Date: 2/26/09, End Date: 3/04/09
1132 # duration => 6
1132 # duration => 6
1133 def duration
1133 def duration
1134 (start_date && due_date) ? due_date - start_date : 0
1134 (start_date && due_date) ? due_date - start_date : 0
1135 end
1135 end
1136
1136
1137 # Returns the duration in working days
1137 # Returns the duration in working days
1138 def working_duration
1138 def working_duration
1139 (start_date && due_date) ? working_days(start_date, due_date) : 0
1139 (start_date && due_date) ? working_days(start_date, due_date) : 0
1140 end
1140 end
1141
1141
1142 def soonest_start(reload=false)
1142 def soonest_start(reload=false)
1143 if @soonest_start.nil? || reload
1143 if @soonest_start.nil? || reload
1144 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1144 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1145 p = @parent_issue || parent
1145 p = @parent_issue || parent
1146 if p && Setting.parent_issue_dates == 'derived'
1146 if p && Setting.parent_issue_dates == 'derived'
1147 dates << p.soonest_start
1147 dates << p.soonest_start
1148 end
1148 end
1149 @soonest_start = dates.compact.max
1149 @soonest_start = dates.compact.max
1150 end
1150 end
1151 @soonest_start
1151 @soonest_start
1152 end
1152 end
1153
1153
1154 # Sets start_date on the given date or the next working day
1154 # Sets start_date on the given date or the next working day
1155 # and changes due_date to keep the same working duration.
1155 # and changes due_date to keep the same working duration.
1156 def reschedule_on(date)
1156 def reschedule_on(date)
1157 wd = working_duration
1157 wd = working_duration
1158 date = next_working_date(date)
1158 date = next_working_date(date)
1159 self.start_date = date
1159 self.start_date = date
1160 self.due_date = add_working_days(date, wd)
1160 self.due_date = add_working_days(date, wd)
1161 end
1161 end
1162
1162
1163 # Reschedules the issue on the given date or the next working day and saves the record.
1163 # Reschedules the issue on the given date or the next working day and saves the record.
1164 # If the issue is a parent task, this is done by rescheduling its subtasks.
1164 # If the issue is a parent task, this is done by rescheduling its subtasks.
1165 def reschedule_on!(date)
1165 def reschedule_on!(date)
1166 return if date.nil?
1166 return if date.nil?
1167 if leaf? || !dates_derived?
1167 if leaf? || !dates_derived?
1168 if start_date.nil? || start_date != date
1168 if start_date.nil? || start_date != date
1169 if start_date && start_date > date
1169 if start_date && start_date > date
1170 # Issue can not be moved earlier than its soonest start date
1170 # Issue can not be moved earlier than its soonest start date
1171 date = [soonest_start(true), date].compact.max
1171 date = [soonest_start(true), date].compact.max
1172 end
1172 end
1173 reschedule_on(date)
1173 reschedule_on(date)
1174 begin
1174 begin
1175 save
1175 save
1176 rescue ActiveRecord::StaleObjectError
1176 rescue ActiveRecord::StaleObjectError
1177 reload
1177 reload
1178 reschedule_on(date)
1178 reschedule_on(date)
1179 save
1179 save
1180 end
1180 end
1181 end
1181 end
1182 else
1182 else
1183 leaves.each do |leaf|
1183 leaves.each do |leaf|
1184 if leaf.start_date
1184 if leaf.start_date
1185 # Only move subtask if it starts at the same date as the parent
1185 # Only move subtask if it starts at the same date as the parent
1186 # or if it starts before the given date
1186 # or if it starts before the given date
1187 if start_date == leaf.start_date || date > leaf.start_date
1187 if start_date == leaf.start_date || date > leaf.start_date
1188 leaf.reschedule_on!(date)
1188 leaf.reschedule_on!(date)
1189 end
1189 end
1190 else
1190 else
1191 leaf.reschedule_on!(date)
1191 leaf.reschedule_on!(date)
1192 end
1192 end
1193 end
1193 end
1194 end
1194 end
1195 end
1195 end
1196
1196
1197 def dates_derived?
1197 def dates_derived?
1198 !leaf? && Setting.parent_issue_dates == 'derived'
1198 !leaf? && Setting.parent_issue_dates == 'derived'
1199 end
1199 end
1200
1200
1201 def priority_derived?
1201 def priority_derived?
1202 !leaf? && Setting.parent_issue_priority == 'derived'
1202 !leaf? && Setting.parent_issue_priority == 'derived'
1203 end
1203 end
1204
1204
1205 def done_ratio_derived?
1205 def done_ratio_derived?
1206 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1206 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1207 end
1207 end
1208
1208
1209 def <=>(issue)
1209 def <=>(issue)
1210 if issue.nil?
1210 if issue.nil?
1211 -1
1211 -1
1212 elsif root_id != issue.root_id
1212 elsif root_id != issue.root_id
1213 (root_id || 0) <=> (issue.root_id || 0)
1213 (root_id || 0) <=> (issue.root_id || 0)
1214 else
1214 else
1215 (lft || 0) <=> (issue.lft || 0)
1215 (lft || 0) <=> (issue.lft || 0)
1216 end
1216 end
1217 end
1217 end
1218
1218
1219 def to_s
1219 def to_s
1220 "#{tracker} ##{id}: #{subject}"
1220 "#{tracker} ##{id}: #{subject}"
1221 end
1221 end
1222
1222
1223 # Returns a string of css classes that apply to the issue
1223 # Returns a string of css classes that apply to the issue
1224 def css_classes(user=User.current)
1224 def css_classes(user=User.current)
1225 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1225 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1226 s << ' closed' if closed?
1226 s << ' closed' if closed?
1227 s << ' overdue' if overdue?
1227 s << ' overdue' if overdue?
1228 s << ' child' if child?
1228 s << ' child' if child?
1229 s << ' parent' unless leaf?
1229 s << ' parent' unless leaf?
1230 s << ' private' if is_private?
1230 s << ' private' if is_private?
1231 if user.logged?
1231 if user.logged?
1232 s << ' created-by-me' if author_id == user.id
1232 s << ' created-by-me' if author_id == user.id
1233 s << ' assigned-to-me' if assigned_to_id == user.id
1233 s << ' assigned-to-me' if assigned_to_id == user.id
1234 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1234 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1235 end
1235 end
1236 s
1236 s
1237 end
1237 end
1238
1238
1239 # Unassigns issues from +version+ if it's no longer shared with issue's project
1239 # Unassigns issues from +version+ if it's no longer shared with issue's project
1240 def self.update_versions_from_sharing_change(version)
1240 def self.update_versions_from_sharing_change(version)
1241 # Update issues assigned to the version
1241 # Update issues assigned to the version
1242 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1242 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1243 end
1243 end
1244
1244
1245 # Unassigns issues from versions that are no longer shared
1245 # Unassigns issues from versions that are no longer shared
1246 # after +project+ was moved
1246 # after +project+ was moved
1247 def self.update_versions_from_hierarchy_change(project)
1247 def self.update_versions_from_hierarchy_change(project)
1248 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1248 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1249 # Update issues of the moved projects and issues assigned to a version of a moved project
1249 # Update issues of the moved projects and issues assigned to a version of a moved project
1250 Issue.update_versions(
1250 Issue.update_versions(
1251 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1251 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1252 moved_project_ids, moved_project_ids]
1252 moved_project_ids, moved_project_ids]
1253 )
1253 )
1254 end
1254 end
1255
1255
1256 def parent_issue_id=(arg)
1256 def parent_issue_id=(arg)
1257 s = arg.to_s.strip.presence
1257 s = arg.to_s.strip.presence
1258 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1258 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1259 @invalid_parent_issue_id = nil
1259 @invalid_parent_issue_id = nil
1260 elsif s.blank?
1260 elsif s.blank?
1261 @parent_issue = nil
1261 @parent_issue = nil
1262 @invalid_parent_issue_id = nil
1262 @invalid_parent_issue_id = nil
1263 else
1263 else
1264 @parent_issue = nil
1264 @parent_issue = nil
1265 @invalid_parent_issue_id = arg
1265 @invalid_parent_issue_id = arg
1266 end
1266 end
1267 end
1267 end
1268
1268
1269 def parent_issue_id
1269 def parent_issue_id
1270 if @invalid_parent_issue_id
1270 if @invalid_parent_issue_id
1271 @invalid_parent_issue_id
1271 @invalid_parent_issue_id
1272 elsif instance_variable_defined? :@parent_issue
1272 elsif instance_variable_defined? :@parent_issue
1273 @parent_issue.nil? ? nil : @parent_issue.id
1273 @parent_issue.nil? ? nil : @parent_issue.id
1274 else
1274 else
1275 parent_id
1275 parent_id
1276 end
1276 end
1277 end
1277 end
1278
1278
1279 def set_parent_id
1279 def set_parent_id
1280 self.parent_id = parent_issue_id
1280 self.parent_id = parent_issue_id
1281 end
1281 end
1282
1282
1283 # Returns true if issue's project is a valid
1283 # Returns true if issue's project is a valid
1284 # parent issue project
1284 # parent issue project
1285 def valid_parent_project?(issue=parent)
1285 def valid_parent_project?(issue=parent)
1286 return true if issue.nil? || issue.project_id == project_id
1286 return true if issue.nil? || issue.project_id == project_id
1287
1287
1288 case Setting.cross_project_subtasks
1288 case Setting.cross_project_subtasks
1289 when 'system'
1289 when 'system'
1290 true
1290 true
1291 when 'tree'
1291 when 'tree'
1292 issue.project.root == project.root
1292 issue.project.root == project.root
1293 when 'hierarchy'
1293 when 'hierarchy'
1294 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1294 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1295 when 'descendants'
1295 when 'descendants'
1296 issue.project.is_or_is_ancestor_of?(project)
1296 issue.project.is_or_is_ancestor_of?(project)
1297 else
1297 else
1298 false
1298 false
1299 end
1299 end
1300 end
1300 end
1301
1301
1302 # Returns an issue scope based on project and scope
1302 # Returns an issue scope based on project and scope
1303 def self.cross_project_scope(project, scope=nil)
1303 def self.cross_project_scope(project, scope=nil)
1304 if project.nil?
1304 if project.nil?
1305 return Issue
1305 return Issue
1306 end
1306 end
1307 case scope
1307 case scope
1308 when 'all', 'system'
1308 when 'all', 'system'
1309 Issue
1309 Issue
1310 when 'tree'
1310 when 'tree'
1311 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1311 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1312 :lft => project.root.lft, :rgt => project.root.rgt)
1312 :lft => project.root.lft, :rgt => project.root.rgt)
1313 when 'hierarchy'
1313 when 'hierarchy'
1314 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1314 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1315 :lft => project.lft, :rgt => project.rgt)
1315 :lft => project.lft, :rgt => project.rgt)
1316 when 'descendants'
1316 when 'descendants'
1317 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1317 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1318 :lft => project.lft, :rgt => project.rgt)
1318 :lft => project.lft, :rgt => project.rgt)
1319 else
1319 else
1320 Issue.where(:project_id => project.id)
1320 Issue.where(:project_id => project.id)
1321 end
1321 end
1322 end
1322 end
1323
1323
1324 def self.by_tracker(project)
1324 def self.by_tracker(project)
1325 count_and_group_by(:project => project, :association => :tracker)
1325 count_and_group_by(:project => project, :association => :tracker)
1326 end
1326 end
1327
1327
1328 def self.by_version(project)
1328 def self.by_version(project)
1329 count_and_group_by(:project => project, :association => :fixed_version)
1329 count_and_group_by(:project => project, :association => :fixed_version)
1330 end
1330 end
1331
1331
1332 def self.by_priority(project)
1332 def self.by_priority(project)
1333 count_and_group_by(:project => project, :association => :priority)
1333 count_and_group_by(:project => project, :association => :priority)
1334 end
1334 end
1335
1335
1336 def self.by_category(project)
1336 def self.by_category(project)
1337 count_and_group_by(:project => project, :association => :category)
1337 count_and_group_by(:project => project, :association => :category)
1338 end
1338 end
1339
1339
1340 def self.by_assigned_to(project)
1340 def self.by_assigned_to(project)
1341 count_and_group_by(:project => project, :association => :assigned_to)
1341 count_and_group_by(:project => project, :association => :assigned_to)
1342 end
1342 end
1343
1343
1344 def self.by_author(project)
1344 def self.by_author(project)
1345 count_and_group_by(:project => project, :association => :author)
1345 count_and_group_by(:project => project, :association => :author)
1346 end
1346 end
1347
1347
1348 def self.by_subproject(project)
1348 def self.by_subproject(project)
1349 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1349 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1350 r.reject {|r| r["project_id"] == project.id.to_s}
1350 r.reject {|r| r["project_id"] == project.id.to_s}
1351 end
1351 end
1352
1352
1353 # Query generator for selecting groups of issue counts for a project
1353 # Query generator for selecting groups of issue counts for a project
1354 # based on specific criteria
1354 # based on specific criteria
1355 #
1355 #
1356 # Options
1356 # Options
1357 # * project - Project to search in.
1357 # * project - Project to search in.
1358 # * with_subprojects - Includes subprojects issues if set to true.
1358 # * with_subprojects - Includes subprojects issues if set to true.
1359 # * association - Symbol. Association for grouping.
1359 # * association - Symbol. Association for grouping.
1360 def self.count_and_group_by(options)
1360 def self.count_and_group_by(options)
1361 assoc = reflect_on_association(options[:association])
1361 assoc = reflect_on_association(options[:association])
1362 select_field = assoc.foreign_key
1362 select_field = assoc.foreign_key
1363
1363
1364 Issue.
1364 Issue.
1365 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1365 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1366 joins(:status, assoc.name).
1366 joins(:status, assoc.name).
1367 group(:status_id, :is_closed, select_field).
1367 group(:status_id, :is_closed, select_field).
1368 count.
1368 count.
1369 map do |columns, total|
1369 map do |columns, total|
1370 status_id, is_closed, field_value = columns
1370 status_id, is_closed, field_value = columns
1371 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1371 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1372 {
1372 {
1373 "status_id" => status_id.to_s,
1373 "status_id" => status_id.to_s,
1374 "closed" => is_closed,
1374 "closed" => is_closed,
1375 select_field => field_value.to_s,
1375 select_field => field_value.to_s,
1376 "total" => total.to_s
1376 "total" => total.to_s
1377 }
1377 }
1378 end
1378 end
1379 end
1379 end
1380
1380
1381 # Returns a scope of projects that user can assign the issue to
1381 # Returns a scope of projects that user can assign the issue to
1382 def allowed_target_projects(user=User.current)
1382 def allowed_target_projects(user=User.current)
1383 current_project = new_record? ? nil : project
1383 current_project = new_record? ? nil : project
1384 self.class.allowed_target_projects(user, current_project)
1384 self.class.allowed_target_projects(user, current_project)
1385 end
1385 end
1386
1386
1387 # Returns a scope of projects that user can assign issues to
1387 # Returns a scope of projects that user can assign issues to
1388 # If current_project is given, it will be included in the scope
1388 # If current_project is given, it will be included in the scope
1389 def self.allowed_target_projects(user=User.current, current_project=nil)
1389 def self.allowed_target_projects(user=User.current, current_project=nil)
1390 condition = Project.allowed_to_condition(user, :add_issues)
1390 condition = Project.allowed_to_condition(user, :add_issues)
1391 if current_project
1391 if current_project
1392 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1392 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1393 end
1393 end
1394 Project.where(condition).having_trackers
1394 Project.where(condition).having_trackers
1395 end
1395 end
1396
1396
1397 # Returns a scope of trackers that user can assign the issue to
1397 # Returns a scope of trackers that user can assign the issue to
1398 def allowed_target_trackers(user=User.current)
1398 def allowed_target_trackers(user=User.current)
1399 self.class.allowed_target_trackers(project, user, tracker_id_was)
1399 self.class.allowed_target_trackers(project, user, tracker_id_was)
1400 end
1400 end
1401
1401
1402 # Returns a scope of trackers that user can assign project issues to
1402 # Returns a scope of trackers that user can assign project issues to
1403 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1403 def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1404 if project
1404 if project
1405 scope = project.trackers.sorted
1405 scope = project.trackers.sorted
1406 unless user.admin?
1406 unless user.admin?
1407 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1407 roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1408 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1408 unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1409 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1409 tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1410 if current_tracker
1410 if current_tracker
1411 tracker_ids << current_tracker
1411 tracker_ids << current_tracker
1412 end
1412 end
1413 scope = scope.where(:id => tracker_ids)
1413 scope = scope.where(:id => tracker_ids)
1414 end
1414 end
1415 end
1415 end
1416 scope
1416 scope
1417 else
1417 else
1418 Tracker.none
1418 Tracker.none
1419 end
1419 end
1420 end
1420 end
1421
1421
1422 private
1422 private
1423
1423
1424 def user_tracker_permission?(user, permission)
1424 def user_tracker_permission?(user, permission)
1425 if user.admin?
1425 if user.admin?
1426 true
1426 true
1427 else
1427 else
1428 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1428 roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1429 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1429 roles.any? {|r| r.permissions_all_trackers?(permission) || r.permissions_tracker_ids?(permission, tracker_id)}
1430 end
1430 end
1431 end
1431 end
1432
1432
1433 def after_project_change
1433 def after_project_change
1434 # Update project_id on related time entries
1434 # Update project_id on related time entries
1435 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1435 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1436
1436
1437 # Delete issue relations
1437 # Delete issue relations
1438 unless Setting.cross_project_issue_relations?
1438 unless Setting.cross_project_issue_relations?
1439 relations_from.clear
1439 relations_from.clear
1440 relations_to.clear
1440 relations_to.clear
1441 end
1441 end
1442
1442
1443 # Move subtasks that were in the same project
1443 # Move subtasks that were in the same project
1444 children.each do |child|
1444 children.each do |child|
1445 next unless child.project_id == project_id_was
1445 next unless child.project_id == project_id_was
1446 # Change project and keep project
1446 # Change project and keep project
1447 child.send :project=, project, true
1447 child.send :project=, project, true
1448 unless child.save
1448 unless child.save
1449 raise ActiveRecord::Rollback
1449 raise ActiveRecord::Rollback
1450 end
1450 end
1451 end
1451 end
1452 end
1452 end
1453
1453
1454 # Callback for after the creation of an issue by copy
1454 # Callback for after the creation of an issue by copy
1455 # * adds a "copied to" relation with the copied issue
1455 # * adds a "copied to" relation with the copied issue
1456 # * copies subtasks from the copied issue
1456 # * copies subtasks from the copied issue
1457 def after_create_from_copy
1457 def after_create_from_copy
1458 return unless copy? && !@after_create_from_copy_handled
1458 return unless copy? && !@after_create_from_copy_handled
1459
1459
1460 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1460 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1461 if @current_journal
1461 if @current_journal
1462 @copied_from.init_journal(@current_journal.user)
1462 @copied_from.init_journal(@current_journal.user)
1463 end
1463 end
1464 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1464 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1465 unless relation.save
1465 unless relation.save
1466 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1466 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1467 end
1467 end
1468 end
1468 end
1469
1469
1470 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1470 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1471 copy_options = (@copy_options || {}).merge(:subtasks => false)
1471 copy_options = (@copy_options || {}).merge(:subtasks => false)
1472 copied_issue_ids = {@copied_from.id => self.id}
1472 copied_issue_ids = {@copied_from.id => self.id}
1473 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1473 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1474 # Do not copy self when copying an issue as a descendant of the copied issue
1474 # Do not copy self when copying an issue as a descendant of the copied issue
1475 next if child == self
1475 next if child == self
1476 # Do not copy subtasks of issues that were not copied
1476 # Do not copy subtasks of issues that were not copied
1477 next unless copied_issue_ids[child.parent_id]
1477 next unless copied_issue_ids[child.parent_id]
1478 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1478 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1479 unless child.visible?
1479 unless child.visible?
1480 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1480 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1481 next
1481 next
1482 end
1482 end
1483 copy = Issue.new.copy_from(child, copy_options)
1483 copy = Issue.new.copy_from(child, copy_options)
1484 if @current_journal
1484 if @current_journal
1485 copy.init_journal(@current_journal.user)
1485 copy.init_journal(@current_journal.user)
1486 end
1486 end
1487 copy.author = author
1487 copy.author = author
1488 copy.project = project
1488 copy.project = project
1489 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1489 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1490 unless copy.save
1490 unless copy.save
1491 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1491 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1492 next
1492 next
1493 end
1493 end
1494 copied_issue_ids[child.id] = copy.id
1494 copied_issue_ids[child.id] = copy.id
1495 end
1495 end
1496 end
1496 end
1497 @after_create_from_copy_handled = true
1497 @after_create_from_copy_handled = true
1498 end
1498 end
1499
1499
1500 def update_nested_set_attributes
1500 def update_nested_set_attributes
1501 if parent_id_changed?
1501 if parent_id_changed?
1502 update_nested_set_attributes_on_parent_change
1502 update_nested_set_attributes_on_parent_change
1503 end
1503 end
1504 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1504 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1505 end
1505 end
1506
1506
1507 # Updates the nested set for when an existing issue is moved
1507 # Updates the nested set for when an existing issue is moved
1508 def update_nested_set_attributes_on_parent_change
1508 def update_nested_set_attributes_on_parent_change
1509 former_parent_id = parent_id_was
1509 former_parent_id = parent_id_was
1510 # delete invalid relations of all descendants
1510 # delete invalid relations of all descendants
1511 self_and_descendants.each do |issue|
1511 self_and_descendants.each do |issue|
1512 issue.relations.each do |relation|
1512 issue.relations.each do |relation|
1513 relation.destroy unless relation.valid?
1513 relation.destroy unless relation.valid?
1514 end
1514 end
1515 end
1515 end
1516 # update former parent
1516 # update former parent
1517 recalculate_attributes_for(former_parent_id) if former_parent_id
1517 recalculate_attributes_for(former_parent_id) if former_parent_id
1518 end
1518 end
1519
1519
1520 def update_parent_attributes
1520 def update_parent_attributes
1521 if parent_id
1521 if parent_id
1522 recalculate_attributes_for(parent_id)
1522 recalculate_attributes_for(parent_id)
1523 association(:parent).reset
1523 association(:parent).reset
1524 end
1524 end
1525 end
1525 end
1526
1526
1527 def recalculate_attributes_for(issue_id)
1527 def recalculate_attributes_for(issue_id)
1528 if issue_id && p = Issue.find_by_id(issue_id)
1528 if issue_id && p = Issue.find_by_id(issue_id)
1529 if p.priority_derived?
1529 if p.priority_derived?
1530 # priority = highest priority of open children
1530 # priority = highest priority of open children
1531 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1531 if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1532 p.priority = IssuePriority.find_by_position(priority_position)
1532 p.priority = IssuePriority.find_by_position(priority_position)
1533 else
1533 else
1534 p.priority = IssuePriority.default
1534 p.priority = IssuePriority.default
1535 end
1535 end
1536 end
1536 end
1537
1537
1538 if p.dates_derived?
1538 if p.dates_derived?
1539 # start/due dates = lowest/highest dates of children
1539 # start/due dates = lowest/highest dates of children
1540 p.start_date = p.children.minimum(:start_date)
1540 p.start_date = p.children.minimum(:start_date)
1541 p.due_date = p.children.maximum(:due_date)
1541 p.due_date = p.children.maximum(:due_date)
1542 if p.start_date && p.due_date && p.due_date < p.start_date
1542 if p.start_date && p.due_date && p.due_date < p.start_date
1543 p.start_date, p.due_date = p.due_date, p.start_date
1543 p.start_date, p.due_date = p.due_date, p.start_date
1544 end
1544 end
1545 end
1545 end
1546
1546
1547 if p.done_ratio_derived?
1547 if p.done_ratio_derived?
1548 # done ratio = weighted average ratio of leaves
1548 # done ratio = weighted average ratio of leaves
1549 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1549 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1550 child_count = p.children.count
1550 child_count = p.children.count
1551 if child_count > 0
1551 if child_count > 0
1552 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1552 average = p.children.where("estimated_hours > 0").average(:estimated_hours).to_f
1553 if average == 0
1553 if average == 0
1554 average = 1
1554 average = 1
1555 end
1555 end
1556 done = p.children.joins(:status).
1556 done = p.children.joins(:status).
1557 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1557 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1558 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1558 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1559 progress = done / (average * child_count)
1559 progress = done / (average * child_count)
1560 p.done_ratio = progress.round
1560 p.done_ratio = progress.round
1561 end
1561 end
1562 end
1562 end
1563 end
1563 end
1564
1564
1565 # ancestors will be recursively updated
1565 # ancestors will be recursively updated
1566 p.save(:validate => false)
1566 p.save(:validate => false)
1567 end
1567 end
1568 end
1568 end
1569
1569
1570 # Update issues so their versions are not pointing to a
1570 # Update issues so their versions are not pointing to a
1571 # fixed_version that is not shared with the issue's project
1571 # fixed_version that is not shared with the issue's project
1572 def self.update_versions(conditions=nil)
1572 def self.update_versions(conditions=nil)
1573 # Only need to update issues with a fixed_version from
1573 # Only need to update issues with a fixed_version from
1574 # a different project and that is not systemwide shared
1574 # a different project and that is not systemwide shared
1575 Issue.joins(:project, :fixed_version).
1575 Issue.joins(:project, :fixed_version).
1576 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1576 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1577 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1577 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1578 " AND #{Version.table_name}.sharing <> 'system'").
1578 " AND #{Version.table_name}.sharing <> 'system'").
1579 where(conditions).each do |issue|
1579 where(conditions).each do |issue|
1580 next if issue.project.nil? || issue.fixed_version.nil?
1580 next if issue.project.nil? || issue.fixed_version.nil?
1581 unless issue.project.shared_versions.include?(issue.fixed_version)
1581 unless issue.project.shared_versions.include?(issue.fixed_version)
1582 issue.init_journal(User.current)
1582 issue.init_journal(User.current)
1583 issue.fixed_version = nil
1583 issue.fixed_version = nil
1584 issue.save
1584 issue.save
1585 end
1585 end
1586 end
1586 end
1587 end
1587 end
1588
1588
1589 # Callback on file attachment
1589 # Callback on file attachment
1590 def attachment_added(attachment)
1590 def attachment_added(attachment)
1591 if current_journal && !attachment.new_record?
1591 if current_journal && !attachment.new_record?
1592 current_journal.journalize_attachment(attachment, :added)
1592 current_journal.journalize_attachment(attachment, :added)
1593 end
1593 end
1594 end
1594 end
1595
1595
1596 # Callback on attachment deletion
1596 # Callback on attachment deletion
1597 def attachment_removed(attachment)
1597 def attachment_removed(attachment)
1598 if current_journal && !attachment.new_record?
1598 if current_journal && !attachment.new_record?
1599 current_journal.journalize_attachment(attachment, :removed)
1599 current_journal.journalize_attachment(attachment, :removed)
1600 current_journal.save
1600 current_journal.save
1601 end
1601 end
1602 end
1602 end
1603
1603
1604 # Called after a relation is added
1604 # Called after a relation is added
1605 def relation_added(relation)
1605 def relation_added(relation)
1606 if current_journal
1606 if current_journal
1607 current_journal.journalize_relation(relation, :added)
1607 current_journal.journalize_relation(relation, :added)
1608 current_journal.save
1608 current_journal.save
1609 end
1609 end
1610 end
1610 end
1611
1611
1612 # Called after a relation is removed
1612 # Called after a relation is removed
1613 def relation_removed(relation)
1613 def relation_removed(relation)
1614 if current_journal
1614 if current_journal
1615 current_journal.journalize_relation(relation, :removed)
1615 current_journal.journalize_relation(relation, :removed)
1616 current_journal.save
1616 current_journal.save
1617 end
1617 end
1618 end
1618 end
1619
1619
1620 # Default assignment based on category
1620 # Default assignment based on category
1621 def default_assign
1621 def default_assign
1622 if assigned_to.nil? && category && category.assigned_to
1622 if assigned_to.nil? && category && category.assigned_to
1623 self.assigned_to = category.assigned_to
1623 self.assigned_to = category.assigned_to
1624 end
1624 end
1625 end
1625 end
1626
1626
1627 # Updates start/due dates of following issues
1627 # Updates start/due dates of following issues
1628 def reschedule_following_issues
1628 def reschedule_following_issues
1629 if start_date_changed? || due_date_changed?
1629 if start_date_changed? || due_date_changed?
1630 relations_from.each do |relation|
1630 relations_from.each do |relation|
1631 relation.set_issue_to_dates
1631 relation.set_issue_to_dates
1632 end
1632 end
1633 end
1633 end
1634 end
1634 end
1635
1635
1636 # Closes duplicates if the issue is being closed
1636 # Closes duplicates if the issue is being closed
1637 def close_duplicates
1637 def close_duplicates
1638 if closing?
1638 if closing?
1639 duplicates.each do |duplicate|
1639 duplicates.each do |duplicate|
1640 # Reload is needed in case the duplicate was updated by a previous duplicate
1640 # Reload is needed in case the duplicate was updated by a previous duplicate
1641 duplicate.reload
1641 duplicate.reload
1642 # Don't re-close it if it's already closed
1642 # Don't re-close it if it's already closed
1643 next if duplicate.closed?
1643 next if duplicate.closed?
1644 # Same user and notes
1644 # Same user and notes
1645 if @current_journal
1645 if @current_journal
1646 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1646 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1647 duplicate.private_notes = @current_journal.private_notes
1647 duplicate.private_notes = @current_journal.private_notes
1648 end
1648 end
1649 duplicate.update_attribute :status, self.status
1649 duplicate.update_attribute :status, self.status
1650 end
1650 end
1651 end
1651 end
1652 end
1652 end
1653
1653
1654 # Make sure updated_on is updated when adding a note and set updated_on now
1654 # Make sure updated_on is updated when adding a note and set updated_on now
1655 # so we can set closed_on with the same value on closing
1655 # so we can set closed_on with the same value on closing
1656 def force_updated_on_change
1656 def force_updated_on_change
1657 if @current_journal || changed?
1657 if @current_journal || changed?
1658 self.updated_on = current_time_from_proper_timezone
1658 self.updated_on = current_time_from_proper_timezone
1659 if new_record?
1659 if new_record?
1660 self.created_on = updated_on
1660 self.created_on = updated_on
1661 end
1661 end
1662 end
1662 end
1663 end
1663 end
1664
1664
1665 # Callback for setting closed_on when the issue is closed.
1665 # Callback for setting closed_on when the issue is closed.
1666 # The closed_on attribute stores the time of the last closing
1666 # The closed_on attribute stores the time of the last closing
1667 # and is preserved when the issue is reopened.
1667 # and is preserved when the issue is reopened.
1668 def update_closed_on
1668 def update_closed_on
1669 if closing?
1669 if closing?
1670 self.closed_on = updated_on
1670 self.closed_on = updated_on
1671 end
1671 end
1672 end
1672 end
1673
1673
1674 # Saves the changes in a Journal
1674 # Saves the changes in a Journal
1675 # Called after_save
1675 # Called after_save
1676 def create_journal
1676 def create_journal
1677 if current_journal
1677 if current_journal
1678 current_journal.save
1678 current_journal.save
1679 end
1679 end
1680 end
1680 end
1681
1681
1682 def send_notification
1682 def send_notification
1683 if notify? && Setting.notified_events.include?('issue_added')
1683 if notify? && Setting.notified_events.include?('issue_added')
1684 Mailer.deliver_issue_add(self)
1684 Mailer.deliver_issue_add(self)
1685 end
1685 end
1686 end
1686 end
1687
1687
1688 # Stores the previous assignee so we can still have access
1688 # Stores the previous assignee so we can still have access
1689 # to it during after_save callbacks (assigned_to_id_was is reset)
1689 # to it during after_save callbacks (assigned_to_id_was is reset)
1690 def set_assigned_to_was
1690 def set_assigned_to_was
1691 @previous_assigned_to_id = assigned_to_id_was
1691 @previous_assigned_to_id = assigned_to_id_was
1692 end
1692 end
1693
1693
1694 # Clears the previous assignee at the end of after_save callbacks
1694 # Clears the previous assignee at the end of after_save callbacks
1695 def clear_assigned_to_was
1695 def clear_assigned_to_was
1696 @assigned_to_was = nil
1696 @assigned_to_was = nil
1697 @previous_assigned_to_id = nil
1697 @previous_assigned_to_id = nil
1698 end
1698 end
1699
1699
1700 def clear_disabled_fields
1700 def clear_disabled_fields
1701 if tracker
1701 if tracker
1702 tracker.disabled_core_fields.each do |attribute|
1702 tracker.disabled_core_fields.each do |attribute|
1703 send "#{attribute}=", nil
1703 send "#{attribute}=", nil
1704 end
1704 end
1705 self.done_ratio ||= 0
1705 self.done_ratio ||= 0
1706 end
1706 end
1707 end
1707 end
1708 end
1708 end
@@ -1,1055 +1,1066
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::NestedSet::ProjectNestedSet
20 include Redmine::NestedSet::ProjectNestedSet
21
21
22 # Project statuses
22 # Project statuses
23 STATUS_ACTIVE = 1
23 STATUS_ACTIVE = 1
24 STATUS_CLOSED = 5
24 STATUS_CLOSED = 5
25 STATUS_ARCHIVED = 9
25 STATUS_ARCHIVED = 9
26
26
27 # Maximum length for project identifiers
27 # Maximum length for project identifiers
28 IDENTIFIER_MAX_LENGTH = 100
28 IDENTIFIER_MAX_LENGTH = 100
29
29
30 # Specific overridden Activities
30 # Specific overridden Activities
31 has_many :time_entry_activities
31 has_many :time_entry_activities
32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
32 has_many :memberships, :class_name => 'Member', :inverse_of => :project
33 # Memberships of active users only
33 # Memberships of active users only
34 has_many :members,
34 has_many :members,
35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
35 lambda { joins(:principal).where(:users => {:type => 'User', :status => Principal::STATUS_ACTIVE}) }
36 has_many :enabled_modules, :dependent => :delete_all
36 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, lambda {order(:position)}
37 has_and_belongs_to_many :trackers, lambda {order(:position)}
38 has_many :issues, :dependent => :destroy
38 has_many :issues, :dependent => :destroy
39 has_many :issue_changes, :through => :issues, :source => :journals
39 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
40 has_many :versions, lambda {order("#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC")}, :dependent => :destroy
41 belongs_to :default_version, :class_name => 'Version'
41 belongs_to :default_version, :class_name => 'Version'
42 has_many :time_entries, :dependent => :destroy
42 has_many :time_entries, :dependent => :destroy
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
43 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
44 has_many :documents, :dependent => :destroy
44 has_many :documents, :dependent => :destroy
45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
45 has_many :news, lambda {includes(:author)}, :dependent => :destroy
46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
46 has_many :issue_categories, lambda {order("#{IssueCategory.table_name}.name")}, :dependent => :delete_all
47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
47 has_many :boards, lambda {order("position ASC")}, :dependent => :destroy
48 has_one :repository, lambda {where(["is_default = ?", true])}
48 has_one :repository, lambda {where(["is_default = ?", true])}
49 has_many :repositories, :dependent => :destroy
49 has_many :repositories, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 lambda {order("#{CustomField.table_name}.position")},
54 lambda {order("#{CustomField.table_name}.position")},
55 :class_name => 'IssueCustomField',
55 :class_name => 'IssueCustomField',
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_attachable :view_permission => :view_files,
59 acts_as_attachable :view_permission => :view_files,
60 :edit_permission => :manage_files,
60 :edit_permission => :manage_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => "#{Project.table_name}.id", :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
72 validates_uniqueness_of :identifier, :if => Proc.new {|p| p.identifier_changed?}
73 validates_length_of :name, :maximum => 255
73 validates_length_of :name, :maximum => 255
74 validates_length_of :homepage, :maximum => 255
74 validates_length_of :homepage, :maximum => 255
75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 # downcase letters, digits, dashes but not digits only
76 # downcase letters, digits, dashes but not digits only
77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
77 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
78 # reserved words
78 # reserved words
79 validates_exclusion_of :identifier, :in => %w( new )
79 validates_exclusion_of :identifier, :in => %w( new )
80 validate :validate_parent
80 validate :validate_parent
81
81
82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
82 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
83 after_save :remove_inherited_member_roles, :add_inherited_member_roles, :if => Proc.new {|project| project.parent_id_changed?}
84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
84 after_update :update_versions_from_hierarchy_change, :if => Proc.new {|project| project.parent_id_changed?}
85 before_destroy :delete_all_members
85 before_destroy :delete_all_members
86
86
87 scope :has_module, lambda {|mod|
87 scope :has_module, lambda {|mod|
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 }
89 }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :all_public, lambda { where(:is_public => true) }
92 scope :all_public, lambda { where(:is_public => true) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :allowed_to, lambda {|*args|
94 scope :allowed_to, lambda {|*args|
95 user = User.current
95 user = User.current
96 permission = nil
96 permission = nil
97 if args.first.is_a?(Symbol)
97 if args.first.is_a?(Symbol)
98 permission = args.shift
98 permission = args.shift
99 else
99 else
100 user = args.shift
100 user = args.shift
101 permission = args.shift
101 permission = args.shift
102 end
102 end
103 where(Project.allowed_to_condition(user, permission, *args))
103 where(Project.allowed_to_condition(user, permission, *args))
104 }
104 }
105 scope :like, lambda {|arg|
105 scope :like, lambda {|arg|
106 if arg.blank?
106 if arg.blank?
107 where(nil)
107 where(nil)
108 else
108 else
109 pattern = "%#{arg.to_s.strip.downcase}%"
109 pattern = "%#{arg.to_s.strip.downcase}%"
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 end
111 end
112 }
112 }
113 scope :sorted, lambda {order(:lft)}
113 scope :sorted, lambda {order(:lft)}
114 scope :having_trackers, lambda {
114 scope :having_trackers, lambda {
115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
115 where("#{Project.table_name}.id IN (SELECT DISTINCT project_id FROM #{table_name_prefix}projects_trackers#{table_name_suffix})")
116 }
116 }
117
117
118 def initialize(attributes=nil, *args)
118 def initialize(attributes=nil, *args)
119 super
119 super
120
120
121 initialized = (attributes || {}).stringify_keys
121 initialized = (attributes || {}).stringify_keys
122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
122 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
123 self.identifier = Project.next_identifier
123 self.identifier = Project.next_identifier
124 end
124 end
125 if !initialized.key?('is_public')
125 if !initialized.key?('is_public')
126 self.is_public = Setting.default_projects_public?
126 self.is_public = Setting.default_projects_public?
127 end
127 end
128 if !initialized.key?('enabled_module_names')
128 if !initialized.key?('enabled_module_names')
129 self.enabled_module_names = Setting.default_projects_modules
129 self.enabled_module_names = Setting.default_projects_modules
130 end
130 end
131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
131 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
132 default = Setting.default_projects_tracker_ids
132 default = Setting.default_projects_tracker_ids
133 if default.is_a?(Array)
133 if default.is_a?(Array)
134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
134 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.to_a
135 else
135 else
136 self.trackers = Tracker.sorted.to_a
136 self.trackers = Tracker.sorted.to_a
137 end
137 end
138 end
138 end
139 end
139 end
140
140
141 def identifier=(identifier)
141 def identifier=(identifier)
142 super unless identifier_frozen?
142 super unless identifier_frozen?
143 end
143 end
144
144
145 def identifier_frozen?
145 def identifier_frozen?
146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
146 errors[:identifier].blank? && !(new_record? || identifier.blank?)
147 end
147 end
148
148
149 # returns latest created projects
149 # returns latest created projects
150 # non public projects will be returned only if user is a member of those
150 # non public projects will be returned only if user is a member of those
151 def self.latest(user=nil, count=5)
151 def self.latest(user=nil, count=5)
152 visible(user).limit(count).
152 visible(user).limit(count).
153 order(:created_on => :desc).
153 order(:created_on => :desc).
154 where("#{table_name}.created_on >= ?", 30.days.ago).
154 where("#{table_name}.created_on >= ?", 30.days.ago).
155 to_a
155 to_a
156 end
156 end
157
157
158 # Returns true if the project is visible to +user+ or to the current user.
158 # Returns true if the project is visible to +user+ or to the current user.
159 def visible?(user=User.current)
159 def visible?(user=User.current)
160 user.allowed_to?(:view_project, self)
160 user.allowed_to?(:view_project, self)
161 end
161 end
162
162
163 # Returns a SQL conditions string used to find all projects visible by the specified user.
163 # Returns a SQL conditions string used to find all projects visible by the specified user.
164 #
164 #
165 # Examples:
165 # Examples:
166 # Project.visible_condition(admin) => "projects.status = 1"
166 # Project.visible_condition(admin) => "projects.status = 1"
167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
167 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
168 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
169 def self.visible_condition(user, options={})
169 def self.visible_condition(user, options={})
170 allowed_to_condition(user, :view_project, options)
170 allowed_to_condition(user, :view_project, options)
171 end
171 end
172
172
173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
173 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
174 #
174 #
175 # Valid options:
175 # Valid options:
176 # * :project => limit the condition to project
176 # * :project => limit the condition to project
177 # * :with_subprojects => limit the condition to project and its subprojects
177 # * :with_subprojects => limit the condition to project and its subprojects
178 # * :member => limit the condition to the user projects
178 # * :member => limit the condition to the user projects
179 def self.allowed_to_condition(user, permission, options={})
179 def self.allowed_to_condition(user, permission, options={})
180 perm = Redmine::AccessControl.permission(permission)
180 perm = Redmine::AccessControl.permission(permission)
181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
181 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
182 if perm && perm.project_module
182 if perm && perm.project_module
183 # If the permission belongs to a project module, make sure the module is enabled
183 # If the permission belongs to a project module, make sure the module is enabled
184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
184 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
185 end
185 end
186 if project = options[:project]
186 if project = options[:project]
187 project_statement = project.project_condition(options[:with_subprojects])
187 project_statement = project.project_condition(options[:with_subprojects])
188 base_statement = "(#{project_statement}) AND (#{base_statement})"
188 base_statement = "(#{project_statement}) AND (#{base_statement})"
189 end
189 end
190
190
191 if user.admin?
191 if user.admin?
192 base_statement
192 base_statement
193 else
193 else
194 statement_by_role = {}
194 statement_by_role = {}
195 unless options[:member]
195 unless options[:member]
196 role = user.builtin_role
196 role = user.builtin_role
197 if role.allowed_to?(permission)
197 if role.allowed_to?(permission)
198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
198 s = "#{Project.table_name}.is_public = #{connection.quoted_true}"
199 if user.id
199 if user.id
200 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
200 s = "(#{s} AND #{Project.table_name}.id NOT IN (SELECT project_id FROM #{Member.table_name} WHERE user_id = #{user.id}))"
201 end
201 end
202 statement_by_role[role] = s
202 statement_by_role[role] = s
203 end
203 end
204 end
204 end
205 user.projects_by_role.each do |role, projects|
205 user.projects_by_role.each do |role, projects|
206 if role.allowed_to?(permission) && projects.any?
206 if role.allowed_to?(permission) && projects.any?
207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
207 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
208 end
208 end
209 end
209 end
210 if statement_by_role.empty?
210 if statement_by_role.empty?
211 "1=0"
211 "1=0"
212 else
212 else
213 if block_given?
213 if block_given?
214 statement_by_role.each do |role, statement|
214 statement_by_role.each do |role, statement|
215 if s = yield(role, user)
215 if s = yield(role, user)
216 statement_by_role[role] = "(#{statement} AND (#{s}))"
216 statement_by_role[role] = "(#{statement} AND (#{s}))"
217 end
217 end
218 end
218 end
219 end
219 end
220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
220 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
221 end
221 end
222 end
222 end
223 end
223 end
224
224
225 def override_roles(role)
225 def override_roles(role)
226 @override_members ||= memberships.
226 @override_members ||= memberships.
227 joins(:principal).
227 joins(:principal).
228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
228 where(:users => {:type => ['GroupAnonymous', 'GroupNonMember']}).to_a
229
229
230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
230 group_class = role.anonymous? ? GroupAnonymous : GroupNonMember
231 member = @override_members.detect {|m| m.principal.is_a? group_class}
231 member = @override_members.detect {|m| m.principal.is_a? group_class}
232 member ? member.roles.to_a : [role]
232 member ? member.roles.to_a : [role]
233 end
233 end
234
234
235 def principals
235 def principals
236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
236 @principals ||= Principal.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
237 end
237 end
238
238
239 def users
239 def users
240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
240 @users ||= User.active.joins(:members).where("#{Member.table_name}.project_id = ?", id).uniq
241 end
241 end
242
242
243 # Returns the Systemwide and project specific activities
243 # Returns the Systemwide and project specific activities
244 def activities(include_inactive=false)
244 def activities(include_inactive=false)
245 t = TimeEntryActivity.table_name
245 t = TimeEntryActivity.table_name
246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
246 scope = TimeEntryActivity.where("#{t}.project_id IS NULL OR #{t}.project_id = ?", id)
247
247
248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
248 overridden_activity_ids = self.time_entry_activities.pluck(:parent_id).compact
249 if overridden_activity_ids.any?
249 if overridden_activity_ids.any?
250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
250 scope = scope.where("#{t}.id NOT IN (?)", overridden_activity_ids)
251 end
251 end
252 unless include_inactive
252 unless include_inactive
253 scope = scope.active
253 scope = scope.active
254 end
254 end
255 scope
255 scope
256 end
256 end
257
257
258 # Will create a new Project specific Activity or update an existing one
258 # Will create a new Project specific Activity or update an existing one
259 #
259 #
260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
260 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
261 # does not successfully save.
261 # does not successfully save.
262 def update_or_create_time_entry_activity(id, activity_hash)
262 def update_or_create_time_entry_activity(id, activity_hash)
263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
263 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
264 self.create_time_entry_activity_if_needed(activity_hash)
264 self.create_time_entry_activity_if_needed(activity_hash)
265 else
265 else
266 activity = project.time_entry_activities.find_by_id(id.to_i)
266 activity = project.time_entry_activities.find_by_id(id.to_i)
267 activity.update_attributes(activity_hash) if activity
267 activity.update_attributes(activity_hash) if activity
268 end
268 end
269 end
269 end
270
270
271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
271 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
272 #
272 #
273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
273 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
274 # does not successfully save.
274 # does not successfully save.
275 def create_time_entry_activity_if_needed(activity)
275 def create_time_entry_activity_if_needed(activity)
276 if activity['parent_id']
276 if activity['parent_id']
277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
277 parent_activity = TimeEntryActivity.find(activity['parent_id'])
278 activity['name'] = parent_activity.name
278 activity['name'] = parent_activity.name
279 activity['position'] = parent_activity.position
279 activity['position'] = parent_activity.position
280 if Enumeration.overriding_change?(activity, parent_activity)
280 if Enumeration.overriding_change?(activity, parent_activity)
281 project_activity = self.time_entry_activities.create(activity)
281 project_activity = self.time_entry_activities.create(activity)
282 if project_activity.new_record?
282 if project_activity.new_record?
283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
283 raise ActiveRecord::Rollback, "Overriding TimeEntryActivity was not successfully saved"
284 else
284 else
285 self.time_entries.
285 self.time_entries.
286 where(:activity_id => parent_activity.id).
286 where(:activity_id => parent_activity.id).
287 update_all(:activity_id => project_activity.id)
287 update_all(:activity_id => project_activity.id)
288 end
288 end
289 end
289 end
290 end
290 end
291 end
291 end
292
292
293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
293 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
294 #
294 #
295 # Examples:
295 # Examples:
296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
296 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
297 # project.project_condition(false) => "projects.id = 1"
297 # project.project_condition(false) => "projects.id = 1"
298 def project_condition(with_subprojects)
298 def project_condition(with_subprojects)
299 cond = "#{Project.table_name}.id = #{id}"
299 cond = "#{Project.table_name}.id = #{id}"
300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
300 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
301 cond
301 cond
302 end
302 end
303
303
304 def self.find(*args)
304 def self.find(*args)
305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
305 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
306 project = find_by_identifier(*args)
306 project = find_by_identifier(*args)
307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
307 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
308 project
308 project
309 else
309 else
310 super
310 super
311 end
311 end
312 end
312 end
313
313
314 def self.find_by_param(*args)
314 def self.find_by_param(*args)
315 self.find(*args)
315 self.find(*args)
316 end
316 end
317
317
318 alias :base_reload :reload
318 alias :base_reload :reload
319 def reload(*args)
319 def reload(*args)
320 @principals = nil
320 @principals = nil
321 @users = nil
321 @users = nil
322 @shared_versions = nil
322 @shared_versions = nil
323 @rolled_up_versions = nil
323 @rolled_up_versions = nil
324 @rolled_up_trackers = nil
324 @rolled_up_trackers = nil
325 @all_issue_custom_fields = nil
325 @all_issue_custom_fields = nil
326 @all_time_entry_custom_fields = nil
326 @all_time_entry_custom_fields = nil
327 @to_param = nil
327 @to_param = nil
328 @allowed_parents = nil
328 @allowed_parents = nil
329 @allowed_permissions = nil
329 @allowed_permissions = nil
330 @actions_allowed = nil
330 @actions_allowed = nil
331 @start_date = nil
331 @start_date = nil
332 @due_date = nil
332 @due_date = nil
333 @override_members = nil
333 @override_members = nil
334 @assignable_users = nil
334 @assignable_users = nil
335 base_reload(*args)
335 base_reload(*args)
336 end
336 end
337
337
338 def to_param
338 def to_param
339 if new_record?
339 if new_record?
340 nil
340 nil
341 else
341 else
342 # id is used for projects with a numeric identifier (compatibility)
342 # id is used for projects with a numeric identifier (compatibility)
343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
343 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
344 end
344 end
345 end
345 end
346
346
347 def active?
347 def active?
348 self.status == STATUS_ACTIVE
348 self.status == STATUS_ACTIVE
349 end
349 end
350
350
351 def archived?
351 def archived?
352 self.status == STATUS_ARCHIVED
352 self.status == STATUS_ARCHIVED
353 end
353 end
354
354
355 # Archives the project and its descendants
355 # Archives the project and its descendants
356 def archive
356 def archive
357 # Check that there is no issue of a non descendant project that is assigned
357 # Check that there is no issue of a non descendant project that is assigned
358 # to one of the project or descendant versions
358 # to one of the project or descendant versions
359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
359 version_ids = self_and_descendants.joins(:versions).pluck("#{Version.table_name}.id")
360
360
361 if version_ids.any? &&
361 if version_ids.any? &&
362 Issue.
362 Issue.
363 includes(:project).
363 includes(:project).
364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
364 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
365 where(:fixed_version_id => version_ids).
365 where(:fixed_version_id => version_ids).
366 exists?
366 exists?
367 return false
367 return false
368 end
368 end
369 Project.transaction do
369 Project.transaction do
370 archive!
370 archive!
371 end
371 end
372 true
372 true
373 end
373 end
374
374
375 # Unarchives the project
375 # Unarchives the project
376 # All its ancestors must be active
376 # All its ancestors must be active
377 def unarchive
377 def unarchive
378 return false if ancestors.detect {|a| !a.active?}
378 return false if ancestors.detect {|a| !a.active?}
379 update_attribute :status, STATUS_ACTIVE
379 update_attribute :status, STATUS_ACTIVE
380 end
380 end
381
381
382 def close
382 def close
383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
383 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
384 end
384 end
385
385
386 def reopen
386 def reopen
387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
387 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
388 end
388 end
389
389
390 # Returns an array of projects the project can be moved to
390 # Returns an array of projects the project can be moved to
391 # by the current user
391 # by the current user
392 def allowed_parents(user=User.current)
392 def allowed_parents(user=User.current)
393 return @allowed_parents if @allowed_parents
393 return @allowed_parents if @allowed_parents
394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
394 @allowed_parents = Project.allowed_to(user, :add_subprojects).to_a
395 @allowed_parents = @allowed_parents - self_and_descendants
395 @allowed_parents = @allowed_parents - self_and_descendants
396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
396 if user.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
397 @allowed_parents << nil
397 @allowed_parents << nil
398 end
398 end
399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
399 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
400 @allowed_parents << parent
400 @allowed_parents << parent
401 end
401 end
402 @allowed_parents
402 @allowed_parents
403 end
403 end
404
404
405 # Sets the parent of the project with authorization check
405 # Sets the parent of the project with authorization check
406 def set_allowed_parent!(p)
406 def set_allowed_parent!(p)
407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
407 ActiveSupport::Deprecation.warn "Project#set_allowed_parent! is deprecated and will be removed in Redmine 4, use #safe_attributes= instead."
408 p = p.id if p.is_a?(Project)
408 p = p.id if p.is_a?(Project)
409 send :safe_attributes, {:project_id => p}
409 send :safe_attributes, {:project_id => p}
410 save
410 save
411 end
411 end
412
412
413 # Sets the parent of the project and saves the project
413 # Sets the parent of the project and saves the project
414 # Argument can be either a Project, a String, a Fixnum or nil
414 # Argument can be either a Project, a String, a Fixnum or nil
415 def set_parent!(p)
415 def set_parent!(p)
416 if p.is_a?(Project)
416 if p.is_a?(Project)
417 self.parent = p
417 self.parent = p
418 else
418 else
419 self.parent_id = p
419 self.parent_id = p
420 end
420 end
421 save
421 save
422 end
422 end
423
423
424 # Returns a scope of the trackers used by the project and its active sub projects
424 # Returns a scope of the trackers used by the project and its active sub projects
425 def rolled_up_trackers(include_subprojects=true)
425 def rolled_up_trackers(include_subprojects=true)
426 if include_subprojects
426 if include_subprojects
427 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
427 @rolled_up_trackers ||= rolled_up_trackers_base_scope.
428 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
428 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt)
429 else
429 else
430 rolled_up_trackers_base_scope.
430 rolled_up_trackers_base_scope.
431 where(:projects => {:id => id})
431 where(:projects => {:id => id})
432 end
432 end
433 end
433 end
434
434
435 def rolled_up_trackers_base_scope
435 def rolled_up_trackers_base_scope
436 Tracker.
436 Tracker.
437 joins(projects: :enabled_modules).
437 joins(projects: :enabled_modules).
438 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
438 where("#{Project.table_name}.status <> ?", STATUS_ARCHIVED).
439 where(:enabled_modules => {:name => 'issue_tracking'}).
439 where(:enabled_modules => {:name => 'issue_tracking'}).
440 uniq.
440 uniq.
441 sorted
441 sorted
442 end
442 end
443
443
444 # Closes open and locked project versions that are completed
444 # Closes open and locked project versions that are completed
445 def close_completed_versions
445 def close_completed_versions
446 Version.transaction do
446 Version.transaction do
447 versions.where(:status => %w(open locked)).each do |version|
447 versions.where(:status => %w(open locked)).each do |version|
448 if version.completed?
448 if version.completed?
449 version.update_attribute(:status, 'closed')
449 version.update_attribute(:status, 'closed')
450 end
450 end
451 end
451 end
452 end
452 end
453 end
453 end
454
454
455 # Returns a scope of the Versions on subprojects
455 # Returns a scope of the Versions on subprojects
456 def rolled_up_versions
456 def rolled_up_versions
457 @rolled_up_versions ||=
457 @rolled_up_versions ||=
458 Version.
458 Version.
459 joins(:project).
459 joins(:project).
460 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
460 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> ?", lft, rgt, STATUS_ARCHIVED)
461 end
461 end
462
462
463 # Returns a scope of the Versions used by the project
463 # Returns a scope of the Versions used by the project
464 def shared_versions
464 def shared_versions
465 if new_record?
465 if new_record?
466 Version.
466 Version.
467 joins(:project).
467 joins(:project).
468 preload(:project).
468 preload(:project).
469 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
469 where("#{Project.table_name}.status <> ? AND #{Version.table_name}.sharing = 'system'", STATUS_ARCHIVED)
470 else
470 else
471 @shared_versions ||= begin
471 @shared_versions ||= begin
472 r = root? ? self : root
472 r = root? ? self : root
473 Version.
473 Version.
474 joins(:project).
474 joins(:project).
475 preload(:project).
475 preload(:project).
476 where("#{Project.table_name}.id = #{id}" +
476 where("#{Project.table_name}.id = #{id}" +
477 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
477 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
478 " #{Version.table_name}.sharing = 'system'" +
478 " #{Version.table_name}.sharing = 'system'" +
479 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
479 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
480 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
480 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
481 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
481 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
482 "))")
482 "))")
483 end
483 end
484 end
484 end
485 end
485 end
486
486
487 # Returns a hash of project users grouped by role
487 # Returns a hash of project users grouped by role
488 def users_by_role
488 def users_by_role
489 members.includes(:user, :roles).inject({}) do |h, m|
489 members.includes(:user, :roles).inject({}) do |h, m|
490 m.roles.each do |r|
490 m.roles.each do |r|
491 h[r] ||= []
491 h[r] ||= []
492 h[r] << m.user
492 h[r] << m.user
493 end
493 end
494 h
494 h
495 end
495 end
496 end
496 end
497
497
498 # Adds user as a project member with the default role
498 # Adds user as a project member with the default role
499 # Used for when a non-admin user creates a project
499 # Used for when a non-admin user creates a project
500 def add_default_member(user)
500 def add_default_member(user)
501 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
501 role = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
502 member = Member.new(:project => self, :principal => user, :roles => [role])
502 member = Member.new(:project => self, :principal => user, :roles => [role])
503 self.members << member
503 self.members << member
504 member
504 member
505 end
505 end
506
506
507 # Deletes all project's members
507 # Deletes all project's members
508 def delete_all_members
508 def delete_all_members
509 me, mr = Member.table_name, MemberRole.table_name
509 me, mr = Member.table_name, MemberRole.table_name
510 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
510 self.class.connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
511 Member.delete_all(['project_id = ?', id])
511 Member.delete_all(['project_id = ?', id])
512 end
512 end
513
513
514 # Return a Principal scope of users/groups issues can be assigned to
514 # Return a Principal scope of users/groups issues can be assigned to
515 def assignable_users
515 def assignable_users(tracker=nil)
516 return @assignable_users[tracker] if @assignable_users && @assignable_users[tracker]
517
516 types = ['User']
518 types = ['User']
517 types << 'Group' if Setting.issue_group_assignment?
519 types << 'Group' if Setting.issue_group_assignment?
518
520
519 @assignable_users ||= Principal.
521 scope = Principal.
520 active.
522 active.
521 joins(:members => :roles).
523 joins(:members => :roles).
522 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
524 where(:type => types, :members => {:project_id => id}, :roles => {:assignable => true}).
523 uniq.
525 uniq.
524 sorted
526 sorted
527
528 if tracker
529 # Rejects users that cannot the view the tracker
530 roles = Role.where(:assignable => true).select {|role| role.permissions_tracker?(:view_issues, tracker)}
531 scope = scope.where(:roles => {:id => roles.map(&:id)})
532 end
533
534 @assignable_users ||= {}
535 @assignable_users[tracker] = scope
525 end
536 end
526
537
527 # Returns the mail addresses of users that should be always notified on project events
538 # Returns the mail addresses of users that should be always notified on project events
528 def recipients
539 def recipients
529 notified_users.collect {|user| user.mail}
540 notified_users.collect {|user| user.mail}
530 end
541 end
531
542
532 # Returns the users that should be notified on project events
543 # Returns the users that should be notified on project events
533 def notified_users
544 def notified_users
534 # TODO: User part should be extracted to User#notify_about?
545 # TODO: User part should be extracted to User#notify_about?
535 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
546 members.preload(:principal).select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
536 end
547 end
537
548
538 # Returns a scope of all custom fields enabled for project issues
549 # Returns a scope of all custom fields enabled for project issues
539 # (explicitly associated custom fields and custom fields enabled for all projects)
550 # (explicitly associated custom fields and custom fields enabled for all projects)
540 def all_issue_custom_fields
551 def all_issue_custom_fields
541 if new_record?
552 if new_record?
542 @all_issue_custom_fields ||= IssueCustomField.
553 @all_issue_custom_fields ||= IssueCustomField.
543 sorted.
554 sorted.
544 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
555 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
545 else
556 else
546 @all_issue_custom_fields ||= IssueCustomField.
557 @all_issue_custom_fields ||= IssueCustomField.
547 sorted.
558 sorted.
548 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
559 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
549 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
560 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
550 " WHERE cfp.project_id = ?)", true, id)
561 " WHERE cfp.project_id = ?)", true, id)
551 end
562 end
552 end
563 end
553
564
554 def project
565 def project
555 self
566 self
556 end
567 end
557
568
558 def <=>(project)
569 def <=>(project)
559 name.casecmp(project.name)
570 name.casecmp(project.name)
560 end
571 end
561
572
562 def to_s
573 def to_s
563 name
574 name
564 end
575 end
565
576
566 # Returns a short description of the projects (first lines)
577 # Returns a short description of the projects (first lines)
567 def short_description(length = 255)
578 def short_description(length = 255)
568 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
579 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
569 end
580 end
570
581
571 def css_classes
582 def css_classes
572 s = 'project'
583 s = 'project'
573 s << ' root' if root?
584 s << ' root' if root?
574 s << ' child' if child?
585 s << ' child' if child?
575 s << (leaf? ? ' leaf' : ' parent')
586 s << (leaf? ? ' leaf' : ' parent')
576 unless active?
587 unless active?
577 if archived?
588 if archived?
578 s << ' archived'
589 s << ' archived'
579 else
590 else
580 s << ' closed'
591 s << ' closed'
581 end
592 end
582 end
593 end
583 s
594 s
584 end
595 end
585
596
586 # The earliest start date of a project, based on it's issues and versions
597 # The earliest start date of a project, based on it's issues and versions
587 def start_date
598 def start_date
588 @start_date ||= [
599 @start_date ||= [
589 issues.minimum('start_date'),
600 issues.minimum('start_date'),
590 shared_versions.minimum('effective_date'),
601 shared_versions.minimum('effective_date'),
591 Issue.fixed_version(shared_versions).minimum('start_date')
602 Issue.fixed_version(shared_versions).minimum('start_date')
592 ].compact.min
603 ].compact.min
593 end
604 end
594
605
595 # The latest due date of an issue or version
606 # The latest due date of an issue or version
596 def due_date
607 def due_date
597 @due_date ||= [
608 @due_date ||= [
598 issues.maximum('due_date'),
609 issues.maximum('due_date'),
599 shared_versions.maximum('effective_date'),
610 shared_versions.maximum('effective_date'),
600 Issue.fixed_version(shared_versions).maximum('due_date')
611 Issue.fixed_version(shared_versions).maximum('due_date')
601 ].compact.max
612 ].compact.max
602 end
613 end
603
614
604 def overdue?
615 def overdue?
605 active? && !due_date.nil? && (due_date < User.current.today)
616 active? && !due_date.nil? && (due_date < User.current.today)
606 end
617 end
607
618
608 # Returns the percent completed for this project, based on the
619 # Returns the percent completed for this project, based on the
609 # progress on it's versions.
620 # progress on it's versions.
610 def completed_percent(options={:include_subprojects => false})
621 def completed_percent(options={:include_subprojects => false})
611 if options.delete(:include_subprojects)
622 if options.delete(:include_subprojects)
612 total = self_and_descendants.collect(&:completed_percent).sum
623 total = self_and_descendants.collect(&:completed_percent).sum
613
624
614 total / self_and_descendants.count
625 total / self_and_descendants.count
615 else
626 else
616 if versions.count > 0
627 if versions.count > 0
617 total = versions.collect(&:completed_percent).sum
628 total = versions.collect(&:completed_percent).sum
618
629
619 total / versions.count
630 total / versions.count
620 else
631 else
621 100
632 100
622 end
633 end
623 end
634 end
624 end
635 end
625
636
626 # Return true if this project allows to do the specified action.
637 # Return true if this project allows to do the specified action.
627 # action can be:
638 # action can be:
628 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
639 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
629 # * a permission Symbol (eg. :edit_project)
640 # * a permission Symbol (eg. :edit_project)
630 def allows_to?(action)
641 def allows_to?(action)
631 if archived?
642 if archived?
632 # No action allowed on archived projects
643 # No action allowed on archived projects
633 return false
644 return false
634 end
645 end
635 unless active? || Redmine::AccessControl.read_action?(action)
646 unless active? || Redmine::AccessControl.read_action?(action)
636 # No write action allowed on closed projects
647 # No write action allowed on closed projects
637 return false
648 return false
638 end
649 end
639 # No action allowed on disabled modules
650 # No action allowed on disabled modules
640 if action.is_a? Hash
651 if action.is_a? Hash
641 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
652 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
642 else
653 else
643 allowed_permissions.include? action
654 allowed_permissions.include? action
644 end
655 end
645 end
656 end
646
657
647 # Return the enabled module with the given name
658 # Return the enabled module with the given name
648 # or nil if the module is not enabled for the project
659 # or nil if the module is not enabled for the project
649 def enabled_module(name)
660 def enabled_module(name)
650 name = name.to_s
661 name = name.to_s
651 enabled_modules.detect {|m| m.name == name}
662 enabled_modules.detect {|m| m.name == name}
652 end
663 end
653
664
654 # Return true if the module with the given name is enabled
665 # Return true if the module with the given name is enabled
655 def module_enabled?(name)
666 def module_enabled?(name)
656 enabled_module(name).present?
667 enabled_module(name).present?
657 end
668 end
658
669
659 def enabled_module_names=(module_names)
670 def enabled_module_names=(module_names)
660 if module_names && module_names.is_a?(Array)
671 if module_names && module_names.is_a?(Array)
661 module_names = module_names.collect(&:to_s).reject(&:blank?)
672 module_names = module_names.collect(&:to_s).reject(&:blank?)
662 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
673 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
663 else
674 else
664 enabled_modules.clear
675 enabled_modules.clear
665 end
676 end
666 end
677 end
667
678
668 # Returns an array of the enabled modules names
679 # Returns an array of the enabled modules names
669 def enabled_module_names
680 def enabled_module_names
670 enabled_modules.collect(&:name)
681 enabled_modules.collect(&:name)
671 end
682 end
672
683
673 # Enable a specific module
684 # Enable a specific module
674 #
685 #
675 # Examples:
686 # Examples:
676 # project.enable_module!(:issue_tracking)
687 # project.enable_module!(:issue_tracking)
677 # project.enable_module!("issue_tracking")
688 # project.enable_module!("issue_tracking")
678 def enable_module!(name)
689 def enable_module!(name)
679 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
690 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
680 end
691 end
681
692
682 # Disable a module if it exists
693 # Disable a module if it exists
683 #
694 #
684 # Examples:
695 # Examples:
685 # project.disable_module!(:issue_tracking)
696 # project.disable_module!(:issue_tracking)
686 # project.disable_module!("issue_tracking")
697 # project.disable_module!("issue_tracking")
687 # project.disable_module!(project.enabled_modules.first)
698 # project.disable_module!(project.enabled_modules.first)
688 def disable_module!(target)
699 def disable_module!(target)
689 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
700 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
690 target.destroy unless target.blank?
701 target.destroy unless target.blank?
691 end
702 end
692
703
693 safe_attributes 'name',
704 safe_attributes 'name',
694 'description',
705 'description',
695 'homepage',
706 'homepage',
696 'is_public',
707 'is_public',
697 'identifier',
708 'identifier',
698 'custom_field_values',
709 'custom_field_values',
699 'custom_fields',
710 'custom_fields',
700 'tracker_ids',
711 'tracker_ids',
701 'issue_custom_field_ids',
712 'issue_custom_field_ids',
702 'parent_id',
713 'parent_id',
703 'default_version_id'
714 'default_version_id'
704
715
705 safe_attributes 'enabled_module_names',
716 safe_attributes 'enabled_module_names',
706 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
717 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
707
718
708 safe_attributes 'inherit_members',
719 safe_attributes 'inherit_members',
709 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
720 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
710
721
711 def safe_attributes=(attrs, user=User.current)
722 def safe_attributes=(attrs, user=User.current)
712 return unless attrs.is_a?(Hash)
723 return unless attrs.is_a?(Hash)
713 attrs = attrs.deep_dup
724 attrs = attrs.deep_dup
714
725
715 @unallowed_parent_id = nil
726 @unallowed_parent_id = nil
716 if new_record? || attrs.key?('parent_id')
727 if new_record? || attrs.key?('parent_id')
717 parent_id_param = attrs['parent_id'].to_s
728 parent_id_param = attrs['parent_id'].to_s
718 if new_record? || parent_id_param != parent_id.to_s
729 if new_record? || parent_id_param != parent_id.to_s
719 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
730 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
720 unless allowed_parents(user).include?(p)
731 unless allowed_parents(user).include?(p)
721 attrs.delete('parent_id')
732 attrs.delete('parent_id')
722 @unallowed_parent_id = true
733 @unallowed_parent_id = true
723 end
734 end
724 end
735 end
725 end
736 end
726
737
727 super(attrs, user)
738 super(attrs, user)
728 end
739 end
729
740
730 # Returns an auto-generated project identifier based on the last identifier used
741 # Returns an auto-generated project identifier based on the last identifier used
731 def self.next_identifier
742 def self.next_identifier
732 p = Project.order('id DESC').first
743 p = Project.order('id DESC').first
733 p.nil? ? nil : p.identifier.to_s.succ
744 p.nil? ? nil : p.identifier.to_s.succ
734 end
745 end
735
746
736 # Copies and saves the Project instance based on the +project+.
747 # Copies and saves the Project instance based on the +project+.
737 # Duplicates the source project's:
748 # Duplicates the source project's:
738 # * Wiki
749 # * Wiki
739 # * Versions
750 # * Versions
740 # * Categories
751 # * Categories
741 # * Issues
752 # * Issues
742 # * Members
753 # * Members
743 # * Queries
754 # * Queries
744 #
755 #
745 # Accepts an +options+ argument to specify what to copy
756 # Accepts an +options+ argument to specify what to copy
746 #
757 #
747 # Examples:
758 # Examples:
748 # project.copy(1) # => copies everything
759 # project.copy(1) # => copies everything
749 # project.copy(1, :only => 'members') # => copies members only
760 # project.copy(1, :only => 'members') # => copies members only
750 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
761 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
751 def copy(project, options={})
762 def copy(project, options={})
752 project = project.is_a?(Project) ? project : Project.find(project)
763 project = project.is_a?(Project) ? project : Project.find(project)
753
764
754 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
765 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
755 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
766 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
756
767
757 Project.transaction do
768 Project.transaction do
758 if save
769 if save
759 reload
770 reload
760 to_be_copied.each do |name|
771 to_be_copied.each do |name|
761 send "copy_#{name}", project
772 send "copy_#{name}", project
762 end
773 end
763 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
774 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
764 save
775 save
765 else
776 else
766 false
777 false
767 end
778 end
768 end
779 end
769 end
780 end
770
781
771 def member_principals
782 def member_principals
772 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
783 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
773 memberships.active
784 memberships.active
774 end
785 end
775
786
776 # Returns a new unsaved Project instance with attributes copied from +project+
787 # Returns a new unsaved Project instance with attributes copied from +project+
777 def self.copy_from(project)
788 def self.copy_from(project)
778 project = project.is_a?(Project) ? project : Project.find(project)
789 project = project.is_a?(Project) ? project : Project.find(project)
779 # clear unique attributes
790 # clear unique attributes
780 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
791 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
781 copy = Project.new(attributes)
792 copy = Project.new(attributes)
782 copy.enabled_module_names = project.enabled_module_names
793 copy.enabled_module_names = project.enabled_module_names
783 copy.trackers = project.trackers
794 copy.trackers = project.trackers
784 copy.custom_values = project.custom_values.collect {|v| v.clone}
795 copy.custom_values = project.custom_values.collect {|v| v.clone}
785 copy.issue_custom_fields = project.issue_custom_fields
796 copy.issue_custom_fields = project.issue_custom_fields
786 copy
797 copy
787 end
798 end
788
799
789 # Yields the given block for each project with its level in the tree
800 # Yields the given block for each project with its level in the tree
790 def self.project_tree(projects, &block)
801 def self.project_tree(projects, &block)
791 ancestors = []
802 ancestors = []
792 projects.sort_by(&:lft).each do |project|
803 projects.sort_by(&:lft).each do |project|
793 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
804 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
794 ancestors.pop
805 ancestors.pop
795 end
806 end
796 yield project, ancestors.size
807 yield project, ancestors.size
797 ancestors << project
808 ancestors << project
798 end
809 end
799 end
810 end
800
811
801 private
812 private
802
813
803 def update_inherited_members
814 def update_inherited_members
804 if parent
815 if parent
805 if inherit_members? && !inherit_members_was
816 if inherit_members? && !inherit_members_was
806 remove_inherited_member_roles
817 remove_inherited_member_roles
807 add_inherited_member_roles
818 add_inherited_member_roles
808 elsif !inherit_members? && inherit_members_was
819 elsif !inherit_members? && inherit_members_was
809 remove_inherited_member_roles
820 remove_inherited_member_roles
810 end
821 end
811 end
822 end
812 end
823 end
813
824
814 def remove_inherited_member_roles
825 def remove_inherited_member_roles
815 member_roles = memberships.map(&:member_roles).flatten
826 member_roles = memberships.map(&:member_roles).flatten
816 member_role_ids = member_roles.map(&:id)
827 member_role_ids = member_roles.map(&:id)
817 member_roles.each do |member_role|
828 member_roles.each do |member_role|
818 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
829 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
819 member_role.destroy
830 member_role.destroy
820 end
831 end
821 end
832 end
822 end
833 end
823
834
824 def add_inherited_member_roles
835 def add_inherited_member_roles
825 if inherit_members? && parent
836 if inherit_members? && parent
826 parent.memberships.each do |parent_member|
837 parent.memberships.each do |parent_member|
827 member = Member.find_or_new(self.id, parent_member.user_id)
838 member = Member.find_or_new(self.id, parent_member.user_id)
828 parent_member.member_roles.each do |parent_member_role|
839 parent_member.member_roles.each do |parent_member_role|
829 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
840 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
830 end
841 end
831 member.save!
842 member.save!
832 end
843 end
833 memberships.reset
844 memberships.reset
834 end
845 end
835 end
846 end
836
847
837 def update_versions_from_hierarchy_change
848 def update_versions_from_hierarchy_change
838 Issue.update_versions_from_hierarchy_change(self)
849 Issue.update_versions_from_hierarchy_change(self)
839 end
850 end
840
851
841 def validate_parent
852 def validate_parent
842 if @unallowed_parent_id
853 if @unallowed_parent_id
843 errors.add(:parent_id, :invalid)
854 errors.add(:parent_id, :invalid)
844 elsif parent_id_changed?
855 elsif parent_id_changed?
845 unless parent.nil? || (parent.active? && move_possible?(parent))
856 unless parent.nil? || (parent.active? && move_possible?(parent))
846 errors.add(:parent_id, :invalid)
857 errors.add(:parent_id, :invalid)
847 end
858 end
848 end
859 end
849 end
860 end
850
861
851 # Copies wiki from +project+
862 # Copies wiki from +project+
852 def copy_wiki(project)
863 def copy_wiki(project)
853 # Check that the source project has a wiki first
864 # Check that the source project has a wiki first
854 unless project.wiki.nil?
865 unless project.wiki.nil?
855 wiki = self.wiki || Wiki.new
866 wiki = self.wiki || Wiki.new
856 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
867 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
857 wiki_pages_map = {}
868 wiki_pages_map = {}
858 project.wiki.pages.each do |page|
869 project.wiki.pages.each do |page|
859 # Skip pages without content
870 # Skip pages without content
860 next if page.content.nil?
871 next if page.content.nil?
861 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
872 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
862 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
873 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
863 new_wiki_page.content = new_wiki_content
874 new_wiki_page.content = new_wiki_content
864 wiki.pages << new_wiki_page
875 wiki.pages << new_wiki_page
865 wiki_pages_map[page.id] = new_wiki_page
876 wiki_pages_map[page.id] = new_wiki_page
866 end
877 end
867
878
868 self.wiki = wiki
879 self.wiki = wiki
869 wiki.save
880 wiki.save
870 # Reproduce page hierarchy
881 # Reproduce page hierarchy
871 project.wiki.pages.each do |page|
882 project.wiki.pages.each do |page|
872 if page.parent_id && wiki_pages_map[page.id]
883 if page.parent_id && wiki_pages_map[page.id]
873 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
884 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
874 wiki_pages_map[page.id].save
885 wiki_pages_map[page.id].save
875 end
886 end
876 end
887 end
877 end
888 end
878 end
889 end
879
890
880 # Copies versions from +project+
891 # Copies versions from +project+
881 def copy_versions(project)
892 def copy_versions(project)
882 project.versions.each do |version|
893 project.versions.each do |version|
883 new_version = Version.new
894 new_version = Version.new
884 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
895 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
885 self.versions << new_version
896 self.versions << new_version
886 end
897 end
887 end
898 end
888
899
889 # Copies issue categories from +project+
900 # Copies issue categories from +project+
890 def copy_issue_categories(project)
901 def copy_issue_categories(project)
891 project.issue_categories.each do |issue_category|
902 project.issue_categories.each do |issue_category|
892 new_issue_category = IssueCategory.new
903 new_issue_category = IssueCategory.new
893 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
904 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
894 self.issue_categories << new_issue_category
905 self.issue_categories << new_issue_category
895 end
906 end
896 end
907 end
897
908
898 # Copies issues from +project+
909 # Copies issues from +project+
899 def copy_issues(project)
910 def copy_issues(project)
900 # Stores the source issue id as a key and the copied issues as the
911 # Stores the source issue id as a key and the copied issues as the
901 # value. Used to map the two together for issue relations.
912 # value. Used to map the two together for issue relations.
902 issues_map = {}
913 issues_map = {}
903
914
904 # Store status and reopen locked/closed versions
915 # Store status and reopen locked/closed versions
905 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
916 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
906 version_statuses.each do |version, status|
917 version_statuses.each do |version, status|
907 version.update_attribute :status, 'open'
918 version.update_attribute :status, 'open'
908 end
919 end
909
920
910 # Get issues sorted by root_id, lft so that parent issues
921 # Get issues sorted by root_id, lft so that parent issues
911 # get copied before their children
922 # get copied before their children
912 project.issues.reorder('root_id, lft').each do |issue|
923 project.issues.reorder('root_id, lft').each do |issue|
913 new_issue = Issue.new
924 new_issue = Issue.new
914 new_issue.copy_from(issue, :subtasks => false, :link => false)
925 new_issue.copy_from(issue, :subtasks => false, :link => false)
915 new_issue.project = self
926 new_issue.project = self
916 # Changing project resets the custom field values
927 # Changing project resets the custom field values
917 # TODO: handle this in Issue#project=
928 # TODO: handle this in Issue#project=
918 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
929 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
919 # Reassign fixed_versions by name, since names are unique per project
930 # Reassign fixed_versions by name, since names are unique per project
920 if issue.fixed_version && issue.fixed_version.project == project
931 if issue.fixed_version && issue.fixed_version.project == project
921 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
932 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
922 end
933 end
923 # Reassign version custom field values
934 # Reassign version custom field values
924 new_issue.custom_field_values.each do |custom_value|
935 new_issue.custom_field_values.each do |custom_value|
925 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
936 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
926 versions = Version.where(:id => custom_value.value).to_a
937 versions = Version.where(:id => custom_value.value).to_a
927 new_value = versions.map do |version|
938 new_value = versions.map do |version|
928 if version.project == project
939 if version.project == project
929 self.versions.detect {|v| v.name == version.name}.try(:id)
940 self.versions.detect {|v| v.name == version.name}.try(:id)
930 else
941 else
931 version.id
942 version.id
932 end
943 end
933 end
944 end
934 new_value.compact!
945 new_value.compact!
935 new_value = new_value.first unless custom_value.custom_field.multiple?
946 new_value = new_value.first unless custom_value.custom_field.multiple?
936 custom_value.value = new_value
947 custom_value.value = new_value
937 end
948 end
938 end
949 end
939 # Reassign the category by name, since names are unique per project
950 # Reassign the category by name, since names are unique per project
940 if issue.category
951 if issue.category
941 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
952 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
942 end
953 end
943 # Parent issue
954 # Parent issue
944 if issue.parent_id
955 if issue.parent_id
945 if copied_parent = issues_map[issue.parent_id]
956 if copied_parent = issues_map[issue.parent_id]
946 new_issue.parent_issue_id = copied_parent.id
957 new_issue.parent_issue_id = copied_parent.id
947 end
958 end
948 end
959 end
949
960
950 self.issues << new_issue
961 self.issues << new_issue
951 if new_issue.new_record?
962 if new_issue.new_record?
952 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
963 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
953 else
964 else
954 issues_map[issue.id] = new_issue unless new_issue.new_record?
965 issues_map[issue.id] = new_issue unless new_issue.new_record?
955 end
966 end
956 end
967 end
957
968
958 # Restore locked/closed version statuses
969 # Restore locked/closed version statuses
959 version_statuses.each do |version, status|
970 version_statuses.each do |version, status|
960 version.update_attribute :status, status
971 version.update_attribute :status, status
961 end
972 end
962
973
963 # Relations after in case issues related each other
974 # Relations after in case issues related each other
964 project.issues.each do |issue|
975 project.issues.each do |issue|
965 new_issue = issues_map[issue.id]
976 new_issue = issues_map[issue.id]
966 unless new_issue
977 unless new_issue
967 # Issue was not copied
978 # Issue was not copied
968 next
979 next
969 end
980 end
970
981
971 # Relations
982 # Relations
972 issue.relations_from.each do |source_relation|
983 issue.relations_from.each do |source_relation|
973 new_issue_relation = IssueRelation.new
984 new_issue_relation = IssueRelation.new
974 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
985 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
975 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
986 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
976 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
987 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
977 new_issue_relation.issue_to = source_relation.issue_to
988 new_issue_relation.issue_to = source_relation.issue_to
978 end
989 end
979 new_issue.relations_from << new_issue_relation
990 new_issue.relations_from << new_issue_relation
980 end
991 end
981
992
982 issue.relations_to.each do |source_relation|
993 issue.relations_to.each do |source_relation|
983 new_issue_relation = IssueRelation.new
994 new_issue_relation = IssueRelation.new
984 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
995 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
985 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
996 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
986 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
997 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
987 new_issue_relation.issue_from = source_relation.issue_from
998 new_issue_relation.issue_from = source_relation.issue_from
988 end
999 end
989 new_issue.relations_to << new_issue_relation
1000 new_issue.relations_to << new_issue_relation
990 end
1001 end
991 end
1002 end
992 end
1003 end
993
1004
994 # Copies members from +project+
1005 # Copies members from +project+
995 def copy_members(project)
1006 def copy_members(project)
996 # Copy users first, then groups to handle members with inherited and given roles
1007 # Copy users first, then groups to handle members with inherited and given roles
997 members_to_copy = []
1008 members_to_copy = []
998 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
1009 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
999 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1010 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
1000
1011
1001 members_to_copy.each do |member|
1012 members_to_copy.each do |member|
1002 new_member = Member.new
1013 new_member = Member.new
1003 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1014 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
1004 # only copy non inherited roles
1015 # only copy non inherited roles
1005 # inherited roles will be added when copying the group membership
1016 # inherited roles will be added when copying the group membership
1006 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1017 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
1007 next if role_ids.empty?
1018 next if role_ids.empty?
1008 new_member.role_ids = role_ids
1019 new_member.role_ids = role_ids
1009 new_member.project = self
1020 new_member.project = self
1010 self.members << new_member
1021 self.members << new_member
1011 end
1022 end
1012 end
1023 end
1013
1024
1014 # Copies queries from +project+
1025 # Copies queries from +project+
1015 def copy_queries(project)
1026 def copy_queries(project)
1016 project.queries.each do |query|
1027 project.queries.each do |query|
1017 new_query = IssueQuery.new
1028 new_query = IssueQuery.new
1018 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1029 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1019 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1030 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1020 new_query.project = self
1031 new_query.project = self
1021 new_query.user_id = query.user_id
1032 new_query.user_id = query.user_id
1022 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1033 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1023 self.queries << new_query
1034 self.queries << new_query
1024 end
1035 end
1025 end
1036 end
1026
1037
1027 # Copies boards from +project+
1038 # Copies boards from +project+
1028 def copy_boards(project)
1039 def copy_boards(project)
1029 project.boards.each do |board|
1040 project.boards.each do |board|
1030 new_board = Board.new
1041 new_board = Board.new
1031 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1042 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1032 new_board.project = self
1043 new_board.project = self
1033 self.boards << new_board
1044 self.boards << new_board
1034 end
1045 end
1035 end
1046 end
1036
1047
1037 def allowed_permissions
1048 def allowed_permissions
1038 @allowed_permissions ||= begin
1049 @allowed_permissions ||= begin
1039 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1050 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1040 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1051 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1041 end
1052 end
1042 end
1053 end
1043
1054
1044 def allowed_actions
1055 def allowed_actions
1045 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1056 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1046 end
1057 end
1047
1058
1048 # Archives subprojects recursively
1059 # Archives subprojects recursively
1049 def archive!
1060 def archive!
1050 children.each do |subproject|
1061 children.each do |subproject|
1051 subproject.send :archive!
1062 subproject.send :archive!
1052 end
1063 end
1053 update_attribute :status, STATUS_ARCHIVED
1064 update_attribute :status, STATUS_ARCHIVED
1054 end
1065 end
1055 end
1066 end
@@ -1,284 +1,291
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 Role < ActiveRecord::Base
18 class Role < ActiveRecord::Base
19 # Custom coder for the permissions attribute that should be an
19 # Custom coder for the permissions attribute that should be an
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
21 # slow on some platforms (eg. mingw32).
21 # slow on some platforms (eg. mingw32).
22 class PermissionsAttributeCoder
22 class PermissionsAttributeCoder
23 def self.load(str)
23 def self.load(str)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
25 end
25 end
26
26
27 def self.dump(value)
27 def self.dump(value)
28 YAML.dump(value)
28 YAML.dump(value)
29 end
29 end
30 end
30 end
31
31
32 # Built-in roles
32 # Built-in roles
33 BUILTIN_NON_MEMBER = 1
33 BUILTIN_NON_MEMBER = 1
34 BUILTIN_ANONYMOUS = 2
34 BUILTIN_ANONYMOUS = 2
35
35
36 ISSUES_VISIBILITY_OPTIONS = [
36 ISSUES_VISIBILITY_OPTIONS = [
37 ['all', :label_issues_visibility_all],
37 ['all', :label_issues_visibility_all],
38 ['default', :label_issues_visibility_public],
38 ['default', :label_issues_visibility_public],
39 ['own', :label_issues_visibility_own]
39 ['own', :label_issues_visibility_own]
40 ]
40 ]
41
41
42 TIME_ENTRIES_VISIBILITY_OPTIONS = [
42 TIME_ENTRIES_VISIBILITY_OPTIONS = [
43 ['all', :label_time_entries_visibility_all],
43 ['all', :label_time_entries_visibility_all],
44 ['own', :label_time_entries_visibility_own]
44 ['own', :label_time_entries_visibility_own]
45 ]
45 ]
46
46
47 USERS_VISIBILITY_OPTIONS = [
47 USERS_VISIBILITY_OPTIONS = [
48 ['all', :label_users_visibility_all],
48 ['all', :label_users_visibility_all],
49 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
49 ['members_of_visible_projects', :label_users_visibility_members_of_visible_projects]
50 ]
50 ]
51
51
52 scope :sorted, lambda { order(:builtin, :position) }
52 scope :sorted, lambda { order(:builtin, :position) }
53 scope :givable, lambda { order(:position).where(:builtin => 0) }
53 scope :givable, lambda { order(:position).where(:builtin => 0) }
54 scope :builtin, lambda { |*args|
54 scope :builtin, lambda { |*args|
55 compare = (args.first == true ? 'not' : '')
55 compare = (args.first == true ? 'not' : '')
56 where("#{compare} builtin = 0")
56 where("#{compare} builtin = 0")
57 }
57 }
58
58
59 before_destroy :check_deletable
59 before_destroy :check_deletable
60 has_many :workflow_rules, :dependent => :delete_all do
60 has_many :workflow_rules, :dependent => :delete_all do
61 def copy(source_role)
61 def copy(source_role)
62 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
62 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
63 end
63 end
64 end
64 end
65 has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
65 has_and_belongs_to_many :custom_fields, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "role_id"
66
66
67 has_and_belongs_to_many :managed_roles, :class_name => 'Role',
67 has_and_belongs_to_many :managed_roles, :class_name => 'Role',
68 :join_table => "#{table_name_prefix}roles_managed_roles#{table_name_suffix}",
68 :join_table => "#{table_name_prefix}roles_managed_roles#{table_name_suffix}",
69 :association_foreign_key => "managed_role_id"
69 :association_foreign_key => "managed_role_id"
70
70
71 has_many :member_roles, :dependent => :destroy
71 has_many :member_roles, :dependent => :destroy
72 has_many :members, :through => :member_roles
72 has_many :members, :through => :member_roles
73 acts_as_positioned :scope => :builtin
73 acts_as_positioned :scope => :builtin
74
74
75 serialize :permissions, ::Role::PermissionsAttributeCoder
75 serialize :permissions, ::Role::PermissionsAttributeCoder
76 store :settings, :accessors => [:permissions_all_trackers, :permissions_tracker_ids]
76 store :settings, :accessors => [:permissions_all_trackers, :permissions_tracker_ids]
77 attr_protected :builtin
77 attr_protected :builtin
78
78
79 validates_presence_of :name
79 validates_presence_of :name
80 validates_uniqueness_of :name
80 validates_uniqueness_of :name
81 validates_length_of :name, :maximum => 30
81 validates_length_of :name, :maximum => 30
82 validates_inclusion_of :issues_visibility,
82 validates_inclusion_of :issues_visibility,
83 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
83 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
84 :if => lambda {|role| role.respond_to?(:issues_visibility) && role.issues_visibility_changed?}
84 :if => lambda {|role| role.respond_to?(:issues_visibility) && role.issues_visibility_changed?}
85 validates_inclusion_of :users_visibility,
85 validates_inclusion_of :users_visibility,
86 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
86 :in => USERS_VISIBILITY_OPTIONS.collect(&:first),
87 :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?}
87 :if => lambda {|role| role.respond_to?(:users_visibility) && role.users_visibility_changed?}
88 validates_inclusion_of :time_entries_visibility,
88 validates_inclusion_of :time_entries_visibility,
89 :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first),
89 :in => TIME_ENTRIES_VISIBILITY_OPTIONS.collect(&:first),
90 :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?}
90 :if => lambda {|role| role.respond_to?(:time_entries_visibility) && role.time_entries_visibility_changed?}
91
91
92 # Copies attributes from another role, arg can be an id or a Role
92 # Copies attributes from another role, arg can be an id or a Role
93 def copy_from(arg, options={})
93 def copy_from(arg, options={})
94 return unless arg.present?
94 return unless arg.present?
95 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
95 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
96 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
96 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
97 self.permissions = role.permissions.dup
97 self.permissions = role.permissions.dup
98 self
98 self
99 end
99 end
100
100
101 def permissions=(perms)
101 def permissions=(perms)
102 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
102 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
103 write_attribute(:permissions, perms)
103 write_attribute(:permissions, perms)
104 end
104 end
105
105
106 def add_permission!(*perms)
106 def add_permission!(*perms)
107 self.permissions = [] unless permissions.is_a?(Array)
107 self.permissions = [] unless permissions.is_a?(Array)
108
108
109 permissions_will_change!
109 permissions_will_change!
110 perms.each do |p|
110 perms.each do |p|
111 p = p.to_sym
111 p = p.to_sym
112 permissions << p unless permissions.include?(p)
112 permissions << p unless permissions.include?(p)
113 end
113 end
114 save!
114 save!
115 end
115 end
116
116
117 def remove_permission!(*perms)
117 def remove_permission!(*perms)
118 return unless permissions.is_a?(Array)
118 return unless permissions.is_a?(Array)
119 permissions_will_change!
119 permissions_will_change!
120 perms.each { |p| permissions.delete(p.to_sym) }
120 perms.each { |p| permissions.delete(p.to_sym) }
121 save!
121 save!
122 end
122 end
123
123
124 # Returns true if the role has the given permission
124 # Returns true if the role has the given permission
125 def has_permission?(perm)
125 def has_permission?(perm)
126 !permissions.nil? && permissions.include?(perm.to_sym)
126 !permissions.nil? && permissions.include?(perm.to_sym)
127 end
127 end
128
128
129 def consider_workflow?
129 def consider_workflow?
130 has_permission?(:add_issues) || has_permission?(:edit_issues)
130 has_permission?(:add_issues) || has_permission?(:edit_issues)
131 end
131 end
132
132
133 def <=>(role)
133 def <=>(role)
134 if role
134 if role
135 if builtin == role.builtin
135 if builtin == role.builtin
136 position <=> role.position
136 position <=> role.position
137 else
137 else
138 builtin <=> role.builtin
138 builtin <=> role.builtin
139 end
139 end
140 else
140 else
141 -1
141 -1
142 end
142 end
143 end
143 end
144
144
145 def to_s
145 def to_s
146 name
146 name
147 end
147 end
148
148
149 def name
149 def name
150 case builtin
150 case builtin
151 when 1; l(:label_role_non_member, :default => read_attribute(:name))
151 when 1; l(:label_role_non_member, :default => read_attribute(:name))
152 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
152 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
153 else; read_attribute(:name)
153 else; read_attribute(:name)
154 end
154 end
155 end
155 end
156
156
157 # Return true if the role is a builtin role
157 # Return true if the role is a builtin role
158 def builtin?
158 def builtin?
159 self.builtin != 0
159 self.builtin != 0
160 end
160 end
161
161
162 # Return true if the role is the anonymous role
162 # Return true if the role is the anonymous role
163 def anonymous?
163 def anonymous?
164 builtin == 2
164 builtin == 2
165 end
165 end
166
166
167 # Return true if the role is a project member role
167 # Return true if the role is a project member role
168 def member?
168 def member?
169 !self.builtin?
169 !self.builtin?
170 end
170 end
171
171
172 # Return true if role is allowed to do the specified action
172 # Return true if role is allowed to do the specified action
173 # action can be:
173 # action can be:
174 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
174 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
175 # * a permission Symbol (eg. :edit_project)
175 # * a permission Symbol (eg. :edit_project)
176 def allowed_to?(action)
176 def allowed_to?(action)
177 if action.is_a? Hash
177 if action.is_a? Hash
178 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
178 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
179 else
179 else
180 allowed_permissions.include? action
180 allowed_permissions.include? action
181 end
181 end
182 end
182 end
183
183
184 # Return all the permissions that can be given to the role
184 # Return all the permissions that can be given to the role
185 def setable_permissions
185 def setable_permissions
186 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
186 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
187 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
187 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
188 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
188 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
189 setable_permissions
189 setable_permissions
190 end
190 end
191
191
192 def permissions_tracker_ids(*args)
192 def permissions_tracker_ids(*args)
193 if args.any?
193 if args.any?
194 Array(permissions_tracker_ids[args.first.to_s]).map(&:to_i)
194 Array(permissions_tracker_ids[args.first.to_s]).map(&:to_i)
195 else
195 else
196 super || {}
196 super || {}
197 end
197 end
198 end
198 end
199
199
200 def permissions_tracker_ids=(arg)
200 def permissions_tracker_ids=(arg)
201 h = arg.to_hash
201 h = arg.to_hash
202 h.values.each {|v| v.reject!(&:blank?)}
202 h.values.each {|v| v.reject!(&:blank?)}
203 super(h)
203 super(h)
204 end
204 end
205
205
206 # Returns true if tracker_id belongs to the list of
206 # Returns true if tracker_id belongs to the list of
207 # trackers for which permission is given
207 # trackers for which permission is given
208 def permissions_tracker_ids?(permission, tracker_id)
208 def permissions_tracker_ids?(permission, tracker_id)
209 permissions_tracker_ids(permission).include?(tracker_id)
209 permissions_tracker_ids(permission).include?(tracker_id)
210 end
210 end
211
211
212 def permissions_all_trackers
212 def permissions_all_trackers
213 super || {}
213 super || {}
214 end
214 end
215
215
216 def permissions_all_trackers=(arg)
216 def permissions_all_trackers=(arg)
217 super(arg.to_hash)
217 super(arg.to_hash)
218 end
218 end
219
219
220 # Returns true if permission is given for all trackers
220 # Returns true if permission is given for all trackers
221 def permissions_all_trackers?(permission)
221 def permissions_all_trackers?(permission)
222 permissions_all_trackers[permission.to_s].to_s != '0'
222 permissions_all_trackers[permission.to_s].to_s != '0'
223 end
223 end
224
224
225 # Returns true if permission is given for the tracker
226 # (explicitly or for all trackers)
227 def permissions_tracker?(permission, tracker)
228 permissions_all_trackers?(permission) ||
229 permissions_tracker_ids?(permission, tracker.try(:id))
230 end
231
225 # Sets the trackers that are allowed for a permission.
232 # Sets the trackers that are allowed for a permission.
226 # tracker_ids can be an array of tracker ids or :all for
233 # tracker_ids can be an array of tracker ids or :all for
227 # no restrictions.
234 # no restrictions.
228 #
235 #
229 # Examples:
236 # Examples:
230 # role.set_permission_trackers :add_issues, [1, 3]
237 # role.set_permission_trackers :add_issues, [1, 3]
231 # role.set_permission_trackers :add_issues, :all
238 # role.set_permission_trackers :add_issues, :all
232 def set_permission_trackers(permission, tracker_ids)
239 def set_permission_trackers(permission, tracker_ids)
233 h = {permission.to_s => (tracker_ids == :all ? '1' : '0')}
240 h = {permission.to_s => (tracker_ids == :all ? '1' : '0')}
234 self.permissions_all_trackers = permissions_all_trackers.merge(h)
241 self.permissions_all_trackers = permissions_all_trackers.merge(h)
235
242
236 h = {permission.to_s => (tracker_ids == :all ? [] : tracker_ids)}
243 h = {permission.to_s => (tracker_ids == :all ? [] : tracker_ids)}
237 self.permissions_tracker_ids = permissions_tracker_ids.merge(h)
244 self.permissions_tracker_ids = permissions_tracker_ids.merge(h)
238
245
239 self
246 self
240 end
247 end
241
248
242 # Find all the roles that can be given to a project member
249 # Find all the roles that can be given to a project member
243 def self.find_all_givable
250 def self.find_all_givable
244 Role.givable.to_a
251 Role.givable.to_a
245 end
252 end
246
253
247 # Return the builtin 'non member' role. If the role doesn't exist,
254 # Return the builtin 'non member' role. If the role doesn't exist,
248 # it will be created on the fly.
255 # it will be created on the fly.
249 def self.non_member
256 def self.non_member
250 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
257 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
251 end
258 end
252
259
253 # Return the builtin 'anonymous' role. If the role doesn't exist,
260 # Return the builtin 'anonymous' role. If the role doesn't exist,
254 # it will be created on the fly.
261 # it will be created on the fly.
255 def self.anonymous
262 def self.anonymous
256 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
263 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
257 end
264 end
258
265
259 private
266 private
260
267
261 def allowed_permissions
268 def allowed_permissions
262 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
269 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
263 end
270 end
264
271
265 def allowed_actions
272 def allowed_actions
266 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
273 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
267 end
274 end
268
275
269 def check_deletable
276 def check_deletable
270 raise "Cannot delete role" if members.any?
277 raise "Cannot delete role" if members.any?
271 raise "Cannot delete builtin role" if builtin?
278 raise "Cannot delete builtin role" if builtin?
272 end
279 end
273
280
274 def self.find_or_create_system_role(builtin, name)
281 def self.find_or_create_system_role(builtin, name)
275 role = where(:builtin => builtin).first
282 role = where(:builtin => builtin).first
276 if role.nil?
283 if role.nil?
277 role = create(:name => name) do |r|
284 role = create(:name => name) do |r|
278 r.builtin = builtin
285 r.builtin = builtin
279 end
286 end
280 raise "Unable to create the #{name} role (#{role.errors.full_messages.join(',')})." if role.new_record?
287 raise "Unable to create the #{name} role (#{role.errors.full_messages.join(',')})." if role.new_record?
281 end
288 end
282 role
289 role
283 end
290 end
284 end
291 end
@@ -1,2934 +1,2947
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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, :email_addresses, :user_preferences, :members, :member_roles, :roles,
21 fixtures :projects, :users, :email_addresses, :user_preferences, :members, :member_roles, :roles,
22 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues, :journals, :journal_details,
28 :issues, :journals, :journal_details,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 include Redmine::I18n
32 include Redmine::I18n
33
33
34 def setup
34 def setup
35 set_language_if_valid 'en'
35 set_language_if_valid 'en'
36 end
36 end
37
37
38 def teardown
38 def teardown
39 User.current = nil
39 User.current = nil
40 end
40 end
41
41
42 def test_initialize
42 def test_initialize
43 issue = Issue.new
43 issue = Issue.new
44
44
45 assert_nil issue.project_id
45 assert_nil issue.project_id
46 assert_nil issue.tracker_id
46 assert_nil issue.tracker_id
47 assert_nil issue.status_id
47 assert_nil issue.status_id
48 assert_nil issue.author_id
48 assert_nil issue.author_id
49 assert_nil issue.assigned_to_id
49 assert_nil issue.assigned_to_id
50 assert_nil issue.category_id
50 assert_nil issue.category_id
51
51
52 assert_equal IssuePriority.default, issue.priority
52 assert_equal IssuePriority.default, issue.priority
53 end
53 end
54
54
55 def test_create
55 def test_create
56 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
56 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
57 :status_id => 1, :priority => IssuePriority.all.first,
57 :status_id => 1, :priority => IssuePriority.all.first,
58 :subject => 'test_create',
58 :subject => 'test_create',
59 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
59 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
60 assert issue.save
60 assert issue.save
61 issue.reload
61 issue.reload
62 assert_equal 1.5, issue.estimated_hours
62 assert_equal 1.5, issue.estimated_hours
63 end
63 end
64
64
65 def test_create_minimal
65 def test_create_minimal
66 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create')
66 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create')
67 assert issue.save
67 assert issue.save
68 assert_equal issue.tracker.default_status, issue.status
68 assert_equal issue.tracker.default_status, issue.status
69 assert issue.description.nil?
69 assert issue.description.nil?
70 assert_nil issue.estimated_hours
70 assert_nil issue.estimated_hours
71 end
71 end
72
72
73 def test_create_with_all_fields_disabled
73 def test_create_with_all_fields_disabled
74 tracker = Tracker.find(1)
74 tracker = Tracker.find(1)
75 tracker.core_fields = []
75 tracker.core_fields = []
76 tracker.save!
76 tracker.save!
77
77
78 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create_with_all_fields_disabled')
78 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :subject => 'test_create_with_all_fields_disabled')
79 assert_save issue
79 assert_save issue
80 end
80 end
81
81
82 def test_start_date_format_should_be_validated
82 def test_start_date_format_should_be_validated
83 set_language_if_valid 'en'
83 set_language_if_valid 'en'
84 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
84 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
85 issue = Issue.new(:start_date => invalid_date)
85 issue = Issue.new(:start_date => invalid_date)
86 assert !issue.valid?
86 assert !issue.valid?
87 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
87 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
88 end
88 end
89 end
89 end
90
90
91 def test_due_date_format_should_be_validated
91 def test_due_date_format_should_be_validated
92 set_language_if_valid 'en'
92 set_language_if_valid 'en'
93 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
93 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
94 issue = Issue.new(:due_date => invalid_date)
94 issue = Issue.new(:due_date => invalid_date)
95 assert !issue.valid?
95 assert !issue.valid?
96 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
96 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
97 end
97 end
98 end
98 end
99
99
100 def test_due_date_lesser_than_start_date_should_not_validate
100 def test_due_date_lesser_than_start_date_should_not_validate
101 set_language_if_valid 'en'
101 set_language_if_valid 'en'
102 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
102 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
103 assert !issue.valid?
103 assert !issue.valid?
104 assert_include 'Due date must be greater than start date', issue.errors.full_messages
104 assert_include 'Due date must be greater than start date', issue.errors.full_messages
105 end
105 end
106
106
107 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
107 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
108 issue = Issue.generate(:start_date => '2013-06-04')
108 issue = Issue.generate(:start_date => '2013-06-04')
109 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
109 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
110 assert !issue.valid?
110 assert !issue.valid?
111 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
111 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
112 end
112 end
113
113
114 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
114 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
115 issue = Issue.generate!(:start_date => '2013-06-04')
115 issue = Issue.generate!(:start_date => '2013-06-04')
116 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
116 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
117 issue.start_date = '2013-06-07'
117 issue.start_date = '2013-06-07'
118 assert !issue.valid?
118 assert !issue.valid?
119 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
119 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
120 end
120 end
121
121
122 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
122 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
123 issue = Issue.generate!(:start_date => '2013-06-04')
123 issue = Issue.generate!(:start_date => '2013-06-04')
124 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
124 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
125 assert issue.valid?
125 assert issue.valid?
126 end
126 end
127
127
128 def test_estimated_hours_should_be_validated
128 def test_estimated_hours_should_be_validated
129 set_language_if_valid 'en'
129 set_language_if_valid 'en'
130 ['-2'].each do |invalid|
130 ['-2'].each do |invalid|
131 issue = Issue.new(:estimated_hours => invalid)
131 issue = Issue.new(:estimated_hours => invalid)
132 assert !issue.valid?
132 assert !issue.valid?
133 assert_include 'Estimated time is invalid', issue.errors.full_messages
133 assert_include 'Estimated time is invalid', issue.errors.full_messages
134 end
134 end
135 end
135 end
136
136
137 def test_create_with_required_custom_field
137 def test_create_with_required_custom_field
138 set_language_if_valid 'en'
138 set_language_if_valid 'en'
139 field = IssueCustomField.find_by_name('Database')
139 field = IssueCustomField.find_by_name('Database')
140 field.update_attribute(:is_required, true)
140 field.update_attribute(:is_required, true)
141
141
142 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
142 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
143 :status_id => 1, :subject => 'test_create',
143 :status_id => 1, :subject => 'test_create',
144 :description => 'IssueTest#test_create_with_required_custom_field')
144 :description => 'IssueTest#test_create_with_required_custom_field')
145 assert issue.available_custom_fields.include?(field)
145 assert issue.available_custom_fields.include?(field)
146 # No value for the custom field
146 # No value for the custom field
147 assert !issue.save
147 assert !issue.save
148 assert_equal ["Database cannot be blank"], issue.errors.full_messages
148 assert_equal ["Database cannot be blank"], issue.errors.full_messages
149 # Blank value
149 # Blank value
150 issue.custom_field_values = { field.id => '' }
150 issue.custom_field_values = { field.id => '' }
151 assert !issue.save
151 assert !issue.save
152 assert_equal ["Database cannot be blank"], issue.errors.full_messages
152 assert_equal ["Database cannot be blank"], issue.errors.full_messages
153 # Invalid value
153 # Invalid value
154 issue.custom_field_values = { field.id => 'SQLServer' }
154 issue.custom_field_values = { field.id => 'SQLServer' }
155 assert !issue.save
155 assert !issue.save
156 assert_equal ["Database is not included in the list"], issue.errors.full_messages
156 assert_equal ["Database is not included in the list"], issue.errors.full_messages
157 # Valid value
157 # Valid value
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 issue.reload
160 issue.reload
161 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
161 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
162 end
162 end
163
163
164 def test_create_with_group_assignment
164 def test_create_with_group_assignment
165 with_settings :issue_group_assignment => '1' do
165 with_settings :issue_group_assignment => '1' do
166 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
166 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
167 :subject => 'Group assignment',
167 :subject => 'Group assignment',
168 :assigned_to_id => 11).save
168 :assigned_to_id => 11).save
169 issue = Issue.order('id DESC').first
169 issue = Issue.order('id DESC').first
170 assert_kind_of Group, issue.assigned_to
170 assert_kind_of Group, issue.assigned_to
171 assert_equal Group.find(11), issue.assigned_to
171 assert_equal Group.find(11), issue.assigned_to
172 end
172 end
173 end
173 end
174
174
175 def test_create_with_parent_issue_id
175 def test_create_with_parent_issue_id
176 issue = Issue.new(:project_id => 1, :tracker_id => 1,
176 issue = Issue.new(:project_id => 1, :tracker_id => 1,
177 :author_id => 1, :subject => 'Group assignment',
177 :author_id => 1, :subject => 'Group assignment',
178 :parent_issue_id => 1)
178 :parent_issue_id => 1)
179 assert_save issue
179 assert_save issue
180 assert_equal 1, issue.parent_issue_id
180 assert_equal 1, issue.parent_issue_id
181 assert_equal Issue.find(1), issue.parent
181 assert_equal Issue.find(1), issue.parent
182 end
182 end
183
183
184 def test_create_with_sharp_parent_issue_id
184 def test_create_with_sharp_parent_issue_id
185 issue = Issue.new(:project_id => 1, :tracker_id => 1,
185 issue = Issue.new(:project_id => 1, :tracker_id => 1,
186 :author_id => 1, :subject => 'Group assignment',
186 :author_id => 1, :subject => 'Group assignment',
187 :parent_issue_id => "#1")
187 :parent_issue_id => "#1")
188 assert_save issue
188 assert_save issue
189 assert_equal 1, issue.parent_issue_id
189 assert_equal 1, issue.parent_issue_id
190 assert_equal Issue.find(1), issue.parent
190 assert_equal Issue.find(1), issue.parent
191 end
191 end
192
192
193 def test_create_with_invalid_parent_issue_id
193 def test_create_with_invalid_parent_issue_id
194 set_language_if_valid 'en'
194 set_language_if_valid 'en'
195 issue = Issue.new(:project_id => 1, :tracker_id => 1,
195 issue = Issue.new(:project_id => 1, :tracker_id => 1,
196 :author_id => 1, :subject => 'Group assignment',
196 :author_id => 1, :subject => 'Group assignment',
197 :parent_issue_id => '01ABC')
197 :parent_issue_id => '01ABC')
198 assert !issue.save
198 assert !issue.save
199 assert_equal '01ABC', issue.parent_issue_id
199 assert_equal '01ABC', issue.parent_issue_id
200 assert_include 'Parent task is invalid', issue.errors.full_messages
200 assert_include 'Parent task is invalid', issue.errors.full_messages
201 end
201 end
202
202
203 def test_create_with_invalid_sharp_parent_issue_id
203 def test_create_with_invalid_sharp_parent_issue_id
204 set_language_if_valid 'en'
204 set_language_if_valid 'en'
205 issue = Issue.new(:project_id => 1, :tracker_id => 1,
205 issue = Issue.new(:project_id => 1, :tracker_id => 1,
206 :author_id => 1, :subject => 'Group assignment',
206 :author_id => 1, :subject => 'Group assignment',
207 :parent_issue_id => '#01ABC')
207 :parent_issue_id => '#01ABC')
208 assert !issue.save
208 assert !issue.save
209 assert_equal '#01ABC', issue.parent_issue_id
209 assert_equal '#01ABC', issue.parent_issue_id
210 assert_include 'Parent task is invalid', issue.errors.full_messages
210 assert_include 'Parent task is invalid', issue.errors.full_messages
211 end
211 end
212
212
213 def assert_visibility_match(user, issues)
213 def assert_visibility_match(user, issues)
214 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
214 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
215 end
215 end
216
216
217 def test_visible_scope_for_anonymous
217 def test_visible_scope_for_anonymous
218 # Anonymous user should see issues of public projects only
218 # Anonymous user should see issues of public projects only
219 issues = Issue.visible(User.anonymous).to_a
219 issues = Issue.visible(User.anonymous).to_a
220 assert issues.any?
220 assert issues.any?
221 assert_nil issues.detect {|issue| !issue.project.is_public?}
221 assert_nil issues.detect {|issue| !issue.project.is_public?}
222 assert_nil issues.detect {|issue| issue.is_private?}
222 assert_nil issues.detect {|issue| issue.is_private?}
223 assert_visibility_match User.anonymous, issues
223 assert_visibility_match User.anonymous, issues
224 end
224 end
225
225
226 def test_visible_scope_for_anonymous_without_view_issues_permissions
226 def test_visible_scope_for_anonymous_without_view_issues_permissions
227 # Anonymous user should not see issues without permission
227 # Anonymous user should not see issues without permission
228 Role.anonymous.remove_permission!(:view_issues)
228 Role.anonymous.remove_permission!(:view_issues)
229 issues = Issue.visible(User.anonymous).to_a
229 issues = Issue.visible(User.anonymous).to_a
230 assert issues.empty?
230 assert issues.empty?
231 assert_visibility_match User.anonymous, issues
231 assert_visibility_match User.anonymous, issues
232 end
232 end
233
233
234 def test_visible_scope_for_anonymous_without_view_issues_permissions_and_membership
234 def test_visible_scope_for_anonymous_without_view_issues_permissions_and_membership
235 Role.anonymous.remove_permission!(:view_issues)
235 Role.anonymous.remove_permission!(:view_issues)
236 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [2])
236 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [2])
237
237
238 issues = Issue.visible(User.anonymous).all
238 issues = Issue.visible(User.anonymous).all
239 assert issues.any?
239 assert issues.any?
240 assert_equal [1], issues.map(&:project_id).uniq.sort
240 assert_equal [1], issues.map(&:project_id).uniq.sort
241 assert_visibility_match User.anonymous, issues
241 assert_visibility_match User.anonymous, issues
242 end
242 end
243
243
244 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
244 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
245 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
245 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
246 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
246 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
247 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
247 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
248 assert !issue.visible?(User.anonymous)
248 assert !issue.visible?(User.anonymous)
249 end
249 end
250
250
251 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
251 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
252 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
252 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
253 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
253 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
254 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
254 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
255 assert !issue.visible?(User.anonymous)
255 assert !issue.visible?(User.anonymous)
256 end
256 end
257
257
258 def test_visible_scope_for_non_member
258 def test_visible_scope_for_non_member
259 user = User.find(9)
259 user = User.find(9)
260 assert user.projects.empty?
260 assert user.projects.empty?
261 # Non member user should see issues of public projects only
261 # Non member user should see issues of public projects only
262 issues = Issue.visible(user).to_a
262 issues = Issue.visible(user).to_a
263 assert issues.any?
263 assert issues.any?
264 assert_nil issues.detect {|issue| !issue.project.is_public?}
264 assert_nil issues.detect {|issue| !issue.project.is_public?}
265 assert_nil issues.detect {|issue| issue.is_private?}
265 assert_nil issues.detect {|issue| issue.is_private?}
266 assert_visibility_match user, issues
266 assert_visibility_match user, issues
267 end
267 end
268
268
269 def test_visible_scope_for_non_member_with_own_issues_visibility
269 def test_visible_scope_for_non_member_with_own_issues_visibility
270 Role.non_member.update_attribute :issues_visibility, 'own'
270 Role.non_member.update_attribute :issues_visibility, 'own'
271 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
271 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
272 user = User.find(9)
272 user = User.find(9)
273
273
274 issues = Issue.visible(user).to_a
274 issues = Issue.visible(user).to_a
275 assert issues.any?
275 assert issues.any?
276 assert_nil issues.detect {|issue| issue.author != user}
276 assert_nil issues.detect {|issue| issue.author != user}
277 assert_visibility_match user, issues
277 assert_visibility_match user, issues
278 end
278 end
279
279
280 def test_visible_scope_for_non_member_without_view_issues_permissions
280 def test_visible_scope_for_non_member_without_view_issues_permissions
281 # Non member user should not see issues without permission
281 # Non member user should not see issues without permission
282 Role.non_member.remove_permission!(:view_issues)
282 Role.non_member.remove_permission!(:view_issues)
283 user = User.find(9)
283 user = User.find(9)
284 assert user.projects.empty?
284 assert user.projects.empty?
285 issues = Issue.visible(user).to_a
285 issues = Issue.visible(user).to_a
286 assert issues.empty?
286 assert issues.empty?
287 assert_visibility_match user, issues
287 assert_visibility_match user, issues
288 end
288 end
289
289
290 def test_visible_scope_for_non_member_without_view_issues_permissions_and_membership
290 def test_visible_scope_for_non_member_without_view_issues_permissions_and_membership
291 Role.non_member.remove_permission!(:view_issues)
291 Role.non_member.remove_permission!(:view_issues)
292 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [2])
292 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [2])
293 user = User.find(9)
293 user = User.find(9)
294
294
295 issues = Issue.visible(user).all
295 issues = Issue.visible(user).all
296 assert issues.any?
296 assert issues.any?
297 assert_equal [1], issues.map(&:project_id).uniq.sort
297 assert_equal [1], issues.map(&:project_id).uniq.sort
298 assert_visibility_match user, issues
298 assert_visibility_match user, issues
299 end
299 end
300
300
301 def test_visible_scope_for_member
301 def test_visible_scope_for_member
302 user = User.find(9)
302 user = User.find(9)
303 # User should see issues of projects for which user has view_issues permissions only
303 # User should see issues of projects for which user has view_issues permissions only
304 Role.non_member.remove_permission!(:view_issues)
304 Role.non_member.remove_permission!(:view_issues)
305 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
305 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
306 issues = Issue.visible(user).to_a
306 issues = Issue.visible(user).to_a
307 assert issues.any?
307 assert issues.any?
308 assert_nil issues.detect {|issue| issue.project_id != 3}
308 assert_nil issues.detect {|issue| issue.project_id != 3}
309 assert_nil issues.detect {|issue| issue.is_private?}
309 assert_nil issues.detect {|issue| issue.is_private?}
310 assert_visibility_match user, issues
310 assert_visibility_match user, issues
311 end
311 end
312
312
313 def test_visible_scope_for_member_without_view_issues_permission_and_non_member_role_having_the_permission
313 def test_visible_scope_for_member_without_view_issues_permission_and_non_member_role_having_the_permission
314 Role.non_member.add_permission!(:view_issues)
314 Role.non_member.add_permission!(:view_issues)
315 Role.find(1).remove_permission!(:view_issues)
315 Role.find(1).remove_permission!(:view_issues)
316 user = User.find(2)
316 user = User.find(2)
317
317
318 assert_equal 0, Issue.where(:project_id => 1).visible(user).count
318 assert_equal 0, Issue.where(:project_id => 1).visible(user).count
319 assert_equal false, Issue.where(:project_id => 1).first.visible?(user)
319 assert_equal false, Issue.where(:project_id => 1).first.visible?(user)
320 end
320 end
321
321
322 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
322 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
323 user = User.find(8)
323 user = User.find(8)
324 assert user.groups.any?
324 assert user.groups.any?
325 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
325 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
326 Role.non_member.remove_permission!(:view_issues)
326 Role.non_member.remove_permission!(:view_issues)
327
327
328 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 3,
328 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 3,
329 :status_id => 1, :priority => IssuePriority.all.first,
329 :status_id => 1, :priority => IssuePriority.all.first,
330 :subject => 'Assignment test',
330 :subject => 'Assignment test',
331 :assigned_to => user.groups.first,
331 :assigned_to => user.groups.first,
332 :is_private => true)
332 :is_private => true)
333
333
334 Role.find(2).update_attribute :issues_visibility, 'default'
334 Role.find(2).update_attribute :issues_visibility, 'default'
335 issues = Issue.visible(User.find(8)).to_a
335 issues = Issue.visible(User.find(8)).to_a
336 assert issues.any?
336 assert issues.any?
337 assert issues.include?(issue)
337 assert issues.include?(issue)
338
338
339 Role.find(2).update_attribute :issues_visibility, 'own'
339 Role.find(2).update_attribute :issues_visibility, 'own'
340 issues = Issue.visible(User.find(8)).to_a
340 issues = Issue.visible(User.find(8)).to_a
341 assert issues.any?
341 assert issues.any?
342 assert_include issue, issues
342 assert_include issue, issues
343 end
343 end
344
344
345 def test_visible_scope_for_member_with_limited_tracker_ids
345 def test_visible_scope_for_member_with_limited_tracker_ids
346 role = Role.find(1)
346 role = Role.find(1)
347 role.set_permission_trackers :view_issues, [2]
347 role.set_permission_trackers :view_issues, [2]
348 role.save!
348 role.save!
349 user = User.find(2)
349 user = User.find(2)
350
350
351 issues = Issue.where(:project_id => 1).visible(user).to_a
351 issues = Issue.where(:project_id => 1).visible(user).to_a
352 assert issues.any?
352 assert issues.any?
353 assert_equal [2], issues.map(&:tracker_id).uniq
353 assert_equal [2], issues.map(&:tracker_id).uniq
354
354
355 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
355 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
356 end
356 end
357
357
358 def test_visible_scope_should_consider_tracker_ids_on_each_project
358 def test_visible_scope_should_consider_tracker_ids_on_each_project
359 user = User.generate!
359 user = User.generate!
360
360
361 project1 = Project.generate!
361 project1 = Project.generate!
362 role1 = Role.generate!
362 role1 = Role.generate!
363 role1.add_permission! :view_issues
363 role1.add_permission! :view_issues
364 role1.set_permission_trackers :view_issues, :all
364 role1.set_permission_trackers :view_issues, :all
365 role1.save!
365 role1.save!
366 User.add_to_project(user, project1, role1)
366 User.add_to_project(user, project1, role1)
367
367
368 project2 = Project.generate!
368 project2 = Project.generate!
369 role2 = Role.generate!
369 role2 = Role.generate!
370 role2.add_permission! :view_issues
370 role2.add_permission! :view_issues
371 role2.set_permission_trackers :view_issues, [2]
371 role2.set_permission_trackers :view_issues, [2]
372 role2.save!
372 role2.save!
373 User.add_to_project(user, project2, role2)
373 User.add_to_project(user, project2, role2)
374
374
375 visible_issues = [
375 visible_issues = [
376 Issue.generate!(:project => project1, :tracker_id => 1),
376 Issue.generate!(:project => project1, :tracker_id => 1),
377 Issue.generate!(:project => project1, :tracker_id => 2),
377 Issue.generate!(:project => project1, :tracker_id => 2),
378 Issue.generate!(:project => project2, :tracker_id => 2)
378 Issue.generate!(:project => project2, :tracker_id => 2)
379 ]
379 ]
380 hidden_issue = Issue.generate!(:project => project2, :tracker_id => 1)
380 hidden_issue = Issue.generate!(:project => project2, :tracker_id => 1)
381
381
382 issues = Issue.where(:project_id => [project1.id, project2.id]).visible(user)
382 issues = Issue.where(:project_id => [project1.id, project2.id]).visible(user)
383 assert_equal visible_issues.map(&:id), issues.ids.sort
383 assert_equal visible_issues.map(&:id), issues.ids.sort
384
384
385 assert visible_issues.all? {|issue| issue.visible?(user)}
385 assert visible_issues.all? {|issue| issue.visible?(user)}
386 assert !hidden_issue.visible?(user)
386 assert !hidden_issue.visible?(user)
387 end
387 end
388
388
389 def test_visible_scope_should_not_consider_roles_without_view_issues_permission
389 def test_visible_scope_should_not_consider_roles_without_view_issues_permission
390 user = User.generate!
390 user = User.generate!
391 role1 = Role.generate!
391 role1 = Role.generate!
392 role1.remove_permission! :view_issues
392 role1.remove_permission! :view_issues
393 role1.set_permission_trackers :view_issues, :all
393 role1.set_permission_trackers :view_issues, :all
394 role1.save!
394 role1.save!
395 role2 = Role.generate!
395 role2 = Role.generate!
396 role2.add_permission! :view_issues
396 role2.add_permission! :view_issues
397 role2.set_permission_trackers :view_issues, [2]
397 role2.set_permission_trackers :view_issues, [2]
398 role2.save!
398 role2.save!
399 User.add_to_project(user, Project.find(1), [role1, role2])
399 User.add_to_project(user, Project.find(1), [role1, role2])
400
400
401 issues = Issue.where(:project_id => 1).visible(user).to_a
401 issues = Issue.where(:project_id => 1).visible(user).to_a
402 assert issues.any?
402 assert issues.any?
403 assert_equal [2], issues.map(&:tracker_id).uniq
403 assert_equal [2], issues.map(&:tracker_id).uniq
404
404
405 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
405 assert Issue.where(:project_id => 1).all? {|issue| issue.visible?(user) ^ issue.tracker_id != 2}
406 end
406 end
407
407
408 def test_visible_scope_for_admin
408 def test_visible_scope_for_admin
409 user = User.find(1)
409 user = User.find(1)
410 user.members.each(&:destroy)
410 user.members.each(&:destroy)
411 assert user.projects.empty?
411 assert user.projects.empty?
412 issues = Issue.visible(user).to_a
412 issues = Issue.visible(user).to_a
413 assert issues.any?
413 assert issues.any?
414 # Admin should see issues on private projects that admin does not belong to
414 # Admin should see issues on private projects that admin does not belong to
415 assert issues.detect {|issue| !issue.project.is_public?}
415 assert issues.detect {|issue| !issue.project.is_public?}
416 # Admin should see private issues of other users
416 # Admin should see private issues of other users
417 assert issues.detect {|issue| issue.is_private? && issue.author != user}
417 assert issues.detect {|issue| issue.is_private? && issue.author != user}
418 assert_visibility_match user, issues
418 assert_visibility_match user, issues
419 end
419 end
420
420
421 def test_visible_scope_with_project
421 def test_visible_scope_with_project
422 project = Project.find(1)
422 project = Project.find(1)
423 issues = Issue.visible(User.find(2), :project => project).to_a
423 issues = Issue.visible(User.find(2), :project => project).to_a
424 projects = issues.collect(&:project).uniq
424 projects = issues.collect(&:project).uniq
425 assert_equal 1, projects.size
425 assert_equal 1, projects.size
426 assert_equal project, projects.first
426 assert_equal project, projects.first
427 end
427 end
428
428
429 def test_visible_scope_with_project_and_subprojects
429 def test_visible_scope_with_project_and_subprojects
430 project = Project.find(1)
430 project = Project.find(1)
431 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).to_a
431 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).to_a
432 projects = issues.collect(&:project).uniq
432 projects = issues.collect(&:project).uniq
433 assert projects.size > 1
433 assert projects.size > 1
434 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
434 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
435 end
435 end
436
436
437 def test_visible_and_nested_set_scopes
437 def test_visible_and_nested_set_scopes
438 user = User.generate!
438 user = User.generate!
439 parent = Issue.generate!(:assigned_to => user)
439 parent = Issue.generate!(:assigned_to => user)
440 assert parent.visible?(user)
440 assert parent.visible?(user)
441 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
441 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
442 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
442 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
443 parent.reload
443 parent.reload
444 child1.reload
444 child1.reload
445 child2.reload
445 child2.reload
446 assert child1.visible?(user)
446 assert child1.visible?(user)
447 assert child2.visible?(user)
447 assert child2.visible?(user)
448 assert_equal 2, parent.descendants.count
448 assert_equal 2, parent.descendants.count
449 assert_equal 2, parent.descendants.visible(user).count
449 assert_equal 2, parent.descendants.visible(user).count
450 # awesome_nested_set 2-1-stable branch has regression.
450 # awesome_nested_set 2-1-stable branch has regression.
451 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
451 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
452 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
452 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
453 assert_equal 2, parent.descendants.collect{|i| i}.size
453 assert_equal 2, parent.descendants.collect{|i| i}.size
454 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
454 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
455 end
455 end
456
456
457 def test_visible_scope_with_unsaved_user_should_not_raise_an_error
457 def test_visible_scope_with_unsaved_user_should_not_raise_an_error
458 user = User.new
458 user = User.new
459 assert_nothing_raised do
459 assert_nothing_raised do
460 Issue.visible(user).to_a
460 Issue.visible(user).to_a
461 end
461 end
462 end
462 end
463
463
464 def test_open_scope
464 def test_open_scope
465 issues = Issue.open.to_a
465 issues = Issue.open.to_a
466 assert_nil issues.detect(&:closed?)
466 assert_nil issues.detect(&:closed?)
467 end
467 end
468
468
469 def test_open_scope_with_arg
469 def test_open_scope_with_arg
470 issues = Issue.open(false).to_a
470 issues = Issue.open(false).to_a
471 assert_equal issues, issues.select(&:closed?)
471 assert_equal issues, issues.select(&:closed?)
472 end
472 end
473
473
474 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
474 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
475 version = Version.find(2)
475 version = Version.find(2)
476 assert version.fixed_issues.any?
476 assert version.fixed_issues.any?
477 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
477 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
478 end
478 end
479
479
480 def test_fixed_version_scope_with_empty_array_should_return_no_result
480 def test_fixed_version_scope_with_empty_array_should_return_no_result
481 assert_equal 0, Issue.fixed_version([]).count
481 assert_equal 0, Issue.fixed_version([]).count
482 end
482 end
483
483
484 def test_assigned_to_scope_should_return_issues_assigned_to_the_user
484 def test_assigned_to_scope_should_return_issues_assigned_to_the_user
485 user = User.generate!
485 user = User.generate!
486 issue = Issue.generate!
486 issue = Issue.generate!
487 Issue.where(:id => issue.id).update_all :assigned_to_id => user.id
487 Issue.where(:id => issue.id).update_all :assigned_to_id => user.id
488 assert_equal [issue], Issue.assigned_to(user).to_a
488 assert_equal [issue], Issue.assigned_to(user).to_a
489 end
489 end
490
490
491 def test_assigned_to_scope_should_return_issues_assigned_to_the_user_groups
491 def test_assigned_to_scope_should_return_issues_assigned_to_the_user_groups
492 group = Group.generate!
492 group = Group.generate!
493 user = User.generate!
493 user = User.generate!
494 group.users << user
494 group.users << user
495 issue = Issue.generate!
495 issue = Issue.generate!
496 Issue.where(:id => issue.id).update_all :assigned_to_id => group.id
496 Issue.where(:id => issue.id).update_all :assigned_to_id => group.id
497 assert_equal [issue], Issue.assigned_to(user).to_a
497 assert_equal [issue], Issue.assigned_to(user).to_a
498 end
498 end
499
499
500 def test_errors_full_messages_should_include_custom_fields_errors
500 def test_errors_full_messages_should_include_custom_fields_errors
501 field = IssueCustomField.find_by_name('Database')
501 field = IssueCustomField.find_by_name('Database')
502
502
503 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
503 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
504 :status_id => 1, :subject => 'test_create',
504 :status_id => 1, :subject => 'test_create',
505 :description => 'IssueTest#test_create_with_required_custom_field')
505 :description => 'IssueTest#test_create_with_required_custom_field')
506 assert issue.available_custom_fields.include?(field)
506 assert issue.available_custom_fields.include?(field)
507 # Invalid value
507 # Invalid value
508 issue.custom_field_values = { field.id => 'SQLServer' }
508 issue.custom_field_values = { field.id => 'SQLServer' }
509
509
510 assert !issue.valid?
510 assert !issue.valid?
511 assert_equal 1, issue.errors.full_messages.size
511 assert_equal 1, issue.errors.full_messages.size
512 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
512 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
513 issue.errors.full_messages.first
513 issue.errors.full_messages.first
514 end
514 end
515
515
516 def test_update_issue_with_required_custom_field
516 def test_update_issue_with_required_custom_field
517 field = IssueCustomField.find_by_name('Database')
517 field = IssueCustomField.find_by_name('Database')
518 field.update_attribute(:is_required, true)
518 field.update_attribute(:is_required, true)
519
519
520 issue = Issue.find(1)
520 issue = Issue.find(1)
521 assert_nil issue.custom_value_for(field)
521 assert_nil issue.custom_value_for(field)
522 assert issue.available_custom_fields.include?(field)
522 assert issue.available_custom_fields.include?(field)
523 # No change to custom values, issue can be saved
523 # No change to custom values, issue can be saved
524 assert issue.save
524 assert issue.save
525 # Blank value
525 # Blank value
526 issue.custom_field_values = { field.id => '' }
526 issue.custom_field_values = { field.id => '' }
527 assert !issue.save
527 assert !issue.save
528 # Valid value
528 # Valid value
529 issue.custom_field_values = { field.id => 'PostgreSQL' }
529 issue.custom_field_values = { field.id => 'PostgreSQL' }
530 assert issue.save
530 assert issue.save
531 issue.reload
531 issue.reload
532 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
532 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
533 end
533 end
534
534
535 def test_should_not_update_attributes_if_custom_fields_validation_fails
535 def test_should_not_update_attributes_if_custom_fields_validation_fails
536 issue = Issue.find(1)
536 issue = Issue.find(1)
537 field = IssueCustomField.find_by_name('Database')
537 field = IssueCustomField.find_by_name('Database')
538 assert issue.available_custom_fields.include?(field)
538 assert issue.available_custom_fields.include?(field)
539
539
540 issue.custom_field_values = { field.id => 'Invalid' }
540 issue.custom_field_values = { field.id => 'Invalid' }
541 issue.subject = 'Should be not be saved'
541 issue.subject = 'Should be not be saved'
542 assert !issue.save
542 assert !issue.save
543
543
544 issue.reload
544 issue.reload
545 assert_equal "Cannot print recipes", issue.subject
545 assert_equal "Cannot print recipes", issue.subject
546 end
546 end
547
547
548 def test_should_not_recreate_custom_values_objects_on_update
548 def test_should_not_recreate_custom_values_objects_on_update
549 field = IssueCustomField.find_by_name('Database')
549 field = IssueCustomField.find_by_name('Database')
550
550
551 issue = Issue.find(1)
551 issue = Issue.find(1)
552 issue.custom_field_values = { field.id => 'PostgreSQL' }
552 issue.custom_field_values = { field.id => 'PostgreSQL' }
553 assert issue.save
553 assert issue.save
554 custom_value = issue.custom_value_for(field)
554 custom_value = issue.custom_value_for(field)
555 issue.reload
555 issue.reload
556 issue.custom_field_values = { field.id => 'MySQL' }
556 issue.custom_field_values = { field.id => 'MySQL' }
557 assert issue.save
557 assert issue.save
558 issue.reload
558 issue.reload
559 assert_equal custom_value.id, issue.custom_value_for(field).id
559 assert_equal custom_value.id, issue.custom_value_for(field).id
560 end
560 end
561
561
562 def test_setting_project_should_set_version_to_default_version
562 def test_setting_project_should_set_version_to_default_version
563 version = Version.generate!(:project_id => 1)
563 version = Version.generate!(:project_id => 1)
564 Project.find(1).update_attribute(:default_version_id, version.id)
564 Project.find(1).update_attribute(:default_version_id, version.id)
565
565
566 issue = Issue.new(:project_id => 1)
566 issue = Issue.new(:project_id => 1)
567 assert_equal version, issue.fixed_version
567 assert_equal version, issue.fixed_version
568 end
568 end
569
569
570 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
570 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
571 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
571 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
572 :status_id => 1, :subject => 'Test',
572 :status_id => 1, :subject => 'Test',
573 :custom_field_values => {'2' => 'Test'})
573 :custom_field_values => {'2' => 'Test'})
574 assert !Tracker.find(2).custom_field_ids.include?(2)
574 assert !Tracker.find(2).custom_field_ids.include?(2)
575
575
576 issue = Issue.find(issue.id)
576 issue = Issue.find(issue.id)
577 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
577 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
578
578
579 issue = Issue.find(issue.id)
579 issue = Issue.find(issue.id)
580 custom_value = issue.custom_value_for(2)
580 custom_value = issue.custom_value_for(2)
581 assert_not_nil custom_value
581 assert_not_nil custom_value
582 assert_equal 'Test', custom_value.value
582 assert_equal 'Test', custom_value.value
583 end
583 end
584
584
585 def test_assigning_tracker_id_should_reload_custom_fields_values
585 def test_assigning_tracker_id_should_reload_custom_fields_values
586 issue = Issue.new(:project => Project.find(1))
586 issue = Issue.new(:project => Project.find(1))
587 assert issue.custom_field_values.empty?
587 assert issue.custom_field_values.empty?
588 issue.tracker_id = 1
588 issue.tracker_id = 1
589 assert issue.custom_field_values.any?
589 assert issue.custom_field_values.any?
590 end
590 end
591
591
592 def test_assigning_attributes_should_assign_project_and_tracker_first
592 def test_assigning_attributes_should_assign_project_and_tracker_first
593 seq = sequence('seq')
593 seq = sequence('seq')
594 issue = Issue.new
594 issue = Issue.new
595 issue.expects(:project_id=).in_sequence(seq)
595 issue.expects(:project_id=).in_sequence(seq)
596 issue.expects(:tracker_id=).in_sequence(seq)
596 issue.expects(:tracker_id=).in_sequence(seq)
597 issue.expects(:subject=).in_sequence(seq)
597 issue.expects(:subject=).in_sequence(seq)
598 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
598 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
599 end
599 end
600
600
601 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
601 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
602 attributes = ActiveSupport::OrderedHash.new
602 attributes = ActiveSupport::OrderedHash.new
603 attributes['custom_field_values'] = { '1' => 'MySQL' }
603 attributes['custom_field_values'] = { '1' => 'MySQL' }
604 attributes['tracker_id'] = '1'
604 attributes['tracker_id'] = '1'
605 issue = Issue.new(:project => Project.find(1))
605 issue = Issue.new(:project => Project.find(1))
606 issue.attributes = attributes
606 issue.attributes = attributes
607 assert_equal 'MySQL', issue.custom_field_value(1)
607 assert_equal 'MySQL', issue.custom_field_value(1)
608 end
608 end
609
609
610 def test_changing_tracker_should_clear_disabled_core_fields
610 def test_changing_tracker_should_clear_disabled_core_fields
611 tracker = Tracker.find(2)
611 tracker = Tracker.find(2)
612 tracker.core_fields = tracker.core_fields - %w(due_date)
612 tracker.core_fields = tracker.core_fields - %w(due_date)
613 tracker.save!
613 tracker.save!
614
614
615 issue = Issue.generate!(:tracker_id => 1, :start_date => Date.today, :due_date => Date.today)
615 issue = Issue.generate!(:tracker_id => 1, :start_date => Date.today, :due_date => Date.today)
616 issue.save!
616 issue.save!
617
617
618 issue.tracker_id = 2
618 issue.tracker_id = 2
619 issue.save!
619 issue.save!
620 assert_not_nil issue.start_date
620 assert_not_nil issue.start_date
621 assert_nil issue.due_date
621 assert_nil issue.due_date
622 end
622 end
623
623
624 def test_changing_tracker_should_not_add_cleared_fields_to_journal
624 def test_changing_tracker_should_not_add_cleared_fields_to_journal
625 tracker = Tracker.find(2)
625 tracker = Tracker.find(2)
626 tracker.core_fields = tracker.core_fields - %w(due_date)
626 tracker.core_fields = tracker.core_fields - %w(due_date)
627 tracker.save!
627 tracker.save!
628
628
629 issue = Issue.generate!(:tracker_id => 1, :due_date => Date.today)
629 issue = Issue.generate!(:tracker_id => 1, :due_date => Date.today)
630 issue.save!
630 issue.save!
631
631
632 assert_difference 'Journal.count' do
632 assert_difference 'Journal.count' do
633 issue.init_journal User.find(1)
633 issue.init_journal User.find(1)
634 issue.tracker_id = 2
634 issue.tracker_id = 2
635 issue.save!
635 issue.save!
636 assert_nil issue.due_date
636 assert_nil issue.due_date
637 end
637 end
638 journal = Journal.order('id DESC').first
638 journal = Journal.order('id DESC').first
639 assert_equal 1, journal.details.count
639 assert_equal 1, journal.details.count
640 end
640 end
641
641
642 def test_reload_should_reload_custom_field_values
642 def test_reload_should_reload_custom_field_values
643 issue = Issue.generate!
643 issue = Issue.generate!
644 issue.custom_field_values = {'2' => 'Foo'}
644 issue.custom_field_values = {'2' => 'Foo'}
645 issue.save!
645 issue.save!
646
646
647 issue = Issue.order('id desc').first
647 issue = Issue.order('id desc').first
648 assert_equal 'Foo', issue.custom_field_value(2)
648 assert_equal 'Foo', issue.custom_field_value(2)
649
649
650 issue.custom_field_values = {'2' => 'Bar'}
650 issue.custom_field_values = {'2' => 'Bar'}
651 assert_equal 'Bar', issue.custom_field_value(2)
651 assert_equal 'Bar', issue.custom_field_value(2)
652
652
653 issue.reload
653 issue.reload
654 assert_equal 'Foo', issue.custom_field_value(2)
654 assert_equal 'Foo', issue.custom_field_value(2)
655 end
655 end
656
656
657 def test_should_update_issue_with_disabled_tracker
657 def test_should_update_issue_with_disabled_tracker
658 p = Project.find(1)
658 p = Project.find(1)
659 issue = Issue.find(1)
659 issue = Issue.find(1)
660
660
661 p.trackers.delete(issue.tracker)
661 p.trackers.delete(issue.tracker)
662 assert !p.trackers.include?(issue.tracker)
662 assert !p.trackers.include?(issue.tracker)
663
663
664 issue.reload
664 issue.reload
665 issue.subject = 'New subject'
665 issue.subject = 'New subject'
666 assert issue.save
666 assert issue.save
667 end
667 end
668
668
669 def test_should_not_set_a_disabled_tracker
669 def test_should_not_set_a_disabled_tracker
670 p = Project.find(1)
670 p = Project.find(1)
671 p.trackers.delete(Tracker.find(2))
671 p.trackers.delete(Tracker.find(2))
672
672
673 issue = Issue.find(1)
673 issue = Issue.find(1)
674 issue.tracker_id = 2
674 issue.tracker_id = 2
675 issue.subject = 'New subject'
675 issue.subject = 'New subject'
676 assert !issue.save
676 assert !issue.save
677 assert_not_equal [], issue.errors[:tracker_id]
677 assert_not_equal [], issue.errors[:tracker_id]
678 end
678 end
679
679
680 def test_category_based_assignment
680 def test_category_based_assignment
681 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
681 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
682 :status_id => 1, :priority => IssuePriority.all.first,
682 :status_id => 1, :priority => IssuePriority.all.first,
683 :subject => 'Assignment test',
683 :subject => 'Assignment test',
684 :description => 'Assignment test', :category_id => 1)
684 :description => 'Assignment test', :category_id => 1)
685 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
685 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
686 end
686 end
687
687
688 def test_new_statuses_allowed_to
688 def test_new_statuses_allowed_to
689 WorkflowTransition.delete_all
689 WorkflowTransition.delete_all
690 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
690 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
691 :old_status_id => 1, :new_status_id => 2,
691 :old_status_id => 1, :new_status_id => 2,
692 :author => false, :assignee => false)
692 :author => false, :assignee => false)
693 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
693 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
694 :old_status_id => 1, :new_status_id => 3,
694 :old_status_id => 1, :new_status_id => 3,
695 :author => true, :assignee => false)
695 :author => true, :assignee => false)
696 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
696 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
697 :old_status_id => 1, :new_status_id => 4,
697 :old_status_id => 1, :new_status_id => 4,
698 :author => false, :assignee => true)
698 :author => false, :assignee => true)
699 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
699 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
700 :old_status_id => 1, :new_status_id => 5,
700 :old_status_id => 1, :new_status_id => 5,
701 :author => true, :assignee => true)
701 :author => true, :assignee => true)
702 status = IssueStatus.find(1)
702 status = IssueStatus.find(1)
703 role = Role.find(1)
703 role = Role.find(1)
704 tracker = Tracker.find(1)
704 tracker = Tracker.find(1)
705 user = User.find(2)
705 user = User.find(2)
706
706
707 issue = Issue.generate!(:tracker => tracker, :status => status,
707 issue = Issue.generate!(:tracker => tracker, :status => status,
708 :project_id => 1, :author_id => 1)
708 :project_id => 1, :author_id => 1)
709 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
709 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
710
710
711 issue = Issue.generate!(:tracker => tracker, :status => status,
711 issue = Issue.generate!(:tracker => tracker, :status => status,
712 :project_id => 1, :author => user)
712 :project_id => 1, :author => user)
713 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
713 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
714
714
715 issue = Issue.generate!(:tracker => tracker, :status => status,
715 issue = Issue.generate!(:tracker => tracker, :status => status,
716 :project_id => 1, :author_id => 1,
716 :project_id => 1, :author_id => 1,
717 :assigned_to => user)
717 :assigned_to => user)
718 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
718 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
719
719
720 issue = Issue.generate!(:tracker => tracker, :status => status,
720 issue = Issue.generate!(:tracker => tracker, :status => status,
721 :project_id => 1, :author => user,
721 :project_id => 1, :author => user,
722 :assigned_to => user)
722 :assigned_to => user)
723 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
723 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
724
724
725 group = Group.generate!
725 group = Group.generate!
726 group.users << user
726 group.users << user
727 issue = Issue.generate!(:tracker => tracker, :status => status,
727 issue = Issue.generate!(:tracker => tracker, :status => status,
728 :project_id => 1, :author => user,
728 :project_id => 1, :author => user,
729 :assigned_to => group)
729 :assigned_to => group)
730 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
730 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
731 end
731 end
732
732
733 def test_new_statuses_allowed_to_should_consider_group_assignment
733 def test_new_statuses_allowed_to_should_consider_group_assignment
734 WorkflowTransition.delete_all
734 WorkflowTransition.delete_all
735 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
735 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
736 :old_status_id => 1, :new_status_id => 4,
736 :old_status_id => 1, :new_status_id => 4,
737 :author => false, :assignee => true)
737 :author => false, :assignee => true)
738 user = User.find(2)
738 user = User.find(2)
739 group = Group.generate!
739 group = Group.generate!
740 group.users << user
740 group.users << user
741
741
742 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
742 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
743 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
743 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
744 end
744 end
745
745
746 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
746 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
747 admin = User.find(1)
747 admin = User.find(1)
748 issue = Issue.find(1)
748 issue = Issue.find(1)
749 assert !admin.member_of?(issue.project)
749 assert !admin.member_of?(issue.project)
750 expected_statuses = [issue.status] +
750 expected_statuses = [issue.status] +
751 WorkflowTransition.where(:old_status_id => issue.status_id).
751 WorkflowTransition.where(:old_status_id => issue.status_id).
752 map(&:new_status).uniq.sort
752 map(&:new_status).uniq.sort
753 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
753 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
754 end
754 end
755
755
756 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
756 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
757 issue = Issue.find(1).copy
757 issue = Issue.find(1).copy
758 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
758 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
759
759
760 issue = Issue.find(2).copy
760 issue = Issue.find(2).copy
761 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
761 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
762 end
762 end
763
763
764 def test_safe_attributes_names_should_not_include_disabled_field
764 def test_safe_attributes_names_should_not_include_disabled_field
765 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
765 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
766
766
767 issue = Issue.new(:tracker => tracker)
767 issue = Issue.new(:tracker => tracker)
768 assert_include 'tracker_id', issue.safe_attribute_names
768 assert_include 'tracker_id', issue.safe_attribute_names
769 assert_include 'status_id', issue.safe_attribute_names
769 assert_include 'status_id', issue.safe_attribute_names
770 assert_include 'subject', issue.safe_attribute_names
770 assert_include 'subject', issue.safe_attribute_names
771 assert_include 'description', issue.safe_attribute_names
771 assert_include 'description', issue.safe_attribute_names
772 assert_include 'custom_field_values', issue.safe_attribute_names
772 assert_include 'custom_field_values', issue.safe_attribute_names
773 assert_include 'custom_fields', issue.safe_attribute_names
773 assert_include 'custom_fields', issue.safe_attribute_names
774 assert_include 'lock_version', issue.safe_attribute_names
774 assert_include 'lock_version', issue.safe_attribute_names
775
775
776 tracker.core_fields.each do |field|
776 tracker.core_fields.each do |field|
777 assert_include field, issue.safe_attribute_names
777 assert_include field, issue.safe_attribute_names
778 end
778 end
779
779
780 tracker.disabled_core_fields.each do |field|
780 tracker.disabled_core_fields.each do |field|
781 assert_not_include field, issue.safe_attribute_names
781 assert_not_include field, issue.safe_attribute_names
782 end
782 end
783 end
783 end
784
784
785 def test_safe_attributes_should_ignore_disabled_fields
785 def test_safe_attributes_should_ignore_disabled_fields
786 tracker = Tracker.find(1)
786 tracker = Tracker.find(1)
787 tracker.core_fields = %w(assigned_to_id due_date)
787 tracker.core_fields = %w(assigned_to_id due_date)
788 tracker.save!
788 tracker.save!
789
789
790 issue = Issue.new(:tracker => tracker)
790 issue = Issue.new(:tracker => tracker)
791 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
791 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
792 assert_nil issue.start_date
792 assert_nil issue.start_date
793 assert_equal Date.parse('2012-07-14'), issue.due_date
793 assert_equal Date.parse('2012-07-14'), issue.due_date
794 end
794 end
795
795
796 def test_safe_attributes_should_accept_target_tracker_enabled_fields
796 def test_safe_attributes_should_accept_target_tracker_enabled_fields
797 source = Tracker.find(1)
797 source = Tracker.find(1)
798 source.core_fields = []
798 source.core_fields = []
799 source.save!
799 source.save!
800 target = Tracker.find(2)
800 target = Tracker.find(2)
801 target.core_fields = %w(assigned_to_id due_date)
801 target.core_fields = %w(assigned_to_id due_date)
802 target.save!
802 target.save!
803 user = User.find(2)
803 user = User.find(2)
804
804
805 issue = Issue.new(:project => Project.find(1), :tracker => source)
805 issue = Issue.new(:project => Project.find(1), :tracker => source)
806 issue.send :safe_attributes=, {'tracker_id' => 2, 'due_date' => '2012-07-14'}, user
806 issue.send :safe_attributes=, {'tracker_id' => 2, 'due_date' => '2012-07-14'}, user
807 assert_equal target, issue.tracker
807 assert_equal target, issue.tracker
808 assert_equal Date.parse('2012-07-14'), issue.due_date
808 assert_equal Date.parse('2012-07-14'), issue.due_date
809 end
809 end
810
810
811 def test_safe_attributes_should_not_include_readonly_fields
811 def test_safe_attributes_should_not_include_readonly_fields
812 WorkflowPermission.delete_all
812 WorkflowPermission.delete_all
813 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
813 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
814 :role_id => 1, :field_name => 'due_date',
814 :role_id => 1, :field_name => 'due_date',
815 :rule => 'readonly')
815 :rule => 'readonly')
816 user = User.find(2)
816 user = User.find(2)
817
817
818 issue = Issue.new(:project_id => 1, :tracker_id => 1)
818 issue = Issue.new(:project_id => 1, :tracker_id => 1)
819 assert_equal %w(due_date), issue.read_only_attribute_names(user)
819 assert_equal %w(due_date), issue.read_only_attribute_names(user)
820 assert_not_include 'due_date', issue.safe_attribute_names(user)
820 assert_not_include 'due_date', issue.safe_attribute_names(user)
821
821
822 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
822 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
823 assert_equal Date.parse('2012-07-14'), issue.start_date
823 assert_equal Date.parse('2012-07-14'), issue.start_date
824 assert_nil issue.due_date
824 assert_nil issue.due_date
825 end
825 end
826
826
827 def test_safe_attributes_should_not_include_readonly_custom_fields
827 def test_safe_attributes_should_not_include_readonly_custom_fields
828 cf1 = IssueCustomField.create!(:name => 'Writable field',
828 cf1 = IssueCustomField.create!(:name => 'Writable field',
829 :field_format => 'string',
829 :field_format => 'string',
830 :is_for_all => true, :tracker_ids => [1])
830 :is_for_all => true, :tracker_ids => [1])
831 cf2 = IssueCustomField.create!(:name => 'Readonly field',
831 cf2 = IssueCustomField.create!(:name => 'Readonly field',
832 :field_format => 'string',
832 :field_format => 'string',
833 :is_for_all => true, :tracker_ids => [1])
833 :is_for_all => true, :tracker_ids => [1])
834 WorkflowPermission.delete_all
834 WorkflowPermission.delete_all
835 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
835 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
836 :role_id => 1, :field_name => cf2.id.to_s,
836 :role_id => 1, :field_name => cf2.id.to_s,
837 :rule => 'readonly')
837 :rule => 'readonly')
838 user = User.find(2)
838 user = User.find(2)
839 issue = Issue.new(:project_id => 1, :tracker_id => 1)
839 issue = Issue.new(:project_id => 1, :tracker_id => 1)
840 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
840 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
841 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
841 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
842
842
843 issue.send :safe_attributes=, {'custom_field_values' => {
843 issue.send :safe_attributes=, {'custom_field_values' => {
844 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
844 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
845 }}, user
845 }}, user
846 assert_equal 'value1', issue.custom_field_value(cf1)
846 assert_equal 'value1', issue.custom_field_value(cf1)
847 assert_nil issue.custom_field_value(cf2)
847 assert_nil issue.custom_field_value(cf2)
848
848
849 issue.send :safe_attributes=, {'custom_fields' => [
849 issue.send :safe_attributes=, {'custom_fields' => [
850 {'id' => cf1.id.to_s, 'value' => 'valuea'},
850 {'id' => cf1.id.to_s, 'value' => 'valuea'},
851 {'id' => cf2.id.to_s, 'value' => 'valueb'}
851 {'id' => cf2.id.to_s, 'value' => 'valueb'}
852 ]}, user
852 ]}, user
853 assert_equal 'valuea', issue.custom_field_value(cf1)
853 assert_equal 'valuea', issue.custom_field_value(cf1)
854 assert_nil issue.custom_field_value(cf2)
854 assert_nil issue.custom_field_value(cf2)
855 end
855 end
856
856
857 def test_safe_attributes_should_ignore_unassignable_assignee
857 def test_safe_attributes_should_ignore_unassignable_assignee
858 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
858 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
859 :status_id => 1, :priority => IssuePriority.all.first,
859 :status_id => 1, :priority => IssuePriority.all.first,
860 :subject => 'test_create')
860 :subject => 'test_create')
861 assert issue.valid?
861 assert issue.valid?
862
862
863 # locked user, not allowed
863 # locked user, not allowed
864 issue.safe_attributes=({'assigned_to_id' => '5'})
864 issue.safe_attributes=({'assigned_to_id' => '5'})
865 assert_nil issue.assigned_to_id
865 assert_nil issue.assigned_to_id
866 # no member
866 # no member
867 issue.safe_attributes=({'assigned_to_id' => '1'})
867 issue.safe_attributes=({'assigned_to_id' => '1'})
868 assert_nil issue.assigned_to_id
868 assert_nil issue.assigned_to_id
869 # user 2 is ok
869 # user 2 is ok
870 issue.safe_attributes=({'assigned_to_id' => '2'})
870 issue.safe_attributes=({'assigned_to_id' => '2'})
871 assert_equal 2, issue.assigned_to_id
871 assert_equal 2, issue.assigned_to_id
872 assert issue.save
872 assert issue.save
873
873
874 issue.reload
874 issue.reload
875 assert_equal 2, issue.assigned_to_id
875 assert_equal 2, issue.assigned_to_id
876 issue.safe_attributes=({'assigned_to_id' => '5'})
876 issue.safe_attributes=({'assigned_to_id' => '5'})
877 assert_equal 2, issue.assigned_to_id
877 assert_equal 2, issue.assigned_to_id
878 issue.safe_attributes=({'assigned_to_id' => '1'})
878 issue.safe_attributes=({'assigned_to_id' => '1'})
879 assert_equal 2, issue.assigned_to_id
879 assert_equal 2, issue.assigned_to_id
880 # user 3 is also ok
880 # user 3 is also ok
881 issue.safe_attributes=({'assigned_to_id' => '3'})
881 issue.safe_attributes=({'assigned_to_id' => '3'})
882 assert_equal 3, issue.assigned_to_id
882 assert_equal 3, issue.assigned_to_id
883 assert issue.save
883 assert issue.save
884
884
885 # removal of assignee
885 # removal of assignee
886 issue.safe_attributes=({'assigned_to_id' => ''})
886 issue.safe_attributes=({'assigned_to_id' => ''})
887 assert_nil issue.assigned_to_id
887 assert_nil issue.assigned_to_id
888 assert issue.save
888 assert issue.save
889 end
889 end
890
890
891 def test_editable_custom_field_values_should_return_non_readonly_custom_values
891 def test_editable_custom_field_values_should_return_non_readonly_custom_values
892 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
892 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
893 :is_for_all => true, :tracker_ids => [1, 2])
893 :is_for_all => true, :tracker_ids => [1, 2])
894 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
894 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
895 :is_for_all => true, :tracker_ids => [1, 2])
895 :is_for_all => true, :tracker_ids => [1, 2])
896 WorkflowPermission.delete_all
896 WorkflowPermission.delete_all
897 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
897 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
898 :field_name => cf2.id.to_s, :rule => 'readonly')
898 :field_name => cf2.id.to_s, :rule => 'readonly')
899 user = User.find(2)
899 user = User.find(2)
900
900
901 issue = Issue.new(:project_id => 1, :tracker_id => 1)
901 issue = Issue.new(:project_id => 1, :tracker_id => 1)
902 values = issue.editable_custom_field_values(user)
902 values = issue.editable_custom_field_values(user)
903 assert values.detect {|value| value.custom_field == cf1}
903 assert values.detect {|value| value.custom_field == cf1}
904 assert_nil values.detect {|value| value.custom_field == cf2}
904 assert_nil values.detect {|value| value.custom_field == cf2}
905
905
906 issue.tracker_id = 2
906 issue.tracker_id = 2
907 values = issue.editable_custom_field_values(user)
907 values = issue.editable_custom_field_values(user)
908 assert values.detect {|value| value.custom_field == cf1}
908 assert values.detect {|value| value.custom_field == cf1}
909 assert values.detect {|value| value.custom_field == cf2}
909 assert values.detect {|value| value.custom_field == cf2}
910 end
910 end
911
911
912 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
912 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
913 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
913 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
914 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
914 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
915 user = User.find(2)
915 user = User.find(2)
916 issue = Issue.new(:project_id => 1, :tracker_id => 1)
916 issue = Issue.new(:project_id => 1, :tracker_id => 1)
917
917
918 assert_include enabled_cf, issue.editable_custom_fields(user)
918 assert_include enabled_cf, issue.editable_custom_fields(user)
919 assert_not_include disabled_cf, issue.editable_custom_fields(user)
919 assert_not_include disabled_cf, issue.editable_custom_fields(user)
920 end
920 end
921
921
922 def test_safe_attributes_should_accept_target_tracker_writable_fields
922 def test_safe_attributes_should_accept_target_tracker_writable_fields
923 WorkflowPermission.delete_all
923 WorkflowPermission.delete_all
924 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
924 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
925 :role_id => 1, :field_name => 'due_date',
925 :role_id => 1, :field_name => 'due_date',
926 :rule => 'readonly')
926 :rule => 'readonly')
927 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
927 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
928 :role_id => 1, :field_name => 'start_date',
928 :role_id => 1, :field_name => 'start_date',
929 :rule => 'readonly')
929 :rule => 'readonly')
930 user = User.find(2)
930 user = User.find(2)
931
931
932 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
932 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
933
933
934 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
934 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
935 'due_date' => '2012-07-14'}, user
935 'due_date' => '2012-07-14'}, user
936 assert_equal Date.parse('2012-07-12'), issue.start_date
936 assert_equal Date.parse('2012-07-12'), issue.start_date
937 assert_nil issue.due_date
937 assert_nil issue.due_date
938
938
939 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
939 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
940 'due_date' => '2012-07-16',
940 'due_date' => '2012-07-16',
941 'tracker_id' => 2}, user
941 'tracker_id' => 2}, user
942 assert_equal Date.parse('2012-07-12'), issue.start_date
942 assert_equal Date.parse('2012-07-12'), issue.start_date
943 assert_equal Date.parse('2012-07-16'), issue.due_date
943 assert_equal Date.parse('2012-07-16'), issue.due_date
944 end
944 end
945
945
946 def test_safe_attributes_should_accept_target_status_writable_fields
946 def test_safe_attributes_should_accept_target_status_writable_fields
947 WorkflowPermission.delete_all
947 WorkflowPermission.delete_all
948 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
948 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
949 :role_id => 1, :field_name => 'due_date',
949 :role_id => 1, :field_name => 'due_date',
950 :rule => 'readonly')
950 :rule => 'readonly')
951 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
951 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
952 :role_id => 1, :field_name => 'start_date',
952 :role_id => 1, :field_name => 'start_date',
953 :rule => 'readonly')
953 :rule => 'readonly')
954 user = User.find(2)
954 user = User.find(2)
955
955
956 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
956 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
957
957
958 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
958 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
959 'due_date' => '2012-07-14'},
959 'due_date' => '2012-07-14'},
960 user
960 user
961 assert_equal Date.parse('2012-07-12'), issue.start_date
961 assert_equal Date.parse('2012-07-12'), issue.start_date
962 assert_nil issue.due_date
962 assert_nil issue.due_date
963
963
964 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
964 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
965 'due_date' => '2012-07-16',
965 'due_date' => '2012-07-16',
966 'status_id' => 2},
966 'status_id' => 2},
967 user
967 user
968 assert_equal Date.parse('2012-07-12'), issue.start_date
968 assert_equal Date.parse('2012-07-12'), issue.start_date
969 assert_equal Date.parse('2012-07-16'), issue.due_date
969 assert_equal Date.parse('2012-07-16'), issue.due_date
970 end
970 end
971
971
972 def test_required_attributes_should_be_validated
972 def test_required_attributes_should_be_validated
973 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
973 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
974 :is_for_all => true, :tracker_ids => [1, 2])
974 :is_for_all => true, :tracker_ids => [1, 2])
975
975
976 WorkflowPermission.delete_all
976 WorkflowPermission.delete_all
977 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
977 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
978 :role_id => 1, :field_name => 'due_date',
978 :role_id => 1, :field_name => 'due_date',
979 :rule => 'required')
979 :rule => 'required')
980 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
980 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
981 :role_id => 1, :field_name => 'category_id',
981 :role_id => 1, :field_name => 'category_id',
982 :rule => 'required')
982 :rule => 'required')
983 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
983 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
984 :role_id => 1, :field_name => cf.id.to_s,
984 :role_id => 1, :field_name => cf.id.to_s,
985 :rule => 'required')
985 :rule => 'required')
986
986
987 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
987 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
988 :role_id => 1, :field_name => 'start_date',
988 :role_id => 1, :field_name => 'start_date',
989 :rule => 'required')
989 :rule => 'required')
990 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
990 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
991 :role_id => 1, :field_name => cf.id.to_s,
991 :role_id => 1, :field_name => cf.id.to_s,
992 :rule => 'required')
992 :rule => 'required')
993 user = User.find(2)
993 user = User.find(2)
994
994
995 issue = Issue.new(:project_id => 1, :tracker_id => 1,
995 issue = Issue.new(:project_id => 1, :tracker_id => 1,
996 :status_id => 1, :subject => 'Required fields',
996 :status_id => 1, :subject => 'Required fields',
997 :author => user)
997 :author => user)
998 assert_equal [cf.id.to_s, "category_id", "due_date"],
998 assert_equal [cf.id.to_s, "category_id", "due_date"],
999 issue.required_attribute_names(user).sort
999 issue.required_attribute_names(user).sort
1000 assert !issue.save, "Issue was saved"
1000 assert !issue.save, "Issue was saved"
1001 assert_equal ["Category cannot be blank", "Due date cannot be blank", "Foo cannot be blank"],
1001 assert_equal ["Category cannot be blank", "Due date cannot be blank", "Foo cannot be blank"],
1002 issue.errors.full_messages.sort
1002 issue.errors.full_messages.sort
1003
1003
1004 issue.tracker_id = 2
1004 issue.tracker_id = 2
1005 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
1005 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
1006 assert !issue.save, "Issue was saved"
1006 assert !issue.save, "Issue was saved"
1007 assert_equal ["Foo cannot be blank", "Start date cannot be blank"],
1007 assert_equal ["Foo cannot be blank", "Start date cannot be blank"],
1008 issue.errors.full_messages.sort
1008 issue.errors.full_messages.sort
1009
1009
1010 issue.start_date = Date.today
1010 issue.start_date = Date.today
1011 issue.custom_field_values = {cf.id.to_s => 'bar'}
1011 issue.custom_field_values = {cf.id.to_s => 'bar'}
1012 assert issue.save
1012 assert issue.save
1013 end
1013 end
1014
1014
1015 def test_required_attribute_that_is_disabled_for_the_tracker_should_not_be_required
1015 def test_required_attribute_that_is_disabled_for_the_tracker_should_not_be_required
1016 WorkflowPermission.delete_all
1016 WorkflowPermission.delete_all
1017 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1017 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1018 :role_id => 1, :field_name => 'start_date',
1018 :role_id => 1, :field_name => 'start_date',
1019 :rule => 'required')
1019 :rule => 'required')
1020 user = User.find(2)
1020 user = User.find(2)
1021
1021
1022 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1022 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1023 :subject => 'Required fields', :author => user)
1023 :subject => 'Required fields', :author => user)
1024 assert !issue.save
1024 assert !issue.save
1025 assert_include "Start date cannot be blank", issue.errors.full_messages
1025 assert_include "Start date cannot be blank", issue.errors.full_messages
1026
1026
1027 tracker = Tracker.find(1)
1027 tracker = Tracker.find(1)
1028 tracker.core_fields -= %w(start_date)
1028 tracker.core_fields -= %w(start_date)
1029 tracker.save!
1029 tracker.save!
1030 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1030 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1031 :subject => 'Required fields', :author => user)
1031 :subject => 'Required fields', :author => user)
1032 assert issue.save
1032 assert issue.save
1033 end
1033 end
1034
1034
1035 def test_category_should_not_be_required_if_project_has_no_categories
1035 def test_category_should_not_be_required_if_project_has_no_categories
1036 Project.find(1).issue_categories.delete_all
1036 Project.find(1).issue_categories.delete_all
1037 WorkflowPermission.delete_all
1037 WorkflowPermission.delete_all
1038 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1038 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1039 :role_id => 1, :field_name => 'category_id',:rule => 'required')
1039 :role_id => 1, :field_name => 'category_id',:rule => 'required')
1040 user = User.find(2)
1040 user = User.find(2)
1041
1041
1042 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1042 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1043 :subject => 'Required fields', :author => user)
1043 :subject => 'Required fields', :author => user)
1044 assert_save issue
1044 assert_save issue
1045 end
1045 end
1046
1046
1047 def test_fixed_version_should_not_be_required_no_assignable_versions
1047 def test_fixed_version_should_not_be_required_no_assignable_versions
1048 Version.delete_all
1048 Version.delete_all
1049 WorkflowPermission.delete_all
1049 WorkflowPermission.delete_all
1050 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1050 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1051 :role_id => 1, :field_name => 'fixed_version_id',:rule => 'required')
1051 :role_id => 1, :field_name => 'fixed_version_id',:rule => 'required')
1052 user = User.find(2)
1052 user = User.find(2)
1053
1053
1054 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1054 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1055 :subject => 'Required fields', :author => user)
1055 :subject => 'Required fields', :author => user)
1056 assert_save issue
1056 assert_save issue
1057 end
1057 end
1058
1058
1059 def test_required_custom_field_that_is_not_visible_for_the_user_should_not_be_required
1059 def test_required_custom_field_that_is_not_visible_for_the_user_should_not_be_required
1060 CustomField.delete_all
1060 CustomField.delete_all
1061 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1061 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1062 user = User.generate!
1062 user = User.generate!
1063 User.add_to_project(user, Project.find(1), Role.find(2))
1063 User.add_to_project(user, Project.find(1), Role.find(2))
1064
1064
1065 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1065 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1066 :subject => 'Required fields', :author => user)
1066 :subject => 'Required fields', :author => user)
1067 assert_save issue
1067 assert_save issue
1068 end
1068 end
1069
1069
1070 def test_required_custom_field_that_is_visible_for_the_user_should_be_required
1070 def test_required_custom_field_that_is_visible_for_the_user_should_be_required
1071 CustomField.delete_all
1071 CustomField.delete_all
1072 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1072 field = IssueCustomField.generate!(:is_required => true, :visible => false, :role_ids => [1], :trackers => Tracker.all, :is_for_all => true)
1073 user = User.generate!
1073 user = User.generate!
1074 User.add_to_project(user, Project.find(1), Role.find(1))
1074 User.add_to_project(user, Project.find(1), Role.find(1))
1075
1075
1076 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1076 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1077 :subject => 'Required fields', :author => user)
1077 :subject => 'Required fields', :author => user)
1078 assert !issue.save
1078 assert !issue.save
1079 assert_include "#{field.name} cannot be blank", issue.errors.full_messages
1079 assert_include "#{field.name} cannot be blank", issue.errors.full_messages
1080 end
1080 end
1081
1081
1082 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
1082 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
1083 WorkflowPermission.delete_all
1083 WorkflowPermission.delete_all
1084 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1084 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1085 :role_id => 1, :field_name => 'due_date',
1085 :role_id => 1, :field_name => 'due_date',
1086 :rule => 'required')
1086 :rule => 'required')
1087 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1087 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1088 :role_id => 1, :field_name => 'start_date',
1088 :role_id => 1, :field_name => 'start_date',
1089 :rule => 'required')
1089 :rule => 'required')
1090 user = User.find(2)
1090 user = User.find(2)
1091 member = Member.find(1)
1091 member = Member.find(1)
1092 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1092 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1093
1093
1094 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
1094 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
1095
1095
1096 member.role_ids = [1, 2]
1096 member.role_ids = [1, 2]
1097 member.save!
1097 member.save!
1098 assert_equal [], issue.required_attribute_names(user.reload)
1098 assert_equal [], issue.required_attribute_names(user.reload)
1099
1099
1100 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1100 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1101 :role_id => 2, :field_name => 'due_date',
1101 :role_id => 2, :field_name => 'due_date',
1102 :rule => 'required')
1102 :rule => 'required')
1103 assert_equal %w(due_date), issue.required_attribute_names(user)
1103 assert_equal %w(due_date), issue.required_attribute_names(user)
1104
1104
1105 member.role_ids = [1, 2, 3]
1105 member.role_ids = [1, 2, 3]
1106 member.save!
1106 member.save!
1107 assert_equal [], issue.required_attribute_names(user.reload)
1107 assert_equal [], issue.required_attribute_names(user.reload)
1108
1108
1109 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1109 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1110 :role_id => 3, :field_name => 'due_date',
1110 :role_id => 3, :field_name => 'due_date',
1111 :rule => 'readonly')
1111 :rule => 'readonly')
1112 # required + readonly => required
1112 # required + readonly => required
1113 assert_equal %w(due_date), issue.required_attribute_names(user)
1113 assert_equal %w(due_date), issue.required_attribute_names(user)
1114 end
1114 end
1115
1115
1116 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
1116 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
1117 WorkflowPermission.delete_all
1117 WorkflowPermission.delete_all
1118 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1118 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1119 :role_id => 1, :field_name => 'due_date',
1119 :role_id => 1, :field_name => 'due_date',
1120 :rule => 'readonly')
1120 :rule => 'readonly')
1121 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1121 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1122 :role_id => 1, :field_name => 'start_date',
1122 :role_id => 1, :field_name => 'start_date',
1123 :rule => 'readonly')
1123 :rule => 'readonly')
1124 user = User.find(2)
1124 user = User.find(2)
1125 member = Member.find(1)
1125 member = Member.find(1)
1126 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1126 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1127
1127
1128 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
1128 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
1129
1129
1130 member.role_ids = [1, 2]
1130 member.role_ids = [1, 2]
1131 member.save!
1131 member.save!
1132 assert_equal [], issue.read_only_attribute_names(user.reload)
1132 assert_equal [], issue.read_only_attribute_names(user.reload)
1133
1133
1134 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1134 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1135 :role_id => 2, :field_name => 'due_date',
1135 :role_id => 2, :field_name => 'due_date',
1136 :rule => 'readonly')
1136 :rule => 'readonly')
1137 assert_equal %w(due_date), issue.read_only_attribute_names(user)
1137 assert_equal %w(due_date), issue.read_only_attribute_names(user)
1138 end
1138 end
1139
1139
1140 # A field that is not visible by role 2 and readonly by role 1 should be readonly for user with role 1 and 2
1140 # A field that is not visible by role 2 and readonly by role 1 should be readonly for user with role 1 and 2
1141 def test_read_only_attribute_names_should_include_custom_fields_that_combine_readonly_and_not_visible_for_roles
1141 def test_read_only_attribute_names_should_include_custom_fields_that_combine_readonly_and_not_visible_for_roles
1142 field = IssueCustomField.generate!(
1142 field = IssueCustomField.generate!(
1143 :is_for_all => true, :trackers => Tracker.all, :visible => false, :role_ids => [1]
1143 :is_for_all => true, :trackers => Tracker.all, :visible => false, :role_ids => [1]
1144 )
1144 )
1145 WorkflowPermission.delete_all
1145 WorkflowPermission.delete_all
1146 WorkflowPermission.create!(
1146 WorkflowPermission.create!(
1147 :old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => field.id, :rule => 'readonly'
1147 :old_status_id => 1, :tracker_id => 1, :role_id => 1, :field_name => field.id, :rule => 'readonly'
1148 )
1148 )
1149 user = User.generate!
1149 user = User.generate!
1150 project = Project.find(1)
1150 project = Project.find(1)
1151 User.add_to_project(user, project, Role.where(:id => [1, 2]))
1151 User.add_to_project(user, project, Role.where(:id => [1, 2]))
1152
1152
1153 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1153 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1154 assert_equal [field.id.to_s], issue.read_only_attribute_names(user)
1154 assert_equal [field.id.to_s], issue.read_only_attribute_names(user)
1155 end
1155 end
1156
1156
1157 def test_workflow_rules_should_ignore_roles_without_issue_permissions
1157 def test_workflow_rules_should_ignore_roles_without_issue_permissions
1158 role = Role.generate! :permissions => [:view_issues, :edit_issues]
1158 role = Role.generate! :permissions => [:view_issues, :edit_issues]
1159 ignored_role = Role.generate! :permissions => [:view_issues]
1159 ignored_role = Role.generate! :permissions => [:view_issues]
1160
1160
1161 WorkflowPermission.delete_all
1161 WorkflowPermission.delete_all
1162 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1162 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1163 :role => role, :field_name => 'due_date',
1163 :role => role, :field_name => 'due_date',
1164 :rule => 'required')
1164 :rule => 'required')
1165 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1165 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1166 :role => role, :field_name => 'start_date',
1166 :role => role, :field_name => 'start_date',
1167 :rule => 'readonly')
1167 :rule => 'readonly')
1168 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1168 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1169 :role => role, :field_name => 'done_ratio',
1169 :role => role, :field_name => 'done_ratio',
1170 :rule => 'readonly')
1170 :rule => 'readonly')
1171 user = User.generate!
1171 user = User.generate!
1172 User.add_to_project user, Project.find(1), [role, ignored_role]
1172 User.add_to_project user, Project.find(1), [role, ignored_role]
1173
1173
1174 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1174 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1175
1175
1176 assert_equal %w(due_date), issue.required_attribute_names(user)
1176 assert_equal %w(due_date), issue.required_attribute_names(user)
1177 assert_equal %w(done_ratio start_date), issue.read_only_attribute_names(user).sort
1177 assert_equal %w(done_ratio start_date), issue.read_only_attribute_names(user).sort
1178 end
1178 end
1179
1179
1180 def test_workflow_rules_should_work_for_member_with_duplicate_role
1180 def test_workflow_rules_should_work_for_member_with_duplicate_role
1181 WorkflowPermission.delete_all
1181 WorkflowPermission.delete_all
1182 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1182 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1183 :role_id => 1, :field_name => 'due_date',
1183 :role_id => 1, :field_name => 'due_date',
1184 :rule => 'required')
1184 :rule => 'required')
1185 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1185 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
1186 :role_id => 1, :field_name => 'start_date',
1186 :role_id => 1, :field_name => 'start_date',
1187 :rule => 'readonly')
1187 :rule => 'readonly')
1188
1188
1189 user = User.generate!
1189 user = User.generate!
1190 m = Member.new(:user_id => user.id, :project_id => 1)
1190 m = Member.new(:user_id => user.id, :project_id => 1)
1191 m.member_roles.build(:role_id => 1)
1191 m.member_roles.build(:role_id => 1)
1192 m.member_roles.build(:role_id => 1)
1192 m.member_roles.build(:role_id => 1)
1193 m.save!
1193 m.save!
1194
1194
1195 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1195 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
1196
1196
1197 assert_equal %w(due_date), issue.required_attribute_names(user)
1197 assert_equal %w(due_date), issue.required_attribute_names(user)
1198 assert_equal %w(start_date), issue.read_only_attribute_names(user)
1198 assert_equal %w(start_date), issue.read_only_attribute_names(user)
1199 end
1199 end
1200
1200
1201 def test_copy
1201 def test_copy
1202 issue = Issue.new.copy_from(1)
1202 issue = Issue.new.copy_from(1)
1203 assert issue.copy?
1203 assert issue.copy?
1204 assert issue.save
1204 assert issue.save
1205 issue.reload
1205 issue.reload
1206 orig = Issue.find(1)
1206 orig = Issue.find(1)
1207 assert_equal orig.subject, issue.subject
1207 assert_equal orig.subject, issue.subject
1208 assert_equal orig.tracker, issue.tracker
1208 assert_equal orig.tracker, issue.tracker
1209 assert_equal "125", issue.custom_value_for(2).value
1209 assert_equal "125", issue.custom_value_for(2).value
1210 end
1210 end
1211
1211
1212 def test_copy_should_copy_status
1212 def test_copy_should_copy_status
1213 orig = Issue.find(8)
1213 orig = Issue.find(8)
1214 assert orig.status != orig.default_status
1214 assert orig.status != orig.default_status
1215
1215
1216 issue = Issue.new.copy_from(orig)
1216 issue = Issue.new.copy_from(orig)
1217 assert issue.save
1217 assert issue.save
1218 issue.reload
1218 issue.reload
1219 assert_equal orig.status, issue.status
1219 assert_equal orig.status, issue.status
1220 end
1220 end
1221
1221
1222 def test_copy_should_add_relation_with_copied_issue
1222 def test_copy_should_add_relation_with_copied_issue
1223 copied = Issue.find(1)
1223 copied = Issue.find(1)
1224 issue = Issue.new.copy_from(copied)
1224 issue = Issue.new.copy_from(copied)
1225 assert issue.save
1225 assert issue.save
1226 issue.reload
1226 issue.reload
1227
1227
1228 assert_equal 1, issue.relations.size
1228 assert_equal 1, issue.relations.size
1229 relation = issue.relations.first
1229 relation = issue.relations.first
1230 assert_equal 'copied_to', relation.relation_type
1230 assert_equal 'copied_to', relation.relation_type
1231 assert_equal copied, relation.issue_from
1231 assert_equal copied, relation.issue_from
1232 assert_equal issue, relation.issue_to
1232 assert_equal issue, relation.issue_to
1233 end
1233 end
1234
1234
1235 def test_copy_should_copy_subtasks
1235 def test_copy_should_copy_subtasks
1236 issue = Issue.generate_with_descendants!
1236 issue = Issue.generate_with_descendants!
1237
1237
1238 copy = issue.reload.copy
1238 copy = issue.reload.copy
1239 copy.author = User.find(7)
1239 copy.author = User.find(7)
1240 assert_difference 'Issue.count', 1+issue.descendants.count do
1240 assert_difference 'Issue.count', 1+issue.descendants.count do
1241 assert copy.save
1241 assert copy.save
1242 end
1242 end
1243 copy.reload
1243 copy.reload
1244 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
1244 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
1245 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1245 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1246 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
1246 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
1247 assert_equal copy.author, child_copy.author
1247 assert_equal copy.author, child_copy.author
1248 end
1248 end
1249
1249
1250 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
1250 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
1251 parent = Issue.generate!
1251 parent = Issue.generate!
1252 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1252 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1253 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1253 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1254
1254
1255 copy = parent.reload.copy
1255 copy = parent.reload.copy
1256 copy.parent_issue_id = parent.id
1256 copy.parent_issue_id = parent.id
1257 copy.author = User.find(7)
1257 copy.author = User.find(7)
1258 assert_difference 'Issue.count', 3 do
1258 assert_difference 'Issue.count', 3 do
1259 assert copy.save
1259 assert copy.save
1260 end
1260 end
1261 parent.reload
1261 parent.reload
1262 copy.reload
1262 copy.reload
1263 assert_equal parent, copy.parent
1263 assert_equal parent, copy.parent
1264 assert_equal 3, parent.children.count
1264 assert_equal 3, parent.children.count
1265 assert_equal 5, parent.descendants.count
1265 assert_equal 5, parent.descendants.count
1266 assert_equal 2, copy.children.count
1266 assert_equal 2, copy.children.count
1267 assert_equal 2, copy.descendants.count
1267 assert_equal 2, copy.descendants.count
1268 end
1268 end
1269
1269
1270 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
1270 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
1271 parent = Issue.generate!
1271 parent = Issue.generate!
1272 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1272 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
1273 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1273 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
1274
1274
1275 copy = parent.reload.copy
1275 copy = parent.reload.copy
1276 copy.parent_issue_id = child1.id
1276 copy.parent_issue_id = child1.id
1277 copy.author = User.find(7)
1277 copy.author = User.find(7)
1278 assert_difference 'Issue.count', 3 do
1278 assert_difference 'Issue.count', 3 do
1279 assert copy.save
1279 assert copy.save
1280 end
1280 end
1281 parent.reload
1281 parent.reload
1282 child1.reload
1282 child1.reload
1283 copy.reload
1283 copy.reload
1284 assert_equal child1, copy.parent
1284 assert_equal child1, copy.parent
1285 assert_equal 2, parent.children.count
1285 assert_equal 2, parent.children.count
1286 assert_equal 5, parent.descendants.count
1286 assert_equal 5, parent.descendants.count
1287 assert_equal 1, child1.children.count
1287 assert_equal 1, child1.children.count
1288 assert_equal 3, child1.descendants.count
1288 assert_equal 3, child1.descendants.count
1289 assert_equal 2, copy.children.count
1289 assert_equal 2, copy.children.count
1290 assert_equal 2, copy.descendants.count
1290 assert_equal 2, copy.descendants.count
1291 end
1291 end
1292
1292
1293 def test_copy_should_copy_subtasks_to_target_project
1293 def test_copy_should_copy_subtasks_to_target_project
1294 issue = Issue.generate_with_descendants!
1294 issue = Issue.generate_with_descendants!
1295
1295
1296 copy = issue.copy(:project_id => 3)
1296 copy = issue.copy(:project_id => 3)
1297 assert_difference 'Issue.count', 1+issue.descendants.count do
1297 assert_difference 'Issue.count', 1+issue.descendants.count do
1298 assert copy.save
1298 assert copy.save
1299 end
1299 end
1300 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
1300 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
1301 end
1301 end
1302
1302
1303 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
1303 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
1304 issue = Issue.generate_with_descendants!
1304 issue = Issue.generate_with_descendants!
1305
1305
1306 copy = issue.reload.copy
1306 copy = issue.reload.copy
1307 assert_difference 'Issue.count', 1+issue.descendants.count do
1307 assert_difference 'Issue.count', 1+issue.descendants.count do
1308 assert copy.save
1308 assert copy.save
1309 assert copy.save
1309 assert copy.save
1310 end
1310 end
1311 end
1311 end
1312
1312
1313 def test_should_not_call_after_project_change_on_creation
1313 def test_should_not_call_after_project_change_on_creation
1314 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1314 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
1315 :subject => 'Test', :author_id => 1)
1315 :subject => 'Test', :author_id => 1)
1316 issue.expects(:after_project_change).never
1316 issue.expects(:after_project_change).never
1317 issue.save!
1317 issue.save!
1318 end
1318 end
1319
1319
1320 def test_should_not_call_after_project_change_on_update
1320 def test_should_not_call_after_project_change_on_update
1321 issue = Issue.find(1)
1321 issue = Issue.find(1)
1322 issue.project = Project.find(1)
1322 issue.project = Project.find(1)
1323 issue.subject = 'No project change'
1323 issue.subject = 'No project change'
1324 issue.expects(:after_project_change).never
1324 issue.expects(:after_project_change).never
1325 issue.save!
1325 issue.save!
1326 end
1326 end
1327
1327
1328 def test_should_call_after_project_change_on_project_change
1328 def test_should_call_after_project_change_on_project_change
1329 issue = Issue.find(1)
1329 issue = Issue.find(1)
1330 issue.project = Project.find(2)
1330 issue.project = Project.find(2)
1331 issue.expects(:after_project_change).once
1331 issue.expects(:after_project_change).once
1332 issue.save!
1332 issue.save!
1333 end
1333 end
1334
1334
1335 def test_adding_journal_should_update_timestamp
1335 def test_adding_journal_should_update_timestamp
1336 issue = Issue.find(1)
1336 issue = Issue.find(1)
1337 updated_on_was = issue.updated_on
1337 updated_on_was = issue.updated_on
1338
1338
1339 issue.init_journal(User.first, "Adding notes")
1339 issue.init_journal(User.first, "Adding notes")
1340 assert_difference 'Journal.count' do
1340 assert_difference 'Journal.count' do
1341 assert issue.save
1341 assert issue.save
1342 end
1342 end
1343 issue.reload
1343 issue.reload
1344
1344
1345 assert_not_equal updated_on_was, issue.updated_on
1345 assert_not_equal updated_on_was, issue.updated_on
1346 end
1346 end
1347
1347
1348 def test_should_close_duplicates
1348 def test_should_close_duplicates
1349 # Create 3 issues
1349 # Create 3 issues
1350 issue1 = Issue.generate!
1350 issue1 = Issue.generate!
1351 issue2 = Issue.generate!
1351 issue2 = Issue.generate!
1352 issue3 = Issue.generate!
1352 issue3 = Issue.generate!
1353
1353
1354 # 2 is a dupe of 1
1354 # 2 is a dupe of 1
1355 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1355 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1356 :relation_type => IssueRelation::TYPE_DUPLICATES)
1356 :relation_type => IssueRelation::TYPE_DUPLICATES)
1357 # And 3 is a dupe of 2
1357 # And 3 is a dupe of 2
1358 # IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1358 # IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1359 # :relation_type => IssueRelation::TYPE_DUPLICATES)
1359 # :relation_type => IssueRelation::TYPE_DUPLICATES)
1360 # And 3 is a dupe of 1 (circular duplicates)
1360 # And 3 is a dupe of 1 (circular duplicates)
1361 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1361 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1362 :relation_type => IssueRelation::TYPE_DUPLICATES)
1362 :relation_type => IssueRelation::TYPE_DUPLICATES)
1363
1363
1364 assert issue1.reload.duplicates.include?(issue2)
1364 assert issue1.reload.duplicates.include?(issue2)
1365
1365
1366 # Closing issue 1
1366 # Closing issue 1
1367 issue1.init_journal(User.first, "Closing issue1")
1367 issue1.init_journal(User.first, "Closing issue1")
1368 issue1.status = IssueStatus.where(:is_closed => true).first
1368 issue1.status = IssueStatus.where(:is_closed => true).first
1369 assert issue1.save
1369 assert issue1.save
1370 # 2 and 3 should be also closed
1370 # 2 and 3 should be also closed
1371 assert issue2.reload.closed?
1371 assert issue2.reload.closed?
1372 assert issue3.reload.closed?
1372 assert issue3.reload.closed?
1373 end
1373 end
1374
1374
1375 def test_should_close_duplicates_with_private_notes
1375 def test_should_close_duplicates_with_private_notes
1376 issue = Issue.generate!
1376 issue = Issue.generate!
1377 duplicate = Issue.generate!
1377 duplicate = Issue.generate!
1378 IssueRelation.create!(:issue_from => duplicate, :issue_to => issue,
1378 IssueRelation.create!(:issue_from => duplicate, :issue_to => issue,
1379 :relation_type => IssueRelation::TYPE_DUPLICATES)
1379 :relation_type => IssueRelation::TYPE_DUPLICATES)
1380 assert issue.reload.duplicates.include?(duplicate)
1380 assert issue.reload.duplicates.include?(duplicate)
1381
1381
1382 # Closing issue with private notes
1382 # Closing issue with private notes
1383 issue.init_journal(User.first, "Private notes")
1383 issue.init_journal(User.first, "Private notes")
1384 issue.private_notes = true
1384 issue.private_notes = true
1385 issue.status = IssueStatus.where(:is_closed => true).first
1385 issue.status = IssueStatus.where(:is_closed => true).first
1386 assert_save issue
1386 assert_save issue
1387
1387
1388 duplicate.reload
1388 duplicate.reload
1389 assert journal = duplicate.journals.detect {|journal| journal.notes == "Private notes"}
1389 assert journal = duplicate.journals.detect {|journal| journal.notes == "Private notes"}
1390 assert_equal true, journal.private_notes
1390 assert_equal true, journal.private_notes
1391 end
1391 end
1392
1392
1393 def test_should_not_close_duplicated_issue
1393 def test_should_not_close_duplicated_issue
1394 issue1 = Issue.generate!
1394 issue1 = Issue.generate!
1395 issue2 = Issue.generate!
1395 issue2 = Issue.generate!
1396
1396
1397 # 2 is a dupe of 1
1397 # 2 is a dupe of 1
1398 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1398 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1399 :relation_type => IssueRelation::TYPE_DUPLICATES)
1399 :relation_type => IssueRelation::TYPE_DUPLICATES)
1400 # 2 is a dup of 1 but 1 is not a duplicate of 2
1400 # 2 is a dup of 1 but 1 is not a duplicate of 2
1401 assert !issue2.reload.duplicates.include?(issue1)
1401 assert !issue2.reload.duplicates.include?(issue1)
1402
1402
1403 # Closing issue 2
1403 # Closing issue 2
1404 issue2.init_journal(User.first, "Closing issue2")
1404 issue2.init_journal(User.first, "Closing issue2")
1405 issue2.status = IssueStatus.where(:is_closed => true).first
1405 issue2.status = IssueStatus.where(:is_closed => true).first
1406 assert issue2.save
1406 assert issue2.save
1407 # 1 should not be also closed
1407 # 1 should not be also closed
1408 assert !issue1.reload.closed?
1408 assert !issue1.reload.closed?
1409 end
1409 end
1410
1410
1411 def test_assignable_versions
1411 def test_assignable_versions
1412 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1412 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1413 :status_id => 1, :fixed_version_id => 1,
1413 :status_id => 1, :fixed_version_id => 1,
1414 :subject => 'New issue')
1414 :subject => 'New issue')
1415 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1415 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1416 end
1416 end
1417
1417
1418 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1418 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1419 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1419 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1420 :status_id => 1, :fixed_version_id => 1,
1420 :status_id => 1, :fixed_version_id => 1,
1421 :subject => 'New issue')
1421 :subject => 'New issue')
1422 assert !issue.save
1422 assert !issue.save
1423 assert_not_equal [], issue.errors[:fixed_version_id]
1423 assert_not_equal [], issue.errors[:fixed_version_id]
1424 end
1424 end
1425
1425
1426 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1426 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1427 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1427 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1428 :status_id => 1, :fixed_version_id => 2,
1428 :status_id => 1, :fixed_version_id => 2,
1429 :subject => 'New issue')
1429 :subject => 'New issue')
1430 assert !issue.save
1430 assert !issue.save
1431 assert_not_equal [], issue.errors[:fixed_version_id]
1431 assert_not_equal [], issue.errors[:fixed_version_id]
1432 end
1432 end
1433
1433
1434 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1434 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1435 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1435 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1436 :status_id => 1, :fixed_version_id => 3,
1436 :status_id => 1, :fixed_version_id => 3,
1437 :subject => 'New issue')
1437 :subject => 'New issue')
1438 assert issue.save
1438 assert issue.save
1439 end
1439 end
1440
1440
1441 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1441 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1442 issue = Issue.find(11)
1442 issue = Issue.find(11)
1443 assert_equal 'closed', issue.fixed_version.status
1443 assert_equal 'closed', issue.fixed_version.status
1444 issue.subject = 'Subject changed'
1444 issue.subject = 'Subject changed'
1445 assert issue.save
1445 assert issue.save
1446 end
1446 end
1447
1447
1448 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1448 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1449 issue = Issue.find(11)
1449 issue = Issue.find(11)
1450 issue.status_id = 1
1450 issue.status_id = 1
1451 assert !issue.save
1451 assert !issue.save
1452 assert_not_equal [], issue.errors[:base]
1452 assert_not_equal [], issue.errors[:base]
1453 end
1453 end
1454
1454
1455 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1455 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1456 issue = Issue.find(11)
1456 issue = Issue.find(11)
1457 issue.status_id = 1
1457 issue.status_id = 1
1458 issue.fixed_version_id = 3
1458 issue.fixed_version_id = 3
1459 assert issue.save
1459 assert issue.save
1460 end
1460 end
1461
1461
1462 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1462 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1463 issue = Issue.find(12)
1463 issue = Issue.find(12)
1464 assert_equal 'locked', issue.fixed_version.status
1464 assert_equal 'locked', issue.fixed_version.status
1465 issue.status_id = 1
1465 issue.status_id = 1
1466 assert issue.save
1466 assert issue.save
1467 end
1467 end
1468
1468
1469 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1469 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1470 issue = Issue.find(2)
1470 issue = Issue.find(2)
1471 assert_equal 2, issue.fixed_version_id
1471 assert_equal 2, issue.fixed_version_id
1472 issue.project_id = 3
1472 issue.project_id = 3
1473 assert_nil issue.fixed_version_id
1473 assert_nil issue.fixed_version_id
1474 issue.fixed_version_id = 2
1474 issue.fixed_version_id = 2
1475 assert !issue.save
1475 assert !issue.save
1476 assert_include 'Target version is not included in the list', issue.errors.full_messages
1476 assert_include 'Target version is not included in the list', issue.errors.full_messages
1477 end
1477 end
1478
1478
1479 def test_should_keep_shared_version_when_changing_project
1479 def test_should_keep_shared_version_when_changing_project
1480 Version.find(2).update_attribute :sharing, 'tree'
1480 Version.find(2).update_attribute :sharing, 'tree'
1481
1481
1482 issue = Issue.find(2)
1482 issue = Issue.find(2)
1483 assert_equal 2, issue.fixed_version_id
1483 assert_equal 2, issue.fixed_version_id
1484 issue.project_id = 3
1484 issue.project_id = 3
1485 assert_equal 2, issue.fixed_version_id
1485 assert_equal 2, issue.fixed_version_id
1486 assert issue.save
1486 assert issue.save
1487 end
1487 end
1488
1488
1489 def test_allowed_target_projects_should_include_projects_with_issue_tracking_enabled
1489 def test_allowed_target_projects_should_include_projects_with_issue_tracking_enabled
1490 assert_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1490 assert_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1491 end
1491 end
1492
1492
1493 def test_allowed_target_projects_should_not_include_projects_with_issue_tracking_disabled
1493 def test_allowed_target_projects_should_not_include_projects_with_issue_tracking_disabled
1494 Project.find(2).disable_module! :issue_tracking
1494 Project.find(2).disable_module! :issue_tracking
1495 assert_not_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1495 assert_not_include Project.find(2), Issue.allowed_target_projects(User.find(2))
1496 end
1496 end
1497
1497
1498 def test_allowed_target_projects_should_not_include_projects_without_trackers
1498 def test_allowed_target_projects_should_not_include_projects_without_trackers
1499 project = Project.generate!(:tracker_ids => [])
1499 project = Project.generate!(:tracker_ids => [])
1500 assert project.trackers.empty?
1500 assert project.trackers.empty?
1501 assert_not_include project, Issue.allowed_target_projects(User.find(1))
1501 assert_not_include project, Issue.allowed_target_projects(User.find(1))
1502 end
1502 end
1503
1503
1504 def test_allowed_target_trackers_with_one_role_allowed_on_all_trackers
1504 def test_allowed_target_trackers_with_one_role_allowed_on_all_trackers
1505 user = User.generate!
1505 user = User.generate!
1506 role = Role.generate!
1506 role = Role.generate!
1507 role.add_permission! :add_issues
1507 role.add_permission! :add_issues
1508 role.set_permission_trackers :add_issues, :all
1508 role.set_permission_trackers :add_issues, :all
1509 role.save!
1509 role.save!
1510 User.add_to_project(user, Project.find(1), role)
1510 User.add_to_project(user, Project.find(1), role)
1511
1511
1512 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1512 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1513 end
1513 end
1514
1514
1515 def test_allowed_target_trackers_with_one_role_allowed_on_some_trackers
1515 def test_allowed_target_trackers_with_one_role_allowed_on_some_trackers
1516 user = User.generate!
1516 user = User.generate!
1517 role = Role.generate!
1517 role = Role.generate!
1518 role.add_permission! :add_issues
1518 role.add_permission! :add_issues
1519 role.set_permission_trackers :add_issues, [1, 3]
1519 role.set_permission_trackers :add_issues, [1, 3]
1520 role.save!
1520 role.save!
1521 User.add_to_project(user, Project.find(1), role)
1521 User.add_to_project(user, Project.find(1), role)
1522
1522
1523 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1523 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1524 end
1524 end
1525
1525
1526 def test_allowed_target_trackers_with_two_roles_allowed_on_some_trackers
1526 def test_allowed_target_trackers_with_two_roles_allowed_on_some_trackers
1527 user = User.generate!
1527 user = User.generate!
1528 role1 = Role.generate!
1528 role1 = Role.generate!
1529 role1.add_permission! :add_issues
1529 role1.add_permission! :add_issues
1530 role1.set_permission_trackers :add_issues, [1]
1530 role1.set_permission_trackers :add_issues, [1]
1531 role1.save!
1531 role1.save!
1532 role2 = Role.generate!
1532 role2 = Role.generate!
1533 role2.add_permission! :add_issues
1533 role2.add_permission! :add_issues
1534 role2.set_permission_trackers :add_issues, [3]
1534 role2.set_permission_trackers :add_issues, [3]
1535 role2.save!
1535 role2.save!
1536 User.add_to_project(user, Project.find(1), [role1, role2])
1536 User.add_to_project(user, Project.find(1), [role1, role2])
1537
1537
1538 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1538 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1539 end
1539 end
1540
1540
1541 def test_allowed_target_trackers_with_two_roles_allowed_on_all_trackers_and_some_trackers
1541 def test_allowed_target_trackers_with_two_roles_allowed_on_all_trackers_and_some_trackers
1542 user = User.generate!
1542 user = User.generate!
1543 role1 = Role.generate!
1543 role1 = Role.generate!
1544 role1.add_permission! :add_issues
1544 role1.add_permission! :add_issues
1545 role1.set_permission_trackers :add_issues, :all
1545 role1.set_permission_trackers :add_issues, :all
1546 role1.save!
1546 role1.save!
1547 role2 = Role.generate!
1547 role2 = Role.generate!
1548 role2.add_permission! :add_issues
1548 role2.add_permission! :add_issues
1549 role2.set_permission_trackers :add_issues, [1, 3]
1549 role2.set_permission_trackers :add_issues, [1, 3]
1550 role2.save!
1550 role2.save!
1551 User.add_to_project(user, Project.find(1), [role1, role2])
1551 User.add_to_project(user, Project.find(1), [role1, role2])
1552
1552
1553 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1553 assert_equal [1, 2, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1554 end
1554 end
1555
1555
1556 def test_allowed_target_trackers_should_not_consider_roles_without_add_issues_permission
1556 def test_allowed_target_trackers_should_not_consider_roles_without_add_issues_permission
1557 user = User.generate!
1557 user = User.generate!
1558 role1 = Role.generate!
1558 role1 = Role.generate!
1559 role1.remove_permission! :add_issues
1559 role1.remove_permission! :add_issues
1560 role1.set_permission_trackers :add_issues, :all
1560 role1.set_permission_trackers :add_issues, :all
1561 role1.save!
1561 role1.save!
1562 role2 = Role.generate!
1562 role2 = Role.generate!
1563 role2.add_permission! :add_issues
1563 role2.add_permission! :add_issues
1564 role2.set_permission_trackers :add_issues, [1, 3]
1564 role2.set_permission_trackers :add_issues, [1, 3]
1565 role2.save!
1565 role2.save!
1566 User.add_to_project(user, Project.find(1), [role1, role2])
1566 User.add_to_project(user, Project.find(1), [role1, role2])
1567
1567
1568 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1568 assert_equal [1, 3], Issue.new(:project => Project.find(1)).allowed_target_trackers(user).ids.sort
1569 end
1569 end
1570
1570
1571 def test_allowed_target_trackers_without_project_should_be_empty
1571 def test_allowed_target_trackers_without_project_should_be_empty
1572 issue = Issue.new
1572 issue = Issue.new
1573 assert_nil issue.project
1573 assert_nil issue.project
1574 assert_equal [], issue.allowed_target_trackers(User.find(2)).ids
1574 assert_equal [], issue.allowed_target_trackers(User.find(2)).ids
1575 end
1575 end
1576
1576
1577 def test_allowed_target_trackers_should_include_current_tracker
1577 def test_allowed_target_trackers_should_include_current_tracker
1578 user = User.generate!
1578 user = User.generate!
1579 role = Role.generate!
1579 role = Role.generate!
1580 role.add_permission! :add_issues
1580 role.add_permission! :add_issues
1581 role.set_permission_trackers :add_issues, [3]
1581 role.set_permission_trackers :add_issues, [3]
1582 role.save!
1582 role.save!
1583 User.add_to_project(user, Project.find(1), role)
1583 User.add_to_project(user, Project.find(1), role)
1584
1584
1585 issue = Issue.generate!(:project => Project.find(1), :tracker => Tracker.find(1))
1585 issue = Issue.generate!(:project => Project.find(1), :tracker => Tracker.find(1))
1586 assert_equal [1, 3], issue.allowed_target_trackers(user).ids.sort
1586 assert_equal [1, 3], issue.allowed_target_trackers(user).ids.sort
1587 end
1587 end
1588
1588
1589 def test_move_to_another_project_with_same_category
1589 def test_move_to_another_project_with_same_category
1590 issue = Issue.find(1)
1590 issue = Issue.find(1)
1591 issue.project = Project.find(2)
1591 issue.project = Project.find(2)
1592 assert issue.save
1592 assert issue.save
1593 issue.reload
1593 issue.reload
1594 assert_equal 2, issue.project_id
1594 assert_equal 2, issue.project_id
1595 # Category changes
1595 # Category changes
1596 assert_equal 4, issue.category_id
1596 assert_equal 4, issue.category_id
1597 # Make sure time entries were move to the target project
1597 # Make sure time entries were move to the target project
1598 assert_equal 2, issue.time_entries.first.project_id
1598 assert_equal 2, issue.time_entries.first.project_id
1599 end
1599 end
1600
1600
1601 def test_move_to_another_project_without_same_category
1601 def test_move_to_another_project_without_same_category
1602 issue = Issue.find(2)
1602 issue = Issue.find(2)
1603 issue.project = Project.find(2)
1603 issue.project = Project.find(2)
1604 assert issue.save
1604 assert issue.save
1605 issue.reload
1605 issue.reload
1606 assert_equal 2, issue.project_id
1606 assert_equal 2, issue.project_id
1607 # Category cleared
1607 # Category cleared
1608 assert_nil issue.category_id
1608 assert_nil issue.category_id
1609 end
1609 end
1610
1610
1611 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1611 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1612 issue = Issue.find(1)
1612 issue = Issue.find(1)
1613 issue.update_attribute(:fixed_version_id, 1)
1613 issue.update_attribute(:fixed_version_id, 1)
1614 issue.project = Project.find(2)
1614 issue.project = Project.find(2)
1615 assert issue.save
1615 assert issue.save
1616 issue.reload
1616 issue.reload
1617 assert_equal 2, issue.project_id
1617 assert_equal 2, issue.project_id
1618 # Cleared fixed_version
1618 # Cleared fixed_version
1619 assert_equal nil, issue.fixed_version
1619 assert_equal nil, issue.fixed_version
1620 end
1620 end
1621
1621
1622 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1622 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1623 issue = Issue.find(1)
1623 issue = Issue.find(1)
1624 issue.update_attribute(:fixed_version_id, 4)
1624 issue.update_attribute(:fixed_version_id, 4)
1625 issue.project = Project.find(5)
1625 issue.project = Project.find(5)
1626 assert issue.save
1626 assert issue.save
1627 issue.reload
1627 issue.reload
1628 assert_equal 5, issue.project_id
1628 assert_equal 5, issue.project_id
1629 # Keep fixed_version
1629 # Keep fixed_version
1630 assert_equal 4, issue.fixed_version_id
1630 assert_equal 4, issue.fixed_version_id
1631 end
1631 end
1632
1632
1633 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1633 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1634 issue = Issue.find(1)
1634 issue = Issue.find(1)
1635 issue.update_attribute(:fixed_version_id, 1)
1635 issue.update_attribute(:fixed_version_id, 1)
1636 issue.project = Project.find(5)
1636 issue.project = Project.find(5)
1637 assert issue.save
1637 assert issue.save
1638 issue.reload
1638 issue.reload
1639 assert_equal 5, issue.project_id
1639 assert_equal 5, issue.project_id
1640 # Cleared fixed_version
1640 # Cleared fixed_version
1641 assert_equal nil, issue.fixed_version
1641 assert_equal nil, issue.fixed_version
1642 end
1642 end
1643
1643
1644 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1644 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1645 issue = Issue.find(1)
1645 issue = Issue.find(1)
1646 issue.update_attribute(:fixed_version_id, 7)
1646 issue.update_attribute(:fixed_version_id, 7)
1647 issue.project = Project.find(2)
1647 issue.project = Project.find(2)
1648 assert issue.save
1648 assert issue.save
1649 issue.reload
1649 issue.reload
1650 assert_equal 2, issue.project_id
1650 assert_equal 2, issue.project_id
1651 # Keep fixed_version
1651 # Keep fixed_version
1652 assert_equal 7, issue.fixed_version_id
1652 assert_equal 7, issue.fixed_version_id
1653 end
1653 end
1654
1654
1655 def test_move_to_another_project_should_keep_parent_if_valid
1655 def test_move_to_another_project_should_keep_parent_if_valid
1656 issue = Issue.find(1)
1656 issue = Issue.find(1)
1657 issue.update_attribute(:parent_issue_id, 2)
1657 issue.update_attribute(:parent_issue_id, 2)
1658 issue.project = Project.find(3)
1658 issue.project = Project.find(3)
1659 assert issue.save
1659 assert issue.save
1660 issue.reload
1660 issue.reload
1661 assert_equal 2, issue.parent_id
1661 assert_equal 2, issue.parent_id
1662 end
1662 end
1663
1663
1664 def test_move_to_another_project_should_clear_parent_if_not_valid
1664 def test_move_to_another_project_should_clear_parent_if_not_valid
1665 issue = Issue.find(1)
1665 issue = Issue.find(1)
1666 issue.update_attribute(:parent_issue_id, 2)
1666 issue.update_attribute(:parent_issue_id, 2)
1667 issue.project = Project.find(2)
1667 issue.project = Project.find(2)
1668 assert issue.save
1668 assert issue.save
1669 issue.reload
1669 issue.reload
1670 assert_nil issue.parent_id
1670 assert_nil issue.parent_id
1671 end
1671 end
1672
1672
1673 def test_move_to_another_project_with_disabled_tracker
1673 def test_move_to_another_project_with_disabled_tracker
1674 issue = Issue.find(1)
1674 issue = Issue.find(1)
1675 target = Project.find(2)
1675 target = Project.find(2)
1676 target.tracker_ids = [3]
1676 target.tracker_ids = [3]
1677 target.save
1677 target.save
1678 issue.project = target
1678 issue.project = target
1679 assert issue.save
1679 assert issue.save
1680 issue.reload
1680 issue.reload
1681 assert_equal 2, issue.project_id
1681 assert_equal 2, issue.project_id
1682 assert_equal 3, issue.tracker_id
1682 assert_equal 3, issue.tracker_id
1683 end
1683 end
1684
1684
1685 def test_copy_to_the_same_project
1685 def test_copy_to_the_same_project
1686 issue = Issue.find(1)
1686 issue = Issue.find(1)
1687 copy = issue.copy
1687 copy = issue.copy
1688 assert_difference 'Issue.count' do
1688 assert_difference 'Issue.count' do
1689 copy.save!
1689 copy.save!
1690 end
1690 end
1691 assert_kind_of Issue, copy
1691 assert_kind_of Issue, copy
1692 assert_equal issue.project, copy.project
1692 assert_equal issue.project, copy.project
1693 assert_equal "125", copy.custom_value_for(2).value
1693 assert_equal "125", copy.custom_value_for(2).value
1694 end
1694 end
1695
1695
1696 def test_copy_to_another_project_and_tracker
1696 def test_copy_to_another_project_and_tracker
1697 issue = Issue.find(1)
1697 issue = Issue.find(1)
1698 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1698 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1699 assert_difference 'Issue.count' do
1699 assert_difference 'Issue.count' do
1700 copy.save!
1700 copy.save!
1701 end
1701 end
1702 copy.reload
1702 copy.reload
1703 assert_kind_of Issue, copy
1703 assert_kind_of Issue, copy
1704 assert_equal Project.find(3), copy.project
1704 assert_equal Project.find(3), copy.project
1705 assert_equal Tracker.find(2), copy.tracker
1705 assert_equal Tracker.find(2), copy.tracker
1706 # Custom field #2 is not associated with target tracker
1706 # Custom field #2 is not associated with target tracker
1707 assert_nil copy.custom_value_for(2)
1707 assert_nil copy.custom_value_for(2)
1708 end
1708 end
1709
1709
1710 test "#copy should not create a journal" do
1710 test "#copy should not create a journal" do
1711 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :assigned_to_id => 3}, :link => false)
1711 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :assigned_to_id => 3}, :link => false)
1712 copy.save!
1712 copy.save!
1713 assert_equal 0, copy.reload.journals.size
1713 assert_equal 0, copy.reload.journals.size
1714 end
1714 end
1715
1715
1716 test "#copy should allow assigned_to changes" do
1716 test "#copy should allow assigned_to changes" do
1717 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1717 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1718 assert_equal 3, copy.assigned_to_id
1718 assert_equal 3, copy.assigned_to_id
1719 end
1719 end
1720
1720
1721 test "#copy should allow status changes" do
1721 test "#copy should allow status changes" do
1722 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1722 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1723 assert_equal 2, copy.status_id
1723 assert_equal 2, copy.status_id
1724 end
1724 end
1725
1725
1726 test "#copy should allow start date changes" do
1726 test "#copy should allow start date changes" do
1727 date = Date.today
1727 date = Date.today
1728 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1728 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1729 assert_equal date, copy.start_date
1729 assert_equal date, copy.start_date
1730 end
1730 end
1731
1731
1732 test "#copy should allow due date changes" do
1732 test "#copy should allow due date changes" do
1733 date = Date.today
1733 date = Date.today
1734 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1734 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1735 assert_equal date, copy.due_date
1735 assert_equal date, copy.due_date
1736 end
1736 end
1737
1737
1738 test "#copy should set current user as author" do
1738 test "#copy should set current user as author" do
1739 User.current = User.find(9)
1739 User.current = User.find(9)
1740 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1740 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1741 assert_equal User.current, copy.author
1741 assert_equal User.current, copy.author
1742 end
1742 end
1743
1743
1744 test "#copy should create a journal with notes" do
1744 test "#copy should create a journal with notes" do
1745 date = Date.today
1745 date = Date.today
1746 notes = "Notes added when copying"
1746 notes = "Notes added when copying"
1747 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :start_date => date}, :link => false)
1747 copy = Issue.find(1).copy({:project_id => 3, :tracker_id => 2, :start_date => date}, :link => false)
1748 copy.init_journal(User.current, notes)
1748 copy.init_journal(User.current, notes)
1749 copy.save!
1749 copy.save!
1750
1750
1751 assert_equal 1, copy.journals.size
1751 assert_equal 1, copy.journals.size
1752 journal = copy.journals.first
1752 journal = copy.journals.first
1753 assert_equal 0, journal.details.size
1753 assert_equal 0, journal.details.size
1754 assert_equal notes, journal.notes
1754 assert_equal notes, journal.notes
1755 end
1755 end
1756
1756
1757 def test_valid_parent_project
1757 def test_valid_parent_project
1758 issue = Issue.find(1)
1758 issue = Issue.find(1)
1759 issue_in_same_project = Issue.find(2)
1759 issue_in_same_project = Issue.find(2)
1760 issue_in_child_project = Issue.find(5)
1760 issue_in_child_project = Issue.find(5)
1761 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1761 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1762 issue_in_other_child_project = Issue.find(6)
1762 issue_in_other_child_project = Issue.find(6)
1763 issue_in_different_tree = Issue.find(4)
1763 issue_in_different_tree = Issue.find(4)
1764
1764
1765 with_settings :cross_project_subtasks => '' do
1765 with_settings :cross_project_subtasks => '' do
1766 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1766 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1767 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1767 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1768 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1768 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1769 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1769 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1770 end
1770 end
1771
1771
1772 with_settings :cross_project_subtasks => 'system' do
1772 with_settings :cross_project_subtasks => 'system' do
1773 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1773 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1774 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1774 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1775 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1775 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1776 end
1776 end
1777
1777
1778 with_settings :cross_project_subtasks => 'tree' do
1778 with_settings :cross_project_subtasks => 'tree' do
1779 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1779 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1780 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1780 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1781 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1781 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1782 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1782 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1783
1783
1784 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1784 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1785 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1785 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1786 end
1786 end
1787
1787
1788 with_settings :cross_project_subtasks => 'descendants' do
1788 with_settings :cross_project_subtasks => 'descendants' do
1789 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1789 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1790 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1790 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1791 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1791 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1792 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1792 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1793
1793
1794 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1794 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1795 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1795 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1796 end
1796 end
1797 end
1797 end
1798
1798
1799 def test_recipients_should_include_previous_assignee
1799 def test_recipients_should_include_previous_assignee
1800 user = User.find(3)
1800 user = User.find(3)
1801 user.members.update_all ["mail_notification = ?", false]
1801 user.members.update_all ["mail_notification = ?", false]
1802 user.update_attribute :mail_notification, 'only_assigned'
1802 user.update_attribute :mail_notification, 'only_assigned'
1803
1803
1804 issue = Issue.find(2)
1804 issue = Issue.find(2)
1805 issue.assigned_to = nil
1805 issue.assigned_to = nil
1806 assert_include user.mail, issue.recipients
1806 assert_include user.mail, issue.recipients
1807 issue.save!
1807 issue.save!
1808 assert !issue.recipients.include?(user.mail)
1808 assert !issue.recipients.include?(user.mail)
1809 end
1809 end
1810
1810
1811 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1811 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1812 issue = Issue.find(12)
1812 issue = Issue.find(12)
1813 assert issue.recipients.include?(issue.author.mail)
1813 assert issue.recipients.include?(issue.author.mail)
1814 # copy the issue to a private project
1814 # copy the issue to a private project
1815 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1815 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1816 # author is not a member of project anymore
1816 # author is not a member of project anymore
1817 assert !copy.recipients.include?(copy.author.mail)
1817 assert !copy.recipients.include?(copy.author.mail)
1818 end
1818 end
1819
1819
1820 def test_recipients_should_include_the_assigned_group_members
1820 def test_recipients_should_include_the_assigned_group_members
1821 group_member = User.generate!
1821 group_member = User.generate!
1822 group = Group.generate!
1822 group = Group.generate!
1823 group.users << group_member
1823 group.users << group_member
1824
1824
1825 issue = Issue.find(12)
1825 issue = Issue.find(12)
1826 issue.assigned_to = group
1826 issue.assigned_to = group
1827 assert issue.recipients.include?(group_member.mail)
1827 assert issue.recipients.include?(group_member.mail)
1828 end
1828 end
1829
1829
1830 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1830 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1831 user = User.find(3)
1831 user = User.find(3)
1832 issue = Issue.find(9)
1832 issue = Issue.find(9)
1833 Watcher.create!(:user => user, :watchable => issue)
1833 Watcher.create!(:user => user, :watchable => issue)
1834 assert issue.watched_by?(user)
1834 assert issue.watched_by?(user)
1835 assert !issue.watcher_recipients.include?(user.mail)
1835 assert !issue.watcher_recipients.include?(user.mail)
1836 end
1836 end
1837
1837
1838 def test_issue_destroy
1838 def test_issue_destroy
1839 Issue.find(1).destroy
1839 Issue.find(1).destroy
1840 assert_nil Issue.find_by_id(1)
1840 assert_nil Issue.find_by_id(1)
1841 assert_nil TimeEntry.find_by_issue_id(1)
1841 assert_nil TimeEntry.find_by_issue_id(1)
1842 end
1842 end
1843
1843
1844 def test_destroy_should_delete_time_entries_custom_values
1844 def test_destroy_should_delete_time_entries_custom_values
1845 issue = Issue.generate!
1845 issue = Issue.generate!
1846 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1846 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1847
1847
1848 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1848 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1849 assert issue.destroy
1849 assert issue.destroy
1850 end
1850 end
1851 end
1851 end
1852
1852
1853 def test_destroying_a_deleted_issue_should_not_raise_an_error
1853 def test_destroying_a_deleted_issue_should_not_raise_an_error
1854 issue = Issue.find(1)
1854 issue = Issue.find(1)
1855 Issue.find(1).destroy
1855 Issue.find(1).destroy
1856
1856
1857 assert_nothing_raised do
1857 assert_nothing_raised do
1858 assert_no_difference 'Issue.count' do
1858 assert_no_difference 'Issue.count' do
1859 issue.destroy
1859 issue.destroy
1860 end
1860 end
1861 assert issue.destroyed?
1861 assert issue.destroyed?
1862 end
1862 end
1863 end
1863 end
1864
1864
1865 def test_destroying_a_stale_issue_should_not_raise_an_error
1865 def test_destroying_a_stale_issue_should_not_raise_an_error
1866 issue = Issue.find(1)
1866 issue = Issue.find(1)
1867 Issue.find(1).update_attribute :subject, "Updated"
1867 Issue.find(1).update_attribute :subject, "Updated"
1868
1868
1869 assert_nothing_raised do
1869 assert_nothing_raised do
1870 assert_difference 'Issue.count', -1 do
1870 assert_difference 'Issue.count', -1 do
1871 issue.destroy
1871 issue.destroy
1872 end
1872 end
1873 assert issue.destroyed?
1873 assert issue.destroyed?
1874 end
1874 end
1875 end
1875 end
1876
1876
1877 def test_blocked
1877 def test_blocked
1878 blocked_issue = Issue.find(9)
1878 blocked_issue = Issue.find(9)
1879 blocking_issue = Issue.find(10)
1879 blocking_issue = Issue.find(10)
1880
1880
1881 assert blocked_issue.blocked?
1881 assert blocked_issue.blocked?
1882 assert !blocking_issue.blocked?
1882 assert !blocking_issue.blocked?
1883 end
1883 end
1884
1884
1885 def test_blocked_issues_dont_allow_closed_statuses
1885 def test_blocked_issues_dont_allow_closed_statuses
1886 blocked_issue = Issue.find(9)
1886 blocked_issue = Issue.find(9)
1887
1887
1888 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1888 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1889 assert !allowed_statuses.empty?
1889 assert !allowed_statuses.empty?
1890 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1890 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1891 assert closed_statuses.empty?
1891 assert closed_statuses.empty?
1892 end
1892 end
1893
1893
1894 def test_unblocked_issues_allow_closed_statuses
1894 def test_unblocked_issues_allow_closed_statuses
1895 blocking_issue = Issue.find(10)
1895 blocking_issue = Issue.find(10)
1896
1896
1897 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1897 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1898 assert !allowed_statuses.empty?
1898 assert !allowed_statuses.empty?
1899 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1899 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1900 assert !closed_statuses.empty?
1900 assert !closed_statuses.empty?
1901 end
1901 end
1902
1902
1903 def test_reschedule_an_issue_without_dates
1903 def test_reschedule_an_issue_without_dates
1904 with_settings :non_working_week_days => [] do
1904 with_settings :non_working_week_days => [] do
1905 issue = Issue.new(:start_date => nil, :due_date => nil)
1905 issue = Issue.new(:start_date => nil, :due_date => nil)
1906 issue.reschedule_on '2012-10-09'.to_date
1906 issue.reschedule_on '2012-10-09'.to_date
1907 assert_equal '2012-10-09'.to_date, issue.start_date
1907 assert_equal '2012-10-09'.to_date, issue.start_date
1908 assert_equal '2012-10-09'.to_date, issue.due_date
1908 assert_equal '2012-10-09'.to_date, issue.due_date
1909 end
1909 end
1910
1910
1911 with_settings :non_working_week_days => %w(6 7) do
1911 with_settings :non_working_week_days => %w(6 7) do
1912 issue = Issue.new(:start_date => nil, :due_date => nil)
1912 issue = Issue.new(:start_date => nil, :due_date => nil)
1913 issue.reschedule_on '2012-10-09'.to_date
1913 issue.reschedule_on '2012-10-09'.to_date
1914 assert_equal '2012-10-09'.to_date, issue.start_date
1914 assert_equal '2012-10-09'.to_date, issue.start_date
1915 assert_equal '2012-10-09'.to_date, issue.due_date
1915 assert_equal '2012-10-09'.to_date, issue.due_date
1916
1916
1917 issue = Issue.new(:start_date => nil, :due_date => nil)
1917 issue = Issue.new(:start_date => nil, :due_date => nil)
1918 issue.reschedule_on '2012-10-13'.to_date
1918 issue.reschedule_on '2012-10-13'.to_date
1919 assert_equal '2012-10-15'.to_date, issue.start_date
1919 assert_equal '2012-10-15'.to_date, issue.start_date
1920 assert_equal '2012-10-15'.to_date, issue.due_date
1920 assert_equal '2012-10-15'.to_date, issue.due_date
1921 end
1921 end
1922 end
1922 end
1923
1923
1924 def test_reschedule_an_issue_with_start_date
1924 def test_reschedule_an_issue_with_start_date
1925 with_settings :non_working_week_days => [] do
1925 with_settings :non_working_week_days => [] do
1926 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1926 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1927 issue.reschedule_on '2012-10-13'.to_date
1927 issue.reschedule_on '2012-10-13'.to_date
1928 assert_equal '2012-10-13'.to_date, issue.start_date
1928 assert_equal '2012-10-13'.to_date, issue.start_date
1929 assert_equal '2012-10-13'.to_date, issue.due_date
1929 assert_equal '2012-10-13'.to_date, issue.due_date
1930 end
1930 end
1931
1931
1932 with_settings :non_working_week_days => %w(6 7) do
1932 with_settings :non_working_week_days => %w(6 7) do
1933 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1933 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1934 issue.reschedule_on '2012-10-11'.to_date
1934 issue.reschedule_on '2012-10-11'.to_date
1935 assert_equal '2012-10-11'.to_date, issue.start_date
1935 assert_equal '2012-10-11'.to_date, issue.start_date
1936 assert_equal '2012-10-11'.to_date, issue.due_date
1936 assert_equal '2012-10-11'.to_date, issue.due_date
1937
1937
1938 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1938 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1939 issue.reschedule_on '2012-10-13'.to_date
1939 issue.reschedule_on '2012-10-13'.to_date
1940 assert_equal '2012-10-15'.to_date, issue.start_date
1940 assert_equal '2012-10-15'.to_date, issue.start_date
1941 assert_equal '2012-10-15'.to_date, issue.due_date
1941 assert_equal '2012-10-15'.to_date, issue.due_date
1942 end
1942 end
1943 end
1943 end
1944
1944
1945 def test_reschedule_an_issue_with_start_and_due_dates
1945 def test_reschedule_an_issue_with_start_and_due_dates
1946 with_settings :non_working_week_days => [] do
1946 with_settings :non_working_week_days => [] do
1947 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1947 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1948 issue.reschedule_on '2012-10-13'.to_date
1948 issue.reschedule_on '2012-10-13'.to_date
1949 assert_equal '2012-10-13'.to_date, issue.start_date
1949 assert_equal '2012-10-13'.to_date, issue.start_date
1950 assert_equal '2012-10-19'.to_date, issue.due_date
1950 assert_equal '2012-10-19'.to_date, issue.due_date
1951 end
1951 end
1952
1952
1953 with_settings :non_working_week_days => %w(6 7) do
1953 with_settings :non_working_week_days => %w(6 7) do
1954 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1954 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1955 issue.reschedule_on '2012-10-11'.to_date
1955 issue.reschedule_on '2012-10-11'.to_date
1956 assert_equal '2012-10-11'.to_date, issue.start_date
1956 assert_equal '2012-10-11'.to_date, issue.start_date
1957 assert_equal '2012-10-23'.to_date, issue.due_date
1957 assert_equal '2012-10-23'.to_date, issue.due_date
1958
1958
1959 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1959 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1960 issue.reschedule_on '2012-10-13'.to_date
1960 issue.reschedule_on '2012-10-13'.to_date
1961 assert_equal '2012-10-15'.to_date, issue.start_date
1961 assert_equal '2012-10-15'.to_date, issue.start_date
1962 assert_equal '2012-10-25'.to_date, issue.due_date
1962 assert_equal '2012-10-25'.to_date, issue.due_date
1963 end
1963 end
1964 end
1964 end
1965
1965
1966 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1966 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1967 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1967 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1968 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1968 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1969 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1969 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1970 :relation_type => IssueRelation::TYPE_PRECEDES)
1970 :relation_type => IssueRelation::TYPE_PRECEDES)
1971 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1971 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1972
1972
1973 issue1.reload
1973 issue1.reload
1974 issue1.due_date = '2012-10-23'
1974 issue1.due_date = '2012-10-23'
1975 issue1.save!
1975 issue1.save!
1976 issue2.reload
1976 issue2.reload
1977 assert_equal Date.parse('2012-10-24'), issue2.start_date
1977 assert_equal Date.parse('2012-10-24'), issue2.start_date
1978 assert_equal Date.parse('2012-10-26'), issue2.due_date
1978 assert_equal Date.parse('2012-10-26'), issue2.due_date
1979 end
1979 end
1980
1980
1981 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1981 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1982 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1982 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1983 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1983 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1984 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1984 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1985 :relation_type => IssueRelation::TYPE_PRECEDES)
1985 :relation_type => IssueRelation::TYPE_PRECEDES)
1986 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1986 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1987
1987
1988 issue1.reload
1988 issue1.reload
1989 issue1.start_date = '2012-09-17'
1989 issue1.start_date = '2012-09-17'
1990 issue1.due_date = '2012-09-18'
1990 issue1.due_date = '2012-09-18'
1991 issue1.save!
1991 issue1.save!
1992 issue2.reload
1992 issue2.reload
1993 assert_equal Date.parse('2012-09-19'), issue2.start_date
1993 assert_equal Date.parse('2012-09-19'), issue2.start_date
1994 assert_equal Date.parse('2012-09-21'), issue2.due_date
1994 assert_equal Date.parse('2012-09-21'), issue2.due_date
1995 end
1995 end
1996
1996
1997 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1997 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1998 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1998 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1999 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1999 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2000 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
2000 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
2001 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2001 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2002 :relation_type => IssueRelation::TYPE_PRECEDES)
2002 :relation_type => IssueRelation::TYPE_PRECEDES)
2003 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
2003 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
2004 :relation_type => IssueRelation::TYPE_PRECEDES)
2004 :relation_type => IssueRelation::TYPE_PRECEDES)
2005 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
2005 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
2006
2006
2007 issue1.reload
2007 issue1.reload
2008 issue1.start_date = '2012-09-17'
2008 issue1.start_date = '2012-09-17'
2009 issue1.due_date = '2012-09-18'
2009 issue1.due_date = '2012-09-18'
2010 issue1.save!
2010 issue1.save!
2011 issue2.reload
2011 issue2.reload
2012 # Issue 2 must start after Issue 3
2012 # Issue 2 must start after Issue 3
2013 assert_equal Date.parse('2012-10-03'), issue2.start_date
2013 assert_equal Date.parse('2012-10-03'), issue2.start_date
2014 assert_equal Date.parse('2012-10-05'), issue2.due_date
2014 assert_equal Date.parse('2012-10-05'), issue2.due_date
2015 end
2015 end
2016
2016
2017 def test_rescheduling_a_stale_issue_should_not_raise_an_error
2017 def test_rescheduling_a_stale_issue_should_not_raise_an_error
2018 with_settings :non_working_week_days => [] do
2018 with_settings :non_working_week_days => [] do
2019 stale = Issue.find(1)
2019 stale = Issue.find(1)
2020 issue = Issue.find(1)
2020 issue = Issue.find(1)
2021 issue.subject = "Updated"
2021 issue.subject = "Updated"
2022 issue.save!
2022 issue.save!
2023 date = 10.days.from_now.to_date
2023 date = 10.days.from_now.to_date
2024 assert_nothing_raised do
2024 assert_nothing_raised do
2025 stale.reschedule_on!(date)
2025 stale.reschedule_on!(date)
2026 end
2026 end
2027 assert_equal date, stale.reload.start_date
2027 assert_equal date, stale.reload.start_date
2028 end
2028 end
2029 end
2029 end
2030
2030
2031 def test_child_issue_should_consider_parent_soonest_start_on_create
2031 def test_child_issue_should_consider_parent_soonest_start_on_create
2032 set_language_if_valid 'en'
2032 set_language_if_valid 'en'
2033 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2033 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
2034 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
2034 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
2035 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2035 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
2036 :relation_type => IssueRelation::TYPE_PRECEDES)
2036 :relation_type => IssueRelation::TYPE_PRECEDES)
2037 issue1.reload
2037 issue1.reload
2038 issue2.reload
2038 issue2.reload
2039 assert_equal Date.parse('2012-10-18'), issue2.start_date
2039 assert_equal Date.parse('2012-10-18'), issue2.start_date
2040
2040
2041 with_settings :date_format => '%m/%d/%Y' do
2041 with_settings :date_format => '%m/%d/%Y' do
2042 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
2042 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
2043 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
2043 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
2044 assert !child.valid?
2044 assert !child.valid?
2045 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
2045 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
2046 assert_equal Date.parse('2012-10-18'), child.soonest_start
2046 assert_equal Date.parse('2012-10-18'), child.soonest_start
2047 child.start_date = '2012-10-18'
2047 child.start_date = '2012-10-18'
2048 assert child.save
2048 assert child.save
2049 end
2049 end
2050 end
2050 end
2051
2051
2052 def test_setting_parent_to_a_an_issue_that_precedes_should_not_validate
2052 def test_setting_parent_to_a_an_issue_that_precedes_should_not_validate
2053 # tests that 3 cannot have 1 as parent:
2053 # tests that 3 cannot have 1 as parent:
2054 #
2054 #
2055 # 1 -> 2 -> 3
2055 # 1 -> 2 -> 3
2056 #
2056 #
2057 set_language_if_valid 'en'
2057 set_language_if_valid 'en'
2058 issue1 = Issue.generate!
2058 issue1 = Issue.generate!
2059 issue2 = Issue.generate!
2059 issue2 = Issue.generate!
2060 issue3 = Issue.generate!
2060 issue3 = Issue.generate!
2061 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2061 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2062 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2062 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2063 issue3.reload
2063 issue3.reload
2064 issue3.parent_issue_id = issue1.id
2064 issue3.parent_issue_id = issue1.id
2065 assert !issue3.valid?
2065 assert !issue3.valid?
2066 assert_include 'Parent task is invalid', issue3.errors.full_messages
2066 assert_include 'Parent task is invalid', issue3.errors.full_messages
2067 end
2067 end
2068
2068
2069 def test_setting_parent_to_a_an_issue_that_follows_should_not_validate
2069 def test_setting_parent_to_a_an_issue_that_follows_should_not_validate
2070 # tests that 1 cannot have 3 as parent:
2070 # tests that 1 cannot have 3 as parent:
2071 #
2071 #
2072 # 1 -> 2 -> 3
2072 # 1 -> 2 -> 3
2073 #
2073 #
2074 set_language_if_valid 'en'
2074 set_language_if_valid 'en'
2075 issue1 = Issue.generate!
2075 issue1 = Issue.generate!
2076 issue2 = Issue.generate!
2076 issue2 = Issue.generate!
2077 issue3 = Issue.generate!
2077 issue3 = Issue.generate!
2078 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2078 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2079 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2079 IssueRelation.create!(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
2080 issue1.reload
2080 issue1.reload
2081 issue1.parent_issue_id = issue3.id
2081 issue1.parent_issue_id = issue3.id
2082 assert !issue1.valid?
2082 assert !issue1.valid?
2083 assert_include 'Parent task is invalid', issue1.errors.full_messages
2083 assert_include 'Parent task is invalid', issue1.errors.full_messages
2084 end
2084 end
2085
2085
2086 def test_setting_parent_to_a_an_issue_that_precedes_through_hierarchy_should_not_validate
2086 def test_setting_parent_to_a_an_issue_that_precedes_through_hierarchy_should_not_validate
2087 # tests that 4 cannot have 1 as parent:
2087 # tests that 4 cannot have 1 as parent:
2088 # changing the due date of 4 would update the end date of 1 which would reschedule 2
2088 # changing the due date of 4 would update the end date of 1 which would reschedule 2
2089 # which would change the end date of 3 which would reschedule 4 and so on...
2089 # which would change the end date of 3 which would reschedule 4 and so on...
2090 #
2090 #
2091 # 3 -> 4
2091 # 3 -> 4
2092 # ^
2092 # ^
2093 # 1 -> 2
2093 # 1 -> 2
2094 #
2094 #
2095 set_language_if_valid 'en'
2095 set_language_if_valid 'en'
2096 issue1 = Issue.generate!
2096 issue1 = Issue.generate!
2097 issue2 = Issue.generate!
2097 issue2 = Issue.generate!
2098 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2098 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
2099 issue3 = Issue.generate!
2099 issue3 = Issue.generate!
2100 issue2.reload
2100 issue2.reload
2101 issue2.parent_issue_id = issue3.id
2101 issue2.parent_issue_id = issue3.id
2102 issue2.save!
2102 issue2.save!
2103 issue4 = Issue.generate!
2103 issue4 = Issue.generate!
2104 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
2104 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
2105 issue4.reload
2105 issue4.reload
2106 issue4.parent_issue_id = issue1.id
2106 issue4.parent_issue_id = issue1.id
2107 assert !issue4.valid?
2107 assert !issue4.valid?
2108 assert_include 'Parent task is invalid', issue4.errors.full_messages
2108 assert_include 'Parent task is invalid', issue4.errors.full_messages
2109 end
2109 end
2110
2110
2111 def test_issue_and_following_issue_should_be_able_to_be_moved_to_the_same_parent
2111 def test_issue_and_following_issue_should_be_able_to_be_moved_to_the_same_parent
2112 set_language_if_valid 'en'
2112 set_language_if_valid 'en'
2113 issue1 = Issue.generate!
2113 issue1 = Issue.generate!
2114 issue2 = Issue.generate!
2114 issue2 = Issue.generate!
2115 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_FOLLOWS)
2115 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_FOLLOWS)
2116 parent = Issue.generate!
2116 parent = Issue.generate!
2117 issue1.reload.parent_issue_id = parent.id
2117 issue1.reload.parent_issue_id = parent.id
2118 assert_save issue1
2118 assert_save issue1
2119 parent.reload
2119 parent.reload
2120 issue2.reload.parent_issue_id = parent.id
2120 issue2.reload.parent_issue_id = parent.id
2121 assert_save issue2
2121 assert_save issue2
2122 assert IssueRelation.exists?(relation.id)
2122 assert IssueRelation.exists?(relation.id)
2123 end
2123 end
2124
2124
2125 def test_issue_and_preceding_issue_should_be_able_to_be_moved_to_the_same_parent
2125 def test_issue_and_preceding_issue_should_be_able_to_be_moved_to_the_same_parent
2126 set_language_if_valid 'en'
2126 set_language_if_valid 'en'
2127 issue1 = Issue.generate!
2127 issue1 = Issue.generate!
2128 issue2 = Issue.generate!
2128 issue2 = Issue.generate!
2129 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
2129 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
2130 parent = Issue.generate!
2130 parent = Issue.generate!
2131 issue1.reload.parent_issue_id = parent.id
2131 issue1.reload.parent_issue_id = parent.id
2132 assert_save issue1
2132 assert_save issue1
2133 parent.reload
2133 parent.reload
2134 issue2.reload.parent_issue_id = parent.id
2134 issue2.reload.parent_issue_id = parent.id
2135 assert_save issue2
2135 assert_save issue2
2136 assert IssueRelation.exists?(relation.id)
2136 assert IssueRelation.exists?(relation.id)
2137 end
2137 end
2138
2138
2139 def test_issue_and_blocked_issue_should_be_able_to_be_moved_to_the_same_parent
2139 def test_issue_and_blocked_issue_should_be_able_to_be_moved_to_the_same_parent
2140 set_language_if_valid 'en'
2140 set_language_if_valid 'en'
2141 issue1 = Issue.generate!
2141 issue1 = Issue.generate!
2142 issue2 = Issue.generate!
2142 issue2 = Issue.generate!
2143 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKED)
2143 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKED)
2144 parent = Issue.generate!
2144 parent = Issue.generate!
2145 issue1.reload.parent_issue_id = parent.id
2145 issue1.reload.parent_issue_id = parent.id
2146 assert_save issue1
2146 assert_save issue1
2147 parent.reload
2147 parent.reload
2148 issue2.reload.parent_issue_id = parent.id
2148 issue2.reload.parent_issue_id = parent.id
2149 assert_save issue2
2149 assert_save issue2
2150 assert IssueRelation.exists?(relation.id)
2150 assert IssueRelation.exists?(relation.id)
2151 end
2151 end
2152
2152
2153 def test_issue_and_blocking_issue_should_be_able_to_be_moved_to_the_same_parent
2153 def test_issue_and_blocking_issue_should_be_able_to_be_moved_to_the_same_parent
2154 set_language_if_valid 'en'
2154 set_language_if_valid 'en'
2155 issue1 = Issue.generate!
2155 issue1 = Issue.generate!
2156 issue2 = Issue.generate!
2156 issue2 = Issue.generate!
2157 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKS)
2157 relation = IssueRelation.create!(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_BLOCKS)
2158 parent = Issue.generate!
2158 parent = Issue.generate!
2159 issue1.reload.parent_issue_id = parent.id
2159 issue1.reload.parent_issue_id = parent.id
2160 assert_save issue1
2160 assert_save issue1
2161 parent.reload
2161 parent.reload
2162 issue2.reload.parent_issue_id = parent.id
2162 issue2.reload.parent_issue_id = parent.id
2163 assert_save issue2
2163 assert_save issue2
2164 assert IssueRelation.exists?(relation.id)
2164 assert IssueRelation.exists?(relation.id)
2165 end
2165 end
2166
2166
2167 def test_issue_copy_should_be_able_to_be_moved_to_the_same_parent_as_copied_issue
2167 def test_issue_copy_should_be_able_to_be_moved_to_the_same_parent_as_copied_issue
2168 issue = Issue.generate!
2168 issue = Issue.generate!
2169 parent = Issue.generate!
2169 parent = Issue.generate!
2170 issue.parent_issue_id = parent.id
2170 issue.parent_issue_id = parent.id
2171 issue.save!
2171 issue.save!
2172 issue.reload
2172 issue.reload
2173
2173
2174 copy = Issue.new.copy_from(issue, :link => true)
2174 copy = Issue.new.copy_from(issue, :link => true)
2175 relation = new_record(IssueRelation) do
2175 relation = new_record(IssueRelation) do
2176 copy.save!
2176 copy.save!
2177 end
2177 end
2178
2178
2179 copy.parent_issue_id = parent.id
2179 copy.parent_issue_id = parent.id
2180 assert_save copy
2180 assert_save copy
2181 assert IssueRelation.exists?(relation.id)
2181 assert IssueRelation.exists?(relation.id)
2182 end
2182 end
2183
2183
2184 def test_overdue
2184 def test_overdue
2185 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
2185 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
2186 assert !Issue.new(:due_date => Date.today).overdue?
2186 assert !Issue.new(:due_date => Date.today).overdue?
2187 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
2187 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
2188 assert !Issue.new(:due_date => nil).overdue?
2188 assert !Issue.new(:due_date => nil).overdue?
2189 assert !Issue.new(:due_date => 1.day.ago.to_date,
2189 assert !Issue.new(:due_date => 1.day.ago.to_date,
2190 :status => IssueStatus.where(:is_closed => true).first
2190 :status => IssueStatus.where(:is_closed => true).first
2191 ).overdue?
2191 ).overdue?
2192 end
2192 end
2193
2193
2194 test "#behind_schedule? should be false if the issue has no start_date" do
2194 test "#behind_schedule? should be false if the issue has no start_date" do
2195 assert !Issue.new(:start_date => nil,
2195 assert !Issue.new(:start_date => nil,
2196 :due_date => 1.day.from_now.to_date,
2196 :due_date => 1.day.from_now.to_date,
2197 :done_ratio => 0).behind_schedule?
2197 :done_ratio => 0).behind_schedule?
2198 end
2198 end
2199
2199
2200 test "#behind_schedule? should be false if the issue has no end_date" do
2200 test "#behind_schedule? should be false if the issue has no end_date" do
2201 assert !Issue.new(:start_date => 1.day.from_now.to_date,
2201 assert !Issue.new(:start_date => 1.day.from_now.to_date,
2202 :due_date => nil,
2202 :due_date => nil,
2203 :done_ratio => 0).behind_schedule?
2203 :done_ratio => 0).behind_schedule?
2204 end
2204 end
2205
2205
2206 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
2206 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
2207 assert !Issue.new(:start_date => 50.days.ago.to_date,
2207 assert !Issue.new(:start_date => 50.days.ago.to_date,
2208 :due_date => 50.days.from_now.to_date,
2208 :due_date => 50.days.from_now.to_date,
2209 :done_ratio => 90).behind_schedule?
2209 :done_ratio => 90).behind_schedule?
2210 end
2210 end
2211
2211
2212 test "#behind_schedule? should be true if the issue hasn't been started at all" do
2212 test "#behind_schedule? should be true if the issue hasn't been started at all" do
2213 assert Issue.new(:start_date => 1.day.ago.to_date,
2213 assert Issue.new(:start_date => 1.day.ago.to_date,
2214 :due_date => 1.day.from_now.to_date,
2214 :due_date => 1.day.from_now.to_date,
2215 :done_ratio => 0).behind_schedule?
2215 :done_ratio => 0).behind_schedule?
2216 end
2216 end
2217
2217
2218 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
2218 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
2219 assert Issue.new(:start_date => 100.days.ago.to_date,
2219 assert Issue.new(:start_date => 100.days.ago.to_date,
2220 :due_date => Date.today,
2220 :due_date => Date.today,
2221 :done_ratio => 90).behind_schedule?
2221 :done_ratio => 90).behind_schedule?
2222 end
2222 end
2223
2223
2224 test "#assignable_users should be Users" do
2224 test "#assignable_users should be Users" do
2225 assert_kind_of User, Issue.find(1).assignable_users.first
2225 assert_kind_of User, Issue.find(1).assignable_users.first
2226 end
2226 end
2227
2227
2228 test "#assignable_users should include the issue author" do
2228 test "#assignable_users should include the issue author" do
2229 non_project_member = User.generate!
2229 non_project_member = User.generate!
2230 issue = Issue.generate!(:author => non_project_member)
2230 issue = Issue.generate!(:author => non_project_member)
2231
2231
2232 assert issue.assignable_users.include?(non_project_member)
2232 assert issue.assignable_users.include?(non_project_member)
2233 end
2233 end
2234
2234
2235 def test_assignable_users_should_not_include_anonymous_user
2235 def test_assignable_users_should_not_include_anonymous_user
2236 issue = Issue.generate!(:author => User.anonymous)
2236 issue = Issue.generate!(:author => User.anonymous)
2237
2237
2238 assert !issue.assignable_users.include?(User.anonymous)
2238 assert !issue.assignable_users.include?(User.anonymous)
2239 end
2239 end
2240
2240
2241 def test_assignable_users_should_not_include_locked_user
2241 def test_assignable_users_should_not_include_locked_user
2242 user = User.generate!
2242 user = User.generate!
2243 issue = Issue.generate!(:author => user)
2243 issue = Issue.generate!(:author => user)
2244 user.lock!
2244 user.lock!
2245
2245
2246 assert !issue.assignable_users.include?(user)
2246 assert !issue.assignable_users.include?(user)
2247 end
2247 end
2248
2248
2249 test "#assignable_users should include the current assignee" do
2249 test "#assignable_users should include the current assignee" do
2250 user = User.generate!
2250 user = User.generate!
2251 issue = Issue.generate!(:assigned_to => user)
2251 issue = Issue.generate!(:assigned_to => user)
2252 user.lock!
2252 user.lock!
2253
2253
2254 assert Issue.find(issue.id).assignable_users.include?(user)
2254 assert Issue.find(issue.id).assignable_users.include?(user)
2255 end
2255 end
2256
2256
2257 test "#assignable_users should not show the issue author twice" do
2257 test "#assignable_users should not show the issue author twice" do
2258 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
2258 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
2259 assert_equal 2, assignable_user_ids.length
2259 assert_equal 2, assignable_user_ids.length
2260
2260
2261 assignable_user_ids.each do |user_id|
2261 assignable_user_ids.each do |user_id|
2262 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
2262 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
2263 "User #{user_id} appears more or less than once"
2263 "User #{user_id} appears more or less than once"
2264 end
2264 end
2265 end
2265 end
2266
2266
2267 test "#assignable_users with issue_group_assignment should include groups" do
2267 test "#assignable_users with issue_group_assignment should include groups" do
2268 issue = Issue.new(:project => Project.find(2))
2268 issue = Issue.new(:project => Project.find(2))
2269
2269
2270 with_settings :issue_group_assignment => '1' do
2270 with_settings :issue_group_assignment => '1' do
2271 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2271 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2272 assert issue.assignable_users.include?(Group.find(11))
2272 assert issue.assignable_users.include?(Group.find(11))
2273 end
2273 end
2274 end
2274 end
2275
2275
2276 test "#assignable_users without issue_group_assignment should not include groups" do
2276 test "#assignable_users without issue_group_assignment should not include groups" do
2277 issue = Issue.new(:project => Project.find(2))
2277 issue = Issue.new(:project => Project.find(2))
2278
2278
2279 with_settings :issue_group_assignment => '0' do
2279 with_settings :issue_group_assignment => '0' do
2280 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2280 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
2281 assert !issue.assignable_users.include?(Group.find(11))
2281 assert !issue.assignable_users.include?(Group.find(11))
2282 end
2282 end
2283 end
2283 end
2284
2284
2285 def test_assignable_users_should_not_include_builtin_groups
2285 def test_assignable_users_should_not_include_builtin_groups
2286 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [1])
2286 Member.create!(:project_id => 1, :principal => Group.non_member, :role_ids => [1])
2287 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [1])
2287 Member.create!(:project_id => 1, :principal => Group.anonymous, :role_ids => [1])
2288 issue = Issue.new(:project => Project.find(1))
2288 issue = Issue.new(:project => Project.find(1))
2289
2289
2290 with_settings :issue_group_assignment => '1' do
2290 with_settings :issue_group_assignment => '1' do
2291 assert_nil issue.assignable_users.detect {|u| u.is_a?(GroupBuiltin)}
2291 assert_nil issue.assignable_users.detect {|u| u.is_a?(GroupBuiltin)}
2292 end
2292 end
2293 end
2293 end
2294
2294
2295 def test_assignable_users_should_not_include_users_that_cannot_view_the_tracker
2296 user = User.find(3)
2297 role = Role.find(2)
2298 role.set_permission_trackers :view_issues, [1, 3]
2299 role.save!
2300
2301 issue1 = Issue.new(:project_id => 1, :tracker_id => 1)
2302 issue2 = Issue.new(:project_id => 1, :tracker_id => 2)
2303
2304 assert_include user, issue1.assignable_users
2305 assert_not_include user, issue2.assignable_users
2306 end
2307
2295 def test_create_should_send_email_notification
2308 def test_create_should_send_email_notification
2296 ActionMailer::Base.deliveries.clear
2309 ActionMailer::Base.deliveries.clear
2297 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2310 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2298 :author_id => 3, :status_id => 1,
2311 :author_id => 3, :status_id => 1,
2299 :priority => IssuePriority.all.first,
2312 :priority => IssuePriority.all.first,
2300 :subject => 'test_create', :estimated_hours => '1:30')
2313 :subject => 'test_create', :estimated_hours => '1:30')
2301 with_settings :notified_events => %w(issue_added) do
2314 with_settings :notified_events => %w(issue_added) do
2302 assert issue.save
2315 assert issue.save
2303 assert_equal 1, ActionMailer::Base.deliveries.size
2316 assert_equal 1, ActionMailer::Base.deliveries.size
2304 end
2317 end
2305 end
2318 end
2306
2319
2307 def test_create_should_send_one_email_notification_with_both_settings
2320 def test_create_should_send_one_email_notification_with_both_settings
2308 ActionMailer::Base.deliveries.clear
2321 ActionMailer::Base.deliveries.clear
2309 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2322 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2310 :author_id => 3, :status_id => 1,
2323 :author_id => 3, :status_id => 1,
2311 :priority => IssuePriority.all.first,
2324 :priority => IssuePriority.all.first,
2312 :subject => 'test_create', :estimated_hours => '1:30')
2325 :subject => 'test_create', :estimated_hours => '1:30')
2313 with_settings :notified_events => %w(issue_added issue_updated) do
2326 with_settings :notified_events => %w(issue_added issue_updated) do
2314 assert issue.save
2327 assert issue.save
2315 assert_equal 1, ActionMailer::Base.deliveries.size
2328 assert_equal 1, ActionMailer::Base.deliveries.size
2316 end
2329 end
2317 end
2330 end
2318
2331
2319 def test_create_should_not_send_email_notification_with_no_setting
2332 def test_create_should_not_send_email_notification_with_no_setting
2320 ActionMailer::Base.deliveries.clear
2333 ActionMailer::Base.deliveries.clear
2321 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2334 issue = Issue.new(:project_id => 1, :tracker_id => 1,
2322 :author_id => 3, :status_id => 1,
2335 :author_id => 3, :status_id => 1,
2323 :priority => IssuePriority.all.first,
2336 :priority => IssuePriority.all.first,
2324 :subject => 'test_create', :estimated_hours => '1:30')
2337 :subject => 'test_create', :estimated_hours => '1:30')
2325 with_settings :notified_events => [] do
2338 with_settings :notified_events => [] do
2326 assert issue.save
2339 assert issue.save
2327 assert_equal 0, ActionMailer::Base.deliveries.size
2340 assert_equal 0, ActionMailer::Base.deliveries.size
2328 end
2341 end
2329 end
2342 end
2330
2343
2331 def test_update_should_notify_previous_assignee
2344 def test_update_should_notify_previous_assignee
2332 ActionMailer::Base.deliveries.clear
2345 ActionMailer::Base.deliveries.clear
2333 user = User.find(3)
2346 user = User.find(3)
2334 user.members.update_all ["mail_notification = ?", false]
2347 user.members.update_all ["mail_notification = ?", false]
2335 user.update_attribute :mail_notification, 'only_assigned'
2348 user.update_attribute :mail_notification, 'only_assigned'
2336
2349
2337 with_settings :notified_events => %w(issue_updated) do
2350 with_settings :notified_events => %w(issue_updated) do
2338 issue = Issue.find(2)
2351 issue = Issue.find(2)
2339 issue.init_journal User.find(1)
2352 issue.init_journal User.find(1)
2340 issue.assigned_to = nil
2353 issue.assigned_to = nil
2341 issue.save!
2354 issue.save!
2342 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
2355 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
2343 end
2356 end
2344 end
2357 end
2345
2358
2346 def test_stale_issue_should_not_send_email_notification
2359 def test_stale_issue_should_not_send_email_notification
2347 ActionMailer::Base.deliveries.clear
2360 ActionMailer::Base.deliveries.clear
2348 issue = Issue.find(1)
2361 issue = Issue.find(1)
2349 stale = Issue.find(1)
2362 stale = Issue.find(1)
2350
2363
2351 issue.init_journal(User.find(1))
2364 issue.init_journal(User.find(1))
2352 issue.subject = 'Subjet update'
2365 issue.subject = 'Subjet update'
2353 with_settings :notified_events => %w(issue_updated) do
2366 with_settings :notified_events => %w(issue_updated) do
2354 assert issue.save
2367 assert issue.save
2355 assert_equal 1, ActionMailer::Base.deliveries.size
2368 assert_equal 1, ActionMailer::Base.deliveries.size
2356 ActionMailer::Base.deliveries.clear
2369 ActionMailer::Base.deliveries.clear
2357
2370
2358 stale.init_journal(User.find(1))
2371 stale.init_journal(User.find(1))
2359 stale.subject = 'Another subjet update'
2372 stale.subject = 'Another subjet update'
2360 assert_raise ActiveRecord::StaleObjectError do
2373 assert_raise ActiveRecord::StaleObjectError do
2361 stale.save
2374 stale.save
2362 end
2375 end
2363 assert ActionMailer::Base.deliveries.empty?
2376 assert ActionMailer::Base.deliveries.empty?
2364 end
2377 end
2365 end
2378 end
2366
2379
2367 def test_journalized_description
2380 def test_journalized_description
2368 IssueCustomField.delete_all
2381 IssueCustomField.delete_all
2369
2382
2370 i = Issue.first
2383 i = Issue.first
2371 old_description = i.description
2384 old_description = i.description
2372 new_description = "This is the new description"
2385 new_description = "This is the new description"
2373
2386
2374 i.init_journal(User.find(2))
2387 i.init_journal(User.find(2))
2375 i.description = new_description
2388 i.description = new_description
2376 assert_difference 'Journal.count', 1 do
2389 assert_difference 'Journal.count', 1 do
2377 assert_difference 'JournalDetail.count', 1 do
2390 assert_difference 'JournalDetail.count', 1 do
2378 i.save!
2391 i.save!
2379 end
2392 end
2380 end
2393 end
2381
2394
2382 detail = JournalDetail.order('id DESC').first
2395 detail = JournalDetail.order('id DESC').first
2383 assert_equal i, detail.journal.journalized
2396 assert_equal i, detail.journal.journalized
2384 assert_equal 'attr', detail.property
2397 assert_equal 'attr', detail.property
2385 assert_equal 'description', detail.prop_key
2398 assert_equal 'description', detail.prop_key
2386 assert_equal old_description, detail.old_value
2399 assert_equal old_description, detail.old_value
2387 assert_equal new_description, detail.value
2400 assert_equal new_description, detail.value
2388 end
2401 end
2389
2402
2390 def test_blank_descriptions_should_not_be_journalized
2403 def test_blank_descriptions_should_not_be_journalized
2391 IssueCustomField.delete_all
2404 IssueCustomField.delete_all
2392 Issue.where(:id => 1).update_all("description = NULL")
2405 Issue.where(:id => 1).update_all("description = NULL")
2393
2406
2394 i = Issue.find(1)
2407 i = Issue.find(1)
2395 i.init_journal(User.find(2))
2408 i.init_journal(User.find(2))
2396 i.subject = "blank description"
2409 i.subject = "blank description"
2397 i.description = "\r\n"
2410 i.description = "\r\n"
2398
2411
2399 assert_difference 'Journal.count', 1 do
2412 assert_difference 'Journal.count', 1 do
2400 assert_difference 'JournalDetail.count', 1 do
2413 assert_difference 'JournalDetail.count', 1 do
2401 i.save!
2414 i.save!
2402 end
2415 end
2403 end
2416 end
2404 end
2417 end
2405
2418
2406 def test_journalized_multi_custom_field
2419 def test_journalized_multi_custom_field
2407 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
2420 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
2408 :is_filter => true, :is_for_all => true,
2421 :is_filter => true, :is_for_all => true,
2409 :tracker_ids => [1],
2422 :tracker_ids => [1],
2410 :possible_values => ['value1', 'value2', 'value3'],
2423 :possible_values => ['value1', 'value2', 'value3'],
2411 :multiple => true)
2424 :multiple => true)
2412
2425
2413 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
2426 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
2414 :subject => 'Test', :author_id => 1)
2427 :subject => 'Test', :author_id => 1)
2415
2428
2416 assert_difference 'Journal.count' do
2429 assert_difference 'Journal.count' do
2417 assert_difference 'JournalDetail.count' do
2430 assert_difference 'JournalDetail.count' do
2418 issue.init_journal(User.first)
2431 issue.init_journal(User.first)
2419 issue.custom_field_values = {field.id => ['value1']}
2432 issue.custom_field_values = {field.id => ['value1']}
2420 issue.save!
2433 issue.save!
2421 end
2434 end
2422 assert_difference 'JournalDetail.count' do
2435 assert_difference 'JournalDetail.count' do
2423 issue.init_journal(User.first)
2436 issue.init_journal(User.first)
2424 issue.custom_field_values = {field.id => ['value1', 'value2']}
2437 issue.custom_field_values = {field.id => ['value1', 'value2']}
2425 issue.save!
2438 issue.save!
2426 end
2439 end
2427 assert_difference 'JournalDetail.count', 2 do
2440 assert_difference 'JournalDetail.count', 2 do
2428 issue.init_journal(User.first)
2441 issue.init_journal(User.first)
2429 issue.custom_field_values = {field.id => ['value3', 'value2']}
2442 issue.custom_field_values = {field.id => ['value3', 'value2']}
2430 issue.save!
2443 issue.save!
2431 end
2444 end
2432 assert_difference 'JournalDetail.count', 2 do
2445 assert_difference 'JournalDetail.count', 2 do
2433 issue.init_journal(User.first)
2446 issue.init_journal(User.first)
2434 issue.custom_field_values = {field.id => nil}
2447 issue.custom_field_values = {field.id => nil}
2435 issue.save!
2448 issue.save!
2436 end
2449 end
2437 end
2450 end
2438 end
2451 end
2439
2452
2440 def test_description_eol_should_be_normalized
2453 def test_description_eol_should_be_normalized
2441 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
2454 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
2442 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
2455 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
2443 end
2456 end
2444
2457
2445 def test_saving_twice_should_not_duplicate_journal_details
2458 def test_saving_twice_should_not_duplicate_journal_details
2446 i = Issue.first
2459 i = Issue.first
2447 i.init_journal(User.find(2), 'Some notes')
2460 i.init_journal(User.find(2), 'Some notes')
2448 # initial changes
2461 # initial changes
2449 i.subject = 'New subject'
2462 i.subject = 'New subject'
2450 i.done_ratio = i.done_ratio + 10
2463 i.done_ratio = i.done_ratio + 10
2451 assert_difference 'Journal.count' do
2464 assert_difference 'Journal.count' do
2452 assert i.save
2465 assert i.save
2453 end
2466 end
2454 # 1 more change
2467 # 1 more change
2455 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
2468 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
2456 assert_no_difference 'Journal.count' do
2469 assert_no_difference 'Journal.count' do
2457 assert_difference 'JournalDetail.count', 1 do
2470 assert_difference 'JournalDetail.count', 1 do
2458 i.save
2471 i.save
2459 end
2472 end
2460 end
2473 end
2461 # no more change
2474 # no more change
2462 assert_no_difference 'Journal.count' do
2475 assert_no_difference 'Journal.count' do
2463 assert_no_difference 'JournalDetail.count' do
2476 assert_no_difference 'JournalDetail.count' do
2464 i.save
2477 i.save
2465 end
2478 end
2466 end
2479 end
2467 end
2480 end
2468
2481
2469 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2482 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2470 @issue = Issue.find(1)
2483 @issue = Issue.find(1)
2471 @issue_status = IssueStatus.find(1)
2484 @issue_status = IssueStatus.find(1)
2472 @issue_status.update_attribute(:default_done_ratio, 50)
2485 @issue_status.update_attribute(:default_done_ratio, 50)
2473 @issue2 = Issue.find(2)
2486 @issue2 = Issue.find(2)
2474 @issue_status2 = IssueStatus.find(2)
2487 @issue_status2 = IssueStatus.find(2)
2475 @issue_status2.update_attribute(:default_done_ratio, 0)
2488 @issue_status2.update_attribute(:default_done_ratio, 0)
2476
2489
2477 with_settings :issue_done_ratio => 'issue_field' do
2490 with_settings :issue_done_ratio => 'issue_field' do
2478 assert_equal 0, @issue.done_ratio
2491 assert_equal 0, @issue.done_ratio
2479 assert_equal 30, @issue2.done_ratio
2492 assert_equal 30, @issue2.done_ratio
2480 end
2493 end
2481
2494
2482 with_settings :issue_done_ratio => 'issue_status' do
2495 with_settings :issue_done_ratio => 'issue_status' do
2483 assert_equal 50, @issue.done_ratio
2496 assert_equal 50, @issue.done_ratio
2484 assert_equal 0, @issue2.done_ratio
2497 assert_equal 0, @issue2.done_ratio
2485 end
2498 end
2486 end
2499 end
2487
2500
2488 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2501 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2489 @issue = Issue.find(1)
2502 @issue = Issue.find(1)
2490 @issue_status = IssueStatus.find(1)
2503 @issue_status = IssueStatus.find(1)
2491 @issue_status.update_attribute(:default_done_ratio, 50)
2504 @issue_status.update_attribute(:default_done_ratio, 50)
2492 @issue2 = Issue.find(2)
2505 @issue2 = Issue.find(2)
2493 @issue_status2 = IssueStatus.find(2)
2506 @issue_status2 = IssueStatus.find(2)
2494 @issue_status2.update_attribute(:default_done_ratio, 0)
2507 @issue_status2.update_attribute(:default_done_ratio, 0)
2495
2508
2496 with_settings :issue_done_ratio => 'issue_field' do
2509 with_settings :issue_done_ratio => 'issue_field' do
2497 @issue.update_done_ratio_from_issue_status
2510 @issue.update_done_ratio_from_issue_status
2498 @issue2.update_done_ratio_from_issue_status
2511 @issue2.update_done_ratio_from_issue_status
2499
2512
2500 assert_equal 0, @issue.read_attribute(:done_ratio)
2513 assert_equal 0, @issue.read_attribute(:done_ratio)
2501 assert_equal 30, @issue2.read_attribute(:done_ratio)
2514 assert_equal 30, @issue2.read_attribute(:done_ratio)
2502 end
2515 end
2503
2516
2504 with_settings :issue_done_ratio => 'issue_status' do
2517 with_settings :issue_done_ratio => 'issue_status' do
2505 @issue.update_done_ratio_from_issue_status
2518 @issue.update_done_ratio_from_issue_status
2506 @issue2.update_done_ratio_from_issue_status
2519 @issue2.update_done_ratio_from_issue_status
2507
2520
2508 assert_equal 50, @issue.read_attribute(:done_ratio)
2521 assert_equal 50, @issue.read_attribute(:done_ratio)
2509 assert_equal 0, @issue2.read_attribute(:done_ratio)
2522 assert_equal 0, @issue2.read_attribute(:done_ratio)
2510 end
2523 end
2511 end
2524 end
2512
2525
2513 test "#by_tracker" do
2526 test "#by_tracker" do
2514 User.current = User.anonymous
2527 User.current = User.anonymous
2515 groups = Issue.by_tracker(Project.find(1))
2528 groups = Issue.by_tracker(Project.find(1))
2516 assert_equal 3, groups.count
2529 assert_equal 3, groups.count
2517 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2530 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2518 end
2531 end
2519
2532
2520 test "#by_version" do
2533 test "#by_version" do
2521 User.current = User.anonymous
2534 User.current = User.anonymous
2522 groups = Issue.by_version(Project.find(1))
2535 groups = Issue.by_version(Project.find(1))
2523 assert_equal 3, groups.count
2536 assert_equal 3, groups.count
2524 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2537 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2525 end
2538 end
2526
2539
2527 test "#by_priority" do
2540 test "#by_priority" do
2528 User.current = User.anonymous
2541 User.current = User.anonymous
2529 groups = Issue.by_priority(Project.find(1))
2542 groups = Issue.by_priority(Project.find(1))
2530 assert_equal 4, groups.count
2543 assert_equal 4, groups.count
2531 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2544 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2532 end
2545 end
2533
2546
2534 test "#by_category" do
2547 test "#by_category" do
2535 User.current = User.anonymous
2548 User.current = User.anonymous
2536 groups = Issue.by_category(Project.find(1))
2549 groups = Issue.by_category(Project.find(1))
2537 assert_equal 2, groups.count
2550 assert_equal 2, groups.count
2538 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2551 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2539 end
2552 end
2540
2553
2541 test "#by_assigned_to" do
2554 test "#by_assigned_to" do
2542 User.current = User.anonymous
2555 User.current = User.anonymous
2543 groups = Issue.by_assigned_to(Project.find(1))
2556 groups = Issue.by_assigned_to(Project.find(1))
2544 assert_equal 2, groups.count
2557 assert_equal 2, groups.count
2545 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2558 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2546 end
2559 end
2547
2560
2548 test "#by_author" do
2561 test "#by_author" do
2549 User.current = User.anonymous
2562 User.current = User.anonymous
2550 groups = Issue.by_author(Project.find(1))
2563 groups = Issue.by_author(Project.find(1))
2551 assert_equal 4, groups.count
2564 assert_equal 4, groups.count
2552 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2565 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2553 end
2566 end
2554
2567
2555 test "#by_subproject" do
2568 test "#by_subproject" do
2556 User.current = User.anonymous
2569 User.current = User.anonymous
2557 groups = Issue.by_subproject(Project.find(1))
2570 groups = Issue.by_subproject(Project.find(1))
2558 # Private descendant not visible
2571 # Private descendant not visible
2559 assert_equal 1, groups.count
2572 assert_equal 1, groups.count
2560 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2573 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2561 end
2574 end
2562
2575
2563 def test_recently_updated_scope
2576 def test_recently_updated_scope
2564 #should return the last updated issue
2577 #should return the last updated issue
2565 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2578 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2566 end
2579 end
2567
2580
2568 def test_on_active_projects_scope
2581 def test_on_active_projects_scope
2569 assert Project.find(2).archive
2582 assert Project.find(2).archive
2570
2583
2571 before = Issue.on_active_project.length
2584 before = Issue.on_active_project.length
2572 # test inclusion to results
2585 # test inclusion to results
2573 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2586 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2574 assert_equal before + 1, Issue.on_active_project.length
2587 assert_equal before + 1, Issue.on_active_project.length
2575
2588
2576 # Move to an archived project
2589 # Move to an archived project
2577 issue.project = Project.find(2)
2590 issue.project = Project.find(2)
2578 assert issue.save
2591 assert issue.save
2579 assert_equal before, Issue.on_active_project.length
2592 assert_equal before, Issue.on_active_project.length
2580 end
2593 end
2581
2594
2582 test "Issue#recipients should include project recipients" do
2595 test "Issue#recipients should include project recipients" do
2583 issue = Issue.generate!
2596 issue = Issue.generate!
2584 assert issue.project.recipients.present?
2597 assert issue.project.recipients.present?
2585 issue.project.recipients.each do |project_recipient|
2598 issue.project.recipients.each do |project_recipient|
2586 assert issue.recipients.include?(project_recipient)
2599 assert issue.recipients.include?(project_recipient)
2587 end
2600 end
2588 end
2601 end
2589
2602
2590 test "Issue#recipients should include the author if the author is active" do
2603 test "Issue#recipients should include the author if the author is active" do
2591 issue = Issue.generate!(:author => User.generate!)
2604 issue = Issue.generate!(:author => User.generate!)
2592 assert issue.author, "No author set for Issue"
2605 assert issue.author, "No author set for Issue"
2593 assert issue.recipients.include?(issue.author.mail)
2606 assert issue.recipients.include?(issue.author.mail)
2594 end
2607 end
2595
2608
2596 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2609 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2597 issue = Issue.generate!(:assigned_to => User.generate!)
2610 issue = Issue.generate!(:assigned_to => User.generate!)
2598 assert issue.assigned_to, "No assigned_to set for Issue"
2611 assert issue.assigned_to, "No assigned_to set for Issue"
2599 assert issue.recipients.include?(issue.assigned_to.mail)
2612 assert issue.recipients.include?(issue.assigned_to.mail)
2600 end
2613 end
2601
2614
2602 test "Issue#recipients should not include users who opt out of all email" do
2615 test "Issue#recipients should not include users who opt out of all email" do
2603 issue = Issue.generate!(:author => User.generate!)
2616 issue = Issue.generate!(:author => User.generate!)
2604 issue.author.update_attribute(:mail_notification, :none)
2617 issue.author.update_attribute(:mail_notification, :none)
2605 assert !issue.recipients.include?(issue.author.mail)
2618 assert !issue.recipients.include?(issue.author.mail)
2606 end
2619 end
2607
2620
2608 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2621 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2609 issue = Issue.generate!(:author => User.generate!)
2622 issue = Issue.generate!(:author => User.generate!)
2610 issue.author.update_attribute(:mail_notification, :only_assigned)
2623 issue.author.update_attribute(:mail_notification, :only_assigned)
2611 assert !issue.recipients.include?(issue.author.mail)
2624 assert !issue.recipients.include?(issue.author.mail)
2612 end
2625 end
2613
2626
2614 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2627 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2615 issue = Issue.generate!(:assigned_to => User.generate!)
2628 issue = Issue.generate!(:assigned_to => User.generate!)
2616 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2629 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2617 assert !issue.recipients.include?(issue.assigned_to.mail)
2630 assert !issue.recipients.include?(issue.assigned_to.mail)
2618 end
2631 end
2619
2632
2620 def test_last_journal_id_with_journals_should_return_the_journal_id
2633 def test_last_journal_id_with_journals_should_return_the_journal_id
2621 assert_equal 2, Issue.find(1).last_journal_id
2634 assert_equal 2, Issue.find(1).last_journal_id
2622 end
2635 end
2623
2636
2624 def test_last_journal_id_without_journals_should_return_nil
2637 def test_last_journal_id_without_journals_should_return_nil
2625 assert_nil Issue.find(3).last_journal_id
2638 assert_nil Issue.find(3).last_journal_id
2626 end
2639 end
2627
2640
2628 def test_journals_after_should_return_journals_with_greater_id
2641 def test_journals_after_should_return_journals_with_greater_id
2629 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2642 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2630 assert_equal [], Issue.find(1).journals_after('2')
2643 assert_equal [], Issue.find(1).journals_after('2')
2631 end
2644 end
2632
2645
2633 def test_journals_after_with_blank_arg_should_return_all_journals
2646 def test_journals_after_with_blank_arg_should_return_all_journals
2634 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2647 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2635 end
2648 end
2636
2649
2637 def test_css_classes_should_include_tracker
2650 def test_css_classes_should_include_tracker
2638 issue = Issue.new(:tracker => Tracker.find(2))
2651 issue = Issue.new(:tracker => Tracker.find(2))
2639 classes = issue.css_classes.split(' ')
2652 classes = issue.css_classes.split(' ')
2640 assert_include 'tracker-2', classes
2653 assert_include 'tracker-2', classes
2641 end
2654 end
2642
2655
2643 def test_css_classes_should_include_priority
2656 def test_css_classes_should_include_priority
2644 issue = Issue.new(:priority => IssuePriority.find(8))
2657 issue = Issue.new(:priority => IssuePriority.find(8))
2645 classes = issue.css_classes.split(' ')
2658 classes = issue.css_classes.split(' ')
2646 assert_include 'priority-8', classes
2659 assert_include 'priority-8', classes
2647 assert_include 'priority-highest', classes
2660 assert_include 'priority-highest', classes
2648 end
2661 end
2649
2662
2650 def test_css_classes_should_include_user_and_group_assignment
2663 def test_css_classes_should_include_user_and_group_assignment
2651 project = Project.first
2664 project = Project.first
2652 user = User.generate!
2665 user = User.generate!
2653 group = Group.generate!
2666 group = Group.generate!
2654 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2667 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2655 group.users << user
2668 group.users << user
2656 assert user.member_of?(project)
2669 assert user.member_of?(project)
2657 issue1 = Issue.generate(:assigned_to_id => group.id)
2670 issue1 = Issue.generate(:assigned_to_id => group.id)
2658 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2671 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2659 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2672 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2660 issue2 = Issue.generate(:assigned_to_id => user.id)
2673 issue2 = Issue.generate(:assigned_to_id => user.id)
2661 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2674 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2662 assert_include 'assigned-to-me', issue2.css_classes(user)
2675 assert_include 'assigned-to-me', issue2.css_classes(user)
2663 end
2676 end
2664
2677
2665 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2678 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2666 set_tmp_attachments_directory
2679 set_tmp_attachments_directory
2667 issue = Issue.generate!
2680 issue = Issue.generate!
2668 issue.save_attachments({
2681 issue.save_attachments({
2669 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2682 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2670 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2683 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2671 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2684 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2672 })
2685 })
2673 issue.attach_saved_attachments
2686 issue.attach_saved_attachments
2674
2687
2675 assert_equal 3, issue.reload.attachments.count
2688 assert_equal 3, issue.reload.attachments.count
2676 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2689 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2677 end
2690 end
2678
2691
2679 def test_save_attachments_with_array_should_warn_about_missing_tokens
2692 def test_save_attachments_with_array_should_warn_about_missing_tokens
2680 set_tmp_attachments_directory
2693 set_tmp_attachments_directory
2681 issue = Issue.generate!
2694 issue = Issue.generate!
2682 issue.save_attachments([
2695 issue.save_attachments([
2683 {'token' => 'missing'}
2696 {'token' => 'missing'}
2684 ])
2697 ])
2685 assert !issue.save
2698 assert !issue.save
2686 assert issue.errors[:base].present?
2699 assert issue.errors[:base].present?
2687 assert_equal 0, issue.reload.attachments.count
2700 assert_equal 0, issue.reload.attachments.count
2688 end
2701 end
2689
2702
2690 def test_closed_on_should_be_nil_when_creating_an_open_issue
2703 def test_closed_on_should_be_nil_when_creating_an_open_issue
2691 issue = Issue.generate!(:status_id => 1).reload
2704 issue = Issue.generate!(:status_id => 1).reload
2692 assert !issue.closed?
2705 assert !issue.closed?
2693 assert_nil issue.closed_on
2706 assert_nil issue.closed_on
2694 end
2707 end
2695
2708
2696 def test_closed_on_should_be_set_when_creating_a_closed_issue
2709 def test_closed_on_should_be_set_when_creating_a_closed_issue
2697 issue = Issue.generate!(:status_id => 5).reload
2710 issue = Issue.generate!(:status_id => 5).reload
2698 assert issue.closed?
2711 assert issue.closed?
2699 assert_not_nil issue.closed_on
2712 assert_not_nil issue.closed_on
2700 assert_equal issue.updated_on, issue.closed_on
2713 assert_equal issue.updated_on, issue.closed_on
2701 assert_equal issue.created_on, issue.closed_on
2714 assert_equal issue.created_on, issue.closed_on
2702 end
2715 end
2703
2716
2704 def test_closed_on_should_be_nil_when_updating_an_open_issue
2717 def test_closed_on_should_be_nil_when_updating_an_open_issue
2705 issue = Issue.find(1)
2718 issue = Issue.find(1)
2706 issue.subject = 'Not closed yet'
2719 issue.subject = 'Not closed yet'
2707 issue.save!
2720 issue.save!
2708 issue.reload
2721 issue.reload
2709 assert_nil issue.closed_on
2722 assert_nil issue.closed_on
2710 end
2723 end
2711
2724
2712 def test_closed_on_should_be_set_when_closing_an_open_issue
2725 def test_closed_on_should_be_set_when_closing_an_open_issue
2713 issue = Issue.find(1)
2726 issue = Issue.find(1)
2714 issue.subject = 'Now closed'
2727 issue.subject = 'Now closed'
2715 issue.status_id = 5
2728 issue.status_id = 5
2716 issue.save!
2729 issue.save!
2717 issue.reload
2730 issue.reload
2718 assert_not_nil issue.closed_on
2731 assert_not_nil issue.closed_on
2719 assert_equal issue.updated_on, issue.closed_on
2732 assert_equal issue.updated_on, issue.closed_on
2720 end
2733 end
2721
2734
2722 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2735 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2723 issue = Issue.open(false).first
2736 issue = Issue.open(false).first
2724 was_closed_on = issue.closed_on
2737 was_closed_on = issue.closed_on
2725 assert_not_nil was_closed_on
2738 assert_not_nil was_closed_on
2726 issue.subject = 'Updating a closed issue'
2739 issue.subject = 'Updating a closed issue'
2727 issue.save!
2740 issue.save!
2728 issue.reload
2741 issue.reload
2729 assert_equal was_closed_on, issue.closed_on
2742 assert_equal was_closed_on, issue.closed_on
2730 end
2743 end
2731
2744
2732 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2745 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2733 issue = Issue.open(false).first
2746 issue = Issue.open(false).first
2734 was_closed_on = issue.closed_on
2747 was_closed_on = issue.closed_on
2735 assert_not_nil was_closed_on
2748 assert_not_nil was_closed_on
2736 issue.subject = 'Reopening a closed issue'
2749 issue.subject = 'Reopening a closed issue'
2737 issue.status_id = 1
2750 issue.status_id = 1
2738 issue.save!
2751 issue.save!
2739 issue.reload
2752 issue.reload
2740 assert !issue.closed?
2753 assert !issue.closed?
2741 assert_equal was_closed_on, issue.closed_on
2754 assert_equal was_closed_on, issue.closed_on
2742 end
2755 end
2743
2756
2744 def test_status_was_should_return_nil_for_new_issue
2757 def test_status_was_should_return_nil_for_new_issue
2745 issue = Issue.new
2758 issue = Issue.new
2746 assert_nil issue.status_was
2759 assert_nil issue.status_was
2747 end
2760 end
2748
2761
2749 def test_status_was_should_return_status_before_change
2762 def test_status_was_should_return_status_before_change
2750 issue = Issue.find(1)
2763 issue = Issue.find(1)
2751 issue.status = IssueStatus.find(2)
2764 issue.status = IssueStatus.find(2)
2752 assert_equal IssueStatus.find(1), issue.status_was
2765 assert_equal IssueStatus.find(1), issue.status_was
2753 end
2766 end
2754
2767
2755 def test_status_was_should_return_status_before_change_with_status_id
2768 def test_status_was_should_return_status_before_change_with_status_id
2756 issue = Issue.find(1)
2769 issue = Issue.find(1)
2757 assert_equal IssueStatus.find(1), issue.status
2770 assert_equal IssueStatus.find(1), issue.status
2758 issue.status_id = 2
2771 issue.status_id = 2
2759 assert_equal IssueStatus.find(1), issue.status_was
2772 assert_equal IssueStatus.find(1), issue.status_was
2760 end
2773 end
2761
2774
2762 def test_status_was_should_be_reset_on_save
2775 def test_status_was_should_be_reset_on_save
2763 issue = Issue.find(1)
2776 issue = Issue.find(1)
2764 issue.status = IssueStatus.find(2)
2777 issue.status = IssueStatus.find(2)
2765 assert_equal IssueStatus.find(1), issue.status_was
2778 assert_equal IssueStatus.find(1), issue.status_was
2766 assert issue.save!
2779 assert issue.save!
2767 assert_equal IssueStatus.find(2), issue.status_was
2780 assert_equal IssueStatus.find(2), issue.status_was
2768 end
2781 end
2769
2782
2770 def test_closing_should_return_true_when_closing_an_issue
2783 def test_closing_should_return_true_when_closing_an_issue
2771 issue = Issue.find(1)
2784 issue = Issue.find(1)
2772 issue.status = IssueStatus.find(2)
2785 issue.status = IssueStatus.find(2)
2773 assert_equal false, issue.closing?
2786 assert_equal false, issue.closing?
2774 issue.status = IssueStatus.find(5)
2787 issue.status = IssueStatus.find(5)
2775 assert_equal true, issue.closing?
2788 assert_equal true, issue.closing?
2776 end
2789 end
2777
2790
2778 def test_closing_should_return_true_when_closing_an_issue_with_status_id
2791 def test_closing_should_return_true_when_closing_an_issue_with_status_id
2779 issue = Issue.find(1)
2792 issue = Issue.find(1)
2780 issue.status_id = 2
2793 issue.status_id = 2
2781 assert_equal false, issue.closing?
2794 assert_equal false, issue.closing?
2782 issue.status_id = 5
2795 issue.status_id = 5
2783 assert_equal true, issue.closing?
2796 assert_equal true, issue.closing?
2784 end
2797 end
2785
2798
2786 def test_closing_should_return_true_for_new_closed_issue
2799 def test_closing_should_return_true_for_new_closed_issue
2787 issue = Issue.new
2800 issue = Issue.new
2788 assert_equal false, issue.closing?
2801 assert_equal false, issue.closing?
2789 issue.status = IssueStatus.find(5)
2802 issue.status = IssueStatus.find(5)
2790 assert_equal true, issue.closing?
2803 assert_equal true, issue.closing?
2791 end
2804 end
2792
2805
2793 def test_closing_should_return_true_for_new_closed_issue_with_status_id
2806 def test_closing_should_return_true_for_new_closed_issue_with_status_id
2794 issue = Issue.new
2807 issue = Issue.new
2795 assert_equal false, issue.closing?
2808 assert_equal false, issue.closing?
2796 issue.status_id = 5
2809 issue.status_id = 5
2797 assert_equal true, issue.closing?
2810 assert_equal true, issue.closing?
2798 end
2811 end
2799
2812
2800 def test_closing_should_be_reset_after_save
2813 def test_closing_should_be_reset_after_save
2801 issue = Issue.find(1)
2814 issue = Issue.find(1)
2802 issue.status_id = 5
2815 issue.status_id = 5
2803 assert_equal true, issue.closing?
2816 assert_equal true, issue.closing?
2804 issue.save!
2817 issue.save!
2805 assert_equal false, issue.closing?
2818 assert_equal false, issue.closing?
2806 end
2819 end
2807
2820
2808 def test_reopening_should_return_true_when_reopening_an_issue
2821 def test_reopening_should_return_true_when_reopening_an_issue
2809 issue = Issue.find(8)
2822 issue = Issue.find(8)
2810 issue.status = IssueStatus.find(6)
2823 issue.status = IssueStatus.find(6)
2811 assert_equal false, issue.reopening?
2824 assert_equal false, issue.reopening?
2812 issue.status = IssueStatus.find(2)
2825 issue.status = IssueStatus.find(2)
2813 assert_equal true, issue.reopening?
2826 assert_equal true, issue.reopening?
2814 end
2827 end
2815
2828
2816 def test_reopening_should_return_true_when_reopening_an_issue_with_status_id
2829 def test_reopening_should_return_true_when_reopening_an_issue_with_status_id
2817 issue = Issue.find(8)
2830 issue = Issue.find(8)
2818 issue.status_id = 6
2831 issue.status_id = 6
2819 assert_equal false, issue.reopening?
2832 assert_equal false, issue.reopening?
2820 issue.status_id = 2
2833 issue.status_id = 2
2821 assert_equal true, issue.reopening?
2834 assert_equal true, issue.reopening?
2822 end
2835 end
2823
2836
2824 def test_reopening_should_return_false_for_new_open_issue
2837 def test_reopening_should_return_false_for_new_open_issue
2825 issue = Issue.new
2838 issue = Issue.new
2826 issue.status = IssueStatus.find(1)
2839 issue.status = IssueStatus.find(1)
2827 assert_equal false, issue.reopening?
2840 assert_equal false, issue.reopening?
2828 end
2841 end
2829
2842
2830 def test_reopening_should_be_reset_after_save
2843 def test_reopening_should_be_reset_after_save
2831 issue = Issue.find(8)
2844 issue = Issue.find(8)
2832 issue.status_id = 2
2845 issue.status_id = 2
2833 assert_equal true, issue.reopening?
2846 assert_equal true, issue.reopening?
2834 issue.save!
2847 issue.save!
2835 assert_equal false, issue.reopening?
2848 assert_equal false, issue.reopening?
2836 end
2849 end
2837
2850
2838 def test_default_status_without_tracker_should_be_nil
2851 def test_default_status_without_tracker_should_be_nil
2839 issue = Issue.new
2852 issue = Issue.new
2840 assert_nil issue.tracker
2853 assert_nil issue.tracker
2841 assert_nil issue.default_status
2854 assert_nil issue.default_status
2842 end
2855 end
2843
2856
2844 def test_default_status_should_be_tracker_default_status
2857 def test_default_status_should_be_tracker_default_status
2845 issue = Issue.new(:tracker_id => 1)
2858 issue = Issue.new(:tracker_id => 1)
2846 assert_not_nil issue.status
2859 assert_not_nil issue.status
2847 assert_equal issue.tracker.default_status, issue.default_status
2860 assert_equal issue.tracker.default_status, issue.default_status
2848 end
2861 end
2849
2862
2850 def test_initializing_with_tracker_should_set_default_status
2863 def test_initializing_with_tracker_should_set_default_status
2851 issue = Issue.new(:tracker => Tracker.find(1))
2864 issue = Issue.new(:tracker => Tracker.find(1))
2852 assert_not_nil issue.status
2865 assert_not_nil issue.status
2853 assert_equal issue.default_status, issue.status
2866 assert_equal issue.default_status, issue.status
2854 end
2867 end
2855
2868
2856 def test_initializing_with_tracker_id_should_set_default_status
2869 def test_initializing_with_tracker_id_should_set_default_status
2857 issue = Issue.new(:tracker_id => 1)
2870 issue = Issue.new(:tracker_id => 1)
2858 assert_not_nil issue.status
2871 assert_not_nil issue.status
2859 assert_equal issue.default_status, issue.status
2872 assert_equal issue.default_status, issue.status
2860 end
2873 end
2861
2874
2862 def test_setting_tracker_should_set_default_status
2875 def test_setting_tracker_should_set_default_status
2863 issue = Issue.new
2876 issue = Issue.new
2864 issue.tracker = Tracker.find(1)
2877 issue.tracker = Tracker.find(1)
2865 assert_not_nil issue.status
2878 assert_not_nil issue.status
2866 assert_equal issue.default_status, issue.status
2879 assert_equal issue.default_status, issue.status
2867 end
2880 end
2868
2881
2869 def test_changing_tracker_should_set_default_status_if_status_was_default
2882 def test_changing_tracker_should_set_default_status_if_status_was_default
2870 WorkflowTransition.delete_all
2883 WorkflowTransition.delete_all
2871 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1
2884 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 1
2872 Tracker.find(2).update! :default_status_id => 2
2885 Tracker.find(2).update! :default_status_id => 2
2873
2886
2874 issue = Issue.new(:tracker_id => 1, :status_id => 1)
2887 issue = Issue.new(:tracker_id => 1, :status_id => 1)
2875 assert_equal IssueStatus.find(1), issue.status
2888 assert_equal IssueStatus.find(1), issue.status
2876 issue.tracker = Tracker.find(2)
2889 issue.tracker = Tracker.find(2)
2877 assert_equal IssueStatus.find(2), issue.status
2890 assert_equal IssueStatus.find(2), issue.status
2878 end
2891 end
2879
2892
2880 def test_changing_tracker_should_set_default_status_if_status_is_not_used_by_tracker
2893 def test_changing_tracker_should_set_default_status_if_status_is_not_used_by_tracker
2881 WorkflowTransition.delete_all
2894 WorkflowTransition.delete_all
2882 Tracker.find(2).update! :default_status_id => 2
2895 Tracker.find(2).update! :default_status_id => 2
2883
2896
2884 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2897 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2885 assert_equal IssueStatus.find(3), issue.status
2898 assert_equal IssueStatus.find(3), issue.status
2886 issue.tracker = Tracker.find(2)
2899 issue.tracker = Tracker.find(2)
2887 assert_equal IssueStatus.find(2), issue.status
2900 assert_equal IssueStatus.find(2), issue.status
2888 end
2901 end
2889
2902
2890 def test_changing_tracker_should_keep_status_if_status_was_not_default_and_is_used_by_tracker
2903 def test_changing_tracker_should_keep_status_if_status_was_not_default_and_is_used_by_tracker
2891 WorkflowTransition.delete_all
2904 WorkflowTransition.delete_all
2892 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3
2905 WorkflowTransition.create! :role_id => 1, :tracker_id => 2, :old_status_id => 2, :new_status_id => 3
2893 Tracker.find(2).update! :default_status_id => 2
2906 Tracker.find(2).update! :default_status_id => 2
2894
2907
2895 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2908 issue = Issue.new(:tracker_id => 1, :status_id => 3)
2896 assert_equal IssueStatus.find(3), issue.status
2909 assert_equal IssueStatus.find(3), issue.status
2897 issue.tracker = Tracker.find(2)
2910 issue.tracker = Tracker.find(2)
2898 assert_equal IssueStatus.find(3), issue.status
2911 assert_equal IssueStatus.find(3), issue.status
2899 end
2912 end
2900
2913
2901 def test_assigned_to_was_with_a_group
2914 def test_assigned_to_was_with_a_group
2902 group = Group.find(10)
2915 group = Group.find(10)
2903
2916
2904 issue = Issue.generate!(:assigned_to => group)
2917 issue = Issue.generate!(:assigned_to => group)
2905 issue.reload.assigned_to = nil
2918 issue.reload.assigned_to = nil
2906 assert_equal group, issue.assigned_to_was
2919 assert_equal group, issue.assigned_to_was
2907 end
2920 end
2908
2921
2909 def test_issue_overdue_should_respect_user_timezone
2922 def test_issue_overdue_should_respect_user_timezone
2910 user_in_europe = users(:users_001)
2923 user_in_europe = users(:users_001)
2911 user_in_europe.pref.update_attribute :time_zone, 'UTC'
2924 user_in_europe.pref.update_attribute :time_zone, 'UTC'
2912
2925
2913 user_in_asia = users(:users_002)
2926 user_in_asia = users(:users_002)
2914 user_in_asia.pref.update_attribute :time_zone, 'Hongkong'
2927 user_in_asia.pref.update_attribute :time_zone, 'Hongkong'
2915
2928
2916 issue = Issue.generate! :due_date => Date.parse('2016-03-20')
2929 issue = Issue.generate! :due_date => Date.parse('2016-03-20')
2917
2930
2918 # server time is UTC
2931 # server time is UTC
2919 time = Time.parse '2016-03-20 20:00 UTC'
2932 time = Time.parse '2016-03-20 20:00 UTC'
2920 Time.stubs(:now).returns(time)
2933 Time.stubs(:now).returns(time)
2921 Date.stubs(:today).returns(time.to_date)
2934 Date.stubs(:today).returns(time.to_date)
2922
2935
2923 # for a user in the same time zone as the server the issue is not overdue
2936 # for a user in the same time zone as the server the issue is not overdue
2924 # yet
2937 # yet
2925 User.current = user_in_europe
2938 User.current = user_in_europe
2926 assert !issue.overdue?
2939 assert !issue.overdue?
2927
2940
2928 # at the same time, a user in East Asia looks at the issue - it's already
2941 # at the same time, a user in East Asia looks at the issue - it's already
2929 # March 21st and the issue should be marked overdue
2942 # March 21st and the issue should be marked overdue
2930 User.current = user_in_asia
2943 User.current = user_in_asia
2931 assert issue.overdue?
2944 assert issue.overdue?
2932
2945
2933 end
2946 end
2934 end
2947 end
General Comments 0
You need to be logged in to leave comments. Login now