##// END OF EJS Templates
Bulk-edit custom fields through context menu (#6296)....
Jean-Philippe Lang -
r8704:5e0c1cc5ce9e
parent child
Show More
@@ -1,60 +1,73
1 class ContextMenusController < ApplicationController
1 class ContextMenusController < ApplicationController
2 helper :watchers
2 helper :watchers
3 helper :issues
3 helper :issues
4
4
5 def issues
5 def issues
6 @issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
6 @issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
7
7
8 if (@issues.size == 1)
8 if (@issues.size == 1)
9 @issue = @issues.first
9 @issue = @issues.first
10 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
10 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
11 else
11 else
12 @allowed_statuses = @issues.map do |i|
12 @allowed_statuses = @issues.map do |i|
13 i.new_statuses_allowed_to(User.current)
13 i.new_statuses_allowed_to(User.current)
14 end.inject do |memo,s|
14 end.inject do |memo,s|
15 memo & s
15 memo & s
16 end
16 end
17 end
17 end
18 @projects = @issues.collect(&:project).compact.uniq
18 @projects = @issues.collect(&:project).compact.uniq
19 @project = @projects.first if @projects.size == 1
19 @project = @projects.first if @projects.size == 1
20
20
21 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
21 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
22 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
22 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
23 :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
23 :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
24 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
24 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
25 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
25 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
26 :delete => User.current.allowed_to?(:delete_issues, @projects)
26 :delete => User.current.allowed_to?(:delete_issues, @projects)
27 }
27 }
28 if @project
28 if @project
29 if @issue
29 if @issue
30 @assignables = @issue.assignable_users
30 @assignables = @issue.assignable_users
31 else
31 else
32 @assignables = @project.assignable_users
32 @assignables = @project.assignable_users
33 end
33 end
34 @trackers = @project.trackers
34 @trackers = @project.trackers
35 else
35 else
36 #when multiple projects, we only keep the intersection of each set
36 #when multiple projects, we only keep the intersection of each set
37 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
37 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
38 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
38 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
39 end
39 end
40
40
41 @priorities = IssuePriority.active.reverse
41 @priorities = IssuePriority.active.reverse
42 @statuses = IssueStatus.find(:all, :order => 'position')
42 @statuses = IssueStatus.find(:all, :order => 'position')
43 @back = back_url
43 @back = back_url
44
44
45 @options_by_custom_field = {}
46 if @can[:edit]
47 custom_fields = @issues.map(&:available_custom_fields).inject {|memo, f| memo & f}.select do |f|
48 %w(bool list user version).include?(f.field_format) && !f.multiple?
49 end
50 custom_fields.each do |field|
51 values = field.possible_values_options(@projects)
52 if values.any?
53 @options_by_custom_field[field] = values
54 end
55 end
56 end
57
45 render :layout => false
58 render :layout => false
46 end
59 end
47
60
48 def time_entries
61 def time_entries
49 @time_entries = TimeEntry.all(
62 @time_entries = TimeEntry.all(
50 :conditions => {:id => params[:ids]}, :include => :project)
63 :conditions => {:id => params[:ids]}, :include => :project)
51 @projects = @time_entries.collect(&:project).compact.uniq
64 @projects = @time_entries.collect(&:project).compact.uniq
52 @project = @projects.first if @projects.size == 1
65 @project = @projects.first if @projects.size == 1
53 @activities = TimeEntryActivity.shared.active
66 @activities = TimeEntryActivity.shared.active
54 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
67 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
55 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
68 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
56 }
69 }
57 @back = back_url
70 @back = back_url
58 render :layout => false
71 render :layout => false
59 end
72 end
60 end
73 end
@@ -1,36 +1,43
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module ContextMenusHelper
20 module ContextMenusHelper
21 def context_menu_link(name, url, options={})
21 def context_menu_link(name, url, options={})
22 options[:class] ||= ''
22 options[:class] ||= ''
23 if options.delete(:selected)
23 if options.delete(:selected)
24 options[:class] << ' icon-checked disabled'
24 options[:class] << ' icon-checked disabled'
25 options[:disabled] = true
25 options[:disabled] = true
26 end
26 end
27 if options.delete(:disabled)
27 if options.delete(:disabled)
28 options.delete(:method)
28 options.delete(:method)
29 options.delete(:confirm)
29 options.delete(:confirm)
30 options.delete(:onclick)
30 options.delete(:onclick)
31 options[:class] << ' disabled'
31 options[:class] << ' disabled'
32 url = '#'
32 url = '#'
33 end
33 end
34 link_to h(name), url, options
34 link_to h(name), url, options
35 end
35 end
36
37 def bulk_update_custom_field_context_menu_link(field, text, value)
38 context_menu_link h(text),
39 {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back},
40 :method => :post,
41 :selected => (@issue && @issue.custom_field_value(field) == value)
42 end
36 end
43 end
@@ -1,217 +1,221
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :custom_values, :dependent => :delete_all
21 has_many :custom_values, :dependent => :delete_all
22 acts_as_list :scope => 'type = \'#{self.class}\''
22 acts_as_list :scope => 'type = \'#{self.class}\''
23 serialize :possible_values
23 serialize :possible_values
24
24
25 validates_presence_of :name, :field_format
25 validates_presence_of :name, :field_format
26 validates_uniqueness_of :name, :scope => :type
26 validates_uniqueness_of :name, :scope => :type
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29
29
30 validate :validate_custom_field
30 validate :validate_custom_field
31 before_validation :set_searchable
31 before_validation :set_searchable
32
32
33 def initialize(attributes=nil, *args)
33 def initialize(attributes=nil, *args)
34 super
34 super
35 self.possible_values ||= []
35 self.possible_values ||= []
36 end
36 end
37
37
38 def set_searchable
38 def set_searchable
39 # make sure these fields are not searchable
39 # make sure these fields are not searchable
40 self.searchable = false if %w(int float date bool).include?(field_format)
40 self.searchable = false if %w(int float date bool).include?(field_format)
41 # make sure only these fields can have multiple values
41 # make sure only these fields can have multiple values
42 self.multiple = false unless %w(list user version).include?(field_format)
42 self.multiple = false unless %w(list user version).include?(field_format)
43 true
43 true
44 end
44 end
45
45
46 def validate_custom_field
46 def validate_custom_field
47 if self.field_format == "list"
47 if self.field_format == "list"
48 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
48 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
49 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
49 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
50 end
50 end
51
51
52 if regexp.present?
52 if regexp.present?
53 begin
53 begin
54 Regexp.new(regexp)
54 Regexp.new(regexp)
55 rescue
55 rescue
56 errors.add(:regexp, :invalid)
56 errors.add(:regexp, :invalid)
57 end
57 end
58 end
58 end
59
59
60 if default_value.present? && !valid_field_value?(default_value)
60 if default_value.present? && !valid_field_value?(default_value)
61 errors.add(:default_value, :invalid)
61 errors.add(:default_value, :invalid)
62 end
62 end
63 end
63 end
64
64
65 def possible_values_options(obj=nil)
65 def possible_values_options(obj=nil)
66 case field_format
66 case field_format
67 when 'user', 'version'
67 when 'user', 'version'
68 if obj.respond_to?(:project) && obj.project
68 if obj.respond_to?(:project) && obj.project
69 case field_format
69 case field_format
70 when 'user'
70 when 'user'
71 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
71 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
72 when 'version'
72 when 'version'
73 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
73 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
74 end
74 end
75 elsif obj.is_a?(Array)
75 elsif obj.is_a?(Array)
76 obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
76 obj.collect {|o| possible_values_options(o)}.inject {|memo, v| memo & v}
77 else
77 else
78 []
78 []
79 end
79 end
80 when 'bool'
81 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
80 else
82 else
81 read_attribute :possible_values
83 read_attribute(:possible_values) || []
82 end
84 end
83 end
85 end
84
86
85 def possible_values(obj=nil)
87 def possible_values(obj=nil)
86 case field_format
88 case field_format
87 when 'user', 'version'
89 when 'user', 'version'
88 possible_values_options(obj).collect(&:last)
90 possible_values_options(obj).collect(&:last)
91 when 'bool'
92 ['1', '0']
89 else
93 else
90 read_attribute :possible_values
94 read_attribute :possible_values
91 end
95 end
92 end
96 end
93
97
94 # Makes possible_values accept a multiline string
98 # Makes possible_values accept a multiline string
95 def possible_values=(arg)
99 def possible_values=(arg)
96 if arg.is_a?(Array)
100 if arg.is_a?(Array)
97 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
101 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
98 else
102 else
99 self.possible_values = arg.to_s.split(/[\n\r]+/)
103 self.possible_values = arg.to_s.split(/[\n\r]+/)
100 end
104 end
101 end
105 end
102
106
103 def cast_value(value)
107 def cast_value(value)
104 casted = nil
108 casted = nil
105 unless value.blank?
109 unless value.blank?
106 case field_format
110 case field_format
107 when 'string', 'text', 'list'
111 when 'string', 'text', 'list'
108 casted = value
112 casted = value
109 when 'date'
113 when 'date'
110 casted = begin; value.to_date; rescue; nil end
114 casted = begin; value.to_date; rescue; nil end
111 when 'bool'
115 when 'bool'
112 casted = (value == '1' ? true : false)
116 casted = (value == '1' ? true : false)
113 when 'int'
117 when 'int'
114 casted = value.to_i
118 casted = value.to_i
115 when 'float'
119 when 'float'
116 casted = value.to_f
120 casted = value.to_f
117 when 'user', 'version'
121 when 'user', 'version'
118 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
122 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
119 end
123 end
120 end
124 end
121 casted
125 casted
122 end
126 end
123
127
124 # Returns a ORDER BY clause that can used to sort customized
128 # Returns a ORDER BY clause that can used to sort customized
125 # objects by their value of the custom field.
129 # objects by their value of the custom field.
126 # Returns false, if the custom field can not be used for sorting.
130 # Returns false, if the custom field can not be used for sorting.
127 def order_statement
131 def order_statement
128 return nil if multiple?
132 return nil if multiple?
129 case field_format
133 case field_format
130 when 'string', 'text', 'list', 'date', 'bool'
134 when 'string', 'text', 'list', 'date', 'bool'
131 # COALESCE is here to make sure that blank and NULL values are sorted equally
135 # COALESCE is here to make sure that blank and NULL values are sorted equally
132 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
136 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
133 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
137 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
134 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
138 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
135 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
139 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
136 when 'int', 'float'
140 when 'int', 'float'
137 # Make the database cast values into numeric
141 # Make the database cast values into numeric
138 # Postgresql will raise an error if a value can not be casted!
142 # Postgresql will raise an error if a value can not be casted!
139 # CustomValue validations should ensure that it doesn't occur
143 # CustomValue validations should ensure that it doesn't occur
140 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
144 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
141 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
145 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
142 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
146 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
143 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
147 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
144 else
148 else
145 nil
149 nil
146 end
150 end
147 end
151 end
148
152
149 def <=>(field)
153 def <=>(field)
150 position <=> field.position
154 position <=> field.position
151 end
155 end
152
156
153 def self.customized_class
157 def self.customized_class
154 self.name =~ /^(.+)CustomField$/
158 self.name =~ /^(.+)CustomField$/
155 begin; $1.constantize; rescue nil; end
159 begin; $1.constantize; rescue nil; end
156 end
160 end
157
161
158 # to move in project_custom_field
162 # to move in project_custom_field
159 def self.for_all
163 def self.for_all
160 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
164 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
161 end
165 end
162
166
163 def type_name
167 def type_name
164 nil
168 nil
165 end
169 end
166
170
167 # Returns the error messages for the given value
171 # Returns the error messages for the given value
168 # or an empty array if value is a valid value for the custom field
172 # or an empty array if value is a valid value for the custom field
169 def validate_field_value(value)
173 def validate_field_value(value)
170 errs = []
174 errs = []
171 if value.is_a?(Array)
175 if value.is_a?(Array)
172 if !multiple?
176 if !multiple?
173 errs << ::I18n.t('activerecord.errors.messages.invalid')
177 errs << ::I18n.t('activerecord.errors.messages.invalid')
174 end
178 end
175 if is_required? && value.detect(&:present?).nil?
179 if is_required? && value.detect(&:present?).nil?
176 errs << ::I18n.t('activerecord.errors.messages.blank')
180 errs << ::I18n.t('activerecord.errors.messages.blank')
177 end
181 end
178 value.each {|v| errs += validate_field_value_format(v)}
182 value.each {|v| errs += validate_field_value_format(v)}
179 else
183 else
180 if is_required? && value.blank?
184 if is_required? && value.blank?
181 errs << ::I18n.t('activerecord.errors.messages.blank')
185 errs << ::I18n.t('activerecord.errors.messages.blank')
182 end
186 end
183 errs += validate_field_value_format(value)
187 errs += validate_field_value_format(value)
184 end
188 end
185 errs
189 errs
186 end
190 end
187
191
188 # Returns true if value is a valid value for the custom field
192 # Returns true if value is a valid value for the custom field
189 def valid_field_value?(value)
193 def valid_field_value?(value)
190 validate_field_value(value).empty?
194 validate_field_value(value).empty?
191 end
195 end
192
196
193 protected
197 protected
194
198
195 # Returns the error message for the given value regarding its format
199 # Returns the error message for the given value regarding its format
196 def validate_field_value_format(value)
200 def validate_field_value_format(value)
197 errs = []
201 errs = []
198 if value.present?
202 if value.present?
199 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
203 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
200 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
204 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
201 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
205 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
202
206
203 # Format specific validations
207 # Format specific validations
204 case field_format
208 case field_format
205 when 'int'
209 when 'int'
206 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
210 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
207 when 'float'
211 when 'float'
208 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
212 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
209 when 'date'
213 when 'date'
210 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
214 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
211 when 'list'
215 when 'list'
212 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
216 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
213 end
217 end
214 end
218 end
215 errs
219 errs
216 end
220 end
217 end
221 end
@@ -1,124 +1,138
1 <ul>
1 <ul>
2 <%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %>
2 <%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %>
3
3
4 <% if !@issue.nil? -%>
4 <% if !@issue.nil? -%>
5 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
5 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
6 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
6 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
7 <% else %>
7 <% else %>
8 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
8 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
9 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
9 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
10 <% end %>
10 <% end %>
11
11
12 <% if @allowed_statuses.present? %>
12 <% if @allowed_statuses.present? %>
13 <li class="folder">
13 <li class="folder">
14 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
14 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
15 <ul>
15 <ul>
16 <% @statuses.each do |s| -%>
16 <% @statuses.each do |s| -%>
17 <li><%= context_menu_link h(s.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {:status_id => s}, :back_url => @back}, :method => :post,
17 <li><%= context_menu_link h(s.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {:status_id => s}, :back_url => @back}, :method => :post,
18 :selected => (@issue && s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
18 :selected => (@issue && s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
19 <% end -%>
19 <% end -%>
20 </ul>
20 </ul>
21 </li>
21 </li>
22 <% end %>
22 <% end %>
23
23
24 <% unless @trackers.nil? %>
24 <% unless @trackers.nil? %>
25 <li class="folder">
25 <li class="folder">
26 <a href="#" class="submenu"><%= l(:field_tracker) %></a>
26 <a href="#" class="submenu"><%= l(:field_tracker) %></a>
27 <ul>
27 <ul>
28 <% @trackers.each do |t| -%>
28 <% @trackers.each do |t| -%>
29 <li><%= context_menu_link h(t.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'tracker_id' => t}, :back_url => @back}, :method => :post,
29 <li><%= context_menu_link h(t.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'tracker_id' => t}, :back_url => @back}, :method => :post,
30 :selected => (@issue && t == @issue.tracker), :disabled => !@can[:edit] %></li>
30 :selected => (@issue && t == @issue.tracker), :disabled => !@can[:edit] %></li>
31 <% end -%>
31 <% end -%>
32 </ul>
32 </ul>
33 </li>
33 </li>
34 <% end %>
34 <% end %>
35
35
36 <li class="folder">
36 <li class="folder">
37 <a href="#" class="submenu"><%= l(:field_priority) %></a>
37 <a href="#" class="submenu"><%= l(:field_priority) %></a>
38 <ul>
38 <ul>
39 <% @priorities.each do |p| -%>
39 <% @priorities.each do |p| -%>
40 <li><%= context_menu_link h(p.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'priority_id' => p}, :back_url => @back}, :method => :post,
40 <li><%= context_menu_link h(p.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'priority_id' => p}, :back_url => @back}, :method => :post,
41 :selected => (@issue && p == @issue.priority), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
41 :selected => (@issue && p == @issue.priority), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
42 <% end -%>
42 <% end -%>
43 </ul>
43 </ul>
44 </li>
44 </li>
45
45
46 <% #TODO: allow editing versions when multiple projects %>
46 <% #TODO: allow editing versions when multiple projects %>
47 <% unless @project.nil? || @project.shared_versions.open.empty? -%>
47 <% unless @project.nil? || @project.shared_versions.open.empty? -%>
48 <li class="folder">
48 <li class="folder">
49 <a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
49 <a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
50 <ul>
50 <ul>
51 <% @project.shared_versions.open.sort.each do |v| -%>
51 <% @project.shared_versions.open.sort.each do |v| -%>
52 <li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => v}, :back_url => @back}, :method => :post,
52 <li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => v}, :back_url => @back}, :method => :post,
53 :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
53 :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
54 <% end -%>
54 <% end -%>
55 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => 'none'}, :back_url => @back}, :method => :post,
55 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => 'none'}, :back_url => @back}, :method => :post,
56 :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
56 :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
57 </ul>
57 </ul>
58 </li>
58 </li>
59 <% end %>
59 <% end %>
60 <% if @assignables.present? -%>
60 <% if @assignables.present? -%>
61 <li class="folder">
61 <li class="folder">
62 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
62 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
63 <ul>
63 <ul>
64 <% if @assignables.include?(User.current) %>
64 <% if @assignables.include?(User.current) %>
65 <li><%= context_menu_link "<< #{l(:label_me)} >>", {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => User.current}, :back_url => @back}, :method => :post,
65 <li><%= context_menu_link "<< #{l(:label_me)} >>", {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => User.current}, :back_url => @back}, :method => :post,
66 :disabled => !@can[:update] %></li>
66 :disabled => !@can[:update] %></li>
67 <% end %>
67 <% end %>
68 <% @assignables.each do |u| -%>
68 <% @assignables.each do |u| -%>
69 <li><%= context_menu_link h(u.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => u}, :back_url => @back}, :method => :post,
69 <li><%= context_menu_link h(u.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => u}, :back_url => @back}, :method => :post,
70 :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li>
70 :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li>
71 <% end -%>
71 <% end -%>
72 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => 'none'}, :back_url => @back}, :method => :post,
72 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => 'none'}, :back_url => @back}, :method => :post,
73 :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
73 :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
74 </ul>
74 </ul>
75 </li>
75 </li>
76 <% end %>
76 <% end %>
77 <% unless @project.nil? || @project.issue_categories.empty? -%>
77 <% unless @project.nil? || @project.issue_categories.empty? -%>
78 <li class="folder">
78 <li class="folder">
79 <a href="#" class="submenu"><%= l(:field_category) %></a>
79 <a href="#" class="submenu"><%= l(:field_category) %></a>
80 <ul>
80 <ul>
81 <% @project.issue_categories.each do |u| -%>
81 <% @project.issue_categories.each do |u| -%>
82 <li><%= context_menu_link h(u.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'category_id' => u}, :back_url => @back}, :method => :post,
82 <li><%= context_menu_link h(u.name), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'category_id' => u}, :back_url => @back}, :method => :post,
83 :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li>
83 :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li>
84 <% end -%>
84 <% end -%>
85 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'category_id' => 'none'}, :back_url => @back}, :method => :post,
85 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'category_id' => 'none'}, :back_url => @back}, :method => :post,
86 :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
86 :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
87 </ul>
87 </ul>
88 </li>
88 </li>
89 <% end -%>
89 <% end -%>
90
90
91 <% if Issue.use_field_for_done_ratio? %>
91 <% if Issue.use_field_for_done_ratio? %>
92 <li class="folder">
92 <li class="folder">
93 <a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
93 <a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
94 <ul>
94 <ul>
95 <% (0..10).map{|x|x*10}.each do |p| -%>
95 <% (0..10).map{|x|x*10}.each do |p| -%>
96 <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'done_ratio' => p}, :back_url => @back}, :method => :post,
96 <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'done_ratio' => p}, :back_url => @back}, :method => :post,
97 :selected => (@issue && p == @issue.done_ratio), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
97 :selected => (@issue && p == @issue.done_ratio), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
98 <% end -%>
98 <% end -%>
99 </ul>
99 </ul>
100 </li>
100 </li>
101 <% end %>
101 <% end %>
102
102
103 <% @options_by_custom_field.each do |field, options| %>
104 <li class="folder">
105 <a href="#" class="submenu"><%= h(field.name) %></a>
106 <ul>
107 <% options.each do |text, value| %>
108 <li><%= bulk_update_custom_field_context_menu_link(field, text, value || text) %></li>
109 <% end %>
110 <% unless field.is_required? %>
111 <li><%= bulk_update_custom_field_context_menu_link(field, l(:label_none), '') %></li>
112 <% end %>
113 </ul>
114 </li>
115 <% end %>
116
103 <% if !@issue.nil? %>
117 <% if !@issue.nil? %>
104 <% if @can[:log_time] -%>
118 <% if @can[:log_time] -%>
105 <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue},
119 <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue},
106 :class => 'icon-time-add' %></li>
120 :class => 'icon-time-add' %></li>
107 <% end %>
121 <% end %>
108 <% if User.current.logged? %>
122 <% if User.current.logged? %>
109 <li><%= watcher_link(@issue, User.current) %></li>
123 <li><%= watcher_link(@issue, User.current) %></li>
110 <% end %>
124 <% end %>
111 <% end %>
125 <% end %>
112
126
113 <% if @issue.present? %>
127 <% if @issue.present? %>
114 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
128 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
115 :class => 'icon-copy', :disabled => !@can[:copy] %></li>
129 :class => 'icon-copy', :disabled => !@can[:copy] %></li>
116 <% else %>
130 <% else %>
117 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :copy => '1'},
131 <li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :copy => '1'},
118 :class => 'icon-copy', :disabled => !@can[:move] %></li>
132 :class => 'icon-copy', :disabled => !@can[:move] %></li>
119 <% end %>
133 <% end %>
120 <li><%= context_menu_link l(:button_delete), issues_path(:ids => @issues.collect(&:id), :back_url => @back),
134 <li><%= context_menu_link l(:button_delete), issues_path(:ids => @issues.collect(&:id), :back_url => @back),
121 :method => :delete, :confirm => issues_destroy_confirmation_message(@issues), :class => 'icon-del', :disabled => !@can[:delete] %></li>
135 :method => :delete, :confirm => issues_destroy_confirmation_message(@issues), :class => 'icon-del', :disabled => !@can[:delete] %></li>
122
136
123 <%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>
137 <%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>
124 </ul>
138 </ul>
@@ -1,52 +1,52
1 #context-menu { position: absolute; z-index: 40; font-size: 0.9em;}
1 #context-menu { position: absolute; z-index: 40; font-size: 0.9em;}
2
2
3 #context-menu ul, #context-menu li, #context-menu a {
3 #context-menu ul, #context-menu li, #context-menu a {
4 display:block;
4 display:block;
5 margin:0;
5 margin:0;
6 padding:0;
6 padding:0;
7 border:0;
7 border:0;
8 }
8 }
9
9
10 #context-menu ul {
10 #context-menu ul {
11 width:150px;
11 width:150px;
12 border-top:1px solid #ddd;
12 border-top:1px solid #ddd;
13 border-left:1px solid #ddd;
13 border-left:1px solid #ddd;
14 border-bottom:1px solid #777;
14 border-bottom:1px solid #777;
15 border-right:1px solid #777;
15 border-right:1px solid #777;
16 background:white;
16 background:white;
17 list-style:none;
17 list-style:none;
18 }
18 }
19
19
20 #context-menu li {
20 #context-menu li {
21 position:relative;
21 position:relative;
22 padding:1px;
22 padding:1px;
23 z-index:39;
23 z-index:39;
24 border:1px solid white;
24 border:1px solid white;
25 }
25 }
26 #context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; max-height:300px; overflow:hidden; overflow-y: auto; }
26 #context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; max-height:300px; overflow:hidden; overflow-y: auto; }
27 #context-menu li.folder>ul { left:148px; }
27 #context-menu li.folder>ul { left:148px; }
28
28
29 #context-menu.reverse-y li.folder>ul { top:auto; bottom:0; }
29 #context-menu.reverse-y li.folder>ul { top:auto; bottom:0; }
30 #context-menu.reverse-x li.folder ul { left:auto; right:168px; /* IE6 */ }
30 #context-menu.reverse-x li.folder ul { left:auto; right:168px; /* IE6 */ }
31 #context-menu.reverse-x li.folder>ul { right:148px; }
31 #context-menu.reverse-x li.folder>ul { right:148px; }
32
32
33 #context-menu a {
33 #context-menu a {
34 text-decoration:none !important;
34 text-decoration:none !important;
35 background-repeat: no-repeat;
35 background-repeat: no-repeat;
36 background-position: 1px 50%;
36 background-position: 1px 50%;
37 padding: 1px 0px 1px 20px;
37 padding: 1px 0px 1px 20px;
38 width:100%; /* IE */
38 width:100%; /* IE */
39 }
39 }
40 #context-menu li>a { width:auto; } /* others */
40 #context-menu li>a { width:auto; } /* others */
41 #context-menu a.disabled, #context-menu a.disabled:hover {color: #ccc;}
41 #context-menu a.disabled, #context-menu a.disabled:hover {color: #ccc;}
42 #context-menu li a.submenu { background:url("../images/bullet_arrow_right.png") right no-repeat; }
42 #context-menu li a.submenu { padding-right:16px; background:url("../images/bullet_arrow_right.png") right no-repeat; }
43 #context-menu li:hover { border:1px solid gray; background-color:#eee; }
43 #context-menu li:hover { border:1px solid gray; background-color:#eee; }
44 #context-menu a:hover {color:#2A5685;}
44 #context-menu a:hover {color:#2A5685;}
45 #context-menu li.folder:hover { z-index:40; }
45 #context-menu li.folder:hover { z-index:40; }
46 #context-menu ul ul, #context-menu li:hover ul ul { display:none; }
46 #context-menu ul ul, #context-menu li:hover ul ul { display:none; }
47 #context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; }
47 #context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; }
48
48
49 /* selected element */
49 /* selected element */
50 .context-menu-selection { background-color:#507AAA !important; color:#f8f8f8 !important; }
50 .context-menu-selection { background-color:#507AAA !important; color:#f8f8f8 !important; }
51 .context-menu-selection a, .context-menu-selection a:hover { color:#f8f8f8 !important; }
51 .context-menu-selection a, .context-menu-selection a:hover { color:#f8f8f8 !important; }
52 .context-menu-selection:hover { background-color:#507AAA !important; color:#f8f8f8 !important; }
52 .context-menu-selection:hover { background-color:#507AAA !important; color:#f8f8f8 !important; }
@@ -1,156 +1,252
1 require File.expand_path('../../test_helper', __FILE__)
1 require File.expand_path('../../test_helper', __FILE__)
2
2
3 class ContextMenusControllerTest < ActionController::TestCase
3 class ContextMenusControllerTest < ActionController::TestCase
4 fixtures :projects,
4 fixtures :projects,
5 :trackers,
5 :trackers,
6 :projects_trackers,
6 :projects_trackers,
7 :roles,
7 :roles,
8 :member_roles,
8 :member_roles,
9 :members,
9 :members,
10 :auth_sources,
10 :auth_sources,
11 :enabled_modules,
11 :enabled_modules,
12 :workflows,
12 :workflows,
13 :journals, :journal_details,
13 :journals, :journal_details,
14 :versions,
14 :versions,
15 :issues, :issue_statuses, :issue_categories,
15 :issues, :issue_statuses, :issue_categories,
16 :users,
16 :users,
17 :enumerations,
17 :enumerations,
18 :time_entries
18 :time_entries
19
19
20 def test_context_menu_one_issue
20 def test_context_menu_one_issue
21 @request.session[:user_id] = 2
21 @request.session[:user_id] = 2
22 get :issues, :ids => [1]
22 get :issues, :ids => [1]
23 assert_response :success
23 assert_response :success
24 assert_template 'context_menu'
24 assert_template 'context_menu'
25 assert_tag :tag => 'a', :content => 'Edit',
25 assert_tag :tag => 'a', :content => 'Edit',
26 :attributes => { :href => '/issues/1/edit',
26 :attributes => { :href => '/issues/1/edit',
27 :class => 'icon-edit' }
27 :class => 'icon-edit' }
28 assert_tag :tag => 'a', :content => 'Closed',
28 assert_tag :tag => 'a', :content => 'Closed',
29 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bstatus_id%5D=5',
29 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bstatus_id%5D=5',
30 :class => '' }
30 :class => '' }
31 assert_tag :tag => 'a', :content => 'Immediate',
31 assert_tag :tag => 'a', :content => 'Immediate',
32 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8',
32 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8',
33 :class => '' }
33 :class => '' }
34 assert_no_tag :tag => 'a', :content => 'Inactive Priority'
34 assert_no_tag :tag => 'a', :content => 'Inactive Priority'
35 # Versions
35 # Versions
36 assert_tag :tag => 'a', :content => '2.0',
36 assert_tag :tag => 'a', :content => '2.0',
37 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3',
37 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3',
38 :class => '' }
38 :class => '' }
39 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
39 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
40 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4',
40 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4',
41 :class => '' }
41 :class => '' }
42
42
43 assert_tag :tag => 'a', :content => 'Dave Lopper',
43 assert_tag :tag => 'a', :content => 'Dave Lopper',
44 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3',
44 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3',
45 :class => '' }
45 :class => '' }
46 assert_tag :tag => 'a', :content => 'Copy',
46 assert_tag :tag => 'a', :content => 'Copy',
47 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
47 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
48 :class => 'icon-copy' }
48 :class => 'icon-copy' }
49 assert_no_tag :tag => 'a', :content => 'Move'
49 assert_no_tag :tag => 'a', :content => 'Move'
50 assert_tag :tag => 'a', :content => 'Delete',
50 assert_tag :tag => 'a', :content => 'Delete',
51 :attributes => { :href => '/issues?ids%5B%5D=1',
51 :attributes => { :href => '/issues?ids%5B%5D=1',
52 :class => 'icon-del' }
52 :class => 'icon-del' }
53 end
53 end
54
54
55 def test_context_menu_one_issue_by_anonymous
55 def test_context_menu_one_issue_by_anonymous
56 get :issues, :ids => [1]
56 get :issues, :ids => [1]
57 assert_response :success
57 assert_response :success
58 assert_template 'context_menu'
58 assert_template 'context_menu'
59 assert_tag :tag => 'a', :content => 'Delete',
59 assert_tag :tag => 'a', :content => 'Delete',
60 :attributes => { :href => '#',
60 :attributes => { :href => '#',
61 :class => 'icon-del disabled' }
61 :class => 'icon-del disabled' }
62 end
62 end
63
63
64 def test_context_menu_multiple_issues_of_same_project
64 def test_context_menu_multiple_issues_of_same_project
65 @request.session[:user_id] = 2
65 @request.session[:user_id] = 2
66 get :issues, :ids => [1, 2]
66 get :issues, :ids => [1, 2]
67 assert_response :success
67 assert_response :success
68 assert_template 'context_menu'
68 assert_template 'context_menu'
69 assert_not_nil assigns(:issues)
69 assert_not_nil assigns(:issues)
70 assert_equal [1, 2], assigns(:issues).map(&:id).sort
70 assert_equal [1, 2], assigns(:issues).map(&:id).sort
71
71
72 ids = assigns(:issues).map(&:id).map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
72 ids = assigns(:issues).map(&:id).map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
73 assert_tag :tag => 'a', :content => 'Edit',
73 assert_tag :tag => 'a', :content => 'Edit',
74 :attributes => { :href => "/issues/bulk_edit?#{ids}",
74 :attributes => { :href => "/issues/bulk_edit?#{ids}",
75 :class => 'icon-edit' }
75 :class => 'icon-edit' }
76 assert_tag :tag => 'a', :content => 'Closed',
76 assert_tag :tag => 'a', :content => 'Closed',
77 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5",
77 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5",
78 :class => '' }
78 :class => '' }
79 assert_tag :tag => 'a', :content => 'Immediate',
79 assert_tag :tag => 'a', :content => 'Immediate',
80 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8",
80 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8",
81 :class => '' }
81 :class => '' }
82 assert_tag :tag => 'a', :content => 'Dave Lopper',
82 assert_tag :tag => 'a', :content => 'Dave Lopper',
83 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=3",
83 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=3",
84 :class => '' }
84 :class => '' }
85 assert_tag :tag => 'a', :content => 'Copy',
85 assert_tag :tag => 'a', :content => 'Copy',
86 :attributes => { :href => "/issues/bulk_edit?copy=1&amp;#{ids}",
86 :attributes => { :href => "/issues/bulk_edit?copy=1&amp;#{ids}",
87 :class => 'icon-copy' }
87 :class => 'icon-copy' }
88 assert_no_tag :tag => 'a', :content => 'Move'
88 assert_no_tag :tag => 'a', :content => 'Move'
89 assert_tag :tag => 'a', :content => 'Delete',
89 assert_tag :tag => 'a', :content => 'Delete',
90 :attributes => { :href => "/issues?#{ids}",
90 :attributes => { :href => "/issues?#{ids}",
91 :class => 'icon-del' }
91 :class => 'icon-del' }
92 end
92 end
93
93
94 def test_context_menu_multiple_issues_of_different_projects
94 def test_context_menu_multiple_issues_of_different_projects
95 @request.session[:user_id] = 2
95 @request.session[:user_id] = 2
96 get :issues, :ids => [1, 2, 6]
96 get :issues, :ids => [1, 2, 6]
97 assert_response :success
97 assert_response :success
98 assert_template 'context_menu'
98 assert_template 'context_menu'
99 assert_not_nil assigns(:issues)
99 assert_not_nil assigns(:issues)
100 assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort
100 assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort
101
101
102 ids = assigns(:issues).map(&:id).map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
102 ids = assigns(:issues).map(&:id).map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
103 assert_tag :tag => 'a', :content => 'Edit',
103 assert_tag :tag => 'a', :content => 'Edit',
104 :attributes => { :href => "/issues/bulk_edit?#{ids}",
104 :attributes => { :href => "/issues/bulk_edit?#{ids}",
105 :class => 'icon-edit' }
105 :class => 'icon-edit' }
106 assert_tag :tag => 'a', :content => 'Closed',
106 assert_tag :tag => 'a', :content => 'Closed',
107 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5",
107 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5",
108 :class => '' }
108 :class => '' }
109 assert_tag :tag => 'a', :content => 'Immediate',
109 assert_tag :tag => 'a', :content => 'Immediate',
110 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8",
110 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8",
111 :class => '' }
111 :class => '' }
112 assert_tag :tag => 'a', :content => 'John Smith',
112 assert_tag :tag => 'a', :content => 'John Smith',
113 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=2",
113 :attributes => { :href => "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=2",
114 :class => '' }
114 :class => '' }
115 assert_tag :tag => 'a', :content => 'Delete',
115 assert_tag :tag => 'a', :content => 'Delete',
116 :attributes => { :href => "/issues?#{ids}",
116 :attributes => { :href => "/issues?#{ids}",
117 :class => 'icon-del' }
117 :class => 'icon-del' }
118 end
118 end
119
119
120 def test_context_menu_should_include_list_custom_fields
121 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
122 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
123 @request.session[:user_id] = 2
124 get :issues, :ids => [1, 2]
125
126 assert_tag 'a',
127 :content => 'List',
128 :attributes => {:href => '#'},
129 :sibling => {:tag => 'ul', :children => {:count => 3}}
130
131 assert_tag 'a',
132 :content => 'Foo',
133 :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo"}
134 assert_tag 'a',
135 :content => 'none',
136 :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D="}
137 end
138
139 def test_context_menu_should_not_include_null_value_for_required_custom_fields
140 field = IssueCustomField.create!(:name => 'List', :is_required => true, :field_format => 'list',
141 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
142 @request.session[:user_id] = 2
143 get :issues, :ids => [1, 2]
144
145 assert_tag 'a',
146 :content => 'List',
147 :attributes => {:href => '#'},
148 :sibling => {:tag => 'ul', :children => {:count => 2}}
149 end
150
151 def test_context_menu_on_single_issue_should_select_current_custom_field_value
152 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
153 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
154 issue = Issue.find(1)
155 issue.custom_field_values = {field.id => 'Bar'}
156 issue.save!
157 @request.session[:user_id] = 2
158 get :issues, :ids => [1]
159
160 assert_tag 'a',
161 :content => 'List',
162 :attributes => {:href => '#'},
163 :sibling => {:tag => 'ul', :children => {:count => 3}}
164 assert_tag 'a',
165 :content => 'Bar',
166 :attributes => {:class => /icon-checked/}
167 end
168
169 def test_context_menu_should_include_bool_custom_fields
170 field = IssueCustomField.create!(:name => 'Bool', :field_format => 'bool',
171 :is_for_all => true, :tracker_ids => [1, 2, 3])
172 @request.session[:user_id] = 2
173 get :issues, :ids => [1, 2]
174
175 assert_tag 'a',
176 :content => 'Bool',
177 :attributes => {:href => '#'},
178 :sibling => {:tag => 'ul', :children => {:count => 3}}
179
180 assert_tag 'a',
181 :content => 'Yes',
182 :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1"}
183 end
184
185 def test_context_menu_should_include_user_custom_fields
186 field = IssueCustomField.create!(:name => 'User', :field_format => 'user',
187 :is_for_all => true, :tracker_ids => [1, 2, 3])
188 @request.session[:user_id] = 2
189 get :issues, :ids => [1, 2]
190
191 assert_tag 'a',
192 :content => 'User',
193 :attributes => {:href => '#'},
194 :sibling => {:tag => 'ul', :children => {:count => Project.find(1).members.count + 1}}
195
196 assert_tag 'a',
197 :content => 'John Smith',
198 :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2"}
199 end
200
201 def test_context_menu_should_include_version_custom_fields
202 field = IssueCustomField.create!(:name => 'Version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1, 2, 3])
203 @request.session[:user_id] = 2
204 get :issues, :ids => [1, 2]
205
206 assert_tag 'a',
207 :content => 'Version',
208 :attributes => {:href => '#'},
209 :sibling => {:tag => 'ul', :children => {:count => Project.find(1).shared_versions.count + 1}}
210
211 assert_tag 'a',
212 :content => '2.0',
213 :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3"}
214 end
215
120 def test_context_menu_by_assignable_user_should_include_assigned_to_me_link
216 def test_context_menu_by_assignable_user_should_include_assigned_to_me_link
121 @request.session[:user_id] = 2
217 @request.session[:user_id] = 2
122 get :issues, :ids => [1]
218 get :issues, :ids => [1]
123 assert_response :success
219 assert_response :success
124 assert_template 'context_menu'
220 assert_template 'context_menu'
125
221
126 assert_tag :tag => 'a', :content => / me /,
222 assert_tag :tag => 'a', :content => / me /,
127 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=2',
223 :attributes => { :href => '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=2',
128 :class => '' }
224 :class => '' }
129 end
225 end
130
226
131 def test_context_menu_issue_visibility
227 def test_context_menu_issue_visibility
132 get :issues, :ids => [1, 4]
228 get :issues, :ids => [1, 4]
133 assert_response :success
229 assert_response :success
134 assert_template 'context_menu'
230 assert_template 'context_menu'
135 assert_equal [1], assigns(:issues).collect(&:id)
231 assert_equal [1], assigns(:issues).collect(&:id)
136 end
232 end
137
233
138 def test_time_entries_context_menu
234 def test_time_entries_context_menu
139 @request.session[:user_id] = 2
235 @request.session[:user_id] = 2
140 get :time_entries, :ids => [1, 2]
236 get :time_entries, :ids => [1, 2]
141 assert_response :success
237 assert_response :success
142 assert_template 'time_entries'
238 assert_template 'time_entries'
143 assert_tag 'a', :content => 'Edit'
239 assert_tag 'a', :content => 'Edit'
144 assert_no_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/}
240 assert_no_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/}
145 end
241 end
146
242
147 def test_time_entries_context_menu_without_edit_permission
243 def test_time_entries_context_menu_without_edit_permission
148 @request.session[:user_id] = 2
244 @request.session[:user_id] = 2
149 Role.find_by_name('Manager').remove_permission! :edit_time_entries
245 Role.find_by_name('Manager').remove_permission! :edit_time_entries
150
246
151 get :time_entries, :ids => [1, 2]
247 get :time_entries, :ids => [1, 2]
152 assert_response :success
248 assert_response :success
153 assert_template 'time_entries'
249 assert_template 'time_entries'
154 assert_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/}
250 assert_tag 'a', :content => 'Edit', :attributes => {:class => /disabled/}
155 end
251 end
156 end
252 end
General Comments 0
You need to be logged in to leave comments. Login now