##// END OF EJS Templates
Prevent mass-assignment when adding/updating a version (#10390)....
Jean-Philippe Lang -
r9017:fef2e4b67252
parent child
Show More
@@ -1,203 +1,205
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 VersionsController < ApplicationController
19 19 menu_item :roadmap
20 20 model_object Version
21 21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
22 22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23 23 before_filter :find_project, :only => [:index, :new, :create, :close_completed]
24 24 before_filter :authorize
25 25
26 26 accept_api_auth :index, :show, :create, :update, :destroy
27 27
28 28 helper :custom_fields
29 29 helper :projects
30 30
31 31 def index
32 32 respond_to do |format|
33 33 format.html {
34 34 @trackers = @project.trackers.find(:all, :order => 'position')
35 35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
36 36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
37 37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
38 38
39 39 @versions = @project.shared_versions || []
40 40 @versions += @project.rolled_up_versions.visible if @with_subprojects
41 41 @versions = @versions.uniq.sort
42 42 unless params[:completed]
43 43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
44 44 @versions -= @completed_versions
45 45 end
46 46
47 47 @issues_by_version = {}
48 48 if @selected_tracker_ids.any? && @versions.any?
49 49 issues = Issue.visible.all(
50 50 :include => [:project, :status, :tracker, :priority, :fixed_version],
51 51 :conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)},
52 52 :order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id"
53 53 )
54 54 @issues_by_version = issues.group_by(&:fixed_version)
55 55 end
56 56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
57 57 }
58 58 format.api {
59 59 @versions = @project.shared_versions.all
60 60 }
61 61 end
62 62 end
63 63
64 64 def show
65 65 respond_to do |format|
66 66 format.html {
67 67 @issues = @version.fixed_issues.visible.find(:all,
68 68 :include => [:status, :tracker, :priority],
69 69 :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
70 70 }
71 71 format.api
72 72 end
73 73 end
74 74
75 75 def new
76 @version = @project.versions.build(params[:version])
76 @version = @project.versions.build
77 @version.safe_attributes = params[:version]
77 78
78 79 respond_to do |format|
79 80 format.html
80 81 format.js do
81 82 render :update do |page|
82 83 page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
83 84 page << "showModal('ajax-modal', '600px');"
84 85 page << "Form.Element.focus('version_name');"
85 86 end
86 87 end
87 88 end
88 89 end
89 90
90 91 def create
91 92 @version = @project.versions.build
92 93 if params[:version]
93 94 attributes = params[:version].dup
94 95 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
95 @version.attributes = attributes
96 @version.safe_attributes = attributes
96 97 end
97 98
98 99 if request.post?
99 100 if @version.save
100 101 respond_to do |format|
101 102 format.html do
102 103 flash[:notice] = l(:notice_successful_create)
103 104 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
104 105 end
105 106 format.js do
106 107 render(:update) {|page|
107 108 page << 'hideModal();'
108 109 # IE doesn't support the replace_html rjs method for select box options
109 110 page.replace "issue_fixed_version_id",
110 111 content_tag('select', content_tag('option') + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
111 112 }
112 113 end
113 114 format.api do
114 115 render :action => 'show', :status => :created, :location => version_url(@version)
115 116 end
116 117 end
117 118 else
118 119 respond_to do |format|
119 120 format.html { render :action => 'new' }
120 121 format.js do
121 122 render :update do |page|
122 123 page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
123 124 page << "Form.Element.focus('version_name');"
124 125 end
125 126 end
126 127 format.api { render_validation_errors(@version) }
127 128 end
128 129 end
129 130 end
130 131 end
131 132
132 133 def edit
133 134 end
134 135
135 136 def update
136 137 if request.put? && params[:version]
137 138 attributes = params[:version].dup
138 139 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
139 if @version.update_attributes(attributes)
140 @version.safe_attributes = attributes
141 if @version.save
140 142 respond_to do |format|
141 143 format.html {
142 144 flash[:notice] = l(:notice_successful_update)
143 145 redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
144 146 }
145 147 format.api { head :ok }
146 148 end
147 149 else
148 150 respond_to do |format|
149 151 format.html { render :action => 'edit' }
150 152 format.api { render_validation_errors(@version) }
151 153 end
152 154 end
153 155 end
154 156 end
155 157
156 158 def close_completed
157 159 if request.put?
158 160 @project.close_completed_versions
159 161 end
160 162 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
161 163 end
162 164
163 165 def destroy
164 166 if @version.fixed_issues.empty?
165 167 @version.destroy
166 168 respond_to do |format|
167 169 format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
168 170 format.api { head :ok }
169 171 end
170 172 else
171 173 respond_to do |format|
172 174 format.html {
173 175 flash[:error] = l(:notice_unable_delete_version)
174 176 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
175 177 }
176 178 format.api { head :unprocessable_entity }
177 179 end
178 180 end
179 181 end
180 182
181 183 def status_by
182 184 respond_to do |format|
183 185 format.html { render :action => 'show' }
184 186 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
185 187 end
186 188 end
187 189
188 190 private
189 191 def find_project
190 192 @project = Project.find(params[:project_id])
191 193 rescue ActiveRecord::RecordNotFound
192 194 render_404
193 195 end
194 196
195 197 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
196 198 if ids = params[:tracker_ids]
197 199 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
198 200 else
199 201 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
200 202 end
201 203 end
202 204
203 205 end
@@ -1,261 +1,271
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 Version < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 20 after_update :update_issues_from_sharing_change
20 21 belongs_to :project
21 22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
22 23 acts_as_customizable
23 24 acts_as_attachable :view_permission => :view_files,
24 25 :delete_permission => :manage_files
25 26
26 27 VERSION_STATUSES = %w(open locked closed)
27 28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
28 29
29 30 validates_presence_of :name
30 31 validates_uniqueness_of :name, :scope => [:project_id]
31 32 validates_length_of :name, :maximum => 60
32 33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
33 34 validates_inclusion_of :status, :in => VERSION_STATUSES
34 35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
35 36
36 37 named_scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
37 38 named_scope :open, :conditions => {:status => 'open'}
38 39 named_scope :visible, lambda {|*args| { :include => :project,
39 40 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
40 41
42 safe_attributes 'name',
43 'description',
44 'effective_date',
45 'due_date',
46 'wiki_page_title',
47 'status',
48 'sharing',
49 'custom_field_values'
50
41 51 # Returns true if +user+ or current user is allowed to view the version
42 52 def visible?(user=User.current)
43 53 user.allowed_to?(:view_issues, self.project)
44 54 end
45 55
46 56 # Version files have same visibility as project files
47 57 def attachments_visible?(*args)
48 58 project.present? && project.attachments_visible?(*args)
49 59 end
50 60
51 61 def start_date
52 62 @start_date ||= fixed_issues.minimum('start_date')
53 63 end
54 64
55 65 def due_date
56 66 effective_date
57 67 end
58 68
59 69 def due_date=(arg)
60 70 self.effective_date=(arg)
61 71 end
62 72
63 73 # Returns the total estimated time for this version
64 74 # (sum of leaves estimated_hours)
65 75 def estimated_hours
66 76 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
67 77 end
68 78
69 79 # Returns the total reported time for this version
70 80 def spent_hours
71 81 @spent_hours ||= TimeEntry.sum(:hours, :joins => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
72 82 end
73 83
74 84 def closed?
75 85 status == 'closed'
76 86 end
77 87
78 88 def open?
79 89 status == 'open'
80 90 end
81 91
82 92 # Returns true if the version is completed: due date reached and no open issues
83 93 def completed?
84 94 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
85 95 end
86 96
87 97 def behind_schedule?
88 98 if completed_pourcent == 100
89 99 return false
90 100 elsif due_date && start_date
91 101 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
92 102 return done_date <= Date.today
93 103 else
94 104 false # No issues so it's not late
95 105 end
96 106 end
97 107
98 108 # Returns the completion percentage of this version based on the amount of open/closed issues
99 109 # and the time spent on the open issues.
100 110 def completed_pourcent
101 111 if issues_count == 0
102 112 0
103 113 elsif open_issues_count == 0
104 114 100
105 115 else
106 116 issues_progress(false) + issues_progress(true)
107 117 end
108 118 end
109 119
110 120 # Returns the percentage of issues that have been marked as 'closed'.
111 121 def closed_pourcent
112 122 if issues_count == 0
113 123 0
114 124 else
115 125 issues_progress(false)
116 126 end
117 127 end
118 128
119 129 # Returns true if the version is overdue: due date reached and some open issues
120 130 def overdue?
121 131 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
122 132 end
123 133
124 134 # Returns assigned issues count
125 135 def issues_count
126 136 load_issue_counts
127 137 @issue_count
128 138 end
129 139
130 140 # Returns the total amount of open issues for this version.
131 141 def open_issues_count
132 142 load_issue_counts
133 143 @open_issues_count
134 144 end
135 145
136 146 # Returns the total amount of closed issues for this version.
137 147 def closed_issues_count
138 148 load_issue_counts
139 149 @closed_issues_count
140 150 end
141 151
142 152 def wiki_page
143 153 if project.wiki && !wiki_page_title.blank?
144 154 @wiki_page ||= project.wiki.find_page(wiki_page_title)
145 155 end
146 156 @wiki_page
147 157 end
148 158
149 159 def to_s; name end
150 160
151 161 def to_s_with_project
152 162 "#{project} - #{name}"
153 163 end
154 164
155 165 # Versions are sorted by effective_date and "Project Name - Version name"
156 166 # Those with no effective_date are at the end, sorted by "Project Name - Version name"
157 167 def <=>(version)
158 168 if self.effective_date
159 169 if version.effective_date
160 170 if self.effective_date == version.effective_date
161 171 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
162 172 else
163 173 self.effective_date <=> version.effective_date
164 174 end
165 175 else
166 176 -1
167 177 end
168 178 else
169 179 if version.effective_date
170 180 1
171 181 else
172 182 "#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
173 183 end
174 184 end
175 185 end
176 186
177 187 # Returns the sharings that +user+ can set the version to
178 188 def allowed_sharings(user = User.current)
179 189 VERSION_SHARINGS.select do |s|
180 190 if sharing == s
181 191 true
182 192 else
183 193 case s
184 194 when 'system'
185 195 # Only admin users can set a systemwide sharing
186 196 user.admin?
187 197 when 'hierarchy', 'tree'
188 198 # Only users allowed to manage versions of the root project can
189 199 # set sharing to hierarchy or tree
190 200 project.nil? || user.allowed_to?(:manage_versions, project.root)
191 201 else
192 202 true
193 203 end
194 204 end
195 205 end
196 206 end
197 207
198 208 private
199 209
200 210 def load_issue_counts
201 211 unless @issue_count
202 212 @open_issues_count = 0
203 213 @closed_issues_count = 0
204 214 fixed_issues.count(:all, :group => :status).each do |status, count|
205 215 if status.is_closed?
206 216 @closed_issues_count += count
207 217 else
208 218 @open_issues_count += count
209 219 end
210 220 end
211 221 @issue_count = @open_issues_count + @closed_issues_count
212 222 end
213 223 end
214 224
215 225 # Update the issue's fixed versions. Used if a version's sharing changes.
216 226 def update_issues_from_sharing_change
217 227 if sharing_changed?
218 228 if VERSION_SHARINGS.index(sharing_was).nil? ||
219 229 VERSION_SHARINGS.index(sharing).nil? ||
220 230 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
221 231 Issue.update_versions_from_sharing_change self
222 232 end
223 233 end
224 234 end
225 235
226 236 # Returns the average estimated time of assigned issues
227 237 # or 1 if no issue has an estimated time
228 238 # Used to weigth unestimated issues in progress calculation
229 239 def estimated_average
230 240 if @estimated_average.nil?
231 241 average = fixed_issues.average(:estimated_hours).to_f
232 242 if average == 0
233 243 average = 1
234 244 end
235 245 @estimated_average = average
236 246 end
237 247 @estimated_average
238 248 end
239 249
240 250 # Returns the total progress of open or closed issues. The returned percentage takes into account
241 251 # the amount of estimated time set for this version.
242 252 #
243 253 # Examples:
244 254 # issues_progress(true) => returns the progress percentage for open issues.
245 255 # issues_progress(false) => returns the progress percentage for closed issues.
246 256 def issues_progress(open)
247 257 @issues_progress ||= {}
248 258 @issues_progress[open] ||= begin
249 259 progress = 0
250 260 if issues_count > 0
251 261 ratio = open ? 'done_ratio' : 100
252 262
253 263 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
254 264 :joins => :status,
255 265 :conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
256 266 progress = done / (estimated_average * issues_count)
257 267 end
258 268 progress
259 269 end
260 270 end
261 271 end
General Comments 0
You need to be logged in to leave comments. Login now