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