##// END OF EJS Templates
Overrides IssueRelation#to_s....
Jean-Philippe Lang -
r13181:5326eb233e39
parent child
Show More
@@ -1,205 +1,215
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 # Class used to represent the relations of an issue
20 20 class Relations < Array
21 21 include Redmine::I18n
22 22
23 23 def initialize(issue, *args)
24 24 @issue = issue
25 25 super(*args)
26 26 end
27 27
28 28 def to_s(*args)
29 29 map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
30 30 end
31 31 end
32 32
33 33 belongs_to :issue_from, :class_name => 'Issue'
34 34 belongs_to :issue_to, :class_name => 'Issue'
35 35
36 36 TYPE_RELATES = "relates"
37 37 TYPE_DUPLICATES = "duplicates"
38 38 TYPE_DUPLICATED = "duplicated"
39 39 TYPE_BLOCKS = "blocks"
40 40 TYPE_BLOCKED = "blocked"
41 41 TYPE_PRECEDES = "precedes"
42 42 TYPE_FOLLOWS = "follows"
43 43 TYPE_COPIED_TO = "copied_to"
44 44 TYPE_COPIED_FROM = "copied_from"
45 45
46 46 TYPES = {
47 47 TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
48 48 :order => 1, :sym => TYPE_RELATES },
49 49 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
50 50 :order => 2, :sym => TYPE_DUPLICATED },
51 51 TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
52 52 :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
53 53 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by,
54 54 :order => 4, :sym => TYPE_BLOCKED },
55 55 TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks,
56 56 :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
57 57 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows,
58 58 :order => 6, :sym => TYPE_FOLLOWS },
59 59 TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes,
60 60 :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
61 61 TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from,
62 62 :order => 8, :sym => TYPE_COPIED_FROM },
63 63 TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to,
64 64 :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
65 65 }.freeze
66 66
67 67 validates_presence_of :issue_from, :issue_to, :relation_type
68 68 validates_inclusion_of :relation_type, :in => TYPES.keys
69 69 validates_numericality_of :delay, :allow_nil => true
70 70 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
71 71 validate :validate_issue_relation
72 72
73 73 attr_protected :issue_from_id, :issue_to_id
74 74 before_save :handle_issue_order
75 75 after_create :call_issues_relation_added_callback
76 76 after_destroy :call_issues_relation_removed_callback
77 77
78 78 def visible?(user=User.current)
79 79 (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
80 80 end
81 81
82 82 def deletable?(user=User.current)
83 83 visible?(user) &&
84 84 ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
85 85 (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
86 86 end
87 87
88 88 def initialize(attributes=nil, *args)
89 89 super
90 90 if new_record?
91 91 if relation_type.blank?
92 92 self.relation_type = IssueRelation::TYPE_RELATES
93 93 end
94 94 end
95 95 end
96 96
97 97 def validate_issue_relation
98 98 if issue_from && issue_to
99 99 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
100 100 unless issue_from.project_id == issue_to.project_id ||
101 101 Setting.cross_project_issue_relations?
102 102 errors.add :issue_to_id, :not_same_project
103 103 end
104 104 # detect circular dependencies depending wether the relation should be reversed
105 105 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
106 106 errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
107 107 else
108 108 errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
109 109 end
110 110 if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
111 111 errors.add :base, :cant_link_an_issue_with_a_descendant
112 112 end
113 113 end
114 114 end
115 115
116 116 def other_issue(issue)
117 117 (self.issue_from_id == issue.id) ? issue_to : issue_from
118 118 end
119 119
120 120 # Returns the relation type for +issue+
121 121 def relation_type_for(issue)
122 122 if TYPES[relation_type]
123 123 if self.issue_from_id == issue.id
124 124 relation_type
125 125 else
126 126 TYPES[relation_type][:sym]
127 127 end
128 128 end
129 129 end
130 130
131 131 def label_for(issue)
132 132 TYPES[relation_type] ?
133 133 TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] :
134 134 :unknow
135 135 end
136 136
137 def to_s(issue=nil)
138 issue ||= issue_from
139 issue_text = block_given? ? yield(other_issue(issue)) : "##{other_issue(issue).try(:id)}"
140 s = []
141 s << l(label_for(issue))
142 s << "(#{l('datetime.distance_in_words.x_days', :count => delay)})" if delay && delay != 0
143 s << issue_text
144 s.join(' ')
145 end
146
137 147 def css_classes_for(issue)
138 148 "rel-#{relation_type_for(issue)}"
139 149 end
140 150
141 151 def handle_issue_order
142 152 reverse_if_needed
143 153
144 154 if TYPE_PRECEDES == relation_type
145 155 self.delay ||= 0
146 156 else
147 157 self.delay = nil
148 158 end
149 159 set_issue_to_dates
150 160 end
151 161
152 162 def set_issue_to_dates
153 163 soonest_start = self.successor_soonest_start
154 164 if soonest_start && issue_to
155 165 issue_to.reschedule_on!(soonest_start)
156 166 end
157 167 end
158 168
159 169 def successor_soonest_start
160 170 if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
161 171 (issue_from.start_date || issue_from.due_date)
162 172 (issue_from.due_date || issue_from.start_date) + 1 + delay
163 173 end
164 174 end
165 175
166 176 def <=>(relation)
167 177 r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
168 178 r == 0 ? id <=> relation.id : r
169 179 end
170 180
171 181 def init_journals(user)
172 182 issue_from.init_journal(user) if issue_from
173 183 issue_to.init_journal(user) if issue_to
174 184 end
175 185
176 186 private
177 187
178 188 # Reverses the relation if needed so that it gets stored in the proper way
179 189 # Should not be reversed before validation so that it can be displayed back
180 190 # as entered on new relation form
181 191 def reverse_if_needed
182 192 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
183 193 issue_tmp = issue_to
184 194 self.issue_to = issue_from
185 195 self.issue_from = issue_tmp
186 196 self.relation_type = TYPES[relation_type][:reverse]
187 197 end
188 198 end
189 199
190 200 def call_issues_relation_added_callback
191 201 call_issues_callback :relation_added
192 202 end
193 203
194 204 def call_issues_relation_removed_callback
195 205 call_issues_callback :relation_removed
196 206 end
197 207
198 208 def call_issues_callback(name)
199 209 [issue_from, issue_to].each do |issue|
200 210 if issue
201 211 issue.send name, self
202 212 end
203 213 end
204 214 end
205 215 end
@@ -1,44 +1,41
1 1 <div class="contextual">
2 2 <% if User.current.allowed_to?(:manage_issue_relations, @project) %>
3 3 <%= toggle_link l(:button_add), 'new-relation-form', {:focus => 'relation_issue_to_id'} %>
4 4 <% end %>
5 5 </div>
6 6
7 7 <p><strong><%=l(:label_related_issues)%></strong></p>
8 8
9 9 <% if @relations.present? %>
10 10 <form>
11 11 <table class="list issues">
12 12 <% @relations.each do |relation| %>
13 13 <% other_issue = relation.other_issue(@issue) -%>
14 14 <tr class="issue hascontextmenu" id="relation-<%= relation.id %>">
15 15 <td class="checkbox"><%= check_box_tag("ids[]", other_issue.id, false, :id => nil) %></td>
16 16 <td class="subject">
17 <%= l(relation.label_for(@issue)) %>
18 <%= "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)})" if relation.delay && relation.delay != 0 %>
19 <%= h(other_issue.project) + ' - ' if Setting.cross_project_issue_relations? %>
20 <%= link_to_issue(other_issue, :truncate => 60) %>
17 <%= relation.to_s(@issue) {|other| link_to_issue(other, :truncate => 60, :project => Setting.cross_project_issue_relations?)}.html_safe %>
21 18 </td>
22 19 <td class="status"><%=h other_issue.status.name %></td>
23 20 <td class="start_date"><%= format_date(other_issue.start_date) %></td>
24 21 <td class="due_date"><%= format_date(other_issue.due_date) %></td>
25 22 <td class="buttons"><%= link_to image_tag('link_break.png'),
26 23 relation_path(relation),
27 24 :remote => true,
28 25 :method => :delete,
29 26 :data => {:confirm => l(:text_are_you_sure)},
30 27 :title => l(:label_relation_delete) if User.current.allowed_to?(:manage_issue_relations, @project) %></td>
31 28 </tr>
32 29 <% end %>
33 30 </table>
34 31 </form>
35 32 <% end %>
36 33
37 34 <%= form_for @relation, {
38 35 :as => :relation, :remote => true,
39 36 :url => issue_relations_path(@issue),
40 37 :method => :post,
41 38 :html => {:id => 'new-relation-form', :style => 'display: none;'}
42 39 } do |f| %>
43 40 <%= render :partial => 'issue_relations/form', :locals => {:f => f}%>
44 41 <% end %>
@@ -1,217 +1,236
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueRelationTest < ActiveSupport::TestCase
21 21 fixtures :projects,
22 22 :users,
23 23 :roles,
24 24 :members,
25 25 :member_roles,
26 26 :issues,
27 27 :issue_statuses,
28 28 :issue_relations,
29 29 :enabled_modules,
30 30 :enumerations,
31 31 :trackers,
32 32 :projects_trackers
33 33
34 34 include Redmine::I18n
35 35
36 36 def test_create
37 37 from = Issue.find(1)
38 38 to = Issue.find(2)
39 39
40 40 relation = IssueRelation.new :issue_from => from, :issue_to => to,
41 41 :relation_type => IssueRelation::TYPE_PRECEDES
42 42 assert relation.save
43 43 relation.reload
44 44 assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type
45 45 assert_equal from, relation.issue_from
46 46 assert_equal to, relation.issue_to
47 47 end
48 48
49 49 def test_create_minimum
50 50 relation = IssueRelation.new :issue_from => Issue.find(1), :issue_to => Issue.find(2)
51 51 assert relation.save
52 52 assert_equal IssueRelation::TYPE_RELATES, relation.relation_type
53 53 end
54 54
55 55 def test_follows_relation_should_be_reversed
56 56 from = Issue.find(1)
57 57 to = Issue.find(2)
58 58
59 59 relation = IssueRelation.new :issue_from => from, :issue_to => to,
60 60 :relation_type => IssueRelation::TYPE_FOLLOWS
61 61 assert relation.save
62 62 relation.reload
63 63 assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type
64 64 assert_equal to, relation.issue_from
65 65 assert_equal from, relation.issue_to
66 66 end
67 67
68 68 def test_follows_relation_should_not_be_reversed_if_validation_fails
69 69 from = Issue.find(1)
70 70 to = Issue.find(2)
71 71
72 72 relation = IssueRelation.new :issue_from => from, :issue_to => to,
73 73 :relation_type => IssueRelation::TYPE_FOLLOWS,
74 74 :delay => 'xx'
75 75 assert !relation.save
76 76 assert_equal IssueRelation::TYPE_FOLLOWS, relation.relation_type
77 77 assert_equal from, relation.issue_from
78 78 assert_equal to, relation.issue_to
79 79 end
80 80
81 81 def test_relation_type_for
82 82 from = Issue.find(1)
83 83 to = Issue.find(2)
84 84
85 85 relation = IssueRelation.new :issue_from => from, :issue_to => to,
86 86 :relation_type => IssueRelation::TYPE_PRECEDES
87 87 assert_equal IssueRelation::TYPE_PRECEDES, relation.relation_type_for(from)
88 88 assert_equal IssueRelation::TYPE_FOLLOWS, relation.relation_type_for(to)
89 89 end
90 90
91 91 def test_set_issue_to_dates_without_issue_to
92 92 r = IssueRelation.new(:issue_from => Issue.new(:start_date => Date.today),
93 93 :relation_type => IssueRelation::TYPE_PRECEDES,
94 94 :delay => 1)
95 95 assert_nil r.set_issue_to_dates
96 96 end
97 97
98 98 def test_set_issue_to_dates_without_issues
99 99 r = IssueRelation.new(:relation_type => IssueRelation::TYPE_PRECEDES, :delay => 1)
100 100 assert_nil r.set_issue_to_dates
101 101 end
102 102
103 103 def test_validates_circular_dependency
104 104 IssueRelation.delete_all
105 105 assert IssueRelation.create!(
106 106 :issue_from => Issue.find(1), :issue_to => Issue.find(2),
107 107 :relation_type => IssueRelation::TYPE_PRECEDES
108 108 )
109 109 assert IssueRelation.create!(
110 110 :issue_from => Issue.find(2), :issue_to => Issue.find(3),
111 111 :relation_type => IssueRelation::TYPE_PRECEDES
112 112 )
113 113 r = IssueRelation.new(
114 114 :issue_from => Issue.find(3), :issue_to => Issue.find(1),
115 115 :relation_type => IssueRelation::TYPE_PRECEDES
116 116 )
117 117 assert !r.save
118 118 assert_not_equal [], r.errors[:base]
119 119 end
120 120
121 121 def test_validates_circular_dependency_of_subtask
122 122 set_language_if_valid 'en'
123 123 issue1 = Issue.generate!
124 124 issue2 = Issue.generate!
125 125 IssueRelation.create!(
126 126 :issue_from => issue1, :issue_to => issue2,
127 127 :relation_type => IssueRelation::TYPE_PRECEDES
128 128 )
129 129 child = Issue.generate!(:parent_issue_id => issue2.id)
130 130 issue1.reload
131 131 child.reload
132 132
133 133 r = IssueRelation.new(
134 134 :issue_from => child, :issue_to => issue1,
135 135 :relation_type => IssueRelation::TYPE_PRECEDES
136 136 )
137 137 assert !r.save
138 138 assert_include 'This relation would create a circular dependency', r.errors.full_messages
139 139 end
140 140
141 141 def test_subtasks_should_allow_precedes_relation
142 142 parent = Issue.generate!
143 143 child1 = Issue.generate!(:parent_issue_id => parent.id)
144 144 child2 = Issue.generate!(:parent_issue_id => parent.id)
145 145
146 146 r = IssueRelation.new(
147 147 :issue_from => child1, :issue_to => child2,
148 148 :relation_type => IssueRelation::TYPE_PRECEDES
149 149 )
150 150 assert r.valid?
151 151 assert r.save
152 152 end
153 153
154 154 def test_validates_circular_dependency_on_reverse_relations
155 155 IssueRelation.delete_all
156 156 assert IssueRelation.create!(
157 157 :issue_from => Issue.find(1), :issue_to => Issue.find(3),
158 158 :relation_type => IssueRelation::TYPE_BLOCKS
159 159 )
160 160 assert IssueRelation.create!(
161 161 :issue_from => Issue.find(1), :issue_to => Issue.find(2),
162 162 :relation_type => IssueRelation::TYPE_BLOCKED
163 163 )
164 164 r = IssueRelation.new(
165 165 :issue_from => Issue.find(2), :issue_to => Issue.find(1),
166 166 :relation_type => IssueRelation::TYPE_BLOCKED
167 167 )
168 168 assert !r.save
169 169 assert_not_equal [], r.errors[:base]
170 170 end
171 171
172 172 def test_create_with_initialized_journals_should_create_journals
173 173 from = Issue.find(1)
174 174 to = Issue.find(2)
175 175 from_journals = from.journals.size
176 176 to_journals = to.journals.size
177 177 relation = IssueRelation.new(:issue_from => from, :issue_to => to,
178 178 :relation_type => IssueRelation::TYPE_PRECEDES)
179 179 relation.init_journals User.find(1)
180 180 assert relation.save
181 181 from.reload
182 182 to.reload
183 183 relation.reload
184 184 assert_equal from.journals.size, (from_journals + 1)
185 185 assert_equal to.journals.size, (to_journals + 1)
186 186 assert_equal 'relation', from.journals.last.details.last.property
187 187 assert_equal 'precedes', from.journals.last.details.last.prop_key
188 188 assert_equal '2', from.journals.last.details.last.value
189 189 assert_nil from.journals.last.details.last.old_value
190 190 assert_equal 'relation', to.journals.last.details.last.property
191 191 assert_equal 'follows', to.journals.last.details.last.prop_key
192 192 assert_equal '1', to.journals.last.details.last.value
193 193 assert_nil to.journals.last.details.last.old_value
194 194 end
195 195
196 196 def test_destroy_with_initialized_journals_should_create_journals
197 197 relation = IssueRelation.find(1)
198 198 from = relation.issue_from
199 199 to = relation.issue_to
200 200 from_journals = from.journals.size
201 201 to_journals = to.journals.size
202 202 relation.init_journals User.find(1)
203 203 assert relation.destroy
204 204 from.reload
205 205 to.reload
206 206 assert_equal from.journals.size, (from_journals + 1)
207 207 assert_equal to.journals.size, (to_journals + 1)
208 208 assert_equal 'relation', from.journals.last.details.last.property
209 209 assert_equal 'blocks', from.journals.last.details.last.prop_key
210 210 assert_equal '9', from.journals.last.details.last.old_value
211 211 assert_nil from.journals.last.details.last.value
212 212 assert_equal 'relation', to.journals.last.details.last.property
213 213 assert_equal 'blocked', to.journals.last.details.last.prop_key
214 214 assert_equal '10', to.journals.last.details.last.old_value
215 215 assert_nil to.journals.last.details.last.value
216 216 end
217
218 def test_to_s_should_return_the_relation_string
219 set_language_if_valid 'en'
220 relation = IssueRelation.find(1)
221 assert_equal "Blocks #9", relation.to_s(relation.issue_from)
222 assert_equal "Blocked by #10", relation.to_s(relation.issue_to)
223 end
224
225 def test_to_s_without_argument_should_return_the_relation_string_for_issue_from
226 set_language_if_valid 'en'
227 relation = IssueRelation.find(1)
228 assert_equal "Blocks #9", relation.to_s
229 end
230
231 def test_to_s_should_accept_a_block_as_custom_issue_formatting
232 set_language_if_valid 'en'
233 relation = IssueRelation.find(1)
234 assert_equal "Blocks Bug #9", relation.to_s {|issue| "#{issue.tracker} ##{issue.id}"}
235 end
217 236 end
General Comments 0
You need to be logged in to leave comments. Login now