@@ -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 |
|
|
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&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&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&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&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&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('&') |
|
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}&issue%5Bstatus_id%5D=5", |
|
78 | 78 | :class => '' } |
|
79 | 79 | assert_tag :tag => 'a', :content => 'Immediate', |
|
80 | 80 | :attributes => { :href => "/issues/bulk_update?#{ids}&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}&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&#{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('&') |
|
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}&issue%5Bstatus_id%5D=5", |
|
108 | 108 | :class => '' } |
|
109 | 109 | assert_tag :tag => 'a', :content => 'Immediate', |
|
110 | 110 | :attributes => { :href => "/issues/bulk_update?#{ids}&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}&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&ids%5B%5D=2&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&ids%5B%5D=2&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&ids%5B%5D=2&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&ids%5B%5D=2&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&ids%5B%5D=2&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&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