@@ -1,101 +1,112 | |||
|
1 | 1 | # redMine - project management software |
|
2 | 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class IssueRelation < ActiveRecord::Base |
|
19 | 19 | belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' |
|
20 | 20 | belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' |
|
21 | 21 | |
|
22 | 22 | TYPE_RELATES = "relates" |
|
23 | 23 | TYPE_DUPLICATES = "duplicates" |
|
24 | 24 | TYPE_DUPLICATED = "duplicated" |
|
25 | 25 | TYPE_BLOCKS = "blocks" |
|
26 | 26 | TYPE_BLOCKED = "blocked" |
|
27 | 27 | TYPE_PRECEDES = "precedes" |
|
28 | 28 | TYPE_FOLLOWS = "follows" |
|
29 | 29 | |
|
30 | TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 }, | |
|
31 | TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 }, | |
|
32 | TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :reverse => TYPE_DUPLICATES }, | |
|
33 | TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4 }, | |
|
34 | TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :reverse => TYPE_BLOCKS }, | |
|
35 | TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6 }, | |
|
36 | TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :reverse => TYPE_PRECEDES } | |
|
30 | TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES }, | |
|
31 | TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED }, | |
|
32 | TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, | |
|
33 | TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED }, | |
|
34 | TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, | |
|
35 | TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS }, | |
|
36 | TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES } | |
|
37 | 37 | }.freeze |
|
38 | 38 | |
|
39 | 39 | validates_presence_of :issue_from, :issue_to, :relation_type |
|
40 | 40 | validates_inclusion_of :relation_type, :in => TYPES.keys |
|
41 | 41 | validates_numericality_of :delay, :allow_nil => true |
|
42 | 42 | validates_uniqueness_of :issue_to_id, :scope => :issue_from_id |
|
43 | 43 | |
|
44 | 44 | attr_protected :issue_from_id, :issue_to_id |
|
45 | 45 | |
|
46 | 46 | def validate |
|
47 | 47 | if issue_from && issue_to |
|
48 | 48 | errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id |
|
49 | 49 | errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? |
|
50 | 50 | errors.add_to_base :circular_dependency if issue_to.all_dependent_issues.include? issue_from |
|
51 | 51 | errors.add_to_base :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to) |
|
52 | 52 | end |
|
53 | 53 | end |
|
54 | 54 | |
|
55 | 55 | def other_issue(issue) |
|
56 | 56 | (self.issue_from_id == issue.id) ? issue_to : issue_from |
|
57 | 57 | end |
|
58 | 58 | |
|
59 | # Returns the relation type for +issue+ | |
|
60 | def relation_type_for(issue) | |
|
61 | if TYPES[relation_type] | |
|
62 | if self.issue_from_id == issue.id | |
|
63 | relation_type | |
|
64 | else | |
|
65 | TYPES[relation_type][:sym] | |
|
66 | end | |
|
67 | end | |
|
68 | end | |
|
69 | ||
|
59 | 70 | def label_for(issue) |
|
60 | 71 | TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow |
|
61 | 72 | end |
|
62 | 73 | |
|
63 | 74 | def before_save |
|
64 | 75 | reverse_if_needed |
|
65 | 76 | |
|
66 | 77 | if TYPE_PRECEDES == relation_type |
|
67 | 78 | self.delay ||= 0 |
|
68 | 79 | else |
|
69 | 80 | self.delay = nil |
|
70 | 81 | end |
|
71 | 82 | set_issue_to_dates |
|
72 | 83 | end |
|
73 | 84 | |
|
74 | 85 | def set_issue_to_dates |
|
75 | 86 | soonest_start = self.successor_soonest_start |
|
76 | 87 | if soonest_start |
|
77 | 88 | issue_to.reschedule_after(soonest_start) |
|
78 | 89 | end |
|
79 | 90 | end |
|
80 | 91 | |
|
81 | 92 | def successor_soonest_start |
|
82 | 93 | return nil unless (TYPE_PRECEDES == self.relation_type) && (issue_from.start_date || issue_from.due_date) |
|
83 | 94 | (issue_from.due_date || issue_from.start_date) + 1 + delay |
|
84 | 95 | end |
|
85 | 96 | |
|
86 | 97 | def <=>(relation) |
|
87 | 98 | TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order] |
|
88 | 99 | end |
|
89 | 100 | |
|
90 | 101 | private |
|
91 | 102 | |
|
92 | 103 | # Reverses the relation if needed so that it gets stored in the proper way |
|
93 | 104 | def reverse_if_needed |
|
94 | 105 | if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] |
|
95 | 106 | issue_tmp = issue_to |
|
96 | 107 | self.issue_to = issue_from |
|
97 | 108 | self.issue_from = issue_tmp |
|
98 | 109 | self.relation_type = TYPES[relation_type][:reverse] |
|
99 | 110 | end |
|
100 | 111 | end |
|
101 | 112 | end |
@@ -1,56 +1,62 | |||
|
1 | 1 | xml.instruct! |
|
2 | 2 | xml.issue do |
|
3 | 3 | xml.id @issue.id |
|
4 | 4 | xml.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil? |
|
5 | 5 | xml.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil? |
|
6 | 6 | xml.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil? |
|
7 | 7 | xml.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil? |
|
8 | 8 | xml.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil? |
|
9 | 9 | xml.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil? |
|
10 | 10 | xml.category(:id => @issue.category_id, :name => @issue.category.name) unless @issue.category.nil? |
|
11 | 11 | xml.fixed_version(:id => @issue.fixed_version_id, :name => @issue.fixed_version.name) unless @issue.fixed_version.nil? |
|
12 | 12 | xml.parent(:id => @issue.parent_id) unless @issue.parent.nil? |
|
13 | 13 | |
|
14 | 14 | xml.subject @issue.subject |
|
15 | 15 | xml.description @issue.description |
|
16 | 16 | xml.start_date @issue.start_date |
|
17 | 17 | xml.due_date @issue.due_date |
|
18 | 18 | xml.done_ratio @issue.done_ratio |
|
19 | 19 | xml.estimated_hours @issue.estimated_hours |
|
20 | 20 | if User.current.allowed_to?(:view_time_entries, @project) |
|
21 | 21 | xml.spent_hours @issue.spent_hours |
|
22 | 22 | end |
|
23 | 23 | |
|
24 | 24 | xml.custom_fields do |
|
25 | 25 | @issue.custom_field_values.each do |custom_value| |
|
26 | 26 | xml.custom_field custom_value.value, :id => custom_value.custom_field_id, :name => custom_value.custom_field.name |
|
27 | 27 | end |
|
28 | 28 | end unless @issue.custom_field_values.empty? |
|
29 | 29 | |
|
30 | 30 | xml.created_on @issue.created_on |
|
31 | 31 | xml.updated_on @issue.updated_on |
|
32 | 32 | |
|
33 | xml.relations do | |
|
34 | @issue.relations.select {|r| r.other_issue(@issue).visible? }.each do |relation| | |
|
35 | xml.relation(:id => relation.id, :issue_id => relation.other_issue(@issue).id, :relation_type => relation.relation_type_for(@issue), :delay => relation.delay) | |
|
36 | end | |
|
37 | end | |
|
38 | ||
|
33 | 39 | xml.changesets do |
|
34 | 40 | @issue.changesets.each do |changeset| |
|
35 | 41 | xml.changeset :revision => changeset.revision do |
|
36 | 42 | xml.user(:id => changeset.user_id, :name => changeset.user.name) unless changeset.user.nil? |
|
37 | 43 | xml.comments changeset.comments |
|
38 | 44 | xml.committed_on changeset.committed_on |
|
39 | 45 | end |
|
40 | 46 | end |
|
41 | 47 | end if User.current.allowed_to?(:view_changesets, @project) && @issue.changesets.any? |
|
42 | 48 | |
|
43 | 49 | xml.journals do |
|
44 | 50 | @issue.journals.each do |journal| |
|
45 | 51 | xml.journal :id => journal.id do |
|
46 | 52 | xml.user(:id => journal.user_id, :name => journal.user.name) unless journal.user.nil? |
|
47 | 53 | xml.notes journal.notes |
|
48 | 54 | xml.details do |
|
49 | 55 | journal.details.each do |detail| |
|
50 | 56 | xml.detail :property => detail.property, :name => detail.prop_key, :old => detail.old_value, :new => detail.value |
|
51 | 57 | end |
|
52 | 58 | end |
|
53 | 59 | end |
|
54 | 60 | end |
|
55 | 61 | end unless @issue.journals.empty? |
|
56 | 62 | end |
@@ -1,57 +1,66 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2009 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.dirname(__FILE__) + '/../test_helper' |
|
19 | 19 | |
|
20 | 20 | class IssueRelationTest < ActiveSupport::TestCase |
|
21 | 21 | fixtures :issue_relations, :issues |
|
22 | 22 | |
|
23 | 23 | def test_create |
|
24 | 24 | from = Issue.find(1) |
|
25 | 25 | to = Issue.find(2) |
|
26 | 26 | |
|
27 | 27 | relation = IssueRelation.new :issue_from => from, :issue_to => to, :relation_type => IssueRelation::TYPE_PRECEDES |
|
28 | 28 | assert relation.save |
|
29 | 29 | relation.reload |
|
30 | 30 | assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type |
|
31 | 31 | assert_equal from, relation.issue_from |
|
32 | 32 | assert_equal to, relation.issue_to |
|
33 | 33 | end |
|
34 | 34 | |
|
35 | 35 | def test_follows_relation_should_be_reversed |
|
36 | 36 | from = Issue.find(1) |
|
37 | 37 | to = Issue.find(2) |
|
38 | 38 | |
|
39 | 39 | relation = IssueRelation.new :issue_from => from, :issue_to => to, :relation_type => IssueRelation::TYPE_FOLLOWS |
|
40 | 40 | assert relation.save |
|
41 | 41 | relation.reload |
|
42 | 42 | assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type |
|
43 | 43 | assert_equal to, relation.issue_from |
|
44 | 44 | assert_equal from, relation.issue_to |
|
45 | 45 | end |
|
46 | 46 | |
|
47 | 47 | def test_follows_relation_should_not_be_reversed_if_validation_fails |
|
48 | 48 | from = Issue.find(1) |
|
49 | 49 | to = Issue.find(2) |
|
50 | 50 | |
|
51 | 51 | relation = IssueRelation.new :issue_from => from, :issue_to => to, :relation_type => IssueRelation::TYPE_FOLLOWS, :delay => 'xx' |
|
52 | 52 | assert !relation.save |
|
53 | 53 | assert_equal IssueRelation::TYPE_FOLLOWS, relation.relation_type |
|
54 | 54 | assert_equal from, relation.issue_from |
|
55 | 55 | assert_equal to, relation.issue_to |
|
56 | 56 | end |
|
57 | ||
|
58 | def test_relation_type_for | |
|
59 | from = Issue.find(1) | |
|
60 | to = Issue.find(2) | |
|
61 | ||
|
62 | relation = IssueRelation.new :issue_from => from, :issue_to => to, :relation_type => IssueRelation::TYPE_PRECEDES | |
|
63 | assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type_for(from) | |
|
64 | assert_equal IssueRelation::TYPE_FOLLOWS, relation.relation_type_for(to) | |
|
65 | end | |
|
57 | 66 | end |
General Comments 0
You need to be logged in to leave comments.
Login now