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