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