##// END OF EJS Templates
Don't bulk edit file custom fields (#6719)....
Jean-Philippe Lang -
r15541:73ef85f672d9
parent child
Show More
@@ -1,89 +1,89
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 ContextMenusController < ApplicationController
19 19 helper :watchers
20 20 helper :issues
21 21
22 22 before_action :find_issues, :only => :issues
23 23
24 24 def issues
25 25 if (@issues.size == 1)
26 26 @issue = @issues.first
27 27 end
28 28 @issue_ids = @issues.map(&:id).sort
29 29
30 30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
31 31
32 32 @can = {:edit => @issues.all?(&:attributes_editable?),
33 33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
34 34 :copy => User.current.allowed_to?(:copy_issues, @projects) && Issue.allowed_target_projects.any?,
35 35 :add_watchers => User.current.allowed_to?(:add_issue_watchers, @projects),
36 36 :delete => @issues.all?(&:deletable?)
37 37 }
38 38
39 39 @assignables = @issues.map(&:assignable_users).reduce(:&)
40 40 @trackers = @projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
41 41 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
42 42
43 43 @priorities = IssuePriority.active.reverse
44 44 @back = back_url
45 45
46 46 @options_by_custom_field = {}
47 47 if @can[:edit]
48 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
48 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?).select {|field| field.format.bulk_edit_supported}
49 49 custom_fields.each do |field|
50 50 values = field.possible_values_options(@projects)
51 51 if values.present?
52 52 @options_by_custom_field[field] = values
53 53 end
54 54 end
55 55 end
56 56
57 57 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
58 58 render :layout => false
59 59 end
60 60
61 61 def time_entries
62 62 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
63 63 (render_404; return) unless @time_entries.present?
64 64 if (@time_entries.size == 1)
65 65 @time_entry = @time_entries.first
66 66 end
67 67
68 68 @projects = @time_entries.collect(&:project).compact.uniq
69 69 @project = @projects.first if @projects.size == 1
70 70 @activities = TimeEntryActivity.shared.active
71 71
72 72 edit_allowed = @time_entries.all? {|t| t.editable_by?(User.current)}
73 73 @can = {:edit => edit_allowed, :delete => edit_allowed}
74 74 @back = back_url
75 75
76 76 @options_by_custom_field = {}
77 77 if @can[:edit]
78 78 custom_fields = @time_entries.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
79 79 custom_fields.each do |field|
80 80 values = field.possible_values_options(@projects)
81 81 if values.present?
82 82 @options_by_custom_field[field] = values
83 83 end
84 84 end
85 85 end
86 86
87 87 render :layout => false
88 88 end
89 89 end
@@ -1,568 +1,568
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 IssuesController < ApplicationController
19 19 default_search_scope :issues
20 20
21 21 before_action :find_issue, :only => [:show, :edit, :update]
22 22 before_action :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 23 before_action :authorize, :except => [:index, :new, :create]
24 24 before_action :find_optional_project, :only => [:index, :new, :create]
25 25 before_action :build_new_issue_from_params, :only => [:new, :create]
26 26 accept_rss_auth :index, :show
27 27 accept_api_auth :index, :show, :create, :update, :destroy
28 28
29 29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30 30
31 31 helper :journals
32 32 helper :projects
33 33 helper :custom_fields
34 34 helper :issue_relations
35 35 helper :watchers
36 36 helper :attachments
37 37 helper :queries
38 38 include QueriesHelper
39 39 helper :repositories
40 40 helper :sort
41 41 include SortHelper
42 42 helper :timelog
43 43
44 44 def index
45 45 retrieve_query
46 46 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
47 47 sort_update(@query.sortable_columns)
48 48 @query.sort_criteria = sort_criteria.to_a
49 49
50 50 if @query.valid?
51 51 case params[:format]
52 52 when 'csv', 'pdf'
53 53 @limit = Setting.issues_export_limit.to_i
54 54 if params[:columns] == 'all'
55 55 @query.column_names = @query.available_inline_columns.map(&:name)
56 56 end
57 57 when 'atom'
58 58 @limit = Setting.feeds_limit.to_i
59 59 when 'xml', 'json'
60 60 @offset, @limit = api_offset_and_limit
61 61 @query.column_names = %w(author)
62 62 else
63 63 @limit = per_page_option
64 64 end
65 65
66 66 @issue_count = @query.issue_count
67 67 @issue_pages = Paginator.new @issue_count, @limit, params['page']
68 68 @offset ||= @issue_pages.offset
69 69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 70 :order => sort_clause,
71 71 :offset => @offset,
72 72 :limit => @limit)
73 73 @issue_count_by_group = @query.issue_count_by_group
74 74
75 75 respond_to do |format|
76 76 format.html { render :template => 'issues/index', :layout => !request.xhr? }
77 77 format.api {
78 78 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
79 79 }
80 80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
81 81 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
82 82 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
83 83 end
84 84 else
85 85 respond_to do |format|
86 86 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
87 87 format.any(:atom, :csv, :pdf) { head 422 }
88 88 format.api { render_validation_errors(@query) }
89 89 end
90 90 end
91 91 rescue ActiveRecord::RecordNotFound
92 92 render_404
93 93 end
94 94
95 95 def show
96 96 @journals = @issue.journals.
97 97 preload(:details).
98 98 preload(:user => :email_address).
99 99 reorder(:created_on, :id).to_a
100 100 @journals.each_with_index {|j,i| j.indice = i+1}
101 101 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
102 102 Journal.preload_journals_details_custom_fields(@journals)
103 103 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
104 104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 105
106 106 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
107 107 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
108 108
109 109 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
110 110 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
111 111 @priorities = IssuePriority.active
112 112 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
113 113 @relation = IssueRelation.new
114 114
115 115 respond_to do |format|
116 116 format.html {
117 117 retrieve_previous_and_next_issue_ids
118 118 render :template => 'issues/show'
119 119 }
120 120 format.api
121 121 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
122 122 format.pdf {
123 123 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
124 124 }
125 125 end
126 126 end
127 127
128 128 def new
129 129 respond_to do |format|
130 130 format.html { render :action => 'new', :layout => !request.xhr? }
131 131 format.js
132 132 end
133 133 end
134 134
135 135 def create
136 136 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
137 137 raise ::Unauthorized
138 138 end
139 139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
140 140 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
141 141 if @issue.save
142 142 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
143 143 respond_to do |format|
144 144 format.html {
145 145 render_attachment_warning_if_needed(@issue)
146 146 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
147 147 redirect_after_create
148 148 }
149 149 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
150 150 end
151 151 return
152 152 else
153 153 respond_to do |format|
154 154 format.html {
155 155 if @issue.project.nil?
156 156 render_error :status => 422
157 157 else
158 158 render :action => 'new'
159 159 end
160 160 }
161 161 format.api { render_validation_errors(@issue) }
162 162 end
163 163 end
164 164 end
165 165
166 166 def edit
167 167 return unless update_issue_from_params
168 168
169 169 respond_to do |format|
170 170 format.html { }
171 171 format.js
172 172 end
173 173 end
174 174
175 175 def update
176 176 return unless update_issue_from_params
177 177 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
178 178 saved = false
179 179 begin
180 180 saved = save_issue_with_child_records
181 181 rescue ActiveRecord::StaleObjectError
182 182 @conflict = true
183 183 if params[:last_journal_id]
184 184 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
185 185 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
186 186 end
187 187 end
188 188
189 189 if saved
190 190 render_attachment_warning_if_needed(@issue)
191 191 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
192 192
193 193 respond_to do |format|
194 194 format.html { redirect_back_or_default issue_path(@issue, previous_and_next_issue_ids_params) }
195 195 format.api { render_api_ok }
196 196 end
197 197 else
198 198 respond_to do |format|
199 199 format.html { render :action => 'edit' }
200 200 format.api { render_validation_errors(@issue) }
201 201 end
202 202 end
203 203 end
204 204
205 205 # Bulk edit/copy a set of issues
206 206 def bulk_edit
207 207 @issues.sort!
208 208 @copy = params[:copy].present?
209 209 @notes = params[:notes]
210 210
211 211 if @copy
212 212 unless User.current.allowed_to?(:copy_issues, @projects)
213 213 raise ::Unauthorized
214 214 end
215 215 else
216 216 unless @issues.all?(&:attributes_editable?)
217 217 raise ::Unauthorized
218 218 end
219 219 end
220 220
221 221 edited_issues = Issue.where(:id => @issues.map(&:id)).to_a
222 222
223 223 @allowed_projects = Issue.allowed_target_projects
224 224 if params[:issue]
225 225 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
226 226 if @target_project
227 227 target_projects = [@target_project]
228 228 edited_issues.each {|issue| issue.project = @target_project}
229 229 end
230 230 end
231 231 target_projects ||= @projects
232 232
233 233 @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
234 234 if params[:issue]
235 235 @target_tracker = @trackers.detect {|t| t.id.to_s == params[:issue][:tracker_id].to_s}
236 236 if @target_tracker
237 237 edited_issues.each {|issue| issue.tracker = @target_tracker}
238 238 end
239 239 end
240 240
241 241 if @copy
242 242 # Copied issues will get their default statuses
243 243 @available_statuses = []
244 244 else
245 245 @available_statuses = edited_issues.map(&:new_statuses_allowed_to).reduce(:&)
246 246 end
247 247 if params[:issue]
248 248 @target_status = @available_statuses.detect {|t| t.id.to_s == params[:issue][:status_id].to_s}
249 249 if @target_status
250 250 edited_issues.each {|issue| issue.status = @target_status}
251 251 end
252 252 end
253 253
254 @custom_fields = edited_issues.map{|i|i.editable_custom_fields}.reduce(:&)
254 @custom_fields = edited_issues.map{|i|i.editable_custom_fields}.reduce(:&).select {|field| field.format.bulk_edit_supported}
255 255 @assignables = target_projects.map(&:assignable_users).reduce(:&)
256 256 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
257 257 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
258 258 if @copy
259 259 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
260 260 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
261 261 end
262 262
263 263 @safe_attributes = edited_issues.map(&:safe_attribute_names).reduce(:&)
264 264
265 265 @issue_params = params[:issue] || {}
266 266 @issue_params[:custom_field_values] ||= {}
267 267 end
268 268
269 269 def bulk_update
270 270 @issues.sort!
271 271 @copy = params[:copy].present?
272 272
273 273 attributes = parse_params_for_bulk_update(params[:issue])
274 274 copy_subtasks = (params[:copy_subtasks] == '1')
275 275 copy_attachments = (params[:copy_attachments] == '1')
276 276
277 277 if @copy
278 278 unless User.current.allowed_to?(:copy_issues, @projects)
279 279 raise ::Unauthorized
280 280 end
281 281 target_projects = @projects
282 282 if attributes['project_id'].present?
283 283 target_projects = Project.where(:id => attributes['project_id']).to_a
284 284 end
285 285 unless User.current.allowed_to?(:add_issues, target_projects)
286 286 raise ::Unauthorized
287 287 end
288 288 else
289 289 unless @issues.all?(&:attributes_editable?)
290 290 raise ::Unauthorized
291 291 end
292 292 end
293 293
294 294 unsaved_issues = []
295 295 saved_issues = []
296 296
297 297 if @copy && copy_subtasks
298 298 # Descendant issues will be copied with the parent task
299 299 # Don't copy them twice
300 300 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
301 301 end
302 302
303 303 @issues.each do |orig_issue|
304 304 orig_issue.reload
305 305 if @copy
306 306 issue = orig_issue.copy({},
307 307 :attachments => copy_attachments,
308 308 :subtasks => copy_subtasks,
309 309 :link => link_copy?(params[:link_copy])
310 310 )
311 311 else
312 312 issue = orig_issue
313 313 end
314 314 journal = issue.init_journal(User.current, params[:notes])
315 315 issue.safe_attributes = attributes
316 316 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
317 317 if issue.save
318 318 saved_issues << issue
319 319 else
320 320 unsaved_issues << orig_issue
321 321 end
322 322 end
323 323
324 324 if unsaved_issues.empty?
325 325 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
326 326 if params[:follow]
327 327 if @issues.size == 1 && saved_issues.size == 1
328 328 redirect_to issue_path(saved_issues.first)
329 329 elsif saved_issues.map(&:project).uniq.size == 1
330 330 redirect_to project_issues_path(saved_issues.map(&:project).first)
331 331 end
332 332 else
333 333 redirect_back_or_default _project_issues_path(@project)
334 334 end
335 335 else
336 336 @saved_issues = @issues
337 337 @unsaved_issues = unsaved_issues
338 338 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
339 339 bulk_edit
340 340 render :action => 'bulk_edit'
341 341 end
342 342 end
343 343
344 344 def destroy
345 345 raise Unauthorized unless @issues.all?(&:deletable?)
346 346 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
347 347 if @hours > 0
348 348 case params[:todo]
349 349 when 'destroy'
350 350 # nothing to do
351 351 when 'nullify'
352 352 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
353 353 when 'reassign'
354 354 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
355 355 if reassign_to.nil?
356 356 flash.now[:error] = l(:error_issue_not_found_in_project)
357 357 return
358 358 else
359 359 TimeEntry.where(['issue_id IN (?)', @issues]).
360 360 update_all("issue_id = #{reassign_to.id}")
361 361 end
362 362 else
363 363 # display the destroy form if it's a user request
364 364 return unless api_request?
365 365 end
366 366 end
367 367 @issues.each do |issue|
368 368 begin
369 369 issue.reload.destroy
370 370 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
371 371 # nothing to do, issue was already deleted (eg. by a parent)
372 372 end
373 373 end
374 374 respond_to do |format|
375 375 format.html { redirect_back_or_default _project_issues_path(@project) }
376 376 format.api { render_api_ok }
377 377 end
378 378 end
379 379
380 380 # Overrides Redmine::MenuManager::MenuController::ClassMethods for
381 381 # when the "New issue" tab is enabled
382 382 def current_menu_item
383 383 if Setting.new_item_menu_tab == '1' && [:new, :create].include?(action_name.to_sym)
384 384 :new_issue
385 385 else
386 386 super
387 387 end
388 388 end
389 389
390 390 private
391 391
392 392 def retrieve_previous_and_next_issue_ids
393 393 if params[:prev_issue_id].present? || params[:next_issue_id].present?
394 394 @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
395 395 @next_issue_id = params[:next_issue_id].presence.try(:to_i)
396 396 @issue_position = params[:issue_position].presence.try(:to_i)
397 397 @issue_count = params[:issue_count].presence.try(:to_i)
398 398 else
399 399 retrieve_query_from_session
400 400 if @query
401 401 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
402 402 sort_update(@query.sortable_columns, 'issues_index_sort')
403 403 limit = 500
404 404 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
405 405 if (idx = issue_ids.index(@issue.id)) && idx < limit
406 406 if issue_ids.size < 500
407 407 @issue_position = idx + 1
408 408 @issue_count = issue_ids.size
409 409 end
410 410 @prev_issue_id = issue_ids[idx - 1] if idx > 0
411 411 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
412 412 end
413 413 end
414 414 end
415 415 end
416 416
417 417 def previous_and_next_issue_ids_params
418 418 {
419 419 :prev_issue_id => params[:prev_issue_id],
420 420 :next_issue_id => params[:next_issue_id],
421 421 :issue_position => params[:issue_position],
422 422 :issue_count => params[:issue_count]
423 423 }.reject {|k,v| k.blank?}
424 424 end
425 425
426 426 # Used by #edit and #update to set some common instance variables
427 427 # from the params
428 428 def update_issue_from_params
429 429 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
430 430 if params[:time_entry]
431 431 @time_entry.safe_attributes = params[:time_entry]
432 432 end
433 433
434 434 @issue.init_journal(User.current)
435 435
436 436 issue_attributes = params[:issue]
437 437 if issue_attributes && params[:conflict_resolution]
438 438 case params[:conflict_resolution]
439 439 when 'overwrite'
440 440 issue_attributes = issue_attributes.dup
441 441 issue_attributes.delete(:lock_version)
442 442 when 'add_notes'
443 443 issue_attributes = issue_attributes.slice(:notes, :private_notes)
444 444 when 'cancel'
445 445 redirect_to issue_path(@issue)
446 446 return false
447 447 end
448 448 end
449 449 @issue.safe_attributes = issue_attributes
450 450 @priorities = IssuePriority.active
451 451 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
452 452 true
453 453 end
454 454
455 455 # Used by #new and #create to build a new issue from the params
456 456 # The new issue will be copied from an existing one if copy_from parameter is given
457 457 def build_new_issue_from_params
458 458 @issue = Issue.new
459 459 if params[:copy_from]
460 460 begin
461 461 @issue.init_journal(User.current)
462 462 @copy_from = Issue.visible.find(params[:copy_from])
463 463 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
464 464 raise ::Unauthorized
465 465 end
466 466 @link_copy = link_copy?(params[:link_copy]) || request.get?
467 467 @copy_attachments = params[:copy_attachments].present? || request.get?
468 468 @copy_subtasks = params[:copy_subtasks].present? || request.get?
469 469 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
470 470 @issue.parent_issue_id = @copy_from.parent_id
471 471 rescue ActiveRecord::RecordNotFound
472 472 render_404
473 473 return
474 474 end
475 475 end
476 476 @issue.project = @project
477 477 if request.get?
478 478 @issue.project ||= @issue.allowed_target_projects.first
479 479 end
480 480 @issue.author ||= User.current
481 481 @issue.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date?
482 482
483 483 attrs = (params[:issue] || {}).deep_dup
484 484 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
485 485 attrs.delete(:status_id)
486 486 end
487 487 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
488 488 # Discard submitted version when changing the project on the issue form
489 489 # so we can use the default version for the new project
490 490 attrs.delete(:fixed_version_id)
491 491 end
492 492 @issue.safe_attributes = attrs
493 493
494 494 if @issue.project
495 495 @issue.tracker ||= @issue.allowed_target_trackers.first
496 496 if @issue.tracker.nil?
497 497 if @issue.project.trackers.any?
498 498 # None of the project trackers is allowed to the user
499 499 render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
500 500 else
501 501 # Project has no trackers
502 502 render_error l(:error_no_tracker_in_project)
503 503 end
504 504 return false
505 505 end
506 506 if @issue.status.nil?
507 507 render_error l(:error_no_default_issue_status)
508 508 return false
509 509 end
510 510 elsif request.get?
511 511 render_error :message => l(:error_no_projects_with_tracker_allowed_for_new_issue), :status => 403
512 512 return false
513 513 end
514 514
515 515 @priorities = IssuePriority.active
516 516 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
517 517 end
518 518
519 519 # Saves @issue and a time_entry from the parameters
520 520 def save_issue_with_child_records
521 521 Issue.transaction do
522 522 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
523 523 time_entry = @time_entry || TimeEntry.new
524 524 time_entry.project = @issue.project
525 525 time_entry.issue = @issue
526 526 time_entry.user = User.current
527 527 time_entry.spent_on = User.current.today
528 528 time_entry.attributes = params[:time_entry]
529 529 @issue.time_entries << time_entry
530 530 end
531 531
532 532 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
533 533 if @issue.save
534 534 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
535 535 else
536 536 raise ActiveRecord::Rollback
537 537 end
538 538 end
539 539 end
540 540
541 541 # Returns true if the issue copy should be linked
542 542 # to the original issue
543 543 def link_copy?(param)
544 544 case Setting.link_copied_issue
545 545 when 'yes'
546 546 true
547 547 when 'no'
548 548 false
549 549 when 'ask'
550 550 param == '1'
551 551 end
552 552 end
553 553
554 554 # Redirects user after a successful issue creation
555 555 def redirect_after_create
556 556 if params[:continue]
557 557 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
558 558 if params[:project_id]
559 559 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
560 560 else
561 561 attrs.merge! :project_id => @issue.project_id
562 562 redirect_to new_issue_path(:issue => attrs)
563 563 end
564 564 else
565 565 redirect_to issue_path(@issue)
566 566 end
567 567 end
568 568 end
@@ -1,974 +1,979
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 'uri'
19 19
20 20 module Redmine
21 21 module FieldFormat
22 22 def self.add(name, klass)
23 23 all[name.to_s] = klass.instance
24 24 end
25 25
26 26 def self.delete(name)
27 27 all.delete(name.to_s)
28 28 end
29 29
30 30 def self.all
31 31 @formats ||= Hash.new(Base.instance)
32 32 end
33 33
34 34 def self.available_formats
35 35 all.keys
36 36 end
37 37
38 38 def self.find(name)
39 39 all[name.to_s]
40 40 end
41 41
42 42 # Return an array of custom field formats which can be used in select_tag
43 43 def self.as_select(class_name=nil)
44 44 formats = all.values.select do |format|
45 45 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
46 46 end
47 47 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
48 48 end
49 49
50 50 # Returns an array of formats that can be used for a custom field class
51 51 def self.formats_for_custom_field_class(klass=nil)
52 52 all.values.select do |format|
53 53 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(klass.name)
54 54 end
55 55 end
56 56
57 57 class Base
58 58 include Singleton
59 59 include Redmine::I18n
60 60 include Redmine::Helpers::URL
61 61 include ERB::Util
62 62
63 63 class_attribute :format_name
64 64 self.format_name = nil
65 65
66 66 # Set this to true if the format supports multiple values
67 67 class_attribute :multiple_supported
68 68 self.multiple_supported = false
69 69
70 70 # Set this to true if the format supports filtering on custom values
71 71 class_attribute :is_filter_supported
72 72 self.is_filter_supported = true
73 73
74 74 # Set this to true if the format supports textual search on custom values
75 75 class_attribute :searchable_supported
76 76 self.searchable_supported = false
77 77
78 78 # Set this to true if field values can be summed up
79 79 class_attribute :totalable_supported
80 80 self.totalable_supported = false
81 81
82 # Set this to false if field cannot be bulk edited
83 class_attribute :bulk_edit_supported
84 self.bulk_edit_supported = true
85
82 86 # Restricts the classes that the custom field can be added to
83 87 # Set to nil for no restrictions
84 88 class_attribute :customized_class_names
85 89 self.customized_class_names = nil
86 90
87 91 # Name of the partial for editing the custom field
88 92 class_attribute :form_partial
89 93 self.form_partial = nil
90 94
91 95 class_attribute :change_as_diff
92 96 self.change_as_diff = false
93 97
94 98 class_attribute :change_no_details
95 99 self.change_no_details = false
96 100
97 101 def self.add(name)
98 102 self.format_name = name
99 103 Redmine::FieldFormat.add(name, self)
100 104 end
101 105 private_class_method :add
102 106
103 107 def self.field_attributes(*args)
104 108 CustomField.store_accessor :format_store, *args
105 109 end
106 110
107 111 field_attributes :url_pattern
108 112
109 113 def name
110 114 self.class.format_name
111 115 end
112 116
113 117 def label
114 118 "label_#{name}"
115 119 end
116 120
117 121 def set_custom_field_value(custom_field, custom_field_value, value)
118 122 if value.is_a?(Array)
119 123 value = value.map(&:to_s).reject{|v| v==''}.uniq
120 124 if value.empty?
121 125 value << ''
122 126 end
123 127 else
124 128 value = value.to_s
125 129 end
126 130
127 131 value
128 132 end
129 133
130 134 def cast_custom_value(custom_value)
131 135 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
132 136 end
133 137
134 138 def cast_value(custom_field, value, customized=nil)
135 139 if value.blank?
136 140 nil
137 141 elsif value.is_a?(Array)
138 142 casted = value.map do |v|
139 143 cast_single_value(custom_field, v, customized)
140 144 end
141 145 casted.compact.sort
142 146 else
143 147 cast_single_value(custom_field, value, customized)
144 148 end
145 149 end
146 150
147 151 def cast_single_value(custom_field, value, customized=nil)
148 152 value.to_s
149 153 end
150 154
151 155 def target_class
152 156 nil
153 157 end
154 158
155 159 def possible_custom_value_options(custom_value)
156 160 possible_values_options(custom_value.custom_field, custom_value.customized)
157 161 end
158 162
159 163 def possible_values_options(custom_field, object=nil)
160 164 []
161 165 end
162 166
163 167 def value_from_keyword(custom_field, keyword, object)
164 168 possible_values_options = possible_values_options(custom_field, object)
165 169 if possible_values_options.present?
166 170 keyword = keyword.to_s
167 171 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
168 172 if v.is_a?(Array)
169 173 v.last
170 174 else
171 175 v
172 176 end
173 177 end
174 178 else
175 179 keyword
176 180 end
177 181 end
178 182
179 183 # Returns the validation errors for custom_field
180 184 # Should return an empty array if custom_field is valid
181 185 def validate_custom_field(custom_field)
182 186 errors = []
183 187 pattern = custom_field.url_pattern
184 188 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
185 189 errors << [:url_pattern, :invalid]
186 190 end
187 191 errors
188 192 end
189 193
190 194 # Returns the validation error messages for custom_value
191 195 # Should return an empty array if custom_value is valid
192 196 # custom_value is a CustomFieldValue.
193 197 def validate_custom_value(custom_value)
194 198 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
195 199 errors = values.map do |value|
196 200 validate_single_value(custom_value.custom_field, value, custom_value.customized)
197 201 end
198 202 errors.flatten.uniq
199 203 end
200 204
201 205 def validate_single_value(custom_field, value, customized=nil)
202 206 []
203 207 end
204 208
205 209 # CustomValue after_save callback
206 210 def after_save_custom_value(custom_field, custom_value)
207 211 end
208 212
209 213 def formatted_custom_value(view, custom_value, html=false)
210 214 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
211 215 end
212 216
213 217 def formatted_value(view, custom_field, value, customized=nil, html=false)
214 218 casted = cast_value(custom_field, value, customized)
215 219 if html && custom_field.url_pattern.present?
216 220 texts_and_urls = Array.wrap(casted).map do |single_value|
217 221 text = view.format_object(single_value, false).to_s
218 222 url = url_from_pattern(custom_field, single_value, customized)
219 223 [text, url]
220 224 end
221 225 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
222 226 links.join(', ').html_safe
223 227 else
224 228 casted
225 229 end
226 230 end
227 231
228 232 # Returns an URL generated with the custom field URL pattern
229 233 # and variables substitution:
230 234 # %value% => the custom field value
231 235 # %id% => id of the customized object
232 236 # %project_id% => id of the project of the customized object if defined
233 237 # %project_identifier% => identifier of the project of the customized object if defined
234 238 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
235 239 def url_from_pattern(custom_field, value, customized)
236 240 url = custom_field.url_pattern.to_s.dup
237 241 url.gsub!('%value%') {URI.encode value.to_s}
238 242 url.gsub!('%id%') {URI.encode customized.id.to_s}
239 243 url.gsub!('%project_id%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
240 244 url.gsub!('%project_identifier%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
241 245 if custom_field.regexp.present?
242 246 url.gsub!(%r{%m(\d+)%}) do
243 247 m = $1.to_i
244 248 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
245 249 URI.encode matches[m].to_s
246 250 end
247 251 end
248 252 end
249 253 url
250 254 end
251 255 protected :url_from_pattern
252 256
253 257 # Returns the URL pattern with substitution tokens removed,
254 258 # for validation purpose
255 259 def url_pattern_without_tokens(url_pattern)
256 260 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
257 261 end
258 262 protected :url_pattern_without_tokens
259 263
260 264 def edit_tag(view, tag_id, tag_name, custom_value, options={})
261 265 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
262 266 end
263 267
264 268 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
265 269 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
266 270 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
267 271 end
268 272
269 273 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
270 274 if custom_field.is_required?
271 275 ''.html_safe
272 276 else
273 277 view.content_tag('label',
274 278 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
275 279 :class => 'inline'
276 280 )
277 281 end
278 282 end
279 283 protected :bulk_clear_tag
280 284
281 285 def query_filter_options(custom_field, query)
282 286 {:type => :string}
283 287 end
284 288
285 289 def before_custom_field_save(custom_field)
286 290 end
287 291
288 292 # Returns a ORDER BY clause that can used to sort customized
289 293 # objects by their value of the custom field.
290 294 # Returns nil if the custom field can not be used for sorting.
291 295 def order_statement(custom_field)
292 296 # COALESCE is here to make sure that blank and NULL values are sorted equally
293 297 "COALESCE(#{join_alias custom_field}.value, '')"
294 298 end
295 299
296 300 # Returns a GROUP BY clause that can used to group by custom value
297 301 # Returns nil if the custom field can not be used for grouping.
298 302 def group_statement(custom_field)
299 303 nil
300 304 end
301 305
302 306 # Returns a JOIN clause that is added to the query when sorting by custom values
303 307 def join_for_order_statement(custom_field)
304 308 alias_name = join_alias(custom_field)
305 309
306 310 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
307 311 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
308 312 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
309 313 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
310 314 " AND (#{custom_field.visibility_by_project_condition})" +
311 315 " AND #{alias_name}.value <> ''" +
312 316 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
313 317 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
314 318 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
315 319 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
316 320 end
317 321
318 322 def join_alias(custom_field)
319 323 "cf_#{custom_field.id}"
320 324 end
321 325 protected :join_alias
322 326 end
323 327
324 328 class Unbounded < Base
325 329 def validate_single_value(custom_field, value, customized=nil)
326 330 errs = super
327 331 value = value.to_s
328 332 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
329 333 errs << ::I18n.t('activerecord.errors.messages.invalid')
330 334 end
331 335 if custom_field.min_length && value.length < custom_field.min_length
332 336 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
333 337 end
334 338 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
335 339 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
336 340 end
337 341 errs
338 342 end
339 343 end
340 344
341 345 class StringFormat < Unbounded
342 346 add 'string'
343 347 self.searchable_supported = true
344 348 self.form_partial = 'custom_fields/formats/string'
345 349 field_attributes :text_formatting
346 350
347 351 def formatted_value(view, custom_field, value, customized=nil, html=false)
348 352 if html
349 353 if custom_field.url_pattern.present?
350 354 super
351 355 elsif custom_field.text_formatting == 'full'
352 356 view.textilizable(value, :object => customized)
353 357 else
354 358 value.to_s
355 359 end
356 360 else
357 361 value.to_s
358 362 end
359 363 end
360 364 end
361 365
362 366 class TextFormat < Unbounded
363 367 add 'text'
364 368 self.searchable_supported = true
365 369 self.form_partial = 'custom_fields/formats/text'
366 370 self.change_as_diff = true
367 371
368 372 def formatted_value(view, custom_field, value, customized=nil, html=false)
369 373 if html
370 374 if value.present?
371 375 if custom_field.text_formatting == 'full'
372 376 view.textilizable(value, :object => customized)
373 377 else
374 378 view.simple_format(html_escape(value))
375 379 end
376 380 else
377 381 ''
378 382 end
379 383 else
380 384 value.to_s
381 385 end
382 386 end
383 387
384 388 def edit_tag(view, tag_id, tag_name, custom_value, options={})
385 389 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
386 390 end
387 391
388 392 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
389 393 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
390 394 '<br />'.html_safe +
391 395 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
392 396 end
393 397
394 398 def query_filter_options(custom_field, query)
395 399 {:type => :text}
396 400 end
397 401 end
398 402
399 403 class LinkFormat < StringFormat
400 404 add 'link'
401 405 self.searchable_supported = false
402 406 self.form_partial = 'custom_fields/formats/link'
403 407
404 408 def formatted_value(view, custom_field, value, customized=nil, html=false)
405 409 if html && value.present?
406 410 if custom_field.url_pattern.present?
407 411 url = url_from_pattern(custom_field, value, customized)
408 412 else
409 413 url = value.to_s
410 414 unless url =~ %r{\A[a-z]+://}i
411 415 # no protocol found, use http by default
412 416 url = "http://" + url
413 417 end
414 418 end
415 419 view.link_to value.to_s.truncate(40), url
416 420 else
417 421 value.to_s
418 422 end
419 423 end
420 424 end
421 425
422 426 class Numeric < Unbounded
423 427 self.form_partial = 'custom_fields/formats/numeric'
424 428 self.totalable_supported = true
425 429
426 430 def order_statement(custom_field)
427 431 # Make the database cast values into numeric
428 432 # Postgresql will raise an error if a value can not be casted!
429 433 # CustomValue validations should ensure that it doesn't occur
430 434 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
431 435 end
432 436
433 437 # Returns totals for the given scope
434 438 def total_for_scope(custom_field, scope)
435 439 scope.joins(:custom_values).
436 440 where(:custom_values => {:custom_field_id => custom_field.id}).
437 441 where.not(:custom_values => {:value => ''}).
438 442 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
439 443 end
440 444
441 445 def cast_total_value(custom_field, value)
442 446 cast_single_value(custom_field, value)
443 447 end
444 448 end
445 449
446 450 class IntFormat < Numeric
447 451 add 'int'
448 452
449 453 def label
450 454 "label_integer"
451 455 end
452 456
453 457 def cast_single_value(custom_field, value, customized=nil)
454 458 value.to_i
455 459 end
456 460
457 461 def validate_single_value(custom_field, value, customized=nil)
458 462 errs = super
459 463 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
460 464 errs
461 465 end
462 466
463 467 def query_filter_options(custom_field, query)
464 468 {:type => :integer}
465 469 end
466 470
467 471 def group_statement(custom_field)
468 472 order_statement(custom_field)
469 473 end
470 474 end
471 475
472 476 class FloatFormat < Numeric
473 477 add 'float'
474 478
475 479 def cast_single_value(custom_field, value, customized=nil)
476 480 value.to_f
477 481 end
478 482
479 483 def cast_total_value(custom_field, value)
480 484 value.to_f.round(2)
481 485 end
482 486
483 487 def validate_single_value(custom_field, value, customized=nil)
484 488 errs = super
485 489 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
486 490 errs
487 491 end
488 492
489 493 def query_filter_options(custom_field, query)
490 494 {:type => :float}
491 495 end
492 496 end
493 497
494 498 class DateFormat < Unbounded
495 499 add 'date'
496 500 self.form_partial = 'custom_fields/formats/date'
497 501
498 502 def cast_single_value(custom_field, value, customized=nil)
499 503 value.to_date rescue nil
500 504 end
501 505
502 506 def validate_single_value(custom_field, value, customized=nil)
503 507 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
504 508 []
505 509 else
506 510 [::I18n.t('activerecord.errors.messages.not_a_date')]
507 511 end
508 512 end
509 513
510 514 def edit_tag(view, tag_id, tag_name, custom_value, options={})
511 515 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
512 516 view.calendar_for(tag_id)
513 517 end
514 518
515 519 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
516 520 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
517 521 view.calendar_for(tag_id) +
518 522 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
519 523 end
520 524
521 525 def query_filter_options(custom_field, query)
522 526 {:type => :date}
523 527 end
524 528
525 529 def group_statement(custom_field)
526 530 order_statement(custom_field)
527 531 end
528 532 end
529 533
530 534 class List < Base
531 535 self.multiple_supported = true
532 536 field_attributes :edit_tag_style
533 537
534 538 def edit_tag(view, tag_id, tag_name, custom_value, options={})
535 539 if custom_value.custom_field.edit_tag_style == 'check_box'
536 540 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
537 541 else
538 542 select_edit_tag(view, tag_id, tag_name, custom_value, options)
539 543 end
540 544 end
541 545
542 546 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
543 547 opts = []
544 548 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
545 549 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
546 550 opts += possible_values_options(custom_field, objects)
547 551 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
548 552 end
549 553
550 554 def query_filter_options(custom_field, query)
551 555 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
552 556 end
553 557
554 558 protected
555 559
556 560 # Returns the values that are available in the field filter
557 561 def query_filter_values(custom_field, query)
558 562 possible_values_options(custom_field, query.project)
559 563 end
560 564
561 565 # Renders the edit tag as a select tag
562 566 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
563 567 blank_option = ''.html_safe
564 568 unless custom_value.custom_field.multiple?
565 569 if custom_value.custom_field.is_required?
566 570 unless custom_value.custom_field.default_value.present?
567 571 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
568 572 end
569 573 else
570 574 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
571 575 end
572 576 end
573 577 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
574 578 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
575 579 if custom_value.custom_field.multiple?
576 580 s << view.hidden_field_tag(tag_name, '')
577 581 end
578 582 s
579 583 end
580 584
581 585 # Renders the edit tag as check box or radio tags
582 586 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
583 587 opts = []
584 588 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
585 589 opts << ["(#{l(:label_none)})", '']
586 590 end
587 591 opts += possible_custom_value_options(custom_value)
588 592 s = ''.html_safe
589 593 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
590 594 opts.each do |label, value|
591 595 value ||= label
592 596 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
593 597 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
594 598 # set the id on the first tag only
595 599 tag_id = nil
596 600 s << view.content_tag('label', tag + ' ' + label)
597 601 end
598 602 if custom_value.custom_field.multiple?
599 603 s << view.hidden_field_tag(tag_name, '')
600 604 end
601 605 css = "#{options[:class]} check_box_group"
602 606 view.content_tag('span', s, options.merge(:class => css))
603 607 end
604 608 end
605 609
606 610 class ListFormat < List
607 611 add 'list'
608 612 self.searchable_supported = true
609 613 self.form_partial = 'custom_fields/formats/list'
610 614
611 615 def possible_custom_value_options(custom_value)
612 616 options = possible_values_options(custom_value.custom_field)
613 617 missing = [custom_value.value].flatten.reject(&:blank?) - options
614 618 if missing.any?
615 619 options += missing
616 620 end
617 621 options
618 622 end
619 623
620 624 def possible_values_options(custom_field, object=nil)
621 625 custom_field.possible_values
622 626 end
623 627
624 628 def validate_custom_field(custom_field)
625 629 errors = []
626 630 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
627 631 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
628 632 errors
629 633 end
630 634
631 635 def validate_custom_value(custom_value)
632 636 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
633 637 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
634 638 if invalid_values.any?
635 639 [::I18n.t('activerecord.errors.messages.inclusion')]
636 640 else
637 641 []
638 642 end
639 643 end
640 644
641 645 def group_statement(custom_field)
642 646 order_statement(custom_field)
643 647 end
644 648 end
645 649
646 650 class BoolFormat < List
647 651 add 'bool'
648 652 self.multiple_supported = false
649 653 self.form_partial = 'custom_fields/formats/bool'
650 654
651 655 def label
652 656 "label_boolean"
653 657 end
654 658
655 659 def cast_single_value(custom_field, value, customized=nil)
656 660 value == '1' ? true : false
657 661 end
658 662
659 663 def possible_values_options(custom_field, object=nil)
660 664 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
661 665 end
662 666
663 667 def group_statement(custom_field)
664 668 order_statement(custom_field)
665 669 end
666 670
667 671 def edit_tag(view, tag_id, tag_name, custom_value, options={})
668 672 case custom_value.custom_field.edit_tag_style
669 673 when 'check_box'
670 674 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
671 675 when 'radio'
672 676 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
673 677 else
674 678 select_edit_tag(view, tag_id, tag_name, custom_value, options)
675 679 end
676 680 end
677 681
678 682 # Renders the edit tag as a simple check box
679 683 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
680 684 s = ''.html_safe
681 685 s << view.hidden_field_tag(tag_name, '0', :id => nil)
682 686 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
683 687 view.content_tag('span', s, options)
684 688 end
685 689 end
686 690
687 691 class RecordList < List
688 692 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
689 693
690 694 def cast_single_value(custom_field, value, customized=nil)
691 695 target_class.find_by_id(value.to_i) if value.present?
692 696 end
693 697
694 698 def target_class
695 699 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
696 700 end
697 701
698 702 def reset_target_class
699 703 @target_class = nil
700 704 end
701 705
702 706 def possible_custom_value_options(custom_value)
703 707 options = possible_values_options(custom_value.custom_field, custom_value.customized)
704 708 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
705 709 if missing.any?
706 710 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
707 711 end
708 712 options
709 713 end
710 714
711 715 def order_statement(custom_field)
712 716 if target_class.respond_to?(:fields_for_order_statement)
713 717 target_class.fields_for_order_statement(value_join_alias(custom_field))
714 718 end
715 719 end
716 720
717 721 def group_statement(custom_field)
718 722 "COALESCE(#{join_alias custom_field}.value, '')"
719 723 end
720 724
721 725 def join_for_order_statement(custom_field)
722 726 alias_name = join_alias(custom_field)
723 727
724 728 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
725 729 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
726 730 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
727 731 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
728 732 " AND (#{custom_field.visibility_by_project_condition})" +
729 733 " AND #{alias_name}.value <> ''" +
730 734 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
731 735 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
732 736 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
733 737 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
734 738 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
735 739 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
736 740 end
737 741
738 742 def value_join_alias(custom_field)
739 743 join_alias(custom_field) + "_" + custom_field.field_format
740 744 end
741 745 protected :value_join_alias
742 746 end
743 747
744 748 class EnumerationFormat < RecordList
745 749 add 'enumeration'
746 750 self.form_partial = 'custom_fields/formats/enumeration'
747 751
748 752 def label
749 753 "label_field_format_enumeration"
750 754 end
751 755
752 756 def target_class
753 757 @target_class ||= CustomFieldEnumeration
754 758 end
755 759
756 760 def possible_values_options(custom_field, object=nil)
757 761 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
758 762 end
759 763
760 764 def possible_values_records(custom_field, object=nil)
761 765 custom_field.enumerations.active
762 766 end
763 767
764 768 def value_from_keyword(custom_field, keyword, object)
765 769 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
766 770 value ? value.id : nil
767 771 end
768 772 end
769 773
770 774 class UserFormat < RecordList
771 775 add 'user'
772 776 self.form_partial = 'custom_fields/formats/user'
773 777 field_attributes :user_role
774 778
775 779 def possible_values_options(custom_field, object=nil)
776 780 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
777 781 end
778 782
779 783 def possible_values_records(custom_field, object=nil)
780 784 if object.is_a?(Array)
781 785 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
782 786 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
783 787 elsif object.respond_to?(:project) && object.project
784 788 scope = object.project.users
785 789 if custom_field.user_role.is_a?(Array)
786 790 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
787 791 if role_ids.any?
788 792 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
789 793 end
790 794 end
791 795 scope.sorted
792 796 else
793 797 []
794 798 end
795 799 end
796 800
797 801 def value_from_keyword(custom_field, keyword, object)
798 802 users = possible_values_records(custom_field, object).to_a
799 803 user = Principal.detect_by_keyword(users, keyword)
800 804 user ? user.id : nil
801 805 end
802 806
803 807 def before_custom_field_save(custom_field)
804 808 super
805 809 if custom_field.user_role.is_a?(Array)
806 810 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
807 811 end
808 812 end
809 813 end
810 814
811 815 class VersionFormat < RecordList
812 816 add 'version'
813 817 self.form_partial = 'custom_fields/formats/version'
814 818 field_attributes :version_status
815 819
816 820 def possible_values_options(custom_field, object=nil)
817 821 versions_options(custom_field, object)
818 822 end
819 823
820 824 def before_custom_field_save(custom_field)
821 825 super
822 826 if custom_field.version_status.is_a?(Array)
823 827 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
824 828 end
825 829 end
826 830
827 831 protected
828 832
829 833 def query_filter_values(custom_field, query)
830 834 versions_options(custom_field, query.project, true)
831 835 end
832 836
833 837 def versions_options(custom_field, object, all_statuses=false)
834 838 if object.is_a?(Array)
835 839 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
836 840 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
837 841 elsif object.respond_to?(:project) && object.project
838 842 scope = object.project.shared_versions
839 843 filtered_versions_options(custom_field, scope, all_statuses)
840 844 elsif object.nil?
841 845 scope = ::Version.visible.where(:sharing => 'system')
842 846 filtered_versions_options(custom_field, scope, all_statuses)
843 847 else
844 848 []
845 849 end
846 850 end
847 851
848 852 def filtered_versions_options(custom_field, scope, all_statuses=false)
849 853 if !all_statuses && custom_field.version_status.is_a?(Array)
850 854 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
851 855 if statuses.any?
852 856 scope = scope.where(:status => statuses.map(&:to_s))
853 857 end
854 858 end
855 859 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
856 860 end
857 861 end
858 862
859 863 class AttachementFormat < Base
860 864 add 'attachment'
861 865 self.form_partial = 'custom_fields/formats/attachment'
862 866 self.is_filter_supported = false
863 867 self.change_no_details = true
868 self.bulk_edit_supported = false
864 869 field_attributes :extensions_allowed
865 870
866 871 def set_custom_field_value(custom_field, custom_field_value, value)
867 872 attachment_present = false
868 873
869 874 if value.is_a?(Hash)
870 875 attachment_present = true
871 876 value = value.except(:blank)
872 877
873 878 if value.values.any? && value.values.all? {|v| v.is_a?(Hash)}
874 879 value = value.values.first
875 880 end
876 881
877 882 if value.key?(:id)
878 883 value = set_custom_field_value_by_id(custom_field, custom_field_value, value[:id])
879 884 elsif value[:token].present?
880 885 if attachment = Attachment.find_by_token(value[:token])
881 886 value = attachment.id.to_s
882 887 else
883 888 value = ''
884 889 end
885 890 elsif value.key?(:file)
886 891 attachment = Attachment.new(:file => value[:file], :author => User.current)
887 892 if attachment.save
888 893 value = attachment.id.to_s
889 894 else
890 895 value = ''
891 896 end
892 897 else
893 898 attachment_present = false
894 899 value = ''
895 900 end
896 901 elsif value.is_a?(String)
897 902 value = set_custom_field_value_by_id(custom_field, custom_field_value, value)
898 903 end
899 904 custom_field_value.instance_variable_set "@attachment_present", attachment_present
900 905
901 906 value
902 907 end
903 908
904 909 def set_custom_field_value_by_id(custom_field, custom_field_value, id)
905 910 attachment = Attachment.find_by_id(id)
906 911 if attachment && attachment.container.is_a?(CustomValue) && attachment.container.customized == custom_field_value.customized
907 912 id.to_s
908 913 else
909 914 ''
910 915 end
911 916 end
912 917 private :set_custom_field_value_by_id
913 918
914 919 def cast_single_value(custom_field, value, customized=nil)
915 920 Attachment.find_by_id(value.to_i) if value.present? && value.respond_to?(:to_i)
916 921 end
917 922
918 923 def validate_custom_value(custom_value)
919 924 errors = []
920 925
921 926 if custom_value.value.blank?
922 927 if custom_value.instance_variable_get("@attachment_present")
923 928 errors << ::I18n.t('activerecord.errors.messages.invalid')
924 929 end
925 930 else
926 931 if custom_value.value.present?
927 932 attachment = Attachment.where(:id => custom_value.value.to_s).first
928 933 extensions = custom_value.custom_field.extensions_allowed
929 934 if attachment && extensions.present? && !attachment.extension_in?(extensions)
930 935 errors << "#{::I18n.t('activerecord.errors.messages.invalid')} (#{l(:setting_attachment_extensions_allowed)}: #{extensions})"
931 936 end
932 937 end
933 938 end
934 939
935 940 errors.uniq
936 941 end
937 942
938 943 def after_save_custom_value(custom_field, custom_value)
939 944 if custom_value.value_changed?
940 945 if custom_value.value.present?
941 946 attachment = Attachment.where(:id => custom_value.value.to_s).first
942 947 if attachment
943 948 attachment.container = custom_value
944 949 attachment.save!
945 950 end
946 951 end
947 952 if custom_value.value_was.present?
948 953 attachment = Attachment.where(:id => custom_value.value_was.to_s).first
949 954 if attachment
950 955 attachment.destroy
951 956 end
952 957 end
953 958 end
954 959 end
955 960
956 961 def edit_tag(view, tag_id, tag_name, custom_value, options={})
957 962 attachment = nil
958 963 if custom_value.value.present? #&& custom_value.value == custom_value.value_was
959 964 attachment = Attachment.find_by_id(custom_value.value)
960 965 end
961 966
962 967 view.hidden_field_tag("#{tag_name}[blank]", "") +
963 968 view.render(:partial => 'attachments/form',
964 969 :locals => {
965 970 :attachment_param => tag_name,
966 971 :multiple => false,
967 972 :description => false,
968 973 :saved_attachments => [attachment].compact,
969 974 :filedrop => false
970 975 })
971 976 end
972 977 end
973 978 end
974 979 end
General Comments 0
You need to be logged in to leave comments. Login now