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