##// END OF EJS Templates
Added a "Member of Role" to the issues filters. #5869...
Eric Davis -
r3964:41f8d043eb29
parent child
Show More
@@ -1,615 +1,638
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !sortable.nil?
39 !sortable.nil?
40 end
40 end
41
41
42 def value(issue)
42 def value(issue)
43 issue.send name
43 issue.send name
44 end
44 end
45 end
45 end
46
46
47 class QueryCustomFieldColumn < QueryColumn
47 class QueryCustomFieldColumn < QueryColumn
48
48
49 def initialize(custom_field)
49 def initialize(custom_field)
50 self.name = "cf_#{custom_field.id}".to_sym
50 self.name = "cf_#{custom_field.id}".to_sym
51 self.sortable = custom_field.order_statement || false
51 self.sortable = custom_field.order_statement || false
52 if %w(list date bool int).include?(custom_field.field_format)
52 if %w(list date bool int).include?(custom_field.field_format)
53 self.groupable = custom_field.order_statement
53 self.groupable = custom_field.order_statement
54 end
54 end
55 self.groupable ||= false
55 self.groupable ||= false
56 @cf = custom_field
56 @cf = custom_field
57 end
57 end
58
58
59 def caption
59 def caption
60 @cf.name
60 @cf.name
61 end
61 end
62
62
63 def custom_field
63 def custom_field
64 @cf
64 @cf
65 end
65 end
66
66
67 def value(issue)
67 def value(issue)
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69 cv && @cf.cast_value(cv.value)
69 cv && @cf.cast_value(cv.value)
70 end
70 end
71 end
71 end
72
72
73 class Query < ActiveRecord::Base
73 class Query < ActiveRecord::Base
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
75 end
75 end
76
76
77 belongs_to :project
77 belongs_to :project
78 belongs_to :user
78 belongs_to :user
79 serialize :filters
79 serialize :filters
80 serialize :column_names
80 serialize :column_names
81 serialize :sort_criteria, Array
81 serialize :sort_criteria, Array
82
82
83 attr_protected :project_id, :user_id
83 attr_protected :project_id, :user_id
84
84
85 validates_presence_of :name, :on => :save
85 validates_presence_of :name, :on => :save
86 validates_length_of :name, :maximum => 255
86 validates_length_of :name, :maximum => 255
87
87
88 @@operators = { "=" => :label_equals,
88 @@operators = { "=" => :label_equals,
89 "!" => :label_not_equals,
89 "!" => :label_not_equals,
90 "o" => :label_open_issues,
90 "o" => :label_open_issues,
91 "c" => :label_closed_issues,
91 "c" => :label_closed_issues,
92 "!*" => :label_none,
92 "!*" => :label_none,
93 "*" => :label_all,
93 "*" => :label_all,
94 ">=" => :label_greater_or_equal,
94 ">=" => :label_greater_or_equal,
95 "<=" => :label_less_or_equal,
95 "<=" => :label_less_or_equal,
96 "<t+" => :label_in_less_than,
96 "<t+" => :label_in_less_than,
97 ">t+" => :label_in_more_than,
97 ">t+" => :label_in_more_than,
98 "t+" => :label_in,
98 "t+" => :label_in,
99 "t" => :label_today,
99 "t" => :label_today,
100 "w" => :label_this_week,
100 "w" => :label_this_week,
101 ">t-" => :label_less_than_ago,
101 ">t-" => :label_less_than_ago,
102 "<t-" => :label_more_than_ago,
102 "<t-" => :label_more_than_ago,
103 "t-" => :label_ago,
103 "t-" => :label_ago,
104 "~" => :label_contains,
104 "~" => :label_contains,
105 "!~" => :label_not_contains }
105 "!~" => :label_not_contains }
106
106
107 cattr_reader :operators
107 cattr_reader :operators
108
108
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
112 :list_subprojects => [ "*", "!*", "=" ],
112 :list_subprojects => [ "*", "!*", "=" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115 :string => [ "=", "~", "!", "!~" ],
115 :string => [ "=", "~", "!", "!~" ],
116 :text => [ "~", "!~" ],
116 :text => [ "~", "!~" ],
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118
118
119 cattr_reader :operators_by_filter_type
119 cattr_reader :operators_by_filter_type
120
120
121 @@available_columns = [
121 @@available_columns = [
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128 QueryColumn.new(:author),
128 QueryColumn.new(:author),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138 ]
138 ]
139 cattr_reader :available_columns
139 cattr_reader :available_columns
140
140
141 def initialize(attributes = nil)
141 def initialize(attributes = nil)
142 super attributes
142 super attributes
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144 end
144 end
145
145
146 def after_initialize
146 def after_initialize
147 # Store the fact that project is nil (used in #editable_by?)
147 # Store the fact that project is nil (used in #editable_by?)
148 @is_for_all = project.nil?
148 @is_for_all = project.nil?
149 end
149 end
150
150
151 def validate
151 def validate
152 filters.each_key do |field|
152 filters.each_key do |field|
153 errors.add label_for(field), :blank unless
153 errors.add label_for(field), :blank unless
154 # filter requires one or more values
154 # filter requires one or more values
155 (values_for(field) and !values_for(field).first.blank?) or
155 (values_for(field) and !values_for(field).first.blank?) or
156 # filter doesn't require any value
156 # filter doesn't require any value
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158 end if filters
158 end if filters
159 end
159 end
160
160
161 def editable_by?(user)
161 def editable_by?(user)
162 return false unless user
162 return false unless user
163 # Admin can edit them all and regular users can edit their private queries
163 # Admin can edit them all and regular users can edit their private queries
164 return true if user.admin? || (!is_public && self.user_id == user.id)
164 return true if user.admin? || (!is_public && self.user_id == user.id)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167 end
167 end
168
168
169 def available_filters
169 def available_filters
170 return @available_filters if @available_filters
170 return @available_filters if @available_filters
171
171
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173
173
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177 "subject" => { :type => :text, :order => 8 },
177 "subject" => { :type => :text, :order => 8 },
178 "created_on" => { :type => :date_past, :order => 9 },
178 "created_on" => { :type => :date_past, :order => 9 },
179 "updated_on" => { :type => :date_past, :order => 10 },
179 "updated_on" => { :type => :date_past, :order => 10 },
180 "start_date" => { :type => :date, :order => 11 },
180 "start_date" => { :type => :date, :order => 11 },
181 "due_date" => { :type => :date, :order => 12 },
181 "due_date" => { :type => :date, :order => 12 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
183 "done_ratio" => { :type => :integer, :order => 14 }}
183 "done_ratio" => { :type => :integer, :order => 14 }}
184
184
185 user_values = []
185 user_values = []
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187 if project
187 if project
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189 else
189 else
190 project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id)
190 project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id)
191 if project_ids.any?
191 if project_ids.any?
192 # members of the user's projects
192 # members of the user's projects
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
194 end
194 end
195 end
195 end
196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
198
198
199 group_values = Group.all.collect {|g| [g.name, g.id] }
199 group_values = Group.all.collect {|g| [g.name, g.id] }
200 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
200 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
201
202 role_values = Role.givable.collect {|r| [r.name, r.id] }
203 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
201
204
202 if User.current.logged?
205 if User.current.logged?
203 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
206 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
204 end
207 end
205
208
206 if project
209 if project
207 # project specific filters
210 # project specific filters
208 unless @project.issue_categories.empty?
211 unless @project.issue_categories.empty?
209 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
212 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
210 end
213 end
211 unless @project.shared_versions.empty?
214 unless @project.shared_versions.empty?
212 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
215 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
213 end
216 end
214 unless @project.descendants.active.empty?
217 unless @project.descendants.active.empty?
215 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
218 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
216 end
219 end
217 add_custom_fields_filters(@project.all_issue_custom_fields)
220 add_custom_fields_filters(@project.all_issue_custom_fields)
218 else
221 else
219 # global filters for cross project issue list
222 # global filters for cross project issue list
220 system_shared_versions = Version.visible.find_all_by_sharing('system')
223 system_shared_versions = Version.visible.find_all_by_sharing('system')
221 unless system_shared_versions.empty?
224 unless system_shared_versions.empty?
222 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
225 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
223 end
226 end
224 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
227 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
225 # project filter
228 # project filter
226 project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p|
229 project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p|
227 pre = (p.level > 0 ? ('--' * p.level + ' ') : '')
230 pre = (p.level > 0 ? ('--' * p.level + ' ') : '')
228 ["#{pre}#{p.name}",p.id.to_s]
231 ["#{pre}#{p.name}",p.id.to_s]
229 end
232 end
230 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values}
233 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values}
231 end
234 end
232 @available_filters
235 @available_filters
233 end
236 end
234
237
235 def add_filter(field, operator, values)
238 def add_filter(field, operator, values)
236 # values must be an array
239 # values must be an array
237 return unless values and values.is_a? Array # and !values.first.empty?
240 return unless values and values.is_a? Array # and !values.first.empty?
238 # check if field is defined as an available filter
241 # check if field is defined as an available filter
239 if available_filters.has_key? field
242 if available_filters.has_key? field
240 filter_options = available_filters[field]
243 filter_options = available_filters[field]
241 # check if operator is allowed for that filter
244 # check if operator is allowed for that filter
242 #if @@operators_by_filter_type[filter_options[:type]].include? operator
245 #if @@operators_by_filter_type[filter_options[:type]].include? operator
243 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
246 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
244 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
247 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
245 #end
248 #end
246 filters[field] = {:operator => operator, :values => values }
249 filters[field] = {:operator => operator, :values => values }
247 end
250 end
248 end
251 end
249
252
250 def add_short_filter(field, expression)
253 def add_short_filter(field, expression)
251 return unless expression
254 return unless expression
252 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
255 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
253 add_filter field, (parms[0] || "="), [parms[1] || ""]
256 add_filter field, (parms[0] || "="), [parms[1] || ""]
254 end
257 end
255
258
256 # Add multiple filters using +add_filter+
259 # Add multiple filters using +add_filter+
257 def add_filters(fields, operators, values)
260 def add_filters(fields, operators, values)
258 fields.each do |field|
261 fields.each do |field|
259 add_filter(field, operators[field], values[field])
262 add_filter(field, operators[field], values[field])
260 end
263 end
261 end
264 end
262
265
263 def has_filter?(field)
266 def has_filter?(field)
264 filters and filters[field]
267 filters and filters[field]
265 end
268 end
266
269
267 def operator_for(field)
270 def operator_for(field)
268 has_filter?(field) ? filters[field][:operator] : nil
271 has_filter?(field) ? filters[field][:operator] : nil
269 end
272 end
270
273
271 def values_for(field)
274 def values_for(field)
272 has_filter?(field) ? filters[field][:values] : nil
275 has_filter?(field) ? filters[field][:values] : nil
273 end
276 end
274
277
275 def label_for(field)
278 def label_for(field)
276 label = available_filters[field][:name] if available_filters.has_key?(field)
279 label = available_filters[field][:name] if available_filters.has_key?(field)
277 label ||= field.gsub(/\_id$/, "")
280 label ||= field.gsub(/\_id$/, "")
278 end
281 end
279
282
280 def available_columns
283 def available_columns
281 return @available_columns if @available_columns
284 return @available_columns if @available_columns
282 @available_columns = Query.available_columns
285 @available_columns = Query.available_columns
283 @available_columns += (project ?
286 @available_columns += (project ?
284 project.all_issue_custom_fields :
287 project.all_issue_custom_fields :
285 IssueCustomField.find(:all)
288 IssueCustomField.find(:all)
286 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
289 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
287 end
290 end
288
291
289 def self.available_columns=(v)
292 def self.available_columns=(v)
290 self.available_columns = (v)
293 self.available_columns = (v)
291 end
294 end
292
295
293 def self.add_available_column(column)
296 def self.add_available_column(column)
294 self.available_columns << (column) if column.is_a?(QueryColumn)
297 self.available_columns << (column) if column.is_a?(QueryColumn)
295 end
298 end
296
299
297 # Returns an array of columns that can be used to group the results
300 # Returns an array of columns that can be used to group the results
298 def groupable_columns
301 def groupable_columns
299 available_columns.select {|c| c.groupable}
302 available_columns.select {|c| c.groupable}
300 end
303 end
301
304
302 # Returns a Hash of columns and the key for sorting
305 # Returns a Hash of columns and the key for sorting
303 def sortable_columns
306 def sortable_columns
304 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
307 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
305 h[column.name.to_s] = column.sortable
308 h[column.name.to_s] = column.sortable
306 h
309 h
307 })
310 })
308 end
311 end
309
312
310 def columns
313 def columns
311 if has_default_columns?
314 if has_default_columns?
312 available_columns.select do |c|
315 available_columns.select do |c|
313 # Adds the project column by default for cross-project lists
316 # Adds the project column by default for cross-project lists
314 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
317 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
315 end
318 end
316 else
319 else
317 # preserve the column_names order
320 # preserve the column_names order
318 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
321 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
319 end
322 end
320 end
323 end
321
324
322 def column_names=(names)
325 def column_names=(names)
323 if names
326 if names
324 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
327 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
325 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
328 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
326 # Set column_names to nil if default columns
329 # Set column_names to nil if default columns
327 if names.map(&:to_s) == Setting.issue_list_default_columns
330 if names.map(&:to_s) == Setting.issue_list_default_columns
328 names = nil
331 names = nil
329 end
332 end
330 end
333 end
331 write_attribute(:column_names, names)
334 write_attribute(:column_names, names)
332 end
335 end
333
336
334 def has_column?(column)
337 def has_column?(column)
335 column_names && column_names.include?(column.name)
338 column_names && column_names.include?(column.name)
336 end
339 end
337
340
338 def has_default_columns?
341 def has_default_columns?
339 column_names.nil? || column_names.empty?
342 column_names.nil? || column_names.empty?
340 end
343 end
341
344
342 def sort_criteria=(arg)
345 def sort_criteria=(arg)
343 c = []
346 c = []
344 if arg.is_a?(Hash)
347 if arg.is_a?(Hash)
345 arg = arg.keys.sort.collect {|k| arg[k]}
348 arg = arg.keys.sort.collect {|k| arg[k]}
346 end
349 end
347 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
350 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
348 write_attribute(:sort_criteria, c)
351 write_attribute(:sort_criteria, c)
349 end
352 end
350
353
351 def sort_criteria
354 def sort_criteria
352 read_attribute(:sort_criteria) || []
355 read_attribute(:sort_criteria) || []
353 end
356 end
354
357
355 def sort_criteria_key(arg)
358 def sort_criteria_key(arg)
356 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
359 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
357 end
360 end
358
361
359 def sort_criteria_order(arg)
362 def sort_criteria_order(arg)
360 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
363 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
361 end
364 end
362
365
363 # Returns the SQL sort order that should be prepended for grouping
366 # Returns the SQL sort order that should be prepended for grouping
364 def group_by_sort_order
367 def group_by_sort_order
365 if grouped? && (column = group_by_column)
368 if grouped? && (column = group_by_column)
366 column.sortable.is_a?(Array) ?
369 column.sortable.is_a?(Array) ?
367 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
370 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
368 "#{column.sortable} #{column.default_order}"
371 "#{column.sortable} #{column.default_order}"
369 end
372 end
370 end
373 end
371
374
372 # Returns true if the query is a grouped query
375 # Returns true if the query is a grouped query
373 def grouped?
376 def grouped?
374 !group_by.blank?
377 !group_by.blank?
375 end
378 end
376
379
377 def group_by_column
380 def group_by_column
378 groupable_columns.detect {|c| c.name.to_s == group_by}
381 groupable_columns.detect {|c| c.name.to_s == group_by}
379 end
382 end
380
383
381 def group_by_statement
384 def group_by_statement
382 group_by_column.groupable
385 group_by_column.groupable
383 end
386 end
384
387
385 def project_statement
388 def project_statement
386 project_clauses = []
389 project_clauses = []
387 if project && !@project.descendants.active.empty?
390 if project && !@project.descendants.active.empty?
388 ids = [project.id]
391 ids = [project.id]
389 if has_filter?("subproject_id")
392 if has_filter?("subproject_id")
390 case operator_for("subproject_id")
393 case operator_for("subproject_id")
391 when '='
394 when '='
392 # include the selected subprojects
395 # include the selected subprojects
393 ids += values_for("subproject_id").each(&:to_i)
396 ids += values_for("subproject_id").each(&:to_i)
394 when '!*'
397 when '!*'
395 # main project only
398 # main project only
396 else
399 else
397 # all subprojects
400 # all subprojects
398 ids += project.descendants.collect(&:id)
401 ids += project.descendants.collect(&:id)
399 end
402 end
400 elsif Setting.display_subprojects_issues?
403 elsif Setting.display_subprojects_issues?
401 ids += project.descendants.collect(&:id)
404 ids += project.descendants.collect(&:id)
402 end
405 end
403 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
406 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
404 elsif project
407 elsif project
405 project_clauses << "#{Project.table_name}.id = %d" % project.id
408 project_clauses << "#{Project.table_name}.id = %d" % project.id
406 end
409 end
407 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
410 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
408 project_clauses.join(' AND ')
411 project_clauses.join(' AND ')
409 end
412 end
410
413
411 def statement
414 def statement
412 # filters clauses
415 # filters clauses
413 filters_clauses = []
416 filters_clauses = []
414 filters.each_key do |field|
417 filters.each_key do |field|
415 next if field == "subproject_id"
418 next if field == "subproject_id"
416 v = values_for(field).clone
419 v = values_for(field).clone
417 next unless v and !v.empty?
420 next unless v and !v.empty?
418 operator = operator_for(field)
421 operator = operator_for(field)
419
422
420 # "me" value subsitution
423 # "me" value subsitution
421 if %w(assigned_to_id author_id watcher_id).include?(field)
424 if %w(assigned_to_id author_id watcher_id).include?(field)
422 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
425 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
423 end
426 end
424
427
425 sql = ''
428 sql = ''
426 if field =~ /^cf_(\d+)$/
429 if field =~ /^cf_(\d+)$/
427 # custom field
430 # custom field
428 db_table = CustomValue.table_name
431 db_table = CustomValue.table_name
429 db_field = 'value'
432 db_field = 'value'
430 is_custom_filter = true
433 is_custom_filter = true
431 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
434 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
432 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
435 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
433 elsif field == 'watcher_id'
436 elsif field == 'watcher_id'
434 db_table = Watcher.table_name
437 db_table = Watcher.table_name
435 db_field = 'user_id'
438 db_field = 'user_id'
436 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
439 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
437 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
440 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
438 elsif field == "member_of_group" # named field
441 elsif field == "member_of_group" # named field
439 if operator == '*' # Any group
442 if operator == '*' # Any group
440 groups = Group.all
443 groups = Group.all
441 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
444 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
442 operator = '=' # Override the operator since we want to find by assigned_to
445 operator = '=' # Override the operator since we want to find by assigned_to
443 elsif operator == "!*"
446 elsif operator == "!*"
444 groups = Group.all
447 groups = Group.all
445 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
448 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
446 operator = '!' # Override the operator since we want to find by assigned_to
449 operator = '!' # Override the operator since we want to find by assigned_to
447 else
450 else
448 groups = Group.find_all_by_id(v)
451 groups = Group.find_all_by_id(v)
449 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
452 members_of_groups = groups.collect(&:user_ids).flatten.compact.collect(&:to_s)
450 end
453 end
451
454
452 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
455 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
453
456
457 elsif field == "assigned_to_role" # named field
458 if operator == "*" # Any Role
459 roles = Role.givable
460 operator = '=' # Override the operator since we want to find by assigned_to
461 elsif operator == "!*" # No role
462 roles = Role.givable
463 operator = '!' # Override the operator since we want to find by assigned_to
464 else
465 roles = Role.givable.find_all_by_id(v)
466 end
467 roles ||= []
468
469 members_of_roles = roles.inject([]) {|user_ids, role|
470 if role && role.members
471 user_ids << role.members.collect(&:user_id)
472 end
473 user_ids.flatten.uniq.compact
474 }.sort.collect(&:to_s)
475
476 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
454 else
477 else
455 # regular field
478 # regular field
456 db_table = Issue.table_name
479 db_table = Issue.table_name
457 db_field = field
480 db_field = field
458 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
481 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
459 end
482 end
460 filters_clauses << sql
483 filters_clauses << sql
461
484
462 end if filters and valid?
485 end if filters and valid?
463
486
464 (filters_clauses << project_statement).join(' AND ')
487 (filters_clauses << project_statement).join(' AND ')
465 end
488 end
466
489
467 # Returns the issue count
490 # Returns the issue count
468 def issue_count
491 def issue_count
469 Issue.count(:include => [:status, :project], :conditions => statement)
492 Issue.count(:include => [:status, :project], :conditions => statement)
470 rescue ::ActiveRecord::StatementInvalid => e
493 rescue ::ActiveRecord::StatementInvalid => e
471 raise StatementInvalid.new(e.message)
494 raise StatementInvalid.new(e.message)
472 end
495 end
473
496
474 # Returns the issue count by group or nil if query is not grouped
497 # Returns the issue count by group or nil if query is not grouped
475 def issue_count_by_group
498 def issue_count_by_group
476 r = nil
499 r = nil
477 if grouped?
500 if grouped?
478 begin
501 begin
479 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
502 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
480 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
503 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
481 rescue ActiveRecord::RecordNotFound
504 rescue ActiveRecord::RecordNotFound
482 r = {nil => issue_count}
505 r = {nil => issue_count}
483 end
506 end
484 c = group_by_column
507 c = group_by_column
485 if c.is_a?(QueryCustomFieldColumn)
508 if c.is_a?(QueryCustomFieldColumn)
486 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
509 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
487 end
510 end
488 end
511 end
489 r
512 r
490 rescue ::ActiveRecord::StatementInvalid => e
513 rescue ::ActiveRecord::StatementInvalid => e
491 raise StatementInvalid.new(e.message)
514 raise StatementInvalid.new(e.message)
492 end
515 end
493
516
494 # Returns the issues
517 # Returns the issues
495 # Valid options are :order, :offset, :limit, :include, :conditions
518 # Valid options are :order, :offset, :limit, :include, :conditions
496 def issues(options={})
519 def issues(options={})
497 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
520 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
498 order_option = nil if order_option.blank?
521 order_option = nil if order_option.blank?
499
522
500 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
523 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
501 :conditions => Query.merge_conditions(statement, options[:conditions]),
524 :conditions => Query.merge_conditions(statement, options[:conditions]),
502 :order => order_option,
525 :order => order_option,
503 :limit => options[:limit],
526 :limit => options[:limit],
504 :offset => options[:offset]
527 :offset => options[:offset]
505 rescue ::ActiveRecord::StatementInvalid => e
528 rescue ::ActiveRecord::StatementInvalid => e
506 raise StatementInvalid.new(e.message)
529 raise StatementInvalid.new(e.message)
507 end
530 end
508
531
509 # Returns the journals
532 # Returns the journals
510 # Valid options are :order, :offset, :limit
533 # Valid options are :order, :offset, :limit
511 def journals(options={})
534 def journals(options={})
512 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
535 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
513 :conditions => statement,
536 :conditions => statement,
514 :order => options[:order],
537 :order => options[:order],
515 :limit => options[:limit],
538 :limit => options[:limit],
516 :offset => options[:offset]
539 :offset => options[:offset]
517 rescue ::ActiveRecord::StatementInvalid => e
540 rescue ::ActiveRecord::StatementInvalid => e
518 raise StatementInvalid.new(e.message)
541 raise StatementInvalid.new(e.message)
519 end
542 end
520
543
521 # Returns the versions
544 # Returns the versions
522 # Valid options are :conditions
545 # Valid options are :conditions
523 def versions(options={})
546 def versions(options={})
524 Version.find :all, :include => :project,
547 Version.find :all, :include => :project,
525 :conditions => Query.merge_conditions(project_statement, options[:conditions])
548 :conditions => Query.merge_conditions(project_statement, options[:conditions])
526 rescue ::ActiveRecord::StatementInvalid => e
549 rescue ::ActiveRecord::StatementInvalid => e
527 raise StatementInvalid.new(e.message)
550 raise StatementInvalid.new(e.message)
528 end
551 end
529
552
530 private
553 private
531
554
532 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
555 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
533 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
556 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
534 sql = ''
557 sql = ''
535 case operator
558 case operator
536 when "="
559 when "="
537 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
560 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
538 when "!"
561 when "!"
539 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
562 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
540 when "!*"
563 when "!*"
541 sql = "#{db_table}.#{db_field} IS NULL"
564 sql = "#{db_table}.#{db_field} IS NULL"
542 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
565 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
543 when "*"
566 when "*"
544 sql = "#{db_table}.#{db_field} IS NOT NULL"
567 sql = "#{db_table}.#{db_field} IS NOT NULL"
545 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
568 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
546 when ">="
569 when ">="
547 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
570 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
548 when "<="
571 when "<="
549 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
572 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
550 when "o"
573 when "o"
551 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
574 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
552 when "c"
575 when "c"
553 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
576 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
554 when ">t-"
577 when ">t-"
555 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
578 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
556 when "<t-"
579 when "<t-"
557 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
580 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
558 when "t-"
581 when "t-"
559 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
582 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
560 when ">t+"
583 when ">t+"
561 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
584 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
562 when "<t+"
585 when "<t+"
563 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
586 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
564 when "t+"
587 when "t+"
565 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
588 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
566 when "t"
589 when "t"
567 sql = date_range_clause(db_table, db_field, 0, 0)
590 sql = date_range_clause(db_table, db_field, 0, 0)
568 when "w"
591 when "w"
569 from = l(:general_first_day_of_week) == '7' ?
592 from = l(:general_first_day_of_week) == '7' ?
570 # week starts on sunday
593 # week starts on sunday
571 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
594 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
572 # week starts on monday (Rails default)
595 # week starts on monday (Rails default)
573 Time.now.at_beginning_of_week
596 Time.now.at_beginning_of_week
574 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
597 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
575 when "~"
598 when "~"
576 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
599 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
577 when "!~"
600 when "!~"
578 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
601 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
579 end
602 end
580
603
581 return sql
604 return sql
582 end
605 end
583
606
584 def add_custom_fields_filters(custom_fields)
607 def add_custom_fields_filters(custom_fields)
585 @available_filters ||= {}
608 @available_filters ||= {}
586
609
587 custom_fields.select(&:is_filter?).each do |field|
610 custom_fields.select(&:is_filter?).each do |field|
588 case field.field_format
611 case field.field_format
589 when "text"
612 when "text"
590 options = { :type => :text, :order => 20 }
613 options = { :type => :text, :order => 20 }
591 when "list"
614 when "list"
592 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
615 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
593 when "date"
616 when "date"
594 options = { :type => :date, :order => 20 }
617 options = { :type => :date, :order => 20 }
595 when "bool"
618 when "bool"
596 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
619 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
597 else
620 else
598 options = { :type => :string, :order => 20 }
621 options = { :type => :string, :order => 20 }
599 end
622 end
600 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
623 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
601 end
624 end
602 end
625 end
603
626
604 # Returns a SQL clause for a date or datetime field.
627 # Returns a SQL clause for a date or datetime field.
605 def date_range_clause(table, field, from, to)
628 def date_range_clause(table, field, from, to)
606 s = []
629 s = []
607 if from
630 if from
608 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
631 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
609 end
632 end
610 if to
633 if to
611 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
634 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
612 end
635 end
613 s.join(' AND ')
636 s.join(' AND ')
614 end
637 end
615 end
638 end
@@ -1,916 +1,917
1 en:
1 en:
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
2 # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl)
3 direction: ltr
3 direction: ltr
4 date:
4 date:
5 formats:
5 formats:
6 # Use the strftime parameters for formats.
6 # Use the strftime parameters for formats.
7 # When no format has been given, it uses default.
7 # When no format has been given, it uses default.
8 # You can provide other formats here if you like!
8 # You can provide other formats here if you like!
9 default: "%m/%d/%Y"
9 default: "%m/%d/%Y"
10 short: "%b %d"
10 short: "%b %d"
11 long: "%B %d, %Y"
11 long: "%B %d, %Y"
12
12
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
13 day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
14 abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
15
15
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
16 # Don't forget the nil at the beginning; there's no such thing as a 0th month
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
17 month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
18 abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
19 # Used in date_select and datime_select.
19 # Used in date_select and datime_select.
20 order: [ :year, :month, :day ]
20 order: [ :year, :month, :day ]
21
21
22 time:
22 time:
23 formats:
23 formats:
24 default: "%m/%d/%Y %I:%M %p"
24 default: "%m/%d/%Y %I:%M %p"
25 time: "%I:%M %p"
25 time: "%I:%M %p"
26 short: "%d %b %H:%M"
26 short: "%d %b %H:%M"
27 long: "%B %d, %Y %H:%M"
27 long: "%B %d, %Y %H:%M"
28 am: "am"
28 am: "am"
29 pm: "pm"
29 pm: "pm"
30
30
31 datetime:
31 datetime:
32 distance_in_words:
32 distance_in_words:
33 half_a_minute: "half a minute"
33 half_a_minute: "half a minute"
34 less_than_x_seconds:
34 less_than_x_seconds:
35 one: "less than 1 second"
35 one: "less than 1 second"
36 other: "less than {{count}} seconds"
36 other: "less than {{count}} seconds"
37 x_seconds:
37 x_seconds:
38 one: "1 second"
38 one: "1 second"
39 other: "{{count}} seconds"
39 other: "{{count}} seconds"
40 less_than_x_minutes:
40 less_than_x_minutes:
41 one: "less than a minute"
41 one: "less than a minute"
42 other: "less than {{count}} minutes"
42 other: "less than {{count}} minutes"
43 x_minutes:
43 x_minutes:
44 one: "1 minute"
44 one: "1 minute"
45 other: "{{count}} minutes"
45 other: "{{count}} minutes"
46 about_x_hours:
46 about_x_hours:
47 one: "about 1 hour"
47 one: "about 1 hour"
48 other: "about {{count}} hours"
48 other: "about {{count}} hours"
49 x_days:
49 x_days:
50 one: "1 day"
50 one: "1 day"
51 other: "{{count}} days"
51 other: "{{count}} days"
52 about_x_months:
52 about_x_months:
53 one: "about 1 month"
53 one: "about 1 month"
54 other: "about {{count}} months"
54 other: "about {{count}} months"
55 x_months:
55 x_months:
56 one: "1 month"
56 one: "1 month"
57 other: "{{count}} months"
57 other: "{{count}} months"
58 about_x_years:
58 about_x_years:
59 one: "about 1 year"
59 one: "about 1 year"
60 other: "about {{count}} years"
60 other: "about {{count}} years"
61 over_x_years:
61 over_x_years:
62 one: "over 1 year"
62 one: "over 1 year"
63 other: "over {{count}} years"
63 other: "over {{count}} years"
64 almost_x_years:
64 almost_x_years:
65 one: "almost 1 year"
65 one: "almost 1 year"
66 other: "almost {{count}} years"
66 other: "almost {{count}} years"
67
67
68 number:
68 number:
69 # Default format for numbers
69 # Default format for numbers
70 format:
70 format:
71 separator: "."
71 separator: "."
72 delimiter: ""
72 delimiter: ""
73 precision: 3
73 precision: 3
74 human:
74 human:
75 format:
75 format:
76 delimiter: ""
76 delimiter: ""
77 precision: 1
77 precision: 1
78 storage_units:
78 storage_units:
79 format: "%n %u"
79 format: "%n %u"
80 units:
80 units:
81 byte:
81 byte:
82 one: "Byte"
82 one: "Byte"
83 other: "Bytes"
83 other: "Bytes"
84 kb: "KB"
84 kb: "KB"
85 mb: "MB"
85 mb: "MB"
86 gb: "GB"
86 gb: "GB"
87 tb: "TB"
87 tb: "TB"
88
88
89
89
90 # Used in array.to_sentence.
90 # Used in array.to_sentence.
91 support:
91 support:
92 array:
92 array:
93 sentence_connector: "and"
93 sentence_connector: "and"
94 skip_last_comma: false
94 skip_last_comma: false
95
95
96 activerecord:
96 activerecord:
97 errors:
97 errors:
98 messages:
98 messages:
99 inclusion: "is not included in the list"
99 inclusion: "is not included in the list"
100 exclusion: "is reserved"
100 exclusion: "is reserved"
101 invalid: "is invalid"
101 invalid: "is invalid"
102 confirmation: "doesn't match confirmation"
102 confirmation: "doesn't match confirmation"
103 accepted: "must be accepted"
103 accepted: "must be accepted"
104 empty: "can't be empty"
104 empty: "can't be empty"
105 blank: "can't be blank"
105 blank: "can't be blank"
106 too_long: "is too long (maximum is {{count}} characters)"
106 too_long: "is too long (maximum is {{count}} characters)"
107 too_short: "is too short (minimum is {{count}} characters)"
107 too_short: "is too short (minimum is {{count}} characters)"
108 wrong_length: "is the wrong length (should be {{count}} characters)"
108 wrong_length: "is the wrong length (should be {{count}} characters)"
109 taken: "has already been taken"
109 taken: "has already been taken"
110 not_a_number: "is not a number"
110 not_a_number: "is not a number"
111 not_a_date: "is not a valid date"
111 not_a_date: "is not a valid date"
112 greater_than: "must be greater than {{count}}"
112 greater_than: "must be greater than {{count}}"
113 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
113 greater_than_or_equal_to: "must be greater than or equal to {{count}}"
114 equal_to: "must be equal to {{count}}"
114 equal_to: "must be equal to {{count}}"
115 less_than: "must be less than {{count}}"
115 less_than: "must be less than {{count}}"
116 less_than_or_equal_to: "must be less than or equal to {{count}}"
116 less_than_or_equal_to: "must be less than or equal to {{count}}"
117 odd: "must be odd"
117 odd: "must be odd"
118 even: "must be even"
118 even: "must be even"
119 greater_than_start_date: "must be greater than start date"
119 greater_than_start_date: "must be greater than start date"
120 not_same_project: "doesn't belong to the same project"
120 not_same_project: "doesn't belong to the same project"
121 circular_dependency: "This relation would create a circular dependency"
121 circular_dependency: "This relation would create a circular dependency"
122 cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks"
122 cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks"
123
123
124 actionview_instancetag_blank_option: Please select
124 actionview_instancetag_blank_option: Please select
125
125
126 general_text_No: 'No'
126 general_text_No: 'No'
127 general_text_Yes: 'Yes'
127 general_text_Yes: 'Yes'
128 general_text_no: 'no'
128 general_text_no: 'no'
129 general_text_yes: 'yes'
129 general_text_yes: 'yes'
130 general_lang_name: 'English'
130 general_lang_name: 'English'
131 general_csv_separator: ','
131 general_csv_separator: ','
132 general_csv_decimal_separator: '.'
132 general_csv_decimal_separator: '.'
133 general_csv_encoding: ISO-8859-1
133 general_csv_encoding: ISO-8859-1
134 general_pdf_encoding: ISO-8859-1
134 general_pdf_encoding: ISO-8859-1
135 general_first_day_of_week: '7'
135 general_first_day_of_week: '7'
136
136
137 notice_account_updated: Account was successfully updated.
137 notice_account_updated: Account was successfully updated.
138 notice_account_invalid_creditentials: Invalid user or password
138 notice_account_invalid_creditentials: Invalid user or password
139 notice_account_password_updated: Password was successfully updated.
139 notice_account_password_updated: Password was successfully updated.
140 notice_account_wrong_password: Wrong password
140 notice_account_wrong_password: Wrong password
141 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
141 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
142 notice_account_unknown_email: Unknown user.
142 notice_account_unknown_email: Unknown user.
143 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
143 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
144 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
144 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
145 notice_account_activated: Your account has been activated. You can now log in.
145 notice_account_activated: Your account has been activated. You can now log in.
146 notice_successful_create: Successful creation.
146 notice_successful_create: Successful creation.
147 notice_successful_update: Successful update.
147 notice_successful_update: Successful update.
148 notice_successful_delete: Successful deletion.
148 notice_successful_delete: Successful deletion.
149 notice_successful_connection: Successful connection.
149 notice_successful_connection: Successful connection.
150 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
150 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
151 notice_locking_conflict: Data has been updated by another user.
151 notice_locking_conflict: Data has been updated by another user.
152 notice_not_authorized: You are not authorized to access this page.
152 notice_not_authorized: You are not authorized to access this page.
153 notice_email_sent: "An email was sent to {{value}}"
153 notice_email_sent: "An email was sent to {{value}}"
154 notice_email_error: "An error occurred while sending mail ({{value}})"
154 notice_email_error: "An error occurred while sending mail ({{value}})"
155 notice_feeds_access_key_reseted: Your RSS access key was reset.
155 notice_feeds_access_key_reseted: Your RSS access key was reset.
156 notice_api_access_key_reseted: Your API access key was reset.
156 notice_api_access_key_reseted: Your API access key was reset.
157 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
157 notice_failed_to_save_issues: "Failed to save {{count}} issue(s) on {{total}} selected: {{ids}}."
158 notice_failed_to_save_members: "Failed to save member(s): {{errors}}."
158 notice_failed_to_save_members: "Failed to save member(s): {{errors}}."
159 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
159 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
160 notice_account_pending: "Your account was created and is now pending administrator approval."
160 notice_account_pending: "Your account was created and is now pending administrator approval."
161 notice_default_data_loaded: Default configuration successfully loaded.
161 notice_default_data_loaded: Default configuration successfully loaded.
162 notice_unable_delete_version: Unable to delete version.
162 notice_unable_delete_version: Unable to delete version.
163 notice_unable_delete_time_entry: Unable to delete time log entry.
163 notice_unable_delete_time_entry: Unable to delete time log entry.
164 notice_issue_done_ratios_updated: Issue done ratios updated.
164 notice_issue_done_ratios_updated: Issue done ratios updated.
165
165
166 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
166 error_can_t_load_default_data: "Default configuration could not be loaded: {{value}}"
167 error_scm_not_found: "The entry or revision was not found in the repository."
167 error_scm_not_found: "The entry or revision was not found in the repository."
168 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
168 error_scm_command_failed: "An error occurred when trying to access the repository: {{value}}"
169 error_scm_annotate: "The entry does not exist or can not be annotated."
169 error_scm_annotate: "The entry does not exist or can not be annotated."
170 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
170 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
171 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
171 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
172 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
172 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
173 error_can_not_delete_custom_field: Unable to delete custom field
173 error_can_not_delete_custom_field: Unable to delete custom field
174 error_can_not_delete_tracker: "This tracker contains issues and can't be deleted."
174 error_can_not_delete_tracker: "This tracker contains issues and can't be deleted."
175 error_can_not_remove_role: "This role is in use and can not be deleted."
175 error_can_not_remove_role: "This role is in use and can not be deleted."
176 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
176 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version can not be reopened'
177 error_can_not_archive_project: This project can not be archived
177 error_can_not_archive_project: This project can not be archived
178 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
178 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
179 error_workflow_copy_source: 'Please select a source tracker or role'
179 error_workflow_copy_source: 'Please select a source tracker or role'
180 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
180 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
181 error_unable_delete_issue_status: 'Unable to delete issue status'
181 error_unable_delete_issue_status: 'Unable to delete issue status'
182 error_unable_to_connect: "Unable to connect ({{value}})"
182 error_unable_to_connect: "Unable to connect ({{value}})"
183 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
183 warning_attachments_not_saved: "{{count}} file(s) could not be saved."
184
184
185 mail_subject_lost_password: "Your {{value}} password"
185 mail_subject_lost_password: "Your {{value}} password"
186 mail_body_lost_password: 'To change your password, click on the following link:'
186 mail_body_lost_password: 'To change your password, click on the following link:'
187 mail_subject_register: "Your {{value}} account activation"
187 mail_subject_register: "Your {{value}} account activation"
188 mail_body_register: 'To activate your account, click on the following link:'
188 mail_body_register: 'To activate your account, click on the following link:'
189 mail_body_account_information_external: "You can use your {{value}} account to log in."
189 mail_body_account_information_external: "You can use your {{value}} account to log in."
190 mail_body_account_information: Your account information
190 mail_body_account_information: Your account information
191 mail_subject_account_activation_request: "{{value}} account activation request"
191 mail_subject_account_activation_request: "{{value}} account activation request"
192 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
192 mail_body_account_activation_request: "A new user ({{value}}) has registered. The account is pending your approval:"
193 mail_subject_reminder: "{{count}} issue(s) due in the next {{days}} days"
193 mail_subject_reminder: "{{count}} issue(s) due in the next {{days}} days"
194 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
194 mail_body_reminder: "{{count}} issue(s) that are assigned to you are due in the next {{days}} days:"
195 mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
195 mail_subject_wiki_content_added: "'{{page}}' wiki page has been added"
196 mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
196 mail_body_wiki_content_added: "The '{{page}}' wiki page has been added by {{author}}."
197 mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
197 mail_subject_wiki_content_updated: "'{{page}}' wiki page has been updated"
198 mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
198 mail_body_wiki_content_updated: "The '{{page}}' wiki page has been updated by {{author}}."
199
199
200 gui_validation_error: 1 error
200 gui_validation_error: 1 error
201 gui_validation_error_plural: "{{count}} errors"
201 gui_validation_error_plural: "{{count}} errors"
202
202
203 field_name: Name
203 field_name: Name
204 field_description: Description
204 field_description: Description
205 field_summary: Summary
205 field_summary: Summary
206 field_is_required: Required
206 field_is_required: Required
207 field_firstname: Firstname
207 field_firstname: Firstname
208 field_lastname: Lastname
208 field_lastname: Lastname
209 field_mail: Email
209 field_mail: Email
210 field_filename: File
210 field_filename: File
211 field_filesize: Size
211 field_filesize: Size
212 field_downloads: Downloads
212 field_downloads: Downloads
213 field_author: Author
213 field_author: Author
214 field_created_on: Created
214 field_created_on: Created
215 field_updated_on: Updated
215 field_updated_on: Updated
216 field_field_format: Format
216 field_field_format: Format
217 field_is_for_all: For all projects
217 field_is_for_all: For all projects
218 field_possible_values: Possible values
218 field_possible_values: Possible values
219 field_regexp: Regular expression
219 field_regexp: Regular expression
220 field_min_length: Minimum length
220 field_min_length: Minimum length
221 field_max_length: Maximum length
221 field_max_length: Maximum length
222 field_value: Value
222 field_value: Value
223 field_category: Category
223 field_category: Category
224 field_title: Title
224 field_title: Title
225 field_project: Project
225 field_project: Project
226 field_issue: Issue
226 field_issue: Issue
227 field_status: Status
227 field_status: Status
228 field_notes: Notes
228 field_notes: Notes
229 field_is_closed: Issue closed
229 field_is_closed: Issue closed
230 field_is_default: Default value
230 field_is_default: Default value
231 field_tracker: Tracker
231 field_tracker: Tracker
232 field_subject: Subject
232 field_subject: Subject
233 field_due_date: Due date
233 field_due_date: Due date
234 field_assigned_to: Assignee
234 field_assigned_to: Assignee
235 field_priority: Priority
235 field_priority: Priority
236 field_fixed_version: Target version
236 field_fixed_version: Target version
237 field_user: User
237 field_user: User
238 field_principal: Principal
238 field_principal: Principal
239 field_role: Role
239 field_role: Role
240 field_homepage: Homepage
240 field_homepage: Homepage
241 field_is_public: Public
241 field_is_public: Public
242 field_parent: Subproject of
242 field_parent: Subproject of
243 field_is_in_roadmap: Issues displayed in roadmap
243 field_is_in_roadmap: Issues displayed in roadmap
244 field_login: Login
244 field_login: Login
245 field_mail_notification: Email notifications
245 field_mail_notification: Email notifications
246 field_admin: Administrator
246 field_admin: Administrator
247 field_last_login_on: Last connection
247 field_last_login_on: Last connection
248 field_language: Language
248 field_language: Language
249 field_effective_date: Date
249 field_effective_date: Date
250 field_password: Password
250 field_password: Password
251 field_new_password: New password
251 field_new_password: New password
252 field_password_confirmation: Confirmation
252 field_password_confirmation: Confirmation
253 field_version: Version
253 field_version: Version
254 field_type: Type
254 field_type: Type
255 field_host: Host
255 field_host: Host
256 field_port: Port
256 field_port: Port
257 field_account: Account
257 field_account: Account
258 field_base_dn: Base DN
258 field_base_dn: Base DN
259 field_attr_login: Login attribute
259 field_attr_login: Login attribute
260 field_attr_firstname: Firstname attribute
260 field_attr_firstname: Firstname attribute
261 field_attr_lastname: Lastname attribute
261 field_attr_lastname: Lastname attribute
262 field_attr_mail: Email attribute
262 field_attr_mail: Email attribute
263 field_onthefly: On-the-fly user creation
263 field_onthefly: On-the-fly user creation
264 field_start_date: Start
264 field_start_date: Start
265 field_done_ratio: % Done
265 field_done_ratio: % Done
266 field_auth_source: Authentication mode
266 field_auth_source: Authentication mode
267 field_hide_mail: Hide my email address
267 field_hide_mail: Hide my email address
268 field_comments: Comment
268 field_comments: Comment
269 field_url: URL
269 field_url: URL
270 field_start_page: Start page
270 field_start_page: Start page
271 field_subproject: Subproject
271 field_subproject: Subproject
272 field_hours: Hours
272 field_hours: Hours
273 field_activity: Activity
273 field_activity: Activity
274 field_spent_on: Date
274 field_spent_on: Date
275 field_identifier: Identifier
275 field_identifier: Identifier
276 field_is_filter: Used as a filter
276 field_is_filter: Used as a filter
277 field_issue_to: Related issue
277 field_issue_to: Related issue
278 field_delay: Delay
278 field_delay: Delay
279 field_assignable: Issues can be assigned to this role
279 field_assignable: Issues can be assigned to this role
280 field_redirect_existing_links: Redirect existing links
280 field_redirect_existing_links: Redirect existing links
281 field_estimated_hours: Estimated time
281 field_estimated_hours: Estimated time
282 field_column_names: Columns
282 field_column_names: Columns
283 field_time_entries: Log time
283 field_time_entries: Log time
284 field_time_zone: Time zone
284 field_time_zone: Time zone
285 field_searchable: Searchable
285 field_searchable: Searchable
286 field_default_value: Default value
286 field_default_value: Default value
287 field_comments_sorting: Display comments
287 field_comments_sorting: Display comments
288 field_parent_title: Parent page
288 field_parent_title: Parent page
289 field_editable: Editable
289 field_editable: Editable
290 field_watcher: Watcher
290 field_watcher: Watcher
291 field_identity_url: OpenID URL
291 field_identity_url: OpenID URL
292 field_content: Content
292 field_content: Content
293 field_group_by: Group results by
293 field_group_by: Group results by
294 field_sharing: Sharing
294 field_sharing: Sharing
295 field_parent_issue: Parent task
295 field_parent_issue: Parent task
296 field_member_of_group: Member of Group
296 field_member_of_group: Member of Group
297 field_assigned_to_role: Member of Role
297
298
298 setting_app_title: Application title
299 setting_app_title: Application title
299 setting_app_subtitle: Application subtitle
300 setting_app_subtitle: Application subtitle
300 setting_welcome_text: Welcome text
301 setting_welcome_text: Welcome text
301 setting_default_language: Default language
302 setting_default_language: Default language
302 setting_login_required: Authentication required
303 setting_login_required: Authentication required
303 setting_self_registration: Self-registration
304 setting_self_registration: Self-registration
304 setting_attachment_max_size: Attachment max. size
305 setting_attachment_max_size: Attachment max. size
305 setting_issues_export_limit: Issues export limit
306 setting_issues_export_limit: Issues export limit
306 setting_mail_from: Emission email address
307 setting_mail_from: Emission email address
307 setting_bcc_recipients: Blind carbon copy recipients (bcc)
308 setting_bcc_recipients: Blind carbon copy recipients (bcc)
308 setting_plain_text_mail: Plain text mail (no HTML)
309 setting_plain_text_mail: Plain text mail (no HTML)
309 setting_host_name: Host name and path
310 setting_host_name: Host name and path
310 setting_text_formatting: Text formatting
311 setting_text_formatting: Text formatting
311 setting_wiki_compression: Wiki history compression
312 setting_wiki_compression: Wiki history compression
312 setting_feeds_limit: Feed content limit
313 setting_feeds_limit: Feed content limit
313 setting_default_projects_public: New projects are public by default
314 setting_default_projects_public: New projects are public by default
314 setting_autofetch_changesets: Autofetch commits
315 setting_autofetch_changesets: Autofetch commits
315 setting_sys_api_enabled: Enable WS for repository management
316 setting_sys_api_enabled: Enable WS for repository management
316 setting_commit_ref_keywords: Referencing keywords
317 setting_commit_ref_keywords: Referencing keywords
317 setting_commit_fix_keywords: Fixing keywords
318 setting_commit_fix_keywords: Fixing keywords
318 setting_autologin: Autologin
319 setting_autologin: Autologin
319 setting_date_format: Date format
320 setting_date_format: Date format
320 setting_time_format: Time format
321 setting_time_format: Time format
321 setting_cross_project_issue_relations: Allow cross-project issue relations
322 setting_cross_project_issue_relations: Allow cross-project issue relations
322 setting_issue_list_default_columns: Default columns displayed on the issue list
323 setting_issue_list_default_columns: Default columns displayed on the issue list
323 setting_repositories_encodings: Repositories encodings
324 setting_repositories_encodings: Repositories encodings
324 setting_commit_logs_encoding: Commit messages encoding
325 setting_commit_logs_encoding: Commit messages encoding
325 setting_emails_footer: Emails footer
326 setting_emails_footer: Emails footer
326 setting_protocol: Protocol
327 setting_protocol: Protocol
327 setting_per_page_options: Objects per page options
328 setting_per_page_options: Objects per page options
328 setting_user_format: Users display format
329 setting_user_format: Users display format
329 setting_activity_days_default: Days displayed on project activity
330 setting_activity_days_default: Days displayed on project activity
330 setting_display_subprojects_issues: Display subprojects issues on main projects by default
331 setting_display_subprojects_issues: Display subprojects issues on main projects by default
331 setting_enabled_scm: Enabled SCM
332 setting_enabled_scm: Enabled SCM
332 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
333 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
333 setting_mail_handler_api_enabled: Enable WS for incoming emails
334 setting_mail_handler_api_enabled: Enable WS for incoming emails
334 setting_mail_handler_api_key: API key
335 setting_mail_handler_api_key: API key
335 setting_sequential_project_identifiers: Generate sequential project identifiers
336 setting_sequential_project_identifiers: Generate sequential project identifiers
336 setting_gravatar_enabled: Use Gravatar user icons
337 setting_gravatar_enabled: Use Gravatar user icons
337 setting_gravatar_default: Default Gravatar image
338 setting_gravatar_default: Default Gravatar image
338 setting_diff_max_lines_displayed: Max number of diff lines displayed
339 setting_diff_max_lines_displayed: Max number of diff lines displayed
339 setting_file_max_size_displayed: Max size of text files displayed inline
340 setting_file_max_size_displayed: Max size of text files displayed inline
340 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
341 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
341 setting_openid: Allow OpenID login and registration
342 setting_openid: Allow OpenID login and registration
342 setting_password_min_length: Minimum password length
343 setting_password_min_length: Minimum password length
343 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
344 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
344 setting_default_projects_modules: Default enabled modules for new projects
345 setting_default_projects_modules: Default enabled modules for new projects
345 setting_issue_done_ratio: Calculate the issue done ratio with
346 setting_issue_done_ratio: Calculate the issue done ratio with
346 setting_issue_done_ratio_issue_field: Use the issue field
347 setting_issue_done_ratio_issue_field: Use the issue field
347 setting_issue_done_ratio_issue_status: Use the issue status
348 setting_issue_done_ratio_issue_status: Use the issue status
348 setting_start_of_week: Start calendars on
349 setting_start_of_week: Start calendars on
349 setting_rest_api_enabled: Enable REST web service
350 setting_rest_api_enabled: Enable REST web service
350 setting_cache_formatted_text: Cache formatted text
351 setting_cache_formatted_text: Cache formatted text
351
352
352 permission_add_project: Create project
353 permission_add_project: Create project
353 permission_add_subprojects: Create subprojects
354 permission_add_subprojects: Create subprojects
354 permission_edit_project: Edit project
355 permission_edit_project: Edit project
355 permission_select_project_modules: Select project modules
356 permission_select_project_modules: Select project modules
356 permission_manage_members: Manage members
357 permission_manage_members: Manage members
357 permission_manage_project_activities: Manage project activities
358 permission_manage_project_activities: Manage project activities
358 permission_manage_versions: Manage versions
359 permission_manage_versions: Manage versions
359 permission_manage_categories: Manage issue categories
360 permission_manage_categories: Manage issue categories
360 permission_view_issues: View Issues
361 permission_view_issues: View Issues
361 permission_add_issues: Add issues
362 permission_add_issues: Add issues
362 permission_edit_issues: Edit issues
363 permission_edit_issues: Edit issues
363 permission_manage_issue_relations: Manage issue relations
364 permission_manage_issue_relations: Manage issue relations
364 permission_add_issue_notes: Add notes
365 permission_add_issue_notes: Add notes
365 permission_edit_issue_notes: Edit notes
366 permission_edit_issue_notes: Edit notes
366 permission_edit_own_issue_notes: Edit own notes
367 permission_edit_own_issue_notes: Edit own notes
367 permission_move_issues: Move issues
368 permission_move_issues: Move issues
368 permission_delete_issues: Delete issues
369 permission_delete_issues: Delete issues
369 permission_manage_public_queries: Manage public queries
370 permission_manage_public_queries: Manage public queries
370 permission_save_queries: Save queries
371 permission_save_queries: Save queries
371 permission_view_gantt: View gantt chart
372 permission_view_gantt: View gantt chart
372 permission_view_calendar: View calendar
373 permission_view_calendar: View calendar
373 permission_view_issue_watchers: View watchers list
374 permission_view_issue_watchers: View watchers list
374 permission_add_issue_watchers: Add watchers
375 permission_add_issue_watchers: Add watchers
375 permission_delete_issue_watchers: Delete watchers
376 permission_delete_issue_watchers: Delete watchers
376 permission_log_time: Log spent time
377 permission_log_time: Log spent time
377 permission_view_time_entries: View spent time
378 permission_view_time_entries: View spent time
378 permission_edit_time_entries: Edit time logs
379 permission_edit_time_entries: Edit time logs
379 permission_edit_own_time_entries: Edit own time logs
380 permission_edit_own_time_entries: Edit own time logs
380 permission_manage_news: Manage news
381 permission_manage_news: Manage news
381 permission_comment_news: Comment news
382 permission_comment_news: Comment news
382 permission_manage_documents: Manage documents
383 permission_manage_documents: Manage documents
383 permission_view_documents: View documents
384 permission_view_documents: View documents
384 permission_manage_files: Manage files
385 permission_manage_files: Manage files
385 permission_view_files: View files
386 permission_view_files: View files
386 permission_manage_wiki: Manage wiki
387 permission_manage_wiki: Manage wiki
387 permission_rename_wiki_pages: Rename wiki pages
388 permission_rename_wiki_pages: Rename wiki pages
388 permission_delete_wiki_pages: Delete wiki pages
389 permission_delete_wiki_pages: Delete wiki pages
389 permission_view_wiki_pages: View wiki
390 permission_view_wiki_pages: View wiki
390 permission_view_wiki_edits: View wiki history
391 permission_view_wiki_edits: View wiki history
391 permission_edit_wiki_pages: Edit wiki pages
392 permission_edit_wiki_pages: Edit wiki pages
392 permission_delete_wiki_pages_attachments: Delete attachments
393 permission_delete_wiki_pages_attachments: Delete attachments
393 permission_protect_wiki_pages: Protect wiki pages
394 permission_protect_wiki_pages: Protect wiki pages
394 permission_manage_repository: Manage repository
395 permission_manage_repository: Manage repository
395 permission_browse_repository: Browse repository
396 permission_browse_repository: Browse repository
396 permission_view_changesets: View changesets
397 permission_view_changesets: View changesets
397 permission_commit_access: Commit access
398 permission_commit_access: Commit access
398 permission_manage_boards: Manage boards
399 permission_manage_boards: Manage boards
399 permission_view_messages: View messages
400 permission_view_messages: View messages
400 permission_add_messages: Post messages
401 permission_add_messages: Post messages
401 permission_edit_messages: Edit messages
402 permission_edit_messages: Edit messages
402 permission_edit_own_messages: Edit own messages
403 permission_edit_own_messages: Edit own messages
403 permission_delete_messages: Delete messages
404 permission_delete_messages: Delete messages
404 permission_delete_own_messages: Delete own messages
405 permission_delete_own_messages: Delete own messages
405 permission_export_wiki_pages: Export wiki pages
406 permission_export_wiki_pages: Export wiki pages
406 permission_manage_subtasks: Manage subtasks
407 permission_manage_subtasks: Manage subtasks
407
408
408 project_module_issue_tracking: Issue tracking
409 project_module_issue_tracking: Issue tracking
409 project_module_time_tracking: Time tracking
410 project_module_time_tracking: Time tracking
410 project_module_news: News
411 project_module_news: News
411 project_module_documents: Documents
412 project_module_documents: Documents
412 project_module_files: Files
413 project_module_files: Files
413 project_module_wiki: Wiki
414 project_module_wiki: Wiki
414 project_module_repository: Repository
415 project_module_repository: Repository
415 project_module_boards: Boards
416 project_module_boards: Boards
416 project_module_calendar: Calendar
417 project_module_calendar: Calendar
417 project_module_gantt: Gantt
418 project_module_gantt: Gantt
418
419
419 label_user: User
420 label_user: User
420 label_user_plural: Users
421 label_user_plural: Users
421 label_user_new: New user
422 label_user_new: New user
422 label_user_anonymous: Anonymous
423 label_user_anonymous: Anonymous
423 label_project: Project
424 label_project: Project
424 label_project_new: New project
425 label_project_new: New project
425 label_project_plural: Projects
426 label_project_plural: Projects
426 label_x_projects:
427 label_x_projects:
427 zero: no projects
428 zero: no projects
428 one: 1 project
429 one: 1 project
429 other: "{{count}} projects"
430 other: "{{count}} projects"
430 label_project_all: All Projects
431 label_project_all: All Projects
431 label_project_latest: Latest projects
432 label_project_latest: Latest projects
432 label_issue: Issue
433 label_issue: Issue
433 label_issue_new: New issue
434 label_issue_new: New issue
434 label_issue_plural: Issues
435 label_issue_plural: Issues
435 label_issue_view_all: View all issues
436 label_issue_view_all: View all issues
436 label_issues_by: "Issues by {{value}}"
437 label_issues_by: "Issues by {{value}}"
437 label_issue_added: Issue added
438 label_issue_added: Issue added
438 label_issue_updated: Issue updated
439 label_issue_updated: Issue updated
439 label_document: Document
440 label_document: Document
440 label_document_new: New document
441 label_document_new: New document
441 label_document_plural: Documents
442 label_document_plural: Documents
442 label_document_added: Document added
443 label_document_added: Document added
443 label_role: Role
444 label_role: Role
444 label_role_plural: Roles
445 label_role_plural: Roles
445 label_role_new: New role
446 label_role_new: New role
446 label_role_and_permissions: Roles and permissions
447 label_role_and_permissions: Roles and permissions
447 label_member: Member
448 label_member: Member
448 label_member_new: New member
449 label_member_new: New member
449 label_member_plural: Members
450 label_member_plural: Members
450 label_tracker: Tracker
451 label_tracker: Tracker
451 label_tracker_plural: Trackers
452 label_tracker_plural: Trackers
452 label_tracker_new: New tracker
453 label_tracker_new: New tracker
453 label_workflow: Workflow
454 label_workflow: Workflow
454 label_issue_status: Issue status
455 label_issue_status: Issue status
455 label_issue_status_plural: Issue statuses
456 label_issue_status_plural: Issue statuses
456 label_issue_status_new: New status
457 label_issue_status_new: New status
457 label_issue_category: Issue category
458 label_issue_category: Issue category
458 label_issue_category_plural: Issue categories
459 label_issue_category_plural: Issue categories
459 label_issue_category_new: New category
460 label_issue_category_new: New category
460 label_custom_field: Custom field
461 label_custom_field: Custom field
461 label_custom_field_plural: Custom fields
462 label_custom_field_plural: Custom fields
462 label_custom_field_new: New custom field
463 label_custom_field_new: New custom field
463 label_enumerations: Enumerations
464 label_enumerations: Enumerations
464 label_enumeration_new: New value
465 label_enumeration_new: New value
465 label_information: Information
466 label_information: Information
466 label_information_plural: Information
467 label_information_plural: Information
467 label_please_login: Please log in
468 label_please_login: Please log in
468 label_register: Register
469 label_register: Register
469 label_login_with_open_id_option: or login with OpenID
470 label_login_with_open_id_option: or login with OpenID
470 label_password_lost: Lost password
471 label_password_lost: Lost password
471 label_home: Home
472 label_home: Home
472 label_my_page: My page
473 label_my_page: My page
473 label_my_account: My account
474 label_my_account: My account
474 label_my_projects: My projects
475 label_my_projects: My projects
475 label_my_page_block: My page block
476 label_my_page_block: My page block
476 label_administration: Administration
477 label_administration: Administration
477 label_login: Sign in
478 label_login: Sign in
478 label_logout: Sign out
479 label_logout: Sign out
479 label_help: Help
480 label_help: Help
480 label_reported_issues: Reported issues
481 label_reported_issues: Reported issues
481 label_assigned_to_me_issues: Issues assigned to me
482 label_assigned_to_me_issues: Issues assigned to me
482 label_last_login: Last connection
483 label_last_login: Last connection
483 label_registered_on: Registered on
484 label_registered_on: Registered on
484 label_activity: Activity
485 label_activity: Activity
485 label_overall_activity: Overall activity
486 label_overall_activity: Overall activity
486 label_user_activity: "{{value}}'s activity"
487 label_user_activity: "{{value}}'s activity"
487 label_new: New
488 label_new: New
488 label_logged_as: Logged in as
489 label_logged_as: Logged in as
489 label_environment: Environment
490 label_environment: Environment
490 label_authentication: Authentication
491 label_authentication: Authentication
491 label_auth_source: Authentication mode
492 label_auth_source: Authentication mode
492 label_auth_source_new: New authentication mode
493 label_auth_source_new: New authentication mode
493 label_auth_source_plural: Authentication modes
494 label_auth_source_plural: Authentication modes
494 label_subproject_plural: Subprojects
495 label_subproject_plural: Subprojects
495 label_subproject_new: New subproject
496 label_subproject_new: New subproject
496 label_and_its_subprojects: "{{value}} and its subprojects"
497 label_and_its_subprojects: "{{value}} and its subprojects"
497 label_min_max_length: Min - Max length
498 label_min_max_length: Min - Max length
498 label_list: List
499 label_list: List
499 label_date: Date
500 label_date: Date
500 label_integer: Integer
501 label_integer: Integer
501 label_float: Float
502 label_float: Float
502 label_boolean: Boolean
503 label_boolean: Boolean
503 label_string: Text
504 label_string: Text
504 label_text: Long text
505 label_text: Long text
505 label_attribute: Attribute
506 label_attribute: Attribute
506 label_attribute_plural: Attributes
507 label_attribute_plural: Attributes
507 label_download: "{{count}} Download"
508 label_download: "{{count}} Download"
508 label_download_plural: "{{count}} Downloads"
509 label_download_plural: "{{count}} Downloads"
509 label_no_data: No data to display
510 label_no_data: No data to display
510 label_change_status: Change status
511 label_change_status: Change status
511 label_history: History
512 label_history: History
512 label_attachment: File
513 label_attachment: File
513 label_attachment_new: New file
514 label_attachment_new: New file
514 label_attachment_delete: Delete file
515 label_attachment_delete: Delete file
515 label_attachment_plural: Files
516 label_attachment_plural: Files
516 label_file_added: File added
517 label_file_added: File added
517 label_report: Report
518 label_report: Report
518 label_report_plural: Reports
519 label_report_plural: Reports
519 label_news: News
520 label_news: News
520 label_news_new: Add news
521 label_news_new: Add news
521 label_news_plural: News
522 label_news_plural: News
522 label_news_latest: Latest news
523 label_news_latest: Latest news
523 label_news_view_all: View all news
524 label_news_view_all: View all news
524 label_news_added: News added
525 label_news_added: News added
525 label_settings: Settings
526 label_settings: Settings
526 label_overview: Overview
527 label_overview: Overview
527 label_version: Version
528 label_version: Version
528 label_version_new: New version
529 label_version_new: New version
529 label_version_plural: Versions
530 label_version_plural: Versions
530 label_close_versions: Close completed versions
531 label_close_versions: Close completed versions
531 label_confirmation: Confirmation
532 label_confirmation: Confirmation
532 label_export_to: 'Also available in:'
533 label_export_to: 'Also available in:'
533 label_read: Read...
534 label_read: Read...
534 label_public_projects: Public projects
535 label_public_projects: Public projects
535 label_open_issues: open
536 label_open_issues: open
536 label_open_issues_plural: open
537 label_open_issues_plural: open
537 label_closed_issues: closed
538 label_closed_issues: closed
538 label_closed_issues_plural: closed
539 label_closed_issues_plural: closed
539 label_x_open_issues_abbr_on_total:
540 label_x_open_issues_abbr_on_total:
540 zero: 0 open / {{total}}
541 zero: 0 open / {{total}}
541 one: 1 open / {{total}}
542 one: 1 open / {{total}}
542 other: "{{count}} open / {{total}}"
543 other: "{{count}} open / {{total}}"
543 label_x_open_issues_abbr:
544 label_x_open_issues_abbr:
544 zero: 0 open
545 zero: 0 open
545 one: 1 open
546 one: 1 open
546 other: "{{count}} open"
547 other: "{{count}} open"
547 label_x_closed_issues_abbr:
548 label_x_closed_issues_abbr:
548 zero: 0 closed
549 zero: 0 closed
549 one: 1 closed
550 one: 1 closed
550 other: "{{count}} closed"
551 other: "{{count}} closed"
551 label_total: Total
552 label_total: Total
552 label_permissions: Permissions
553 label_permissions: Permissions
553 label_current_status: Current status
554 label_current_status: Current status
554 label_new_statuses_allowed: New statuses allowed
555 label_new_statuses_allowed: New statuses allowed
555 label_all: all
556 label_all: all
556 label_none: none
557 label_none: none
557 label_nobody: nobody
558 label_nobody: nobody
558 label_next: Next
559 label_next: Next
559 label_previous: Previous
560 label_previous: Previous
560 label_used_by: Used by
561 label_used_by: Used by
561 label_details: Details
562 label_details: Details
562 label_add_note: Add a note
563 label_add_note: Add a note
563 label_per_page: Per page
564 label_per_page: Per page
564 label_calendar: Calendar
565 label_calendar: Calendar
565 label_months_from: months from
566 label_months_from: months from
566 label_gantt: Gantt
567 label_gantt: Gantt
567 label_internal: Internal
568 label_internal: Internal
568 label_last_changes: "last {{count}} changes"
569 label_last_changes: "last {{count}} changes"
569 label_change_view_all: View all changes
570 label_change_view_all: View all changes
570 label_personalize_page: Personalize this page
571 label_personalize_page: Personalize this page
571 label_comment: Comment
572 label_comment: Comment
572 label_comment_plural: Comments
573 label_comment_plural: Comments
573 label_x_comments:
574 label_x_comments:
574 zero: no comments
575 zero: no comments
575 one: 1 comment
576 one: 1 comment
576 other: "{{count}} comments"
577 other: "{{count}} comments"
577 label_comment_add: Add a comment
578 label_comment_add: Add a comment
578 label_comment_added: Comment added
579 label_comment_added: Comment added
579 label_comment_delete: Delete comments
580 label_comment_delete: Delete comments
580 label_query: Custom query
581 label_query: Custom query
581 label_query_plural: Custom queries
582 label_query_plural: Custom queries
582 label_query_new: New query
583 label_query_new: New query
583 label_filter_add: Add filter
584 label_filter_add: Add filter
584 label_filter_plural: Filters
585 label_filter_plural: Filters
585 label_equals: is
586 label_equals: is
586 label_not_equals: is not
587 label_not_equals: is not
587 label_in_less_than: in less than
588 label_in_less_than: in less than
588 label_in_more_than: in more than
589 label_in_more_than: in more than
589 label_greater_or_equal: '>='
590 label_greater_or_equal: '>='
590 label_less_or_equal: '<='
591 label_less_or_equal: '<='
591 label_in: in
592 label_in: in
592 label_today: today
593 label_today: today
593 label_all_time: all time
594 label_all_time: all time
594 label_yesterday: yesterday
595 label_yesterday: yesterday
595 label_this_week: this week
596 label_this_week: this week
596 label_last_week: last week
597 label_last_week: last week
597 label_last_n_days: "last {{count}} days"
598 label_last_n_days: "last {{count}} days"
598 label_this_month: this month
599 label_this_month: this month
599 label_last_month: last month
600 label_last_month: last month
600 label_this_year: this year
601 label_this_year: this year
601 label_date_range: Date range
602 label_date_range: Date range
602 label_less_than_ago: less than days ago
603 label_less_than_ago: less than days ago
603 label_more_than_ago: more than days ago
604 label_more_than_ago: more than days ago
604 label_ago: days ago
605 label_ago: days ago
605 label_contains: contains
606 label_contains: contains
606 label_not_contains: doesn't contain
607 label_not_contains: doesn't contain
607 label_day_plural: days
608 label_day_plural: days
608 label_repository: Repository
609 label_repository: Repository
609 label_repository_plural: Repositories
610 label_repository_plural: Repositories
610 label_browse: Browse
611 label_browse: Browse
611 label_modification: "{{count}} change"
612 label_modification: "{{count}} change"
612 label_modification_plural: "{{count}} changes"
613 label_modification_plural: "{{count}} changes"
613 label_branch: Branch
614 label_branch: Branch
614 label_tag: Tag
615 label_tag: Tag
615 label_revision: Revision
616 label_revision: Revision
616 label_revision_plural: Revisions
617 label_revision_plural: Revisions
617 label_revision_id: "Revision {{value}}"
618 label_revision_id: "Revision {{value}}"
618 label_associated_revisions: Associated revisions
619 label_associated_revisions: Associated revisions
619 label_added: added
620 label_added: added
620 label_modified: modified
621 label_modified: modified
621 label_copied: copied
622 label_copied: copied
622 label_renamed: renamed
623 label_renamed: renamed
623 label_deleted: deleted
624 label_deleted: deleted
624 label_latest_revision: Latest revision
625 label_latest_revision: Latest revision
625 label_latest_revision_plural: Latest revisions
626 label_latest_revision_plural: Latest revisions
626 label_view_revisions: View revisions
627 label_view_revisions: View revisions
627 label_view_all_revisions: View all revisions
628 label_view_all_revisions: View all revisions
628 label_max_size: Maximum size
629 label_max_size: Maximum size
629 label_sort_highest: Move to top
630 label_sort_highest: Move to top
630 label_sort_higher: Move up
631 label_sort_higher: Move up
631 label_sort_lower: Move down
632 label_sort_lower: Move down
632 label_sort_lowest: Move to bottom
633 label_sort_lowest: Move to bottom
633 label_roadmap: Roadmap
634 label_roadmap: Roadmap
634 label_roadmap_due_in: "Due in {{value}}"
635 label_roadmap_due_in: "Due in {{value}}"
635 label_roadmap_overdue: "{{value}} late"
636 label_roadmap_overdue: "{{value}} late"
636 label_roadmap_no_issues: No issues for this version
637 label_roadmap_no_issues: No issues for this version
637 label_search: Search
638 label_search: Search
638 label_result_plural: Results
639 label_result_plural: Results
639 label_all_words: All words
640 label_all_words: All words
640 label_wiki: Wiki
641 label_wiki: Wiki
641 label_wiki_edit: Wiki edit
642 label_wiki_edit: Wiki edit
642 label_wiki_edit_plural: Wiki edits
643 label_wiki_edit_plural: Wiki edits
643 label_wiki_page: Wiki page
644 label_wiki_page: Wiki page
644 label_wiki_page_plural: Wiki pages
645 label_wiki_page_plural: Wiki pages
645 label_index_by_title: Index by title
646 label_index_by_title: Index by title
646 label_index_by_date: Index by date
647 label_index_by_date: Index by date
647 label_current_version: Current version
648 label_current_version: Current version
648 label_preview: Preview
649 label_preview: Preview
649 label_feed_plural: Feeds
650 label_feed_plural: Feeds
650 label_changes_details: Details of all changes
651 label_changes_details: Details of all changes
651 label_issue_tracking: Issue tracking
652 label_issue_tracking: Issue tracking
652 label_spent_time: Spent time
653 label_spent_time: Spent time
653 label_overall_spent_time: Overall spent time
654 label_overall_spent_time: Overall spent time
654 label_f_hour: "{{value}} hour"
655 label_f_hour: "{{value}} hour"
655 label_f_hour_plural: "{{value}} hours"
656 label_f_hour_plural: "{{value}} hours"
656 label_time_tracking: Time tracking
657 label_time_tracking: Time tracking
657 label_change_plural: Changes
658 label_change_plural: Changes
658 label_statistics: Statistics
659 label_statistics: Statistics
659 label_commits_per_month: Commits per month
660 label_commits_per_month: Commits per month
660 label_commits_per_author: Commits per author
661 label_commits_per_author: Commits per author
661 label_view_diff: View differences
662 label_view_diff: View differences
662 label_diff_inline: inline
663 label_diff_inline: inline
663 label_diff_side_by_side: side by side
664 label_diff_side_by_side: side by side
664 label_options: Options
665 label_options: Options
665 label_copy_workflow_from: Copy workflow from
666 label_copy_workflow_from: Copy workflow from
666 label_permissions_report: Permissions report
667 label_permissions_report: Permissions report
667 label_watched_issues: Watched issues
668 label_watched_issues: Watched issues
668 label_related_issues: Related issues
669 label_related_issues: Related issues
669 label_applied_status: Applied status
670 label_applied_status: Applied status
670 label_loading: Loading...
671 label_loading: Loading...
671 label_relation_new: New relation
672 label_relation_new: New relation
672 label_relation_delete: Delete relation
673 label_relation_delete: Delete relation
673 label_relates_to: related to
674 label_relates_to: related to
674 label_duplicates: duplicates
675 label_duplicates: duplicates
675 label_duplicated_by: duplicated by
676 label_duplicated_by: duplicated by
676 label_blocks: blocks
677 label_blocks: blocks
677 label_blocked_by: blocked by
678 label_blocked_by: blocked by
678 label_precedes: precedes
679 label_precedes: precedes
679 label_follows: follows
680 label_follows: follows
680 label_end_to_start: end to start
681 label_end_to_start: end to start
681 label_end_to_end: end to end
682 label_end_to_end: end to end
682 label_start_to_start: start to start
683 label_start_to_start: start to start
683 label_start_to_end: start to end
684 label_start_to_end: start to end
684 label_stay_logged_in: Stay logged in
685 label_stay_logged_in: Stay logged in
685 label_disabled: disabled
686 label_disabled: disabled
686 label_show_completed_versions: Show completed versions
687 label_show_completed_versions: Show completed versions
687 label_me: me
688 label_me: me
688 label_board: Forum
689 label_board: Forum
689 label_board_new: New forum
690 label_board_new: New forum
690 label_board_plural: Forums
691 label_board_plural: Forums
691 label_board_locked: Locked
692 label_board_locked: Locked
692 label_board_sticky: Sticky
693 label_board_sticky: Sticky
693 label_topic_plural: Topics
694 label_topic_plural: Topics
694 label_message_plural: Messages
695 label_message_plural: Messages
695 label_message_last: Last message
696 label_message_last: Last message
696 label_message_new: New message
697 label_message_new: New message
697 label_message_posted: Message added
698 label_message_posted: Message added
698 label_reply_plural: Replies
699 label_reply_plural: Replies
699 label_send_information: Send account information to the user
700 label_send_information: Send account information to the user
700 label_year: Year
701 label_year: Year
701 label_month: Month
702 label_month: Month
702 label_week: Week
703 label_week: Week
703 label_date_from: From
704 label_date_from: From
704 label_date_to: To
705 label_date_to: To
705 label_language_based: Based on user's language
706 label_language_based: Based on user's language
706 label_sort_by: "Sort by {{value}}"
707 label_sort_by: "Sort by {{value}}"
707 label_send_test_email: Send a test email
708 label_send_test_email: Send a test email
708 label_feeds_access_key: RSS access key
709 label_feeds_access_key: RSS access key
709 label_missing_feeds_access_key: Missing a RSS access key
710 label_missing_feeds_access_key: Missing a RSS access key
710 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
711 label_feeds_access_key_created_on: "RSS access key created {{value}} ago"
711 label_module_plural: Modules
712 label_module_plural: Modules
712 label_added_time_by: "Added by {{author}} {{age}} ago"
713 label_added_time_by: "Added by {{author}} {{age}} ago"
713 label_updated_time_by: "Updated by {{author}} {{age}} ago"
714 label_updated_time_by: "Updated by {{author}} {{age}} ago"
714 label_updated_time: "Updated {{value}} ago"
715 label_updated_time: "Updated {{value}} ago"
715 label_jump_to_a_project: Jump to a project...
716 label_jump_to_a_project: Jump to a project...
716 label_file_plural: Files
717 label_file_plural: Files
717 label_changeset_plural: Changesets
718 label_changeset_plural: Changesets
718 label_default_columns: Default columns
719 label_default_columns: Default columns
719 label_no_change_option: (No change)
720 label_no_change_option: (No change)
720 label_bulk_edit_selected_issues: Bulk edit selected issues
721 label_bulk_edit_selected_issues: Bulk edit selected issues
721 label_theme: Theme
722 label_theme: Theme
722 label_default: Default
723 label_default: Default
723 label_search_titles_only: Search titles only
724 label_search_titles_only: Search titles only
724 label_user_mail_option_all: "For any event on all my projects"
725 label_user_mail_option_all: "For any event on all my projects"
725 label_user_mail_option_selected: "For any event on the selected projects only..."
726 label_user_mail_option_selected: "For any event on the selected projects only..."
726 label_user_mail_option_none: "Only for things I watch or I'm involved in"
727 label_user_mail_option_none: "Only for things I watch or I'm involved in"
727 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
728 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
728 label_registration_activation_by_email: account activation by email
729 label_registration_activation_by_email: account activation by email
729 label_registration_manual_activation: manual account activation
730 label_registration_manual_activation: manual account activation
730 label_registration_automatic_activation: automatic account activation
731 label_registration_automatic_activation: automatic account activation
731 label_display_per_page: "Per page: {{value}}"
732 label_display_per_page: "Per page: {{value}}"
732 label_age: Age
733 label_age: Age
733 label_change_properties: Change properties
734 label_change_properties: Change properties
734 label_general: General
735 label_general: General
735 label_more: More
736 label_more: More
736 label_scm: SCM
737 label_scm: SCM
737 label_plugins: Plugins
738 label_plugins: Plugins
738 label_ldap_authentication: LDAP authentication
739 label_ldap_authentication: LDAP authentication
739 label_downloads_abbr: D/L
740 label_downloads_abbr: D/L
740 label_optional_description: Optional description
741 label_optional_description: Optional description
741 label_add_another_file: Add another file
742 label_add_another_file: Add another file
742 label_preferences: Preferences
743 label_preferences: Preferences
743 label_chronological_order: In chronological order
744 label_chronological_order: In chronological order
744 label_reverse_chronological_order: In reverse chronological order
745 label_reverse_chronological_order: In reverse chronological order
745 label_planning: Planning
746 label_planning: Planning
746 label_incoming_emails: Incoming emails
747 label_incoming_emails: Incoming emails
747 label_generate_key: Generate a key
748 label_generate_key: Generate a key
748 label_issue_watchers: Watchers
749 label_issue_watchers: Watchers
749 label_example: Example
750 label_example: Example
750 label_display: Display
751 label_display: Display
751 label_sort: Sort
752 label_sort: Sort
752 label_ascending: Ascending
753 label_ascending: Ascending
753 label_descending: Descending
754 label_descending: Descending
754 label_date_from_to: From {{start}} to {{end}}
755 label_date_from_to: From {{start}} to {{end}}
755 label_wiki_content_added: Wiki page added
756 label_wiki_content_added: Wiki page added
756 label_wiki_content_updated: Wiki page updated
757 label_wiki_content_updated: Wiki page updated
757 label_group: Group
758 label_group: Group
758 label_group_plural: Groups
759 label_group_plural: Groups
759 label_group_new: New group
760 label_group_new: New group
760 label_time_entry_plural: Spent time
761 label_time_entry_plural: Spent time
761 label_version_sharing_none: Not shared
762 label_version_sharing_none: Not shared
762 label_version_sharing_descendants: With subprojects
763 label_version_sharing_descendants: With subprojects
763 label_version_sharing_hierarchy: With project hierarchy
764 label_version_sharing_hierarchy: With project hierarchy
764 label_version_sharing_tree: With project tree
765 label_version_sharing_tree: With project tree
765 label_version_sharing_system: With all projects
766 label_version_sharing_system: With all projects
766 label_update_issue_done_ratios: Update issue done ratios
767 label_update_issue_done_ratios: Update issue done ratios
767 label_copy_source: Source
768 label_copy_source: Source
768 label_copy_target: Target
769 label_copy_target: Target
769 label_copy_same_as_target: Same as target
770 label_copy_same_as_target: Same as target
770 label_display_used_statuses_only: Only display statuses that are used by this tracker
771 label_display_used_statuses_only: Only display statuses that are used by this tracker
771 label_api_access_key: API access key
772 label_api_access_key: API access key
772 label_missing_api_access_key: Missing an API access key
773 label_missing_api_access_key: Missing an API access key
773 label_api_access_key_created_on: "API access key created {{value}} ago"
774 label_api_access_key_created_on: "API access key created {{value}} ago"
774 label_profile: Profile
775 label_profile: Profile
775 label_subtask_plural: Subtasks
776 label_subtask_plural: Subtasks
776 label_project_copy_notifications: Send email notifications during the project copy
777 label_project_copy_notifications: Send email notifications during the project copy
777
778
778 button_login: Login
779 button_login: Login
779 button_submit: Submit
780 button_submit: Submit
780 button_save: Save
781 button_save: Save
781 button_check_all: Check all
782 button_check_all: Check all
782 button_uncheck_all: Uncheck all
783 button_uncheck_all: Uncheck all
783 button_delete: Delete
784 button_delete: Delete
784 button_create: Create
785 button_create: Create
785 button_create_and_continue: Create and continue
786 button_create_and_continue: Create and continue
786 button_test: Test
787 button_test: Test
787 button_edit: Edit
788 button_edit: Edit
788 button_add: Add
789 button_add: Add
789 button_change: Change
790 button_change: Change
790 button_apply: Apply
791 button_apply: Apply
791 button_clear: Clear
792 button_clear: Clear
792 button_lock: Lock
793 button_lock: Lock
793 button_unlock: Unlock
794 button_unlock: Unlock
794 button_download: Download
795 button_download: Download
795 button_list: List
796 button_list: List
796 button_view: View
797 button_view: View
797 button_move: Move
798 button_move: Move
798 button_move_and_follow: Move and follow
799 button_move_and_follow: Move and follow
799 button_back: Back
800 button_back: Back
800 button_cancel: Cancel
801 button_cancel: Cancel
801 button_activate: Activate
802 button_activate: Activate
802 button_sort: Sort
803 button_sort: Sort
803 button_log_time: Log time
804 button_log_time: Log time
804 button_rollback: Rollback to this version
805 button_rollback: Rollback to this version
805 button_watch: Watch
806 button_watch: Watch
806 button_unwatch: Unwatch
807 button_unwatch: Unwatch
807 button_reply: Reply
808 button_reply: Reply
808 button_archive: Archive
809 button_archive: Archive
809 button_unarchive: Unarchive
810 button_unarchive: Unarchive
810 button_reset: Reset
811 button_reset: Reset
811 button_rename: Rename
812 button_rename: Rename
812 button_change_password: Change password
813 button_change_password: Change password
813 button_copy: Copy
814 button_copy: Copy
814 button_copy_and_follow: Copy and follow
815 button_copy_and_follow: Copy and follow
815 button_annotate: Annotate
816 button_annotate: Annotate
816 button_update: Update
817 button_update: Update
817 button_configure: Configure
818 button_configure: Configure
818 button_quote: Quote
819 button_quote: Quote
819 button_duplicate: Duplicate
820 button_duplicate: Duplicate
820 button_show: Show
821 button_show: Show
821
822
822 status_active: active
823 status_active: active
823 status_registered: registered
824 status_registered: registered
824 status_locked: locked
825 status_locked: locked
825
826
826 version_status_open: open
827 version_status_open: open
827 version_status_locked: locked
828 version_status_locked: locked
828 version_status_closed: closed
829 version_status_closed: closed
829
830
830 field_active: Active
831 field_active: Active
831
832
832 text_select_mail_notifications: Select actions for which email notifications should be sent.
833 text_select_mail_notifications: Select actions for which email notifications should be sent.
833 text_regexp_info: eg. ^[A-Z0-9]+$
834 text_regexp_info: eg. ^[A-Z0-9]+$
834 text_min_max_length_info: 0 means no restriction
835 text_min_max_length_info: 0 means no restriction
835 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
836 text_project_destroy_confirmation: Are you sure you want to delete this project and related data ?
836 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
837 text_subprojects_destroy_warning: "Its subproject(s): {{value}} will be also deleted."
837 text_workflow_edit: Select a role and a tracker to edit the workflow
838 text_workflow_edit: Select a role and a tracker to edit the workflow
838 text_are_you_sure: Are you sure ?
839 text_are_you_sure: Are you sure ?
839 text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
840 text_journal_changed: "{{label}} changed from {{old}} to {{new}}"
840 text_journal_set_to: "{{label}} set to {{value}}"
841 text_journal_set_to: "{{label}} set to {{value}}"
841 text_journal_deleted: "{{label}} deleted ({{old}})"
842 text_journal_deleted: "{{label}} deleted ({{old}})"
842 text_journal_added: "{{label}} {{value}} added"
843 text_journal_added: "{{label}} {{value}} added"
843 text_tip_task_begin_day: task beginning this day
844 text_tip_task_begin_day: task beginning this day
844 text_tip_task_end_day: task ending this day
845 text_tip_task_end_day: task ending this day
845 text_tip_task_begin_end_day: task beginning and ending this day
846 text_tip_task_begin_end_day: task beginning and ending this day
846 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
847 text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
847 text_caracters_maximum: "{{count}} characters maximum."
848 text_caracters_maximum: "{{count}} characters maximum."
848 text_caracters_minimum: "Must be at least {{count}} characters long."
849 text_caracters_minimum: "Must be at least {{count}} characters long."
849 text_length_between: "Length between {{min}} and {{max}} characters."
850 text_length_between: "Length between {{min}} and {{max}} characters."
850 text_tracker_no_workflow: No workflow defined for this tracker
851 text_tracker_no_workflow: No workflow defined for this tracker
851 text_unallowed_characters: Unallowed characters
852 text_unallowed_characters: Unallowed characters
852 text_comma_separated: Multiple values allowed (comma separated).
853 text_comma_separated: Multiple values allowed (comma separated).
853 text_line_separated: Multiple values allowed (one line for each value).
854 text_line_separated: Multiple values allowed (one line for each value).
854 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
855 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
855 text_issue_added: "Issue {{id}} has been reported by {{author}}."
856 text_issue_added: "Issue {{id}} has been reported by {{author}}."
856 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
857 text_issue_updated: "Issue {{id}} has been updated by {{author}}."
857 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
858 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
858 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
859 text_issue_category_destroy_question: "Some issues ({{count}}) are assigned to this category. What do you want to do ?"
859 text_issue_category_destroy_assignments: Remove category assignments
860 text_issue_category_destroy_assignments: Remove category assignments
860 text_issue_category_reassign_to: Reassign issues to this category
861 text_issue_category_reassign_to: Reassign issues to this category
861 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
862 text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
862 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
863 text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
863 text_load_default_configuration: Load the default configuration
864 text_load_default_configuration: Load the default configuration
864 text_status_changed_by_changeset: "Applied in changeset {{value}}."
865 text_status_changed_by_changeset: "Applied in changeset {{value}}."
865 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
866 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
866 text_select_project_modules: 'Select modules to enable for this project:'
867 text_select_project_modules: 'Select modules to enable for this project:'
867 text_default_administrator_account_changed: Default administrator account changed
868 text_default_administrator_account_changed: Default administrator account changed
868 text_file_repository_writable: Attachments directory writable
869 text_file_repository_writable: Attachments directory writable
869 text_plugin_assets_writable: Plugin assets directory writable
870 text_plugin_assets_writable: Plugin assets directory writable
870 text_rmagick_available: RMagick available (optional)
871 text_rmagick_available: RMagick available (optional)
871 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
872 text_destroy_time_entries_question: "{{hours}} hours were reported on the issues you are about to delete. What do you want to do ?"
872 text_destroy_time_entries: Delete reported hours
873 text_destroy_time_entries: Delete reported hours
873 text_assign_time_entries_to_project: Assign reported hours to the project
874 text_assign_time_entries_to_project: Assign reported hours to the project
874 text_reassign_time_entries: 'Reassign reported hours to this issue:'
875 text_reassign_time_entries: 'Reassign reported hours to this issue:'
875 text_user_wrote: "{{value}} wrote:"
876 text_user_wrote: "{{value}} wrote:"
876 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
877 text_enumeration_destroy_question: "{{count}} objects are assigned to this value."
877 text_enumeration_category_reassign_to: 'Reassign them to this value:'
878 text_enumeration_category_reassign_to: 'Reassign them to this value:'
878 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
879 text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
879 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
880 text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped."
880 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
881 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
881 text_custom_field_possible_values_info: 'One line for each value'
882 text_custom_field_possible_values_info: 'One line for each value'
882 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
883 text_wiki_page_destroy_question: "This page has {{descendants}} child page(s) and descendant(s). What do you want to do?"
883 text_wiki_page_nullify_children: "Keep child pages as root pages"
884 text_wiki_page_nullify_children: "Keep child pages as root pages"
884 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
885 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
885 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
886 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
886 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
887 text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
887 text_zoom_in: Zoom in
888 text_zoom_in: Zoom in
888 text_zoom_out: Zoom out
889 text_zoom_out: Zoom out
889
890
890 default_role_manager: Manager
891 default_role_manager: Manager
891 default_role_developer: Developer
892 default_role_developer: Developer
892 default_role_reporter: Reporter
893 default_role_reporter: Reporter
893 default_tracker_bug: Bug
894 default_tracker_bug: Bug
894 default_tracker_feature: Feature
895 default_tracker_feature: Feature
895 default_tracker_support: Support
896 default_tracker_support: Support
896 default_issue_status_new: New
897 default_issue_status_new: New
897 default_issue_status_in_progress: In Progress
898 default_issue_status_in_progress: In Progress
898 default_issue_status_resolved: Resolved
899 default_issue_status_resolved: Resolved
899 default_issue_status_feedback: Feedback
900 default_issue_status_feedback: Feedback
900 default_issue_status_closed: Closed
901 default_issue_status_closed: Closed
901 default_issue_status_rejected: Rejected
902 default_issue_status_rejected: Rejected
902 default_doc_category_user: User documentation
903 default_doc_category_user: User documentation
903 default_doc_category_tech: Technical documentation
904 default_doc_category_tech: Technical documentation
904 default_priority_low: Low
905 default_priority_low: Low
905 default_priority_normal: Normal
906 default_priority_normal: Normal
906 default_priority_high: High
907 default_priority_high: High
907 default_priority_urgent: Urgent
908 default_priority_urgent: Urgent
908 default_priority_immediate: Immediate
909 default_priority_immediate: Immediate
909 default_activity_design: Design
910 default_activity_design: Design
910 default_activity_development: Development
911 default_activity_development: Development
911
912
912 enumeration_issue_priorities: Issue priorities
913 enumeration_issue_priorities: Issue priorities
913 enumeration_doc_categories: Document categories
914 enumeration_doc_categories: Document categories
914 enumeration_activities: Activities (time tracking)
915 enumeration_activities: Activities (time tracking)
915 enumeration_system_activity: System Activity
916 enumeration_system_activity: System Activity
916
917
@@ -1,35 +1,40
1 module ObjectDaddyHelpers
1 module ObjectDaddyHelpers
2 # TODO: Remove these three once everyone has ported their code to use the
2 # TODO: Remove these three once everyone has ported their code to use the
3 # new object_daddy version with protected attribute support
3 # new object_daddy version with protected attribute support
4 def User.generate_with_protected(attributes={})
4 def User.generate_with_protected(attributes={})
5 User.generate(attributes)
5 User.generate(attributes)
6 end
6 end
7
7
8 def User.generate_with_protected!(attributes={})
8 def User.generate_with_protected!(attributes={})
9 User.generate!(attributes)
9 User.generate!(attributes)
10 end
10 end
11
11
12 def User.spawn_with_protected(attributes={})
12 def User.spawn_with_protected(attributes={})
13 User.spawn(attributes)
13 User.spawn(attributes)
14 end
14 end
15
15
16 def User.add_to_project(user, project, roles)
17 roles = [roles] unless roles.is_a?(Array)
18 Member.generate!(:principal => user, :project => project, :roles => roles)
19 end
20
16 # Generate the default Query
21 # Generate the default Query
17 def Query.generate_default!(attributes={})
22 def Query.generate_default!(attributes={})
18 query = Query.spawn(attributes)
23 query = Query.spawn(attributes)
19 query.name ||= '_'
24 query.name ||= '_'
20 query.save!
25 query.save!
21 query
26 query
22 end
27 end
23
28
24 # Generate an issue for a project, using it's trackers
29 # Generate an issue for a project, using it's trackers
25 def Issue.generate_for_project!(project, attributes={})
30 def Issue.generate_for_project!(project, attributes={})
26 issue = Issue.spawn(attributes) do |issue|
31 issue = Issue.spawn(attributes) do |issue|
27 issue.project = project
32 issue.project = project
28 issue.tracker = project.trackers.first unless project.trackers.empty?
33 issue.tracker = project.trackers.first unless project.trackers.empty?
29 yield issue if block_given?
34 yield issue if block_given?
30 end
35 end
31 issue.save!
36 issue.save!
32 issue
37 issue
33 end
38 end
34
39
35 end
40 end
@@ -1,458 +1,524
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def test_system_shared_versions_should_be_available_in_global_queries
29 def test_system_shared_versions_should_be_available_in_global_queries
30 Version.find(2).update_attribute :sharing, 'system'
30 Version.find(2).update_attribute :sharing, 'system'
31 query = Query.new(:project => nil, :name => '_')
31 query = Query.new(:project => nil, :name => '_')
32 assert query.available_filters.has_key?('fixed_version_id')
32 assert query.available_filters.has_key?('fixed_version_id')
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
34 end
34 end
35
35
36 def test_project_filter_in_global_queries
36 def test_project_filter_in_global_queries
37 query = Query.new(:project => nil, :name => '_')
37 query = Query.new(:project => nil, :name => '_')
38 project_filter = query.available_filters["project_id"]
38 project_filter = query.available_filters["project_id"]
39 assert_not_nil project_filter
39 assert_not_nil project_filter
40 project_ids = project_filter[:values].map{|p| p[1]}
40 project_ids = project_filter[:values].map{|p| p[1]}
41 assert project_ids.include?("1") #public project
41 assert project_ids.include?("1") #public project
42 assert !project_ids.include?("2") #private project user cannot see
42 assert !project_ids.include?("2") #private project user cannot see
43 end
43 end
44
44
45 def find_issues_with_query(query)
45 def find_issues_with_query(query)
46 Issue.find :all,
46 Issue.find :all,
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
48 :conditions => query.statement
48 :conditions => query.statement
49 end
49 end
50
50
51 def assert_find_issues_with_query_is_successful(query)
51 def assert_find_issues_with_query_is_successful(query)
52 assert_nothing_raised do
52 assert_nothing_raised do
53 find_issues_with_query(query)
53 find_issues_with_query(query)
54 end
54 end
55 end
55 end
56
56
57 def assert_query_statement_includes(query, condition)
57 def assert_query_statement_includes(query, condition)
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
59 end
59 end
60
60
61 def test_query_should_allow_shared_versions_for_a_project_query
61 def test_query_should_allow_shared_versions_for_a_project_query
62 subproject_version = Version.find(4)
62 subproject_version = Version.find(4)
63 query = Query.new(:project => Project.find(1), :name => '_')
63 query = Query.new(:project => Project.find(1), :name => '_')
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
65
65
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
67 end
67 end
68
68
69 def test_query_with_multiple_custom_fields
69 def test_query_with_multiple_custom_fields
70 query = Query.find(1)
70 query = Query.find(1)
71 assert query.valid?
71 assert query.valid?
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
73 issues = find_issues_with_query(query)
73 issues = find_issues_with_query(query)
74 assert_equal 1, issues.length
74 assert_equal 1, issues.length
75 assert_equal Issue.find(3), issues.first
75 assert_equal Issue.find(3), issues.first
76 end
76 end
77
77
78 def test_operator_none
78 def test_operator_none
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('fixed_version_id', '!*', [''])
80 query.add_filter('fixed_version_id', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
84 find_issues_with_query(query)
84 find_issues_with_query(query)
85 end
85 end
86
86
87 def test_operator_none_for_integer
87 def test_operator_none_for_integer
88 query = Query.new(:project => Project.find(1), :name => '_')
88 query = Query.new(:project => Project.find(1), :name => '_')
89 query.add_filter('estimated_hours', '!*', [''])
89 query.add_filter('estimated_hours', '!*', [''])
90 issues = find_issues_with_query(query)
90 issues = find_issues_with_query(query)
91 assert !issues.empty?
91 assert !issues.empty?
92 assert issues.all? {|i| !i.estimated_hours}
92 assert issues.all? {|i| !i.estimated_hours}
93 end
93 end
94
94
95 def test_operator_all
95 def test_operator_all
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('fixed_version_id', '*', [''])
97 query.add_filter('fixed_version_id', '*', [''])
98 query.add_filter('cf_1', '*', [''])
98 query.add_filter('cf_1', '*', [''])
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
101 find_issues_with_query(query)
101 find_issues_with_query(query)
102 end
102 end
103
103
104 def test_operator_greater_than
104 def test_operator_greater_than
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('done_ratio', '>=', ['40'])
106 query.add_filter('done_ratio', '>=', ['40'])
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
108 find_issues_with_query(query)
108 find_issues_with_query(query)
109 end
109 end
110
110
111 def test_operator_in_more_than
111 def test_operator_in_more_than
112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
113 query = Query.new(:project => Project.find(1), :name => '_')
113 query = Query.new(:project => Project.find(1), :name => '_')
114 query.add_filter('due_date', '>t+', ['15'])
114 query.add_filter('due_date', '>t+', ['15'])
115 issues = find_issues_with_query(query)
115 issues = find_issues_with_query(query)
116 assert !issues.empty?
116 assert !issues.empty?
117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
118 end
118 end
119
119
120 def test_operator_in_less_than
120 def test_operator_in_less_than
121 query = Query.new(:project => Project.find(1), :name => '_')
121 query = Query.new(:project => Project.find(1), :name => '_')
122 query.add_filter('due_date', '<t+', ['15'])
122 query.add_filter('due_date', '<t+', ['15'])
123 issues = find_issues_with_query(query)
123 issues = find_issues_with_query(query)
124 assert !issues.empty?
124 assert !issues.empty?
125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
126 end
126 end
127
127
128 def test_operator_less_than_ago
128 def test_operator_less_than_ago
129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
130 query = Query.new(:project => Project.find(1), :name => '_')
130 query = Query.new(:project => Project.find(1), :name => '_')
131 query.add_filter('due_date', '>t-', ['3'])
131 query.add_filter('due_date', '>t-', ['3'])
132 issues = find_issues_with_query(query)
132 issues = find_issues_with_query(query)
133 assert !issues.empty?
133 assert !issues.empty?
134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
135 end
135 end
136
136
137 def test_operator_more_than_ago
137 def test_operator_more_than_ago
138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
139 query = Query.new(:project => Project.find(1), :name => '_')
139 query = Query.new(:project => Project.find(1), :name => '_')
140 query.add_filter('due_date', '<t-', ['10'])
140 query.add_filter('due_date', '<t-', ['10'])
141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
142 issues = find_issues_with_query(query)
142 issues = find_issues_with_query(query)
143 assert !issues.empty?
143 assert !issues.empty?
144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
145 end
145 end
146
146
147 def test_operator_in
147 def test_operator_in
148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
149 query = Query.new(:project => Project.find(1), :name => '_')
149 query = Query.new(:project => Project.find(1), :name => '_')
150 query.add_filter('due_date', 't+', ['2'])
150 query.add_filter('due_date', 't+', ['2'])
151 issues = find_issues_with_query(query)
151 issues = find_issues_with_query(query)
152 assert !issues.empty?
152 assert !issues.empty?
153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
154 end
154 end
155
155
156 def test_operator_ago
156 def test_operator_ago
157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
158 query = Query.new(:project => Project.find(1), :name => '_')
158 query = Query.new(:project => Project.find(1), :name => '_')
159 query.add_filter('due_date', 't-', ['3'])
159 query.add_filter('due_date', 't-', ['3'])
160 issues = find_issues_with_query(query)
160 issues = find_issues_with_query(query)
161 assert !issues.empty?
161 assert !issues.empty?
162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
163 end
163 end
164
164
165 def test_operator_today
165 def test_operator_today
166 query = Query.new(:project => Project.find(1), :name => '_')
166 query = Query.new(:project => Project.find(1), :name => '_')
167 query.add_filter('due_date', 't', [''])
167 query.add_filter('due_date', 't', [''])
168 issues = find_issues_with_query(query)
168 issues = find_issues_with_query(query)
169 assert !issues.empty?
169 assert !issues.empty?
170 issues.each {|issue| assert_equal Date.today, issue.due_date}
170 issues.each {|issue| assert_equal Date.today, issue.due_date}
171 end
171 end
172
172
173 def test_operator_this_week_on_date
173 def test_operator_this_week_on_date
174 query = Query.new(:project => Project.find(1), :name => '_')
174 query = Query.new(:project => Project.find(1), :name => '_')
175 query.add_filter('due_date', 'w', [''])
175 query.add_filter('due_date', 'w', [''])
176 find_issues_with_query(query)
176 find_issues_with_query(query)
177 end
177 end
178
178
179 def test_operator_this_week_on_datetime
179 def test_operator_this_week_on_datetime
180 query = Query.new(:project => Project.find(1), :name => '_')
180 query = Query.new(:project => Project.find(1), :name => '_')
181 query.add_filter('created_on', 'w', [''])
181 query.add_filter('created_on', 'w', [''])
182 find_issues_with_query(query)
182 find_issues_with_query(query)
183 end
183 end
184
184
185 def test_operator_contains
185 def test_operator_contains
186 query = Query.new(:project => Project.find(1), :name => '_')
186 query = Query.new(:project => Project.find(1), :name => '_')
187 query.add_filter('subject', '~', ['uNable'])
187 query.add_filter('subject', '~', ['uNable'])
188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
189 result = find_issues_with_query(query)
189 result = find_issues_with_query(query)
190 assert result.empty?
190 assert result.empty?
191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
192 end
192 end
193
193
194 def test_operator_does_not_contains
194 def test_operator_does_not_contains
195 query = Query.new(:project => Project.find(1), :name => '_')
195 query = Query.new(:project => Project.find(1), :name => '_')
196 query.add_filter('subject', '!~', ['uNable'])
196 query.add_filter('subject', '!~', ['uNable'])
197 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
197 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
198 find_issues_with_query(query)
198 find_issues_with_query(query)
199 end
199 end
200
200
201 def test_filter_watched_issues
201 def test_filter_watched_issues
202 User.current = User.find(1)
202 User.current = User.find(1)
203 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
203 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
204 result = find_issues_with_query(query)
204 result = find_issues_with_query(query)
205 assert_not_nil result
205 assert_not_nil result
206 assert !result.empty?
206 assert !result.empty?
207 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
207 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
208 User.current = nil
208 User.current = nil
209 end
209 end
210
210
211 def test_filter_unwatched_issues
211 def test_filter_unwatched_issues
212 User.current = User.find(1)
212 User.current = User.find(1)
213 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
213 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
214 result = find_issues_with_query(query)
214 result = find_issues_with_query(query)
215 assert_not_nil result
215 assert_not_nil result
216 assert !result.empty?
216 assert !result.empty?
217 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
217 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
218 User.current = nil
218 User.current = nil
219 end
219 end
220
220
221 def test_default_columns
221 def test_default_columns
222 q = Query.new
222 q = Query.new
223 assert !q.columns.empty?
223 assert !q.columns.empty?
224 end
224 end
225
225
226 def test_set_column_names
226 def test_set_column_names
227 q = Query.new
227 q = Query.new
228 q.column_names = ['tracker', :subject, '', 'unknonw_column']
228 q.column_names = ['tracker', :subject, '', 'unknonw_column']
229 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
229 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
230 c = q.columns.first
230 c = q.columns.first
231 assert q.has_column?(c)
231 assert q.has_column?(c)
232 end
232 end
233
233
234 def test_groupable_columns_should_include_custom_fields
234 def test_groupable_columns_should_include_custom_fields
235 q = Query.new
235 q = Query.new
236 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
236 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
237 end
237 end
238
238
239 def test_default_sort
239 def test_default_sort
240 q = Query.new
240 q = Query.new
241 assert_equal [], q.sort_criteria
241 assert_equal [], q.sort_criteria
242 end
242 end
243
243
244 def test_set_sort_criteria_with_hash
244 def test_set_sort_criteria_with_hash
245 q = Query.new
245 q = Query.new
246 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
246 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
247 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
247 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
248 end
248 end
249
249
250 def test_set_sort_criteria_with_array
250 def test_set_sort_criteria_with_array
251 q = Query.new
251 q = Query.new
252 q.sort_criteria = [['priority', 'desc'], 'tracker']
252 q.sort_criteria = [['priority', 'desc'], 'tracker']
253 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
253 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
254 end
254 end
255
255
256 def test_create_query_with_sort
256 def test_create_query_with_sort
257 q = Query.new(:name => 'Sorted')
257 q = Query.new(:name => 'Sorted')
258 q.sort_criteria = [['priority', 'desc'], 'tracker']
258 q.sort_criteria = [['priority', 'desc'], 'tracker']
259 assert q.save
259 assert q.save
260 q.reload
260 q.reload
261 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
261 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
262 end
262 end
263
263
264 def test_sort_by_string_custom_field_asc
264 def test_sort_by_string_custom_field_asc
265 q = Query.new
265 q = Query.new
266 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
266 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
267 assert c
267 assert c
268 assert c.sortable
268 assert c.sortable
269 issues = Issue.find :all,
269 issues = Issue.find :all,
270 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
270 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
271 :conditions => q.statement,
271 :conditions => q.statement,
272 :order => "#{c.sortable} ASC"
272 :order => "#{c.sortable} ASC"
273 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
273 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
274 assert !values.empty?
274 assert !values.empty?
275 assert_equal values.sort, values
275 assert_equal values.sort, values
276 end
276 end
277
277
278 def test_sort_by_string_custom_field_desc
278 def test_sort_by_string_custom_field_desc
279 q = Query.new
279 q = Query.new
280 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
280 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
281 assert c
281 assert c
282 assert c.sortable
282 assert c.sortable
283 issues = Issue.find :all,
283 issues = Issue.find :all,
284 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
284 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
285 :conditions => q.statement,
285 :conditions => q.statement,
286 :order => "#{c.sortable} DESC"
286 :order => "#{c.sortable} DESC"
287 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
287 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
288 assert !values.empty?
288 assert !values.empty?
289 assert_equal values.sort.reverse, values
289 assert_equal values.sort.reverse, values
290 end
290 end
291
291
292 def test_sort_by_float_custom_field_asc
292 def test_sort_by_float_custom_field_asc
293 q = Query.new
293 q = Query.new
294 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
294 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
295 assert c
295 assert c
296 assert c.sortable
296 assert c.sortable
297 issues = Issue.find :all,
297 issues = Issue.find :all,
298 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
298 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
299 :conditions => q.statement,
299 :conditions => q.statement,
300 :order => "#{c.sortable} ASC"
300 :order => "#{c.sortable} ASC"
301 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
301 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
302 assert !values.empty?
302 assert !values.empty?
303 assert_equal values.sort, values
303 assert_equal values.sort, values
304 end
304 end
305
305
306 def test_invalid_query_should_raise_query_statement_invalid_error
306 def test_invalid_query_should_raise_query_statement_invalid_error
307 q = Query.new
307 q = Query.new
308 assert_raise Query::StatementInvalid do
308 assert_raise Query::StatementInvalid do
309 q.issues(:conditions => "foo = 1")
309 q.issues(:conditions => "foo = 1")
310 end
310 end
311 end
311 end
312
312
313 def test_issue_count_by_association_group
313 def test_issue_count_by_association_group
314 q = Query.new(:name => '_', :group_by => 'assigned_to')
314 q = Query.new(:name => '_', :group_by => 'assigned_to')
315 count_by_group = q.issue_count_by_group
315 count_by_group = q.issue_count_by_group
316 assert_kind_of Hash, count_by_group
316 assert_kind_of Hash, count_by_group
317 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
317 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
318 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
318 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
319 assert count_by_group.has_key?(User.find(3))
319 assert count_by_group.has_key?(User.find(3))
320 end
320 end
321
321
322 def test_issue_count_by_list_custom_field_group
322 def test_issue_count_by_list_custom_field_group
323 q = Query.new(:name => '_', :group_by => 'cf_1')
323 q = Query.new(:name => '_', :group_by => 'cf_1')
324 count_by_group = q.issue_count_by_group
324 count_by_group = q.issue_count_by_group
325 assert_kind_of Hash, count_by_group
325 assert_kind_of Hash, count_by_group
326 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
326 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
327 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
327 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
328 assert count_by_group.has_key?('MySQL')
328 assert count_by_group.has_key?('MySQL')
329 end
329 end
330
330
331 def test_issue_count_by_date_custom_field_group
331 def test_issue_count_by_date_custom_field_group
332 q = Query.new(:name => '_', :group_by => 'cf_8')
332 q = Query.new(:name => '_', :group_by => 'cf_8')
333 count_by_group = q.issue_count_by_group
333 count_by_group = q.issue_count_by_group
334 assert_kind_of Hash, count_by_group
334 assert_kind_of Hash, count_by_group
335 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
335 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
336 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
336 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
337 end
337 end
338
338
339 def test_label_for
339 def test_label_for
340 q = Query.new
340 q = Query.new
341 assert_equal 'assigned_to', q.label_for('assigned_to_id')
341 assert_equal 'assigned_to', q.label_for('assigned_to_id')
342 end
342 end
343
343
344 def test_editable_by
344 def test_editable_by
345 admin = User.find(1)
345 admin = User.find(1)
346 manager = User.find(2)
346 manager = User.find(2)
347 developer = User.find(3)
347 developer = User.find(3)
348
348
349 # Public query on project 1
349 # Public query on project 1
350 q = Query.find(1)
350 q = Query.find(1)
351 assert q.editable_by?(admin)
351 assert q.editable_by?(admin)
352 assert q.editable_by?(manager)
352 assert q.editable_by?(manager)
353 assert !q.editable_by?(developer)
353 assert !q.editable_by?(developer)
354
354
355 # Private query on project 1
355 # Private query on project 1
356 q = Query.find(2)
356 q = Query.find(2)
357 assert q.editable_by?(admin)
357 assert q.editable_by?(admin)
358 assert !q.editable_by?(manager)
358 assert !q.editable_by?(manager)
359 assert q.editable_by?(developer)
359 assert q.editable_by?(developer)
360
360
361 # Private query for all projects
361 # Private query for all projects
362 q = Query.find(3)
362 q = Query.find(3)
363 assert q.editable_by?(admin)
363 assert q.editable_by?(admin)
364 assert !q.editable_by?(manager)
364 assert !q.editable_by?(manager)
365 assert q.editable_by?(developer)
365 assert q.editable_by?(developer)
366
366
367 # Public query for all projects
367 # Public query for all projects
368 q = Query.find(4)
368 q = Query.find(4)
369 assert q.editable_by?(admin)
369 assert q.editable_by?(admin)
370 assert !q.editable_by?(manager)
370 assert !q.editable_by?(manager)
371 assert !q.editable_by?(developer)
371 assert !q.editable_by?(developer)
372 end
372 end
373
373
374 context "#available_filters" do
374 context "#available_filters" do
375 setup do
375 setup do
376 @query = Query.new(:name => "_")
376 @query = Query.new(:name => "_")
377 end
377 end
378
378
379 should "include users of visible projects in cross-project view" do
379 should "include users of visible projects in cross-project view" do
380 users = @query.available_filters["assigned_to_id"]
380 users = @query.available_filters["assigned_to_id"]
381 assert_not_nil users
381 assert_not_nil users
382 assert users[:values].map{|u|u[1]}.include?("3")
382 assert users[:values].map{|u|u[1]}.include?("3")
383 end
383 end
384
384
385 context "'member_of_group' filter" do
385 context "'member_of_group' filter" do
386 should "be present" do
386 should "be present" do
387 assert @query.available_filters.keys.include?("member_of_group")
387 assert @query.available_filters.keys.include?("member_of_group")
388 end
388 end
389
389
390 should "be an optional list" do
390 should "be an optional list" do
391 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
391 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
392 end
392 end
393
393
394 should "have a list of the groups as values" do
394 should "have a list of the groups as values" do
395 Group.destroy_all # No fixtures
395 Group.destroy_all # No fixtures
396 group1 = Group.generate!.reload
396 group1 = Group.generate!.reload
397 group2 = Group.generate!.reload
397 group2 = Group.generate!.reload
398
398
399 expected_group_list = [
399 expected_group_list = [
400 [group1.name, group1.id],
400 [group1.name, group1.id],
401 [group2.name, group2.id]
401 [group2.name, group2.id]
402 ]
402 ]
403 assert_equal expected_group_list, @query.available_filters["member_of_group"][:values]
403 assert_equal expected_group_list, @query.available_filters["member_of_group"][:values]
404 end
404 end
405
405
406 end
406 end
407
407
408 context "'assigned_to_role' filter" do
409 should "be present" do
410 assert @query.available_filters.keys.include?("assigned_to_role")
411 end
412
413 should "be an optional list" do
414 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
415 end
416
417 should "have a list of the Roles as values" do
418 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager',1])
419 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer',2])
420 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter',3])
421 end
422
423 should "not include the built in Roles as values" do
424 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member',4])
425 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous',5])
426 end
427
428 end
429
408 end
430 end
409
431
410 context "#statement" do
432 context "#statement" do
411 context "with 'member_of_group' filter" do
433 context "with 'member_of_group' filter" do
412 setup do
434 setup do
413 Group.destroy_all # No fixtures
435 Group.destroy_all # No fixtures
414 @user_in_group = User.generate!
436 @user_in_group = User.generate!
415 @second_user_in_group = User.generate!
437 @second_user_in_group = User.generate!
416 @user_in_group2 = User.generate!
438 @user_in_group2 = User.generate!
417 @user_not_in_group = User.generate!
439 @user_not_in_group = User.generate!
418
440
419 @group = Group.generate!.reload
441 @group = Group.generate!.reload
420 @group.users << @user_in_group
442 @group.users << @user_in_group
421 @group.users << @second_user_in_group
443 @group.users << @second_user_in_group
422
444
423 @group2 = Group.generate!.reload
445 @group2 = Group.generate!.reload
424 @group2.users << @user_in_group2
446 @group2.users << @user_in_group2
425
447
426 end
448 end
427
449
428 should "search assigned to for users in the group" do
450 should "search assigned to for users in the group" do
429 @query = Query.new(:name => '_')
451 @query = Query.new(:name => '_')
430 @query.add_filter('member_of_group', '=', [@group.id.to_s])
452 @query.add_filter('member_of_group', '=', [@group.id.to_s])
431
453
432 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
454 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
433 assert_find_issues_with_query_is_successful @query
455 assert_find_issues_with_query_is_successful @query
434 end
456 end
435
457
436 should "search not assigned to any group member (none)" do
458 should "search not assigned to any group member (none)" do
437 @query = Query.new(:name => '_')
459 @query = Query.new(:name => '_')
438 @query.add_filter('member_of_group', '!*', [''])
460 @query.add_filter('member_of_group', '!*', [''])
439
461
440 # Users not in a group
462 # Users not in a group
441 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
463 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
442 assert_find_issues_with_query_is_successful @query
464 assert_find_issues_with_query_is_successful @query
443
465
444 end
466 end
445
467
446 should "search assigned to any group member (all)" do
468 should "search assigned to any group member (all)" do
447 @query = Query.new(:name => '_')
469 @query = Query.new(:name => '_')
448 @query.add_filter('member_of_group', '*', [''])
470 @query.add_filter('member_of_group', '*', [''])
449
471
450 # Only users in a group
472 # Only users in a group
451 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
473 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
452 assert_find_issues_with_query_is_successful @query
474 assert_find_issues_with_query_is_successful @query
453
475
454 end
476 end
455 end
477 end
478
479 context "with 'assigned_to_role' filter" do
480 setup do
481 # No fixtures
482 MemberRole.delete_all
483 Member.delete_all
484 Role.delete_all
485
486 @manager_role = Role.generate!(:name => 'Manager')
487 @developer_role = Role.generate!(:name => 'Developer')
488
489 @project = Project.generate!
490 @manager = User.generate!
491 @developer = User.generate!
492 @boss = User.generate!
493 User.add_to_project(@manager, @project, @manager_role)
494 User.add_to_project(@developer, @project, @developer_role)
495 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
496 end
497
498 should "search assigned to for users with the Role" do
499 @query = Query.new(:name => '_')
500 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
501
502 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
503 assert_find_issues_with_query_is_successful @query
504 end
505
506 should "search assigned to for users not assigned to any Role (none)" do
507 @query = Query.new(:name => '_')
508 @query.add_filter('assigned_to_role', '!*', [''])
509
510 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
511 assert_find_issues_with_query_is_successful @query
512 end
513
514 should "search assigned to for users assigned to any Role (all)" do
515 @query = Query.new(:name => '_')
516 @query.add_filter('assigned_to_role', '*', [''])
517
518 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
519 assert_find_issues_with_query_is_successful @query
520 end
521 end
456 end
522 end
457
523
458 end
524 end
General Comments 0
You need to be logged in to leave comments. Login now