##// END OF EJS Templates
Warning "Can't mass-assign protected attributes for IssueRelation: issue_to_id" (#21695)....
Jean-Philippe Lang -
r14681:b3663ee5c4bf
parent child
Show More
@@ -1,92 +1,90
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 IssueRelationsController < ApplicationController
18 class IssueRelationsController < ApplicationController
19 before_filter :find_issue, :authorize, :only => [:index, :create]
19 before_filter :find_issue, :authorize, :only => [:index, :create]
20 before_filter :find_relation, :only => [:show, :destroy]
20 before_filter :find_relation, :only => [:show, :destroy]
21
21
22 accept_api_auth :index, :show, :create, :destroy
22 accept_api_auth :index, :show, :create, :destroy
23
23
24 def index
24 def index
25 @relations = @issue.relations
25 @relations = @issue.relations
26
26
27 respond_to do |format|
27 respond_to do |format|
28 format.html { render :nothing => true }
28 format.html { render :nothing => true }
29 format.api
29 format.api
30 end
30 end
31 end
31 end
32
32
33 def show
33 def show
34 raise Unauthorized unless @relation.visible?
34 raise Unauthorized unless @relation.visible?
35
35
36 respond_to do |format|
36 respond_to do |format|
37 format.html { render :nothing => true }
37 format.html { render :nothing => true }
38 format.api
38 format.api
39 end
39 end
40 end
40 end
41
41
42 def create
42 def create
43 @relation = IssueRelation.new(params[:relation])
43 @relation = IssueRelation.new
44 @relation.issue_from = @issue
44 @relation.issue_from = @issue
45 if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
45 @relation.safe_attributes = params[:relation]
46 @relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
47 end
48 @relation.init_journals(User.current)
46 @relation.init_journals(User.current)
49 saved = @relation.save
47 saved = @relation.save
50
48
51 respond_to do |format|
49 respond_to do |format|
52 format.html { redirect_to issue_path(@issue) }
50 format.html { redirect_to issue_path(@issue) }
53 format.js {
51 format.js {
54 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
52 @relations = @issue.reload.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
55 }
53 }
56 format.api {
54 format.api {
57 if saved
55 if saved
58 render :action => 'show', :status => :created, :location => relation_url(@relation)
56 render :action => 'show', :status => :created, :location => relation_url(@relation)
59 else
57 else
60 render_validation_errors(@relation)
58 render_validation_errors(@relation)
61 end
59 end
62 }
60 }
63 end
61 end
64 end
62 end
65
63
66 def destroy
64 def destroy
67 raise Unauthorized unless @relation.deletable?
65 raise Unauthorized unless @relation.deletable?
68 @relation.init_journals(User.current)
66 @relation.init_journals(User.current)
69 @relation.destroy
67 @relation.destroy
70
68
71 respond_to do |format|
69 respond_to do |format|
72 format.html { redirect_to issue_path(@relation.issue_from) }
70 format.html { redirect_to issue_path(@relation.issue_from) }
73 format.js
71 format.js
74 format.api { render_api_ok }
72 format.api { render_api_ok }
75 end
73 end
76 end
74 end
77
75
78 private
76 private
79
77
80 def find_issue
78 def find_issue
81 @issue = Issue.find(params[:issue_id])
79 @issue = Issue.find(params[:issue_id])
82 @project = @issue.project
80 @project = @issue.project
83 rescue ActiveRecord::RecordNotFound
81 rescue ActiveRecord::RecordNotFound
84 render_404
82 render_404
85 end
83 end
86
84
87 def find_relation
85 def find_relation
88 @relation = IssueRelation.find(params[:id])
86 @relation = IssueRelation.find(params[:id])
89 rescue ActiveRecord::RecordNotFound
87 rescue ActiveRecord::RecordNotFound
90 render_404
88 render_404
91 end
89 end
92 end
90 end
@@ -1,228 +1,248
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 IssueRelation < ActiveRecord::Base
18 class IssueRelation < ActiveRecord::Base
19 # Class used to represent the relations of an issue
19 # Class used to represent the relations of an issue
20 class Relations < Array
20 class Relations < Array
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 def initialize(issue, *args)
23 def initialize(issue, *args)
24 @issue = issue
24 @issue = issue
25 super(*args)
25 super(*args)
26 end
26 end
27
27
28 def to_s(*args)
28 def to_s(*args)
29 map {|relation| relation.to_s(@issue)}.join(', ')
29 map {|relation| relation.to_s(@issue)}.join(', ')
30 end
30 end
31 end
31 end
32
32
33 include Redmine::SafeAttributes
34
33 belongs_to :issue_from, :class_name => 'Issue'
35 belongs_to :issue_from, :class_name => 'Issue'
34 belongs_to :issue_to, :class_name => 'Issue'
36 belongs_to :issue_to, :class_name => 'Issue'
35
37
36 TYPE_RELATES = "relates"
38 TYPE_RELATES = "relates"
37 TYPE_DUPLICATES = "duplicates"
39 TYPE_DUPLICATES = "duplicates"
38 TYPE_DUPLICATED = "duplicated"
40 TYPE_DUPLICATED = "duplicated"
39 TYPE_BLOCKS = "blocks"
41 TYPE_BLOCKS = "blocks"
40 TYPE_BLOCKED = "blocked"
42 TYPE_BLOCKED = "blocked"
41 TYPE_PRECEDES = "precedes"
43 TYPE_PRECEDES = "precedes"
42 TYPE_FOLLOWS = "follows"
44 TYPE_FOLLOWS = "follows"
43 TYPE_COPIED_TO = "copied_to"
45 TYPE_COPIED_TO = "copied_to"
44 TYPE_COPIED_FROM = "copied_from"
46 TYPE_COPIED_FROM = "copied_from"
45
47
46 TYPES = {
48 TYPES = {
47 TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
49 TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to,
48 :order => 1, :sym => TYPE_RELATES },
50 :order => 1, :sym => TYPE_RELATES },
49 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
51 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by,
50 :order => 2, :sym => TYPE_DUPLICATED },
52 :order => 2, :sym => TYPE_DUPLICATED },
51 TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
53 TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates,
52 :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
54 :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
53 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by,
55 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by,
54 :order => 4, :sym => TYPE_BLOCKED },
56 :order => 4, :sym => TYPE_BLOCKED },
55 TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks,
57 TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks,
56 :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
58 :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
57 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows,
59 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows,
58 :order => 6, :sym => TYPE_FOLLOWS },
60 :order => 6, :sym => TYPE_FOLLOWS },
59 TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes,
61 TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes,
60 :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
62 :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
61 TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from,
63 TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from,
62 :order => 8, :sym => TYPE_COPIED_FROM },
64 :order => 8, :sym => TYPE_COPIED_FROM },
63 TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to,
65 TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to,
64 :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
66 :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
65 }.freeze
67 }.freeze
66
68
67 validates_presence_of :issue_from, :issue_to, :relation_type
69 validates_presence_of :issue_from, :issue_to, :relation_type
68 validates_inclusion_of :relation_type, :in => TYPES.keys
70 validates_inclusion_of :relation_type, :in => TYPES.keys
69 validates_numericality_of :delay, :allow_nil => true
71 validates_numericality_of :delay, :allow_nil => true
70 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
72 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
71 validate :validate_issue_relation
73 validate :validate_issue_relation
72
74
73 attr_protected :issue_from_id, :issue_to_id
75 attr_protected :issue_from_id, :issue_to_id
74 before_save :handle_issue_order
76 before_save :handle_issue_order
75 after_create :call_issues_relation_added_callback
77 after_create :call_issues_relation_added_callback
76 after_destroy :call_issues_relation_removed_callback
78 after_destroy :call_issues_relation_removed_callback
77
79
80 safe_attributes 'relation_type',
81 'delay',
82 'issue_to_id'
83
84 def safe_attributes=(attrs, user=User.current)
85 return unless attrs.is_a?(Hash)
86 attrs = attrs.deep_dup
87
88 if issue_id = attrs.delete('issue_to_id')
89 if issue_id.to_s.strip.match(/\A#?(\d+)\z/)
90 issue_id = $1.to_i
91 self.issue_to = Issue.visible(user).find_by_id(issue_id)
92 end
93 end
94
95 super(attrs)
96 end
97
78 def visible?(user=User.current)
98 def visible?(user=User.current)
79 (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
99 (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
80 end
100 end
81
101
82 def deletable?(user=User.current)
102 def deletable?(user=User.current)
83 visible?(user) &&
103 visible?(user) &&
84 ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
104 ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
85 (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
105 (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
86 end
106 end
87
107
88 def initialize(attributes=nil, *args)
108 def initialize(attributes=nil, *args)
89 super
109 super
90 if new_record?
110 if new_record?
91 if relation_type.blank?
111 if relation_type.blank?
92 self.relation_type = IssueRelation::TYPE_RELATES
112 self.relation_type = IssueRelation::TYPE_RELATES
93 end
113 end
94 end
114 end
95 end
115 end
96
116
97 def validate_issue_relation
117 def validate_issue_relation
98 if issue_from && issue_to
118 if issue_from && issue_to
99 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
119 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
100 unless issue_from.project_id == issue_to.project_id ||
120 unless issue_from.project_id == issue_to.project_id ||
101 Setting.cross_project_issue_relations?
121 Setting.cross_project_issue_relations?
102 errors.add :issue_to_id, :not_same_project
122 errors.add :issue_to_id, :not_same_project
103 end
123 end
104 if circular_dependency?
124 if circular_dependency?
105 errors.add :base, :circular_dependency
125 errors.add :base, :circular_dependency
106 end
126 end
107 if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
127 if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
108 errors.add :base, :cant_link_an_issue_with_a_descendant
128 errors.add :base, :cant_link_an_issue_with_a_descendant
109 end
129 end
110 end
130 end
111 end
131 end
112
132
113 def other_issue(issue)
133 def other_issue(issue)
114 (self.issue_from_id == issue.id) ? issue_to : issue_from
134 (self.issue_from_id == issue.id) ? issue_to : issue_from
115 end
135 end
116
136
117 # Returns the relation type for +issue+
137 # Returns the relation type for +issue+
118 def relation_type_for(issue)
138 def relation_type_for(issue)
119 if TYPES[relation_type]
139 if TYPES[relation_type]
120 if self.issue_from_id == issue.id
140 if self.issue_from_id == issue.id
121 relation_type
141 relation_type
122 else
142 else
123 TYPES[relation_type][:sym]
143 TYPES[relation_type][:sym]
124 end
144 end
125 end
145 end
126 end
146 end
127
147
128 def label_for(issue)
148 def label_for(issue)
129 TYPES[relation_type] ?
149 TYPES[relation_type] ?
130 TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] :
150 TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] :
131 :unknow
151 :unknow
132 end
152 end
133
153
134 def to_s(issue=nil)
154 def to_s(issue=nil)
135 issue ||= issue_from
155 issue ||= issue_from
136 issue_text = block_given? ? yield(other_issue(issue)) : "##{other_issue(issue).try(:id)}"
156 issue_text = block_given? ? yield(other_issue(issue)) : "##{other_issue(issue).try(:id)}"
137 s = []
157 s = []
138 s << l(label_for(issue))
158 s << l(label_for(issue))
139 s << "(#{l('datetime.distance_in_words.x_days', :count => delay)})" if delay && delay != 0
159 s << "(#{l('datetime.distance_in_words.x_days', :count => delay)})" if delay && delay != 0
140 s << issue_text
160 s << issue_text
141 s.join(' ')
161 s.join(' ')
142 end
162 end
143
163
144 def css_classes_for(issue)
164 def css_classes_for(issue)
145 "rel-#{relation_type_for(issue)}"
165 "rel-#{relation_type_for(issue)}"
146 end
166 end
147
167
148 def handle_issue_order
168 def handle_issue_order
149 reverse_if_needed
169 reverse_if_needed
150
170
151 if TYPE_PRECEDES == relation_type
171 if TYPE_PRECEDES == relation_type
152 self.delay ||= 0
172 self.delay ||= 0
153 else
173 else
154 self.delay = nil
174 self.delay = nil
155 end
175 end
156 set_issue_to_dates
176 set_issue_to_dates
157 end
177 end
158
178
159 def set_issue_to_dates
179 def set_issue_to_dates
160 soonest_start = self.successor_soonest_start
180 soonest_start = self.successor_soonest_start
161 if soonest_start && issue_to
181 if soonest_start && issue_to
162 issue_to.reschedule_on!(soonest_start)
182 issue_to.reschedule_on!(soonest_start)
163 end
183 end
164 end
184 end
165
185
166 def successor_soonest_start
186 def successor_soonest_start
167 if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
187 if (TYPE_PRECEDES == self.relation_type) && delay && issue_from &&
168 (issue_from.start_date || issue_from.due_date)
188 (issue_from.start_date || issue_from.due_date)
169 (issue_from.due_date || issue_from.start_date) + 1 + delay
189 (issue_from.due_date || issue_from.start_date) + 1 + delay
170 end
190 end
171 end
191 end
172
192
173 def <=>(relation)
193 def <=>(relation)
174 r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
194 r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
175 r == 0 ? id <=> relation.id : r
195 r == 0 ? id <=> relation.id : r
176 end
196 end
177
197
178 def init_journals(user)
198 def init_journals(user)
179 issue_from.init_journal(user) if issue_from
199 issue_from.init_journal(user) if issue_from
180 issue_to.init_journal(user) if issue_to
200 issue_to.init_journal(user) if issue_to
181 end
201 end
182
202
183 private
203 private
184
204
185 # Reverses the relation if needed so that it gets stored in the proper way
205 # Reverses the relation if needed so that it gets stored in the proper way
186 # Should not be reversed before validation so that it can be displayed back
206 # Should not be reversed before validation so that it can be displayed back
187 # as entered on new relation form
207 # as entered on new relation form
188 def reverse_if_needed
208 def reverse_if_needed
189 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
209 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
190 issue_tmp = issue_to
210 issue_tmp = issue_to
191 self.issue_to = issue_from
211 self.issue_to = issue_from
192 self.issue_from = issue_tmp
212 self.issue_from = issue_tmp
193 self.relation_type = TYPES[relation_type][:reverse]
213 self.relation_type = TYPES[relation_type][:reverse]
194 end
214 end
195 end
215 end
196
216
197 # Returns true if the relation would create a circular dependency
217 # Returns true if the relation would create a circular dependency
198 def circular_dependency?
218 def circular_dependency?
199 case relation_type
219 case relation_type
200 when 'follows'
220 when 'follows'
201 issue_from.would_reschedule? issue_to
221 issue_from.would_reschedule? issue_to
202 when 'precedes'
222 when 'precedes'
203 issue_to.would_reschedule? issue_from
223 issue_to.would_reschedule? issue_from
204 when 'blocked'
224 when 'blocked'
205 issue_from.blocks? issue_to
225 issue_from.blocks? issue_to
206 when 'blocks'
226 when 'blocks'
207 issue_to.blocks? issue_from
227 issue_to.blocks? issue_from
208 else
228 else
209 false
229 false
210 end
230 end
211 end
231 end
212
232
213 def call_issues_relation_added_callback
233 def call_issues_relation_added_callback
214 call_issues_callback :relation_added
234 call_issues_callback :relation_added
215 end
235 end
216
236
217 def call_issues_relation_removed_callback
237 def call_issues_relation_removed_callback
218 call_issues_callback :relation_removed
238 call_issues_callback :relation_removed
219 end
239 end
220
240
221 def call_issues_callback(name)
241 def call_issues_callback(name)
222 [issue_from, issue_to].each do |issue|
242 [issue_from, issue_to].each do |issue|
223 if issue
243 if issue
224 issue.send name, self
244 issue.send name, self
225 end
245 end
226 end
246 end
227 end
247 end
228 end
248 end
General Comments 0
You need to be logged in to leave comments. Login now