##// END OF EJS Templates
Makes custom queries available through the REST API (#5737)....
Jean-Philippe Lang -
r6066:d48ea908761d
parent child
Show More
@@ -0,0 +1,10
1 api.array :queries, api_meta(:total_count => @query_count, :offset => @offset, :limit => @limit) do
2 @queries.each do |query|
3 api.query do
4 api.id query.id
5 api.name query.name
6 api.is_public query.is_public
7 api.project_id query.project_id
8 end
9 end
10 end
@@ -0,0 +1,55
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../test_helper', __FILE__)
19
20 class ApiTest::QueriesTest < ActionController::IntegrationTest
21 fixtures :all
22
23 def setup
24 Setting.rest_api_enabled = '1'
25 end
26
27 context "/queries" do
28 context "GET" do
29
30 should "return queries" do
31 get '/queries.xml'
32
33 assert_response :success
34 assert_equal 'application/xml', @response.content_type
35 assert_tag :tag => 'queries',
36 :attributes => {:type => 'array'},
37 :child => {
38 :tag => 'query',
39 :child => {
40 :tag => 'id',
41 :content => '4',
42 :sibling => {
43 :tag => 'name',
44 :content => 'Public query for all projects'
45 }
46 }
47 }
48 end
49 end
50 end
51
52 def credentials(user, password=nil)
53 ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)
54 end
55 end
@@ -1,80 +1,100
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 menu_item :issues
20 before_filter :find_query, :except => :new
20 before_filter :find_query, :except => [:new, :index]
21 21 before_filter :find_optional_project, :only => :new
22 22
23 accept_key_auth :index
24
25 def index
26 case params[:format]
27 when 'xml', 'json'
28 @offset, @limit = api_offset_and_limit
29 else
30 @limit = per_page_option
31 end
32
33 @query_count = Query.visible.count
34 @query_pages = Paginator.new self, @query_count, @limit, params['page']
35 @queries = Query.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
36
37 respond_to do |format|
38 format.html { render :nothing => true }
39 format.api
40 end
41 end
42
23 43 def new
24 44 @query = Query.new(params[:query])
25 45 @query.project = params[:query_is_for_all] ? nil : @project
26 46 @query.user = User.current
27 47 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
28 48
29 49 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f]
30 50 @query.group_by ||= params[:group_by]
31 51 @query.column_names = params[:c] if params[:c]
32 52 @query.column_names = nil if params[:default_columns]
33 53
34 54 if request.post? && params[:confirm] && @query.save
35 55 flash[:notice] = l(:notice_successful_create)
36 56 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
37 57 return
38 58 end
39 59 render :layout => false if request.xhr?
40 60 end
41 61
42 62 def edit
43 63 if request.post?
44 64 @query.filters = {}
45 65 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) if params[:fields] || params[:f]
46 66 @query.attributes = params[:query]
47 67 @query.project = nil if params[:query_is_for_all]
48 68 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
49 69 @query.group_by ||= params[:group_by]
50 70 @query.column_names = params[:c] if params[:c]
51 71 @query.column_names = nil if params[:default_columns]
52 72
53 73 if @query.save
54 74 flash[:notice] = l(:notice_successful_update)
55 75 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
56 76 end
57 77 end
58 78 end
59 79
60 80 def destroy
61 81 @query.destroy if request.post?
62 82 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
63 83 end
64 84
65 85 private
66 86 def find_query
67 87 @query = Query.find(params[:id])
68 88 @project = @query.project
69 89 render_403 unless @query.editable_by?(User.current)
70 90 rescue ActiveRecord::RecordNotFound
71 91 render_404
72 92 end
73 93
74 94 def find_optional_project
75 95 @project = Project.find(params[:project_id]) if params[:project_id]
76 96 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
77 97 rescue ActiveRecord::RecordNotFound
78 98 render_404
79 99 end
80 100 end
@@ -1,678 +1,688
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !sortable.nil?
40 40 end
41 41
42 42 def value(issue)
43 43 issue.send name
44 44 end
45 45
46 46 def css_classes
47 47 name
48 48 end
49 49 end
50 50
51 51 class QueryCustomFieldColumn < QueryColumn
52 52
53 53 def initialize(custom_field)
54 54 self.name = "cf_#{custom_field.id}".to_sym
55 55 self.sortable = custom_field.order_statement || false
56 56 if %w(list date bool int).include?(custom_field.field_format)
57 57 self.groupable = custom_field.order_statement
58 58 end
59 59 self.groupable ||= false
60 60 @cf = custom_field
61 61 end
62 62
63 63 def caption
64 64 @cf.name
65 65 end
66 66
67 67 def custom_field
68 68 @cf
69 69 end
70 70
71 71 def value(issue)
72 72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
73 73 cv && @cf.cast_value(cv.value)
74 74 end
75 75
76 76 def css_classes
77 77 @css_classes ||= "#{name} #{@cf.field_format}"
78 78 end
79 79 end
80 80
81 81 class Query < ActiveRecord::Base
82 82 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 83 end
84 84
85 85 belongs_to :project
86 86 belongs_to :user
87 87 serialize :filters
88 88 serialize :column_names
89 89 serialize :sort_criteria, Array
90 90
91 91 attr_protected :project_id, :user_id
92 92
93 93 validates_presence_of :name, :on => :save
94 94 validates_length_of :name, :maximum => 255
95 95
96 96 @@operators = { "=" => :label_equals,
97 97 "!" => :label_not_equals,
98 98 "o" => :label_open_issues,
99 99 "c" => :label_closed_issues,
100 100 "!*" => :label_none,
101 101 "*" => :label_all,
102 102 ">=" => :label_greater_or_equal,
103 103 "<=" => :label_less_or_equal,
104 104 "<t+" => :label_in_less_than,
105 105 ">t+" => :label_in_more_than,
106 106 "t+" => :label_in,
107 107 "t" => :label_today,
108 108 "w" => :label_this_week,
109 109 ">t-" => :label_less_than_ago,
110 110 "<t-" => :label_more_than_ago,
111 111 "t-" => :label_ago,
112 112 "~" => :label_contains,
113 113 "!~" => :label_not_contains }
114 114
115 115 cattr_reader :operators
116 116
117 117 @@operators_by_filter_type = { :list => [ "=", "!" ],
118 118 :list_status => [ "o", "=", "!", "c", "*" ],
119 119 :list_optional => [ "=", "!", "!*", "*" ],
120 120 :list_subprojects => [ "*", "!*", "=" ],
121 121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
122 122 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
123 123 :string => [ "=", "~", "!", "!~" ],
124 124 :text => [ "~", "!~" ],
125 125 :integer => [ "=", ">=", "<=", "!*", "*" ] }
126 126
127 127 cattr_reader :operators_by_filter_type
128 128
129 129 @@available_columns = [
130 130 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
131 131 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
132 132 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
133 133 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
134 134 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
135 135 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
136 136 QueryColumn.new(:author),
137 137 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
138 138 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
139 139 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
140 140 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
141 141 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
142 142 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
143 143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
144 144 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
145 145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
146 146 ]
147 147 cattr_reader :available_columns
148 148
149 named_scope :visible, lambda {|*args|
150 user = args.shift || User.current
151 base = Project.allowed_to_condition(user, :view_issues, *args)
152 user_id = user.logged? ? user.id : 0
153 {
154 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
155 :include => :project
156 }
157 }
158
149 159 def initialize(attributes = nil)
150 160 super attributes
151 161 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
152 162 end
153 163
154 164 def after_initialize
155 165 # Store the fact that project is nil (used in #editable_by?)
156 166 @is_for_all = project.nil?
157 167 end
158 168
159 169 def validate
160 170 filters.each_key do |field|
161 171 errors.add label_for(field), :blank unless
162 172 # filter requires one or more values
163 173 (values_for(field) and !values_for(field).first.blank?) or
164 174 # filter doesn't require any value
165 175 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
166 176 end if filters
167 177 end
168 178
169 179 # Returns true if the query is visible to +user+ or the current user.
170 180 def visible?(user=User.current)
171 self.is_public? || self.user_id == user.id
181 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
172 182 end
173 183
174 184 def editable_by?(user)
175 185 return false unless user
176 186 # Admin can edit them all and regular users can edit their private queries
177 187 return true if user.admin? || (!is_public && self.user_id == user.id)
178 188 # Members can not edit public queries that are for all project (only admin is allowed to)
179 189 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
180 190 end
181 191
182 192 def available_filters
183 193 return @available_filters if @available_filters
184 194
185 195 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
186 196
187 197 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
188 198 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
189 199 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
190 200 "subject" => { :type => :text, :order => 8 },
191 201 "created_on" => { :type => :date_past, :order => 9 },
192 202 "updated_on" => { :type => :date_past, :order => 10 },
193 203 "start_date" => { :type => :date, :order => 11 },
194 204 "due_date" => { :type => :date, :order => 12 },
195 205 "estimated_hours" => { :type => :integer, :order => 13 },
196 206 "done_ratio" => { :type => :integer, :order => 14 }}
197 207
198 208 user_values = []
199 209 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
200 210 if project
201 211 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
202 212 else
203 213 all_projects = Project.visible.all
204 214 if all_projects.any?
205 215 # members of visible projects
206 216 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
207 217
208 218 # project filter
209 219 project_values = []
210 220 Project.project_tree(all_projects) do |p, level|
211 221 prefix = (level > 0 ? ('--' * level + ' ') : '')
212 222 project_values << ["#{prefix}#{p.name}", p.id.to_s]
213 223 end
214 224 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
215 225 end
216 226 end
217 227 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
218 228 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
219 229
220 230 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
221 231 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
222 232
223 233 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
224 234 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
225 235
226 236 if User.current.logged?
227 237 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
228 238 end
229 239
230 240 if project
231 241 # project specific filters
232 242 categories = @project.issue_categories.all
233 243 unless categories.empty?
234 244 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
235 245 end
236 246 versions = @project.shared_versions.all
237 247 unless versions.empty?
238 248 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
239 249 end
240 250 unless @project.leaf?
241 251 subprojects = @project.descendants.visible.all
242 252 unless subprojects.empty?
243 253 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
244 254 end
245 255 end
246 256 add_custom_fields_filters(@project.all_issue_custom_fields)
247 257 else
248 258 # global filters for cross project issue list
249 259 system_shared_versions = Version.visible.find_all_by_sharing('system')
250 260 unless system_shared_versions.empty?
251 261 @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] } }
252 262 end
253 263 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
254 264 end
255 265 @available_filters
256 266 end
257 267
258 268 def add_filter(field, operator, values)
259 269 # values must be an array
260 270 return unless values and values.is_a? Array # and !values.first.empty?
261 271 # check if field is defined as an available filter
262 272 if available_filters.has_key? field
263 273 filter_options = available_filters[field]
264 274 # check if operator is allowed for that filter
265 275 #if @@operators_by_filter_type[filter_options[:type]].include? operator
266 276 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
267 277 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
268 278 #end
269 279 filters[field] = {:operator => operator, :values => values }
270 280 end
271 281 end
272 282
273 283 def add_short_filter(field, expression)
274 284 return unless expression
275 285 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
276 286 add_filter field, (parms[0] || "="), [parms[1] || ""]
277 287 end
278 288
279 289 # Add multiple filters using +add_filter+
280 290 def add_filters(fields, operators, values)
281 291 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
282 292 fields.each do |field|
283 293 add_filter(field, operators[field], values[field])
284 294 end
285 295 end
286 296 end
287 297
288 298 def has_filter?(field)
289 299 filters and filters[field]
290 300 end
291 301
292 302 def operator_for(field)
293 303 has_filter?(field) ? filters[field][:operator] : nil
294 304 end
295 305
296 306 def values_for(field)
297 307 has_filter?(field) ? filters[field][:values] : nil
298 308 end
299 309
300 310 def label_for(field)
301 311 label = available_filters[field][:name] if available_filters.has_key?(field)
302 312 label ||= field.gsub(/\_id$/, "")
303 313 end
304 314
305 315 def available_columns
306 316 return @available_columns if @available_columns
307 317 @available_columns = Query.available_columns
308 318 @available_columns += (project ?
309 319 project.all_issue_custom_fields :
310 320 IssueCustomField.find(:all)
311 321 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
312 322 end
313 323
314 324 def self.available_columns=(v)
315 325 self.available_columns = (v)
316 326 end
317 327
318 328 def self.add_available_column(column)
319 329 self.available_columns << (column) if column.is_a?(QueryColumn)
320 330 end
321 331
322 332 # Returns an array of columns that can be used to group the results
323 333 def groupable_columns
324 334 available_columns.select {|c| c.groupable}
325 335 end
326 336
327 337 # Returns a Hash of columns and the key for sorting
328 338 def sortable_columns
329 339 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
330 340 h[column.name.to_s] = column.sortable
331 341 h
332 342 })
333 343 end
334 344
335 345 def columns
336 346 if has_default_columns?
337 347 available_columns.select do |c|
338 348 # Adds the project column by default for cross-project lists
339 349 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
340 350 end
341 351 else
342 352 # preserve the column_names order
343 353 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
344 354 end
345 355 end
346 356
347 357 def column_names=(names)
348 358 if names
349 359 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
350 360 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
351 361 # Set column_names to nil if default columns
352 362 if names.map(&:to_s) == Setting.issue_list_default_columns
353 363 names = nil
354 364 end
355 365 end
356 366 write_attribute(:column_names, names)
357 367 end
358 368
359 369 def has_column?(column)
360 370 column_names && column_names.include?(column.name)
361 371 end
362 372
363 373 def has_default_columns?
364 374 column_names.nil? || column_names.empty?
365 375 end
366 376
367 377 def sort_criteria=(arg)
368 378 c = []
369 379 if arg.is_a?(Hash)
370 380 arg = arg.keys.sort.collect {|k| arg[k]}
371 381 end
372 382 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
373 383 write_attribute(:sort_criteria, c)
374 384 end
375 385
376 386 def sort_criteria
377 387 read_attribute(:sort_criteria) || []
378 388 end
379 389
380 390 def sort_criteria_key(arg)
381 391 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
382 392 end
383 393
384 394 def sort_criteria_order(arg)
385 395 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
386 396 end
387 397
388 398 # Returns the SQL sort order that should be prepended for grouping
389 399 def group_by_sort_order
390 400 if grouped? && (column = group_by_column)
391 401 column.sortable.is_a?(Array) ?
392 402 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
393 403 "#{column.sortable} #{column.default_order}"
394 404 end
395 405 end
396 406
397 407 # Returns true if the query is a grouped query
398 408 def grouped?
399 409 !group_by_column.nil?
400 410 end
401 411
402 412 def group_by_column
403 413 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
404 414 end
405 415
406 416 def group_by_statement
407 417 group_by_column.try(:groupable)
408 418 end
409 419
410 420 def project_statement
411 421 project_clauses = []
412 422 if project && !@project.descendants.active.empty?
413 423 ids = [project.id]
414 424 if has_filter?("subproject_id")
415 425 case operator_for("subproject_id")
416 426 when '='
417 427 # include the selected subprojects
418 428 ids += values_for("subproject_id").each(&:to_i)
419 429 when '!*'
420 430 # main project only
421 431 else
422 432 # all subprojects
423 433 ids += project.descendants.collect(&:id)
424 434 end
425 435 elsif Setting.display_subprojects_issues?
426 436 ids += project.descendants.collect(&:id)
427 437 end
428 438 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
429 439 elsif project
430 440 project_clauses << "#{Project.table_name}.id = %d" % project.id
431 441 end
432 442 project_clauses.any? ? project_clauses.join(' AND ') : nil
433 443 end
434 444
435 445 def statement
436 446 # filters clauses
437 447 filters_clauses = []
438 448 filters.each_key do |field|
439 449 next if field == "subproject_id"
440 450 v = values_for(field).clone
441 451 next unless v and !v.empty?
442 452 operator = operator_for(field)
443 453
444 454 # "me" value subsitution
445 455 if %w(assigned_to_id author_id watcher_id).include?(field)
446 456 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
447 457 end
448 458
449 459 sql = ''
450 460 if field =~ /^cf_(\d+)$/
451 461 # custom field
452 462 db_table = CustomValue.table_name
453 463 db_field = 'value'
454 464 is_custom_filter = true
455 465 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 "
456 466 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
457 467 elsif field == 'watcher_id'
458 468 db_table = Watcher.table_name
459 469 db_field = 'user_id'
460 470 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
461 471 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
462 472 elsif field == "member_of_group" # named field
463 473 if operator == '*' # Any group
464 474 groups = Group.all
465 475 operator = '=' # Override the operator since we want to find by assigned_to
466 476 elsif operator == "!*"
467 477 groups = Group.all
468 478 operator = '!' # Override the operator since we want to find by assigned_to
469 479 else
470 480 groups = Group.find_all_by_id(v)
471 481 end
472 482 groups ||= []
473 483
474 484 members_of_groups = groups.inject([]) {|user_ids, group|
475 485 if group && group.user_ids.present?
476 486 user_ids << group.user_ids
477 487 end
478 488 user_ids.flatten.uniq.compact
479 489 }.sort.collect(&:to_s)
480 490
481 491 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
482 492
483 493 elsif field == "assigned_to_role" # named field
484 494 if operator == "*" # Any Role
485 495 roles = Role.givable
486 496 operator = '=' # Override the operator since we want to find by assigned_to
487 497 elsif operator == "!*" # No role
488 498 roles = Role.givable
489 499 operator = '!' # Override the operator since we want to find by assigned_to
490 500 else
491 501 roles = Role.givable.find_all_by_id(v)
492 502 end
493 503 roles ||= []
494 504
495 505 members_of_roles = roles.inject([]) {|user_ids, role|
496 506 if role && role.members
497 507 user_ids << role.members.collect(&:user_id)
498 508 end
499 509 user_ids.flatten.uniq.compact
500 510 }.sort.collect(&:to_s)
501 511
502 512 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
503 513 else
504 514 # regular field
505 515 db_table = Issue.table_name
506 516 db_field = field
507 517 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
508 518 end
509 519 filters_clauses << sql
510 520
511 521 end if filters and valid?
512 522
513 523 filters_clauses << project_statement
514 524 filters_clauses.reject!(&:blank?)
515 525
516 526 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
517 527 end
518 528
519 529 # Returns the issue count
520 530 def issue_count
521 531 Issue.count(:include => [:status, :project], :conditions => statement)
522 532 rescue ::ActiveRecord::StatementInvalid => e
523 533 raise StatementInvalid.new(e.message)
524 534 end
525 535
526 536 # Returns the issue count by group or nil if query is not grouped
527 537 def issue_count_by_group
528 538 r = nil
529 539 if grouped?
530 540 begin
531 541 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
532 542 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
533 543 rescue ActiveRecord::RecordNotFound
534 544 r = {nil => issue_count}
535 545 end
536 546 c = group_by_column
537 547 if c.is_a?(QueryCustomFieldColumn)
538 548 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
539 549 end
540 550 end
541 551 r
542 552 rescue ::ActiveRecord::StatementInvalid => e
543 553 raise StatementInvalid.new(e.message)
544 554 end
545 555
546 556 # Returns the issues
547 557 # Valid options are :order, :offset, :limit, :include, :conditions
548 558 def issues(options={})
549 559 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
550 560 order_option = nil if order_option.blank?
551 561
552 562 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
553 563 :conditions => Query.merge_conditions(statement, options[:conditions]),
554 564 :order => order_option,
555 565 :limit => options[:limit],
556 566 :offset => options[:offset]
557 567 rescue ::ActiveRecord::StatementInvalid => e
558 568 raise StatementInvalid.new(e.message)
559 569 end
560 570
561 571 # Returns the journals
562 572 # Valid options are :order, :offset, :limit
563 573 def journals(options={})
564 574 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
565 575 :conditions => statement,
566 576 :order => options[:order],
567 577 :limit => options[:limit],
568 578 :offset => options[:offset]
569 579 rescue ::ActiveRecord::StatementInvalid => e
570 580 raise StatementInvalid.new(e.message)
571 581 end
572 582
573 583 # Returns the versions
574 584 # Valid options are :conditions
575 585 def versions(options={})
576 586 Version.visible.find :all, :include => :project,
577 587 :conditions => Query.merge_conditions(project_statement, options[:conditions])
578 588 rescue ::ActiveRecord::StatementInvalid => e
579 589 raise StatementInvalid.new(e.message)
580 590 end
581 591
582 592 private
583 593
584 594 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
585 595 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
586 596 sql = ''
587 597 case operator
588 598 when "="
589 599 if value.any?
590 600 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
591 601 else
592 602 # IN an empty set
593 603 sql = "1=0"
594 604 end
595 605 when "!"
596 606 if value.any?
597 607 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
598 608 else
599 609 # NOT IN an empty set
600 610 sql = "1=1"
601 611 end
602 612 when "!*"
603 613 sql = "#{db_table}.#{db_field} IS NULL"
604 614 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
605 615 when "*"
606 616 sql = "#{db_table}.#{db_field} IS NOT NULL"
607 617 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
608 618 when ">="
609 619 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
610 620 when "<="
611 621 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
612 622 when "o"
613 623 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
614 624 when "c"
615 625 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
616 626 when ">t-"
617 627 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
618 628 when "<t-"
619 629 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
620 630 when "t-"
621 631 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
622 632 when ">t+"
623 633 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
624 634 when "<t+"
625 635 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
626 636 when "t+"
627 637 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
628 638 when "t"
629 639 sql = date_range_clause(db_table, db_field, 0, 0)
630 640 when "w"
631 641 first_day_of_week = l(:general_first_day_of_week).to_i
632 642 day_of_week = Date.today.cwday
633 643 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
634 644 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
635 645 when "~"
636 646 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
637 647 when "!~"
638 648 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
639 649 end
640 650
641 651 return sql
642 652 end
643 653
644 654 def add_custom_fields_filters(custom_fields)
645 655 @available_filters ||= {}
646 656
647 657 custom_fields.select(&:is_filter?).each do |field|
648 658 case field.field_format
649 659 when "text"
650 660 options = { :type => :text, :order => 20 }
651 661 when "list"
652 662 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
653 663 when "date"
654 664 options = { :type => :date, :order => 20 }
655 665 when "bool"
656 666 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
657 667 when "user", "version"
658 668 next unless project
659 669 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
660 670 else
661 671 options = { :type => :string, :order => 20 }
662 672 end
663 673 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
664 674 end
665 675 end
666 676
667 677 # Returns a SQL clause for a date or datetime field.
668 678 def date_range_clause(table, field, from, to)
669 679 s = []
670 680 if from
671 681 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
672 682 end
673 683 if to
674 684 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
675 685 end
676 686 s.join(' AND ')
677 687 end
678 688 end
@@ -1,255 +1,256
1 1 ActionController::Routing::Routes.draw do |map|
2 2 # Add your own custom routes here.
3 3 # The priority is based upon order of creation: first created -> highest priority.
4 4
5 5 # Here's a sample route:
6 6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7 7 # Keep in mind you can assign values other than :controller and :action
8 8
9 9 map.home '', :controller => 'welcome'
10 10
11 11 map.signin 'login', :controller => 'account', :action => 'login'
12 12 map.signout 'logout', :controller => 'account', :action => 'logout'
13 13
14 14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
15 15 map.connect 'help/:ctrl/:page', :controller => 'help'
16 16
17 17 map.with_options :controller => 'time_entry_reports', :action => 'report',:conditions => {:method => :get} do |time_report|
18 18 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report'
19 19 time_report.connect 'projects/:project_id/issues/:issue_id/time_entries/report.:format'
20 20 time_report.connect 'projects/:project_id/time_entries/report'
21 21 time_report.connect 'projects/:project_id/time_entries/report.:format'
22 22 time_report.connect 'time_entries/report'
23 23 time_report.connect 'time_entries/report.:format'
24 24 end
25 25
26 26 map.bulk_edit_time_entry 'time_entries/bulk_edit',
27 27 :controller => 'timelog', :action => 'bulk_edit', :conditions => { :method => :get }
28 28 map.bulk_update_time_entry 'time_entries/bulk_edit',
29 29 :controller => 'timelog', :action => 'bulk_update', :conditions => { :method => :post }
30 30 map.time_entries_context_menu '/time_entries/context_menu',
31 31 :controller => 'context_menus', :action => 'time_entries'
32 32 # TODO: wasteful since this is also nested under issues, projects, and projects/issues
33 33 map.resources :time_entries, :controller => 'timelog'
34 34
35 35 map.connect 'projects/:id/wiki', :controller => 'wikis', :action => 'edit', :conditions => {:method => :post}
36 36 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :get}
37 37 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis', :action => 'destroy', :conditions => {:method => :post}
38 38
39 39 map.with_options :controller => 'messages' do |messages_routes|
40 40 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
41 41 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
42 42 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
43 43 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
44 44 end
45 45 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
46 46 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
47 47 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
48 48 messages_actions.connect 'boards/:board_id/topics/:id/:action', :action => /edit|destroy/
49 49 end
50 50 end
51 51
52 52 map.with_options :controller => 'boards' do |board_routes|
53 53 board_routes.with_options :conditions => {:method => :get} do |board_views|
54 54 board_views.connect 'projects/:project_id/boards', :action => 'index'
55 55 board_views.connect 'projects/:project_id/boards/new', :action => 'new'
56 56 board_views.connect 'projects/:project_id/boards/:id', :action => 'show'
57 57 board_views.connect 'projects/:project_id/boards/:id.:format', :action => 'show'
58 58 board_views.connect 'projects/:project_id/boards/:id/edit', :action => 'edit'
59 59 end
60 60 board_routes.with_options :conditions => {:method => :post} do |board_actions|
61 61 board_actions.connect 'projects/:project_id/boards', :action => 'new'
62 62 board_actions.connect 'projects/:project_id/boards/:id/:action', :action => /edit|destroy/
63 63 end
64 64 end
65 65
66 66 map.with_options :controller => 'documents' do |document_routes|
67 67 document_routes.with_options :conditions => {:method => :get} do |document_views|
68 68 document_views.connect 'projects/:project_id/documents', :action => 'index'
69 69 document_views.connect 'projects/:project_id/documents/new', :action => 'new'
70 70 document_views.connect 'documents/:id', :action => 'show'
71 71 document_views.connect 'documents/:id/edit', :action => 'edit'
72 72 end
73 73 document_routes.with_options :conditions => {:method => :post} do |document_actions|
74 74 document_actions.connect 'projects/:project_id/documents', :action => 'new'
75 75 document_actions.connect 'documents/:id/:action', :action => /destroy|edit/
76 76 end
77 77 end
78 78
79 79 map.resources :issue_moves, :only => [:new, :create], :path_prefix => '/issues', :as => 'move'
80 map.resources :queries, :only => [:index]
80 81
81 82 # Misc issue routes. TODO: move into resources
82 83 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes', :action => 'issues'
83 84 map.preview_issue '/issues/preview/:id', :controller => 'previews', :action => 'issue' # TODO: would look nicer as /issues/:id/preview
84 85 map.issues_context_menu '/issues/context_menu', :controller => 'context_menus', :action => 'issues'
85 86 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
86 87 map.bulk_edit_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_edit', :conditions => { :method => :get }
87 88 map.bulk_update_issue 'issues/bulk_edit', :controller => 'issues', :action => 'bulk_update', :conditions => { :method => :post }
88 89 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new', :id => /\d+/, :conditions => { :method => :post }
89 90 map.connect '/issues/:id/destroy', :controller => 'issues', :action => 'destroy', :conditions => { :method => :post } # legacy
90 91
91 92 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
92 93 gantts_routes.connect '/projects/:project_id/issues/gantt'
93 94 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
94 95 gantts_routes.connect '/issues/gantt.:format'
95 96 end
96 97
97 98 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
98 99 calendars_routes.connect '/projects/:project_id/issues/calendar'
99 100 calendars_routes.connect '/issues/calendar'
100 101 end
101 102
102 103 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
103 104 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
104 105 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
105 106 end
106 107
107 108 # Following two routes conflict with the resources because #index allows POST
108 109 map.connect '/issues', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
109 110 map.connect '/issues/create', :controller => 'issues', :action => 'index', :conditions => { :method => :post }
110 111
111 112 map.resources :issues, :member => { :edit => :post }, :collection => {} do |issues|
112 113 issues.resources :time_entries, :controller => 'timelog'
113 114 issues.resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
114 115 end
115 116
116 117 map.resources :issues, :path_prefix => '/projects/:project_id', :collection => { :create => :post } do |issues|
117 118 issues.resources :time_entries, :controller => 'timelog'
118 119 end
119 120
120 121 map.connect 'projects/:id/members/new', :controller => 'members', :action => 'new'
121 122
122 123 map.with_options :controller => 'users' do |users|
123 124 users.connect 'users/:id/edit/:tab', :action => 'edit', :tab => nil, :conditions => {:method => :get}
124 125
125 126 users.with_options :conditions => {:method => :post} do |user_actions|
126 127 user_actions.connect 'users/:id/memberships', :action => 'edit_membership'
127 128 user_actions.connect 'users/:id/memberships/:membership_id', :action => 'edit_membership'
128 129 user_actions.connect 'users/:id/memberships/:membership_id/destroy', :action => 'destroy_membership'
129 130 end
130 131 end
131 132
132 133 map.resources :users, :member => {
133 134 :edit_membership => :post,
134 135 :destroy_membership => :post
135 136 }
136 137
137 138 # For nice "roadmap" in the url for the index action
138 139 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
139 140
140 141 map.all_news 'news', :controller => 'news', :action => 'index'
141 142 map.formatted_all_news 'news.:format', :controller => 'news', :action => 'index'
142 143 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
143 144 map.connect 'news/:id/comments', :controller => 'comments', :action => 'create', :conditions => {:method => :post}
144 145 map.connect 'news/:id/comments/:comment_id', :controller => 'comments', :action => 'destroy', :conditions => {:method => :delete}
145 146
146 147 map.resources :projects, :member => {
147 148 :copy => [:get, :post],
148 149 :settings => :get,
149 150 :modules => :post,
150 151 :archive => :post,
151 152 :unarchive => :post
152 153 } do |project|
153 154 project.resource :project_enumerations, :as => 'enumerations', :only => [:update, :destroy]
154 155 project.resources :files, :only => [:index, :new, :create]
155 156 project.resources :versions, :shallow => true, :collection => {:close_completed => :put}, :member => {:status_by => :post}
156 157 project.resources :news, :shallow => true
157 158 project.resources :time_entries, :controller => 'timelog', :path_prefix => 'projects/:project_id'
158 159
159 160 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
160 161 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
161 162 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
162 163 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
163 164 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
164 165 project.resources :wiki, :except => [:new, :create], :member => {
165 166 :rename => [:get, :post],
166 167 :history => :get,
167 168 :preview => :any,
168 169 :protect => :post,
169 170 :add_attachment => :post
170 171 }, :collection => {
171 172 :export => :get,
172 173 :date_index => :get
173 174 }
174 175
175 176 end
176 177
177 178 # Destroy uses a get request to prompt the user before the actual DELETE request
178 179 map.project_destroy_confirm 'projects/:id/destroy', :controller => 'projects', :action => 'destroy', :conditions => {:method => :get}
179 180
180 181 # TODO: port to be part of the resources route(s)
181 182 map.with_options :controller => 'projects' do |project_mapper|
182 183 project_mapper.with_options :conditions => {:method => :get} do |project_views|
183 184 project_views.connect 'projects/:id/settings/:tab', :controller => 'projects', :action => 'settings'
184 185 project_views.connect 'projects/:project_id/issues/:copy_from/copy', :controller => 'issues', :action => 'new'
185 186 end
186 187 end
187 188
188 189 map.with_options :controller => 'activities', :action => 'index', :conditions => {:method => :get} do |activity|
189 190 activity.connect 'projects/:id/activity'
190 191 activity.connect 'projects/:id/activity.:format'
191 192 activity.connect 'activity', :id => nil
192 193 activity.connect 'activity.:format', :id => nil
193 194 end
194 195
195 196
196 197 map.with_options :controller => 'issue_categories' do |categories|
197 198 categories.connect 'projects/:project_id/issue_categories/new', :action => 'new'
198 199 end
199 200
200 201 map.with_options :controller => 'repositories' do |repositories|
201 202 repositories.with_options :conditions => {:method => :get} do |repository_views|
202 203 repository_views.connect 'projects/:id/repository', :action => 'show'
203 204 repository_views.connect 'projects/:id/repository/edit', :action => 'edit'
204 205 repository_views.connect 'projects/:id/repository/statistics', :action => 'stats'
205 206 repository_views.connect 'projects/:id/repository/revisions', :action => 'revisions'
206 207 repository_views.connect 'projects/:id/repository/revisions.:format', :action => 'revisions'
207 208 repository_views.connect 'projects/:id/repository/revisions/:rev', :action => 'revision'
208 209 repository_views.connect 'projects/:id/repository/revisions/:rev/diff', :action => 'diff'
209 210 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format', :action => 'diff'
210 211 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path', :action => 'entry', :format => 'raw', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
211 212 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path', :requirements => { :rev => /[a-z0-9\.\-_]+/ }
212 213 repository_views.connect 'projects/:id/repository/raw/*path', :action => 'entry', :format => 'raw'
213 214 # TODO: why the following route is required?
214 215 repository_views.connect 'projects/:id/repository/entry/*path', :action => 'entry'
215 216 repository_views.connect 'projects/:id/repository/:action/*path'
216 217 end
217 218
218 219 repositories.connect 'projects/:id/repository/:action', :conditions => {:method => :post}
219 220 end
220 221
221 222 map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
222 223 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
223 224 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
224 225
225 226 map.resources :groups
226 227
227 228 #left old routes at the bottom for backwards compat
228 229 map.connect 'projects/:project_id/queries/:action', :controller => 'queries'
229 230 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
230 231 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
231 232 map.connect 'projects/:project_id/boards/:action/:id', :controller => 'boards'
232 233 map.connect 'boards/:board_id/topics/:action/:id', :controller => 'messages'
233 234 map.connect 'wiki/:id/:page/:action', :page => nil, :controller => 'wiki'
234 235 map.connect 'projects/:project_id/news/:action', :controller => 'news'
235 236 map.connect 'projects/:project_id/timelog/:action/:id', :controller => 'timelog', :project_id => /.+/
236 237 map.with_options :controller => 'repositories' do |omap|
237 238 omap.repositories_show 'repositories/browse/:id/*path', :action => 'browse'
238 239 omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
239 240 omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
240 241 omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
241 242 omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
242 243 omap.connect 'repositories/revision/:id/:rev', :action => 'revision'
243 244 end
244 245
245 246 map.with_options :controller => 'sys' do |sys|
246 247 sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get}
247 248 sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post}
248 249 end
249 250
250 251 # Install the default route as the lowest priority.
251 252 map.connect ':controller/:action/:id'
252 253 map.connect 'robots.txt', :controller => 'welcome', :action => 'robots'
253 254 # Used for OpenID
254 255 map.root :controller => 'account', :action => 'login'
255 256 end
@@ -1,392 +1,395
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RoutingTest < ActionController::IntegrationTest
21 21 context "activities" do
22 22 should_route :get, "/activity", :controller => 'activities', :action => 'index', :id => nil
23 23 should_route :get, "/activity.atom", :controller => 'activities', :action => 'index', :id => nil, :format => 'atom'
24 24 end
25 25
26 26 context "attachments" do
27 27 should_route :get, "/attachments/1", :controller => 'attachments', :action => 'show', :id => '1'
28 28 should_route :get, "/attachments/1/filename.ext", :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'
29 29 should_route :get, "/attachments/download/1", :controller => 'attachments', :action => 'download', :id => '1'
30 30 should_route :get, "/attachments/download/1/filename.ext", :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'
31 31 end
32 32
33 33 context "boards" do
34 34 should_route :get, "/projects/world_domination/boards", :controller => 'boards', :action => 'index', :project_id => 'world_domination'
35 35 should_route :get, "/projects/world_domination/boards/new", :controller => 'boards', :action => 'new', :project_id => 'world_domination'
36 36 should_route :get, "/projects/world_domination/boards/44", :controller => 'boards', :action => 'show', :project_id => 'world_domination', :id => '44'
37 37 should_route :get, "/projects/world_domination/boards/44.atom", :controller => 'boards', :action => 'show', :project_id => 'world_domination', :id => '44', :format => 'atom'
38 38 should_route :get, "/projects/world_domination/boards/44/edit", :controller => 'boards', :action => 'edit', :project_id => 'world_domination', :id => '44'
39 39
40 40 should_route :post, "/projects/world_domination/boards/new", :controller => 'boards', :action => 'new', :project_id => 'world_domination'
41 41 should_route :post, "/projects/world_domination/boards/44/edit", :controller => 'boards', :action => 'edit', :project_id => 'world_domination', :id => '44'
42 42 should_route :post, "/projects/world_domination/boards/44/destroy", :controller => 'boards', :action => 'destroy', :project_id => 'world_domination', :id => '44'
43 43
44 44 end
45 45
46 46 context "documents" do
47 47 should_route :get, "/projects/567/documents", :controller => 'documents', :action => 'index', :project_id => '567'
48 48 should_route :get, "/projects/567/documents/new", :controller => 'documents', :action => 'new', :project_id => '567'
49 49 should_route :get, "/documents/22", :controller => 'documents', :action => 'show', :id => '22'
50 50 should_route :get, "/documents/22/edit", :controller => 'documents', :action => 'edit', :id => '22'
51 51
52 52 should_route :post, "/projects/567/documents/new", :controller => 'documents', :action => 'new', :project_id => '567'
53 53 should_route :post, "/documents/567/edit", :controller => 'documents', :action => 'edit', :id => '567'
54 54 should_route :post, "/documents/567/destroy", :controller => 'documents', :action => 'destroy', :id => '567'
55 55 end
56 56
57 57 context "issues" do
58 58 # REST actions
59 59 should_route :get, "/issues", :controller => 'issues', :action => 'index'
60 60 should_route :get, "/issues.pdf", :controller => 'issues', :action => 'index', :format => 'pdf'
61 61 should_route :get, "/issues.atom", :controller => 'issues', :action => 'index', :format => 'atom'
62 62 should_route :get, "/issues.xml", :controller => 'issues', :action => 'index', :format => 'xml'
63 63 should_route :get, "/projects/23/issues", :controller => 'issues', :action => 'index', :project_id => '23'
64 64 should_route :get, "/projects/23/issues.pdf", :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
65 65 should_route :get, "/projects/23/issues.atom", :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
66 66 should_route :get, "/projects/23/issues.xml", :controller => 'issues', :action => 'index', :project_id => '23', :format => 'xml'
67 67 should_route :get, "/issues/64", :controller => 'issues', :action => 'show', :id => '64'
68 68 should_route :get, "/issues/64.pdf", :controller => 'issues', :action => 'show', :id => '64', :format => 'pdf'
69 69 should_route :get, "/issues/64.atom", :controller => 'issues', :action => 'show', :id => '64', :format => 'atom'
70 70 should_route :get, "/issues/64.xml", :controller => 'issues', :action => 'show', :id => '64', :format => 'xml'
71 71
72 72 should_route :get, "/projects/23/issues/new", :controller => 'issues', :action => 'new', :project_id => '23'
73 73 should_route :post, "/projects/23/issues", :controller => 'issues', :action => 'create', :project_id => '23'
74 74 should_route :post, "/issues.xml", :controller => 'issues', :action => 'create', :format => 'xml'
75 75
76 76 should_route :get, "/issues/64/edit", :controller => 'issues', :action => 'edit', :id => '64'
77 77 # TODO: Should use PUT
78 78 should_route :post, "/issues/64/edit", :controller => 'issues', :action => 'edit', :id => '64'
79 79 should_route :put, "/issues/1.xml", :controller => 'issues', :action => 'update', :id => '1', :format => 'xml'
80 80
81 81 # TODO: Should use DELETE
82 82 should_route :post, "/issues/64/destroy", :controller => 'issues', :action => 'destroy', :id => '64'
83 83 should_route :delete, "/issues/1.xml", :controller => 'issues', :action => 'destroy', :id => '1', :format => 'xml'
84 84
85 85 # Extra actions
86 86 should_route :get, "/projects/23/issues/64/copy", :controller => 'issues', :action => 'new', :project_id => '23', :copy_from => '64'
87 87
88 88 should_route :get, "/issues/move/new", :controller => 'issue_moves', :action => 'new'
89 89 should_route :post, "/issues/move", :controller => 'issue_moves', :action => 'create'
90 90
91 91 should_route :post, "/issues/1/quoted", :controller => 'journals', :action => 'new', :id => '1'
92 92
93 93 should_route :get, "/issues/calendar", :controller => 'calendars', :action => 'show'
94 94 should_route :get, "/projects/project-name/issues/calendar", :controller => 'calendars', :action => 'show', :project_id => 'project-name'
95 95
96 96 should_route :get, "/issues/gantt", :controller => 'gantts', :action => 'show'
97 97 should_route :get, "/issues/gantt.pdf", :controller => 'gantts', :action => 'show', :format => 'pdf'
98 98 should_route :get, "/projects/project-name/issues/gantt", :controller => 'gantts', :action => 'show', :project_id => 'project-name'
99 99 should_route :get, "/projects/project-name/issues/gantt.pdf", :controller => 'gantts', :action => 'show', :project_id => 'project-name', :format => 'pdf'
100 100
101 101 should_route :get, "/issues/auto_complete", :controller => 'auto_completes', :action => 'issues'
102 102
103 103 should_route :get, "/issues/preview/123", :controller => 'previews', :action => 'issue', :id => '123'
104 104 should_route :post, "/issues/preview/123", :controller => 'previews', :action => 'issue', :id => '123'
105 105 should_route :get, "/issues/context_menu", :controller => 'context_menus', :action => 'issues'
106 106 should_route :post, "/issues/context_menu", :controller => 'context_menus', :action => 'issues'
107 107
108 108 should_route :get, "/issues/changes", :controller => 'journals', :action => 'index'
109 109
110 110 should_route :get, "/issues/bulk_edit", :controller => 'issues', :action => 'bulk_edit'
111 111 should_route :post, "/issues/bulk_edit", :controller => 'issues', :action => 'bulk_update'
112 112 end
113 113
114 114 context "issue categories" do
115 115 should_route :get, "/projects/test/issue_categories/new", :controller => 'issue_categories', :action => 'new', :project_id => 'test'
116 116
117 117 should_route :post, "/projects/test/issue_categories/new", :controller => 'issue_categories', :action => 'new', :project_id => 'test'
118 118 end
119 119
120 120 context "issue relations" do
121 121 should_route :get, "/issues/1/relations", :controller => 'issue_relations', :action => 'index', :issue_id => '1'
122 122 should_route :get, "/issues/1/relations.xml", :controller => 'issue_relations', :action => 'index', :issue_id => '1', :format => 'xml'
123 123 should_route :get, "/issues/1/relations.json", :controller => 'issue_relations', :action => 'index', :issue_id => '1', :format => 'json'
124 124
125 125 should_route :post, "/issues/1/relations", :controller => 'issue_relations', :action => 'create', :issue_id => '1'
126 126 should_route :post, "/issues/1/relations.xml", :controller => 'issue_relations', :action => 'create', :issue_id => '1', :format => 'xml'
127 127 should_route :post, "/issues/1/relations.json", :controller => 'issue_relations', :action => 'create', :issue_id => '1', :format => 'json'
128 128
129 129 should_route :get, "/relations/23", :controller => 'issue_relations', :action => 'show', :id => '23'
130 130 should_route :get, "/relations/23.xml", :controller => 'issue_relations', :action => 'show', :id => '23', :format => 'xml'
131 131 should_route :get, "/relations/23.json", :controller => 'issue_relations', :action => 'show', :id => '23', :format => 'json'
132 132
133 133 should_route :delete, "/relations/23", :controller => 'issue_relations', :action => 'destroy', :id => '23'
134 134 should_route :delete, "/relations/23.xml", :controller => 'issue_relations', :action => 'destroy', :id => '23', :format => 'xml'
135 135 should_route :delete, "/relations/23.json", :controller => 'issue_relations', :action => 'destroy', :id => '23', :format => 'json'
136 136 end
137 137
138 138 context "issue reports" do
139 139 should_route :get, "/projects/567/issues/report", :controller => 'reports', :action => 'issue_report', :id => '567'
140 140 should_route :get, "/projects/567/issues/report/assigned_to", :controller => 'reports', :action => 'issue_report_details', :id => '567', :detail => 'assigned_to'
141 141 end
142 142
143 143 context "members" do
144 144 should_route :post, "/projects/5234/members/new", :controller => 'members', :action => 'new', :id => '5234'
145 145 end
146 146
147 147 context "messages" do
148 148 should_route :get, "/boards/22/topics/2", :controller => 'messages', :action => 'show', :id => '2', :board_id => '22'
149 149 should_route :get, "/boards/lala/topics/new", :controller => 'messages', :action => 'new', :board_id => 'lala'
150 150 should_route :get, "/boards/lala/topics/22/edit", :controller => 'messages', :action => 'edit', :id => '22', :board_id => 'lala'
151 151
152 152 should_route :post, "/boards/lala/topics/new", :controller => 'messages', :action => 'new', :board_id => 'lala'
153 153 should_route :post, "/boards/lala/topics/22/edit", :controller => 'messages', :action => 'edit', :id => '22', :board_id => 'lala'
154 154 should_route :post, "/boards/22/topics/555/replies", :controller => 'messages', :action => 'reply', :id => '555', :board_id => '22'
155 155 should_route :post, "/boards/22/topics/555/destroy", :controller => 'messages', :action => 'destroy', :id => '555', :board_id => '22'
156 156 end
157 157
158 158 context "news" do
159 159 should_route :get, "/news", :controller => 'news', :action => 'index'
160 160 should_route :get, "/news.atom", :controller => 'news', :action => 'index', :format => 'atom'
161 161 should_route :get, "/news.xml", :controller => 'news', :action => 'index', :format => 'xml'
162 162 should_route :get, "/news.json", :controller => 'news', :action => 'index', :format => 'json'
163 163 should_route :get, "/projects/567/news", :controller => 'news', :action => 'index', :project_id => '567'
164 164 should_route :get, "/projects/567/news.atom", :controller => 'news', :action => 'index', :format => 'atom', :project_id => '567'
165 165 should_route :get, "/projects/567/news.xml", :controller => 'news', :action => 'index', :format => 'xml', :project_id => '567'
166 166 should_route :get, "/projects/567/news.json", :controller => 'news', :action => 'index', :format => 'json', :project_id => '567'
167 167 should_route :get, "/news/2", :controller => 'news', :action => 'show', :id => '2'
168 168 should_route :get, "/projects/567/news/new", :controller => 'news', :action => 'new', :project_id => '567'
169 169 should_route :get, "/news/234", :controller => 'news', :action => 'show', :id => '234'
170 170 should_route :get, "/news/567/edit", :controller => 'news', :action => 'edit', :id => '567'
171 171 should_route :get, "/news/preview", :controller => 'previews', :action => 'news'
172 172
173 173 should_route :post, "/projects/567/news", :controller => 'news', :action => 'create', :project_id => '567'
174 174 should_route :post, "/news/567/comments", :controller => 'comments', :action => 'create', :id => '567'
175 175
176 176 should_route :put, "/news/567", :controller => 'news', :action => 'update', :id => '567'
177 177
178 178 should_route :delete, "/news/567", :controller => 'news', :action => 'destroy', :id => '567'
179 179 should_route :delete, "/news/567/comments/15", :controller => 'comments', :action => 'destroy', :id => '567', :comment_id => '15'
180 180 end
181 181
182 182 context "projects" do
183 183 should_route :get, "/projects", :controller => 'projects', :action => 'index'
184 184 should_route :get, "/projects.atom", :controller => 'projects', :action => 'index', :format => 'atom'
185 185 should_route :get, "/projects.xml", :controller => 'projects', :action => 'index', :format => 'xml'
186 186 should_route :get, "/projects/new", :controller => 'projects', :action => 'new'
187 187 should_route :get, "/projects/test", :controller => 'projects', :action => 'show', :id => 'test'
188 188 should_route :get, "/projects/1.xml", :controller => 'projects', :action => 'show', :id => '1', :format => 'xml'
189 189 should_route :get, "/projects/4223/settings", :controller => 'projects', :action => 'settings', :id => '4223'
190 190 should_route :get, "/projects/4223/settings/members", :controller => 'projects', :action => 'settings', :id => '4223', :tab => 'members'
191 191 should_route :get, "/projects/33/files", :controller => 'files', :action => 'index', :project_id => '33'
192 192 should_route :get, "/projects/33/files/new", :controller => 'files', :action => 'new', :project_id => '33'
193 193 should_route :get, "/projects/33/roadmap", :controller => 'versions', :action => 'index', :project_id => '33'
194 194 should_route :get, "/projects/33/activity", :controller => 'activities', :action => 'index', :id => '33'
195 195 should_route :get, "/projects/33/activity.atom", :controller => 'activities', :action => 'index', :id => '33', :format => 'atom'
196 196
197 197 should_route :post, "/projects", :controller => 'projects', :action => 'create'
198 198 should_route :post, "/projects.xml", :controller => 'projects', :action => 'create', :format => 'xml'
199 199 should_route :post, "/projects/33/files", :controller => 'files', :action => 'create', :project_id => '33'
200 200 should_route :post, "/projects/64/archive", :controller => 'projects', :action => 'archive', :id => '64'
201 201 should_route :post, "/projects/64/unarchive", :controller => 'projects', :action => 'unarchive', :id => '64'
202 202
203 203 should_route :put, "/projects/64/enumerations", :controller => 'project_enumerations', :action => 'update', :project_id => '64'
204 204 should_route :put, "/projects/4223", :controller => 'projects', :action => 'update', :id => '4223'
205 205 should_route :put, "/projects/1.xml", :controller => 'projects', :action => 'update', :id => '1', :format => 'xml'
206 206
207 207 should_route :delete, "/projects/64", :controller => 'projects', :action => 'destroy', :id => '64'
208 208 should_route :delete, "/projects/1.xml", :controller => 'projects', :action => 'destroy', :id => '1', :format => 'xml'
209 209 should_route :delete, "/projects/64/enumerations", :controller => 'project_enumerations', :action => 'destroy', :project_id => '64'
210 210 end
211 211
212 212 context "queries" do
213 should_route :get, "/queries.xml", :controller => 'queries', :action => 'index', :format => 'xml'
214 should_route :get, "/queries.json", :controller => 'queries', :action => 'index', :format => 'json'
215
213 216 should_route :get, "/queries/new", :controller => 'queries', :action => 'new'
214 217 should_route :get, "/projects/redmine/queries/new", :controller => 'queries', :action => 'new', :project_id => 'redmine'
215 218
216 219 should_route :post, "/queries/new", :controller => 'queries', :action => 'new'
217 220 should_route :post, "/projects/redmine/queries/new", :controller => 'queries', :action => 'new', :project_id => 'redmine'
218 221 end
219 222
220 223 context "repositories" do
221 224 should_route :get, "/projects/redmine/repository", :controller => 'repositories', :action => 'show', :id => 'redmine'
222 225 should_route :get, "/projects/redmine/repository/edit", :controller => 'repositories', :action => 'edit', :id => 'redmine'
223 226 should_route :get, "/projects/redmine/repository/revisions", :controller => 'repositories', :action => 'revisions', :id => 'redmine'
224 227 should_route :get, "/projects/redmine/repository/revisions.atom", :controller => 'repositories', :action => 'revisions', :id => 'redmine', :format => 'atom'
225 228 should_route :get, "/projects/redmine/repository/revisions/2457", :controller => 'repositories', :action => 'revision', :id => 'redmine', :rev => '2457'
226 229 should_route :get, "/projects/redmine/repository/revisions/2457/diff", :controller => 'repositories', :action => 'diff', :id => 'redmine', :rev => '2457'
227 230 should_route :get, "/projects/redmine/repository/revisions/2457/diff.diff", :controller => 'repositories', :action => 'diff', :id => 'redmine', :rev => '2457', :format => 'diff'
228 231 should_route :get, "/projects/redmine/repository/diff/path/to/file.c", :controller => 'repositories', :action => 'diff', :id => 'redmine', :path => %w[path to file.c]
229 232 should_route :get, "/projects/redmine/repository/revisions/2/diff/path/to/file.c", :controller => 'repositories', :action => 'diff', :id => 'redmine', :path => %w[path to file.c], :rev => '2'
230 233 should_route :get, "/projects/redmine/repository/browse/path/to/file.c", :controller => 'repositories', :action => 'browse', :id => 'redmine', :path => %w[path to file.c]
231 234 should_route :get, "/projects/redmine/repository/entry/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c]
232 235 should_route :get, "/projects/redmine/repository/revisions/2/entry/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c], :rev => '2'
233 236 should_route :get, "/projects/redmine/repository/raw/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c], :format => 'raw'
234 237 should_route :get, "/projects/redmine/repository/revisions/2/raw/path/to/file.c", :controller => 'repositories', :action => 'entry', :id => 'redmine', :path => %w[path to file.c], :rev => '2', :format => 'raw'
235 238 should_route :get, "/projects/redmine/repository/annotate/path/to/file.c", :controller => 'repositories', :action => 'annotate', :id => 'redmine', :path => %w[path to file.c]
236 239 should_route :get, "/projects/redmine/repository/changes/path/to/file.c", :controller => 'repositories', :action => 'changes', :id => 'redmine', :path => %w[path to file.c]
237 240 should_route :get, "/projects/redmine/repository/statistics", :controller => 'repositories', :action => 'stats', :id => 'redmine'
238 241
239 242
240 243 should_route :post, "/projects/redmine/repository/edit", :controller => 'repositories', :action => 'edit', :id => 'redmine'
241 244 end
242 245
243 246 context "timelogs (global)" do
244 247 should_route :get, "/time_entries", :controller => 'timelog', :action => 'index'
245 248 should_route :get, "/time_entries.csv", :controller => 'timelog', :action => 'index', :format => 'csv'
246 249 should_route :get, "/time_entries.atom", :controller => 'timelog', :action => 'index', :format => 'atom'
247 250 should_route :get, "/time_entries/new", :controller => 'timelog', :action => 'new'
248 251 should_route :get, "/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22'
249 252
250 253 should_route :post, "/time_entries", :controller => 'timelog', :action => 'create'
251 254
252 255 should_route :put, "/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22'
253 256
254 257 should_route :delete, "/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55'
255 258 end
256 259
257 260 context "timelogs (scoped under project)" do
258 261 should_route :get, "/projects/567/time_entries", :controller => 'timelog', :action => 'index', :project_id => '567'
259 262 should_route :get, "/projects/567/time_entries.csv", :controller => 'timelog', :action => 'index', :project_id => '567', :format => 'csv'
260 263 should_route :get, "/projects/567/time_entries.atom", :controller => 'timelog', :action => 'index', :project_id => '567', :format => 'atom'
261 264 should_route :get, "/projects/567/time_entries/new", :controller => 'timelog', :action => 'new', :project_id => '567'
262 265 should_route :get, "/projects/567/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22', :project_id => '567'
263 266
264 267 should_route :post, "/projects/567/time_entries", :controller => 'timelog', :action => 'create', :project_id => '567'
265 268
266 269 should_route :put, "/projects/567/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22', :project_id => '567'
267 270
268 271 should_route :delete, "/projects/567/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55', :project_id => '567'
269 272 end
270 273
271 274 context "timelogs (scoped under issues)" do
272 275 should_route :get, "/issues/234/time_entries", :controller => 'timelog', :action => 'index', :issue_id => '234'
273 276 should_route :get, "/issues/234/time_entries.csv", :controller => 'timelog', :action => 'index', :issue_id => '234', :format => 'csv'
274 277 should_route :get, "/issues/234/time_entries.atom", :controller => 'timelog', :action => 'index', :issue_id => '234', :format => 'atom'
275 278 should_route :get, "/issues/234/time_entries/new", :controller => 'timelog', :action => 'new', :issue_id => '234'
276 279 should_route :get, "/issues/234/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22', :issue_id => '234'
277 280
278 281 should_route :post, "/issues/234/time_entries", :controller => 'timelog', :action => 'create', :issue_id => '234'
279 282
280 283 should_route :put, "/issues/234/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22', :issue_id => '234'
281 284
282 285 should_route :delete, "/issues/234/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55', :issue_id => '234'
283 286 end
284 287
285 288 context "timelogs (scoped under project and issues)" do
286 289 should_route :get, "/projects/ecookbook/issues/234/time_entries", :controller => 'timelog', :action => 'index', :issue_id => '234', :project_id => 'ecookbook'
287 290 should_route :get, "/projects/ecookbook/issues/234/time_entries.csv", :controller => 'timelog', :action => 'index', :issue_id => '234', :project_id => 'ecookbook', :format => 'csv'
288 291 should_route :get, "/projects/ecookbook/issues/234/time_entries.atom", :controller => 'timelog', :action => 'index', :issue_id => '234', :project_id => 'ecookbook', :format => 'atom'
289 292 should_route :get, "/projects/ecookbook/issues/234/time_entries/new", :controller => 'timelog', :action => 'new', :issue_id => '234', :project_id => 'ecookbook'
290 293 should_route :get, "/projects/ecookbook/issues/234/time_entries/22/edit", :controller => 'timelog', :action => 'edit', :id => '22', :issue_id => '234', :project_id => 'ecookbook'
291 294
292 295 should_route :post, "/projects/ecookbook/issues/234/time_entries", :controller => 'timelog', :action => 'create', :issue_id => '234', :project_id => 'ecookbook'
293 296
294 297 should_route :put, "/projects/ecookbook/issues/234/time_entries/22", :controller => 'timelog', :action => 'update', :id => '22', :issue_id => '234', :project_id => 'ecookbook'
295 298
296 299 should_route :delete, "/projects/ecookbook/issues/234/time_entries/55", :controller => 'timelog', :action => 'destroy', :id => '55', :issue_id => '234', :project_id => 'ecookbook'
297 300 end
298 301
299 302 context "time_entry_reports" do
300 303 should_route :get, "/time_entries/report", :controller => 'time_entry_reports', :action => 'report'
301 304 should_route :get, "/projects/567/time_entries/report", :controller => 'time_entry_reports', :action => 'report', :project_id => '567'
302 305 should_route :get, "/projects/567/time_entries/report.csv", :controller => 'time_entry_reports', :action => 'report', :project_id => '567', :format => 'csv'
303 306 end
304 307
305 308 context "users" do
306 309 should_route :get, "/users", :controller => 'users', :action => 'index'
307 310 should_route :get, "/users.xml", :controller => 'users', :action => 'index', :format => 'xml'
308 311 should_route :get, "/users/44", :controller => 'users', :action => 'show', :id => '44'
309 312 should_route :get, "/users/44.xml", :controller => 'users', :action => 'show', :id => '44', :format => 'xml'
310 313 should_route :get, "/users/current", :controller => 'users', :action => 'show', :id => 'current'
311 314 should_route :get, "/users/current.xml", :controller => 'users', :action => 'show', :id => 'current', :format => 'xml'
312 315 should_route :get, "/users/new", :controller => 'users', :action => 'new'
313 316 should_route :get, "/users/444/edit", :controller => 'users', :action => 'edit', :id => '444'
314 317 should_route :get, "/users/222/edit/membership", :controller => 'users', :action => 'edit', :id => '222', :tab => 'membership'
315 318
316 319 should_route :post, "/users", :controller => 'users', :action => 'create'
317 320 should_route :post, "/users.xml", :controller => 'users', :action => 'create', :format => 'xml'
318 321 should_route :post, "/users/123/memberships", :controller => 'users', :action => 'edit_membership', :id => '123'
319 322 should_route :post, "/users/123/memberships/55", :controller => 'users', :action => 'edit_membership', :id => '123', :membership_id => '55'
320 323 should_route :post, "/users/567/memberships/12/destroy", :controller => 'users', :action => 'destroy_membership', :id => '567', :membership_id => '12'
321 324
322 325 should_route :put, "/users/444", :controller => 'users', :action => 'update', :id => '444'
323 326 should_route :put, "/users/444.xml", :controller => 'users', :action => 'update', :id => '444', :format => 'xml'
324 327
325 328 should_route :delete, "/users/44", :controller => 'users', :action => 'destroy', :id => '44'
326 329 should_route :delete, "/users/44.xml", :controller => 'users', :action => 'destroy', :id => '44', :format => 'xml'
327 330 end
328 331
329 332 context "versions" do
330 333 # /projects/foo/versions is /projects/foo/roadmap
331 334 should_route :get, "/projects/foo/versions.xml", :controller => 'versions', :action => 'index', :project_id => 'foo', :format => 'xml'
332 335 should_route :get, "/projects/foo/versions.json", :controller => 'versions', :action => 'index', :project_id => 'foo', :format => 'json'
333 336
334 337 should_route :get, "/projects/foo/versions/new", :controller => 'versions', :action => 'new', :project_id => 'foo'
335 338
336 339 should_route :post, "/projects/foo/versions", :controller => 'versions', :action => 'create', :project_id => 'foo'
337 340 should_route :post, "/projects/foo/versions.xml", :controller => 'versions', :action => 'create', :project_id => 'foo', :format => 'xml'
338 341 should_route :post, "/projects/foo/versions.json", :controller => 'versions', :action => 'create', :project_id => 'foo', :format => 'json'
339 342
340 343 should_route :get, "/versions/1", :controller => 'versions', :action => 'show', :id => '1'
341 344 should_route :get, "/versions/1.xml", :controller => 'versions', :action => 'show', :id => '1', :format => 'xml'
342 345 should_route :get, "/versions/1.json", :controller => 'versions', :action => 'show', :id => '1', :format => 'json'
343 346
344 347 should_route :get, "/versions/1/edit", :controller => 'versions', :action => 'edit', :id => '1'
345 348
346 349 should_route :put, "/versions/1", :controller => 'versions', :action => 'update', :id => '1'
347 350 should_route :put, "/versions/1.xml", :controller => 'versions', :action => 'update', :id => '1', :format => 'xml'
348 351 should_route :put, "/versions/1.json", :controller => 'versions', :action => 'update', :id => '1', :format => 'json'
349 352
350 353 should_route :delete, "/versions/1", :controller => 'versions', :action => 'destroy', :id => '1'
351 354 should_route :delete, "/versions/1.xml", :controller => 'versions', :action => 'destroy', :id => '1', :format => 'xml'
352 355 should_route :delete, "/versions/1.json", :controller => 'versions', :action => 'destroy', :id => '1', :format => 'json'
353 356
354 357 should_route :put, "/projects/foo/versions/close_completed", :controller => 'versions', :action => 'close_completed', :project_id => 'foo'
355 358 should_route :post, "/versions/1/status_by", :controller => 'versions', :action => 'status_by', :id => '1'
356 359 end
357 360
358 361 context "wiki (singular, project's pages)" do
359 362 should_route :get, "/projects/567/wiki", :controller => 'wiki', :action => 'show', :project_id => '567'
360 363 should_route :get, "/projects/567/wiki/lalala", :controller => 'wiki', :action => 'show', :project_id => '567', :id => 'lalala'
361 364 should_route :get, "/projects/567/wiki/my_page/edit", :controller => 'wiki', :action => 'edit', :project_id => '567', :id => 'my_page'
362 365 should_route :get, "/projects/1/wiki/CookBook_documentation/history", :controller => 'wiki', :action => 'history', :project_id => '1', :id => 'CookBook_documentation'
363 366 should_route :get, "/projects/1/wiki/CookBook_documentation/diff", :controller => 'wiki', :action => 'diff', :project_id => '1', :id => 'CookBook_documentation'
364 367 should_route :get, "/projects/1/wiki/CookBook_documentation/diff/2", :controller => 'wiki', :action => 'diff', :project_id => '1', :id => 'CookBook_documentation', :version => '2'
365 368 should_route :get, "/projects/1/wiki/CookBook_documentation/diff/2/vs/1", :controller => 'wiki', :action => 'diff', :project_id => '1', :id => 'CookBook_documentation', :version => '2', :version_from => '1'
366 369 should_route :get, "/projects/1/wiki/CookBook_documentation/annotate/2", :controller => 'wiki', :action => 'annotate', :project_id => '1', :id => 'CookBook_documentation', :version => '2'
367 370 should_route :get, "/projects/22/wiki/ladida/rename", :controller => 'wiki', :action => 'rename', :project_id => '22', :id => 'ladida'
368 371 should_route :get, "/projects/567/wiki/index", :controller => 'wiki', :action => 'index', :project_id => '567'
369 372 should_route :get, "/projects/567/wiki/date_index", :controller => 'wiki', :action => 'date_index', :project_id => '567'
370 373 should_route :get, "/projects/567/wiki/export", :controller => 'wiki', :action => 'export', :project_id => '567'
371 374
372 375 should_route :post, "/projects/567/wiki/CookBook_documentation/preview", :controller => 'wiki', :action => 'preview', :project_id => '567', :id => 'CookBook_documentation'
373 376 should_route :post, "/projects/22/wiki/ladida/rename", :controller => 'wiki', :action => 'rename', :project_id => '22', :id => 'ladida'
374 377 should_route :post, "/projects/22/wiki/ladida/protect", :controller => 'wiki', :action => 'protect', :project_id => '22', :id => 'ladida'
375 378 should_route :post, "/projects/22/wiki/ladida/add_attachment", :controller => 'wiki', :action => 'add_attachment', :project_id => '22', :id => 'ladida'
376 379
377 380 should_route :put, "/projects/567/wiki/my_page", :controller => 'wiki', :action => 'update', :project_id => '567', :id => 'my_page'
378 381
379 382 should_route :delete, "/projects/22/wiki/ladida", :controller => 'wiki', :action => 'destroy', :project_id => '22', :id => 'ladida'
380 383 end
381 384
382 385 context "wikis (plural, admin setup)" do
383 386 should_route :get, "/projects/ladida/wiki/destroy", :controller => 'wikis', :action => 'destroy', :id => 'ladida'
384 387
385 388 should_route :post, "/projects/ladida/wiki", :controller => 'wikis', :action => 'edit', :id => 'ladida'
386 389 should_route :post, "/projects/ladida/wiki/destroy", :controller => 'wikis', :action => 'destroy', :id => 'ladida'
387 390 end
388 391
389 392 context "administration panel" do
390 393 should_route :get, "/admin/projects", :controller => 'admin', :action => 'projects'
391 394 end
392 395 end
@@ -1,607 +1,617
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class QueryTest < ActiveSupport::TestCase
21 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 23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 24 query = Query.new(:project => nil, :name => '_')
25 25 assert query.available_filters.has_key?('cf_1')
26 26 assert !query.available_filters.has_key?('cf_3')
27 27 end
28 28
29 29 def test_system_shared_versions_should_be_available_in_global_queries
30 30 Version.find(2).update_attribute :sharing, 'system'
31 31 query = Query.new(:project => nil, :name => '_')
32 32 assert query.available_filters.has_key?('fixed_version_id')
33 33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
34 34 end
35 35
36 36 def test_project_filter_in_global_queries
37 37 query = Query.new(:project => nil, :name => '_')
38 38 project_filter = query.available_filters["project_id"]
39 39 assert_not_nil project_filter
40 40 project_ids = project_filter[:values].map{|p| p[1]}
41 41 assert project_ids.include?("1") #public project
42 42 assert !project_ids.include?("2") #private project user cannot see
43 43 end
44 44
45 45 def find_issues_with_query(query)
46 46 Issue.find :all,
47 47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
48 48 :conditions => query.statement
49 49 end
50 50
51 51 def assert_find_issues_with_query_is_successful(query)
52 52 assert_nothing_raised do
53 53 find_issues_with_query(query)
54 54 end
55 55 end
56 56
57 57 def assert_query_statement_includes(query, condition)
58 58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
59 59 end
60 60
61 61 def test_query_should_allow_shared_versions_for_a_project_query
62 62 subproject_version = Version.find(4)
63 63 query = Query.new(:project => Project.find(1), :name => '_')
64 64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
65 65
66 66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
67 67 end
68 68
69 69 def test_query_with_multiple_custom_fields
70 70 query = Query.find(1)
71 71 assert query.valid?
72 72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
73 73 issues = find_issues_with_query(query)
74 74 assert_equal 1, issues.length
75 75 assert_equal Issue.find(3), issues.first
76 76 end
77 77
78 78 def test_operator_none
79 79 query = Query.new(:project => Project.find(1), :name => '_')
80 80 query.add_filter('fixed_version_id', '!*', [''])
81 81 query.add_filter('cf_1', '!*', [''])
82 82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
83 83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
84 84 find_issues_with_query(query)
85 85 end
86 86
87 87 def test_operator_none_for_integer
88 88 query = Query.new(:project => Project.find(1), :name => '_')
89 89 query.add_filter('estimated_hours', '!*', [''])
90 90 issues = find_issues_with_query(query)
91 91 assert !issues.empty?
92 92 assert issues.all? {|i| !i.estimated_hours}
93 93 end
94 94
95 95 def test_operator_all
96 96 query = Query.new(:project => Project.find(1), :name => '_')
97 97 query.add_filter('fixed_version_id', '*', [''])
98 98 query.add_filter('cf_1', '*', [''])
99 99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
100 100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
101 101 find_issues_with_query(query)
102 102 end
103 103
104 104 def test_operator_greater_than
105 105 query = Query.new(:project => Project.find(1), :name => '_')
106 106 query.add_filter('done_ratio', '>=', ['40'])
107 107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
108 108 find_issues_with_query(query)
109 109 end
110 110
111 111 def test_operator_in_more_than
112 112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
113 113 query = Query.new(:project => Project.find(1), :name => '_')
114 114 query.add_filter('due_date', '>t+', ['15'])
115 115 issues = find_issues_with_query(query)
116 116 assert !issues.empty?
117 117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
118 118 end
119 119
120 120 def test_operator_in_less_than
121 121 query = Query.new(:project => Project.find(1), :name => '_')
122 122 query.add_filter('due_date', '<t+', ['15'])
123 123 issues = find_issues_with_query(query)
124 124 assert !issues.empty?
125 125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
126 126 end
127 127
128 128 def test_operator_less_than_ago
129 129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
130 130 query = Query.new(:project => Project.find(1), :name => '_')
131 131 query.add_filter('due_date', '>t-', ['3'])
132 132 issues = find_issues_with_query(query)
133 133 assert !issues.empty?
134 134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
135 135 end
136 136
137 137 def test_operator_more_than_ago
138 138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
139 139 query = Query.new(:project => Project.find(1), :name => '_')
140 140 query.add_filter('due_date', '<t-', ['10'])
141 141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
142 142 issues = find_issues_with_query(query)
143 143 assert !issues.empty?
144 144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
145 145 end
146 146
147 147 def test_operator_in
148 148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
149 149 query = Query.new(:project => Project.find(1), :name => '_')
150 150 query.add_filter('due_date', 't+', ['2'])
151 151 issues = find_issues_with_query(query)
152 152 assert !issues.empty?
153 153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
154 154 end
155 155
156 156 def test_operator_ago
157 157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
158 158 query = Query.new(:project => Project.find(1), :name => '_')
159 159 query.add_filter('due_date', 't-', ['3'])
160 160 issues = find_issues_with_query(query)
161 161 assert !issues.empty?
162 162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
163 163 end
164 164
165 165 def test_operator_today
166 166 query = Query.new(:project => Project.find(1), :name => '_')
167 167 query.add_filter('due_date', 't', [''])
168 168 issues = find_issues_with_query(query)
169 169 assert !issues.empty?
170 170 issues.each {|issue| assert_equal Date.today, issue.due_date}
171 171 end
172 172
173 173 def test_operator_this_week_on_date
174 174 query = Query.new(:project => Project.find(1), :name => '_')
175 175 query.add_filter('due_date', 'w', [''])
176 176 find_issues_with_query(query)
177 177 end
178 178
179 179 def test_operator_this_week_on_datetime
180 180 query = Query.new(:project => Project.find(1), :name => '_')
181 181 query.add_filter('created_on', 'w', [''])
182 182 find_issues_with_query(query)
183 183 end
184 184
185 185 def test_operator_contains
186 186 query = Query.new(:project => Project.find(1), :name => '_')
187 187 query.add_filter('subject', '~', ['uNable'])
188 188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
189 189 result = find_issues_with_query(query)
190 190 assert result.empty?
191 191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
192 192 end
193 193
194 194 def test_range_for_this_week_with_week_starting_on_monday
195 195 I18n.locale = :fr
196 196 assert_equal '1', I18n.t(:general_first_day_of_week)
197 197
198 198 Date.stubs(:today).returns(Date.parse('2011-04-29'))
199 199
200 200 query = Query.new(:project => Project.find(1), :name => '_')
201 201 query.add_filter('due_date', 'w', [''])
202 202 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
203 203 I18n.locale = :en
204 204 end
205 205
206 206 def test_range_for_this_week_with_week_starting_on_sunday
207 207 I18n.locale = :en
208 208 assert_equal '7', I18n.t(:general_first_day_of_week)
209 209
210 210 Date.stubs(:today).returns(Date.parse('2011-04-29'))
211 211
212 212 query = Query.new(:project => Project.find(1), :name => '_')
213 213 query.add_filter('due_date', 'w', [''])
214 214 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
215 215 end
216 216
217 217 def test_operator_does_not_contains
218 218 query = Query.new(:project => Project.find(1), :name => '_')
219 219 query.add_filter('subject', '!~', ['uNable'])
220 220 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
221 221 find_issues_with_query(query)
222 222 end
223 223
224 224 def test_filter_watched_issues
225 225 User.current = User.find(1)
226 226 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
227 227 result = find_issues_with_query(query)
228 228 assert_not_nil result
229 229 assert !result.empty?
230 230 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
231 231 User.current = nil
232 232 end
233 233
234 234 def test_filter_unwatched_issues
235 235 User.current = User.find(1)
236 236 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
237 237 result = find_issues_with_query(query)
238 238 assert_not_nil result
239 239 assert !result.empty?
240 240 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
241 241 User.current = nil
242 242 end
243 243
244 244 def test_statement_should_be_nil_with_no_filters
245 245 q = Query.new(:name => '_')
246 246 q.filters = {}
247 247
248 248 assert q.valid?
249 249 assert_nil q.statement
250 250 end
251 251
252 252 def test_default_columns
253 253 q = Query.new
254 254 assert !q.columns.empty?
255 255 end
256 256
257 257 def test_set_column_names
258 258 q = Query.new
259 259 q.column_names = ['tracker', :subject, '', 'unknonw_column']
260 260 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
261 261 c = q.columns.first
262 262 assert q.has_column?(c)
263 263 end
264 264
265 265 def test_groupable_columns_should_include_custom_fields
266 266 q = Query.new
267 267 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
268 268 end
269 269
270 270 def test_grouped_with_valid_column
271 271 q = Query.new(:group_by => 'status')
272 272 assert q.grouped?
273 273 assert_not_nil q.group_by_column
274 274 assert_equal :status, q.group_by_column.name
275 275 assert_not_nil q.group_by_statement
276 276 assert_equal 'status', q.group_by_statement
277 277 end
278 278
279 279 def test_grouped_with_invalid_column
280 280 q = Query.new(:group_by => 'foo')
281 281 assert !q.grouped?
282 282 assert_nil q.group_by_column
283 283 assert_nil q.group_by_statement
284 284 end
285 285
286 286 def test_default_sort
287 287 q = Query.new
288 288 assert_equal [], q.sort_criteria
289 289 end
290 290
291 291 def test_set_sort_criteria_with_hash
292 292 q = Query.new
293 293 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
294 294 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
295 295 end
296 296
297 297 def test_set_sort_criteria_with_array
298 298 q = Query.new
299 299 q.sort_criteria = [['priority', 'desc'], 'tracker']
300 300 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
301 301 end
302 302
303 303 def test_create_query_with_sort
304 304 q = Query.new(:name => 'Sorted')
305 305 q.sort_criteria = [['priority', 'desc'], 'tracker']
306 306 assert q.save
307 307 q.reload
308 308 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
309 309 end
310 310
311 311 def test_sort_by_string_custom_field_asc
312 312 q = Query.new
313 313 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
314 314 assert c
315 315 assert c.sortable
316 316 issues = Issue.find :all,
317 317 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
318 318 :conditions => q.statement,
319 319 :order => "#{c.sortable} ASC"
320 320 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
321 321 assert !values.empty?
322 322 assert_equal values.sort, values
323 323 end
324 324
325 325 def test_sort_by_string_custom_field_desc
326 326 q = Query.new
327 327 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
328 328 assert c
329 329 assert c.sortable
330 330 issues = Issue.find :all,
331 331 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
332 332 :conditions => q.statement,
333 333 :order => "#{c.sortable} DESC"
334 334 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
335 335 assert !values.empty?
336 336 assert_equal values.sort.reverse, values
337 337 end
338 338
339 339 def test_sort_by_float_custom_field_asc
340 340 q = Query.new
341 341 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
342 342 assert c
343 343 assert c.sortable
344 344 issues = Issue.find :all,
345 345 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
346 346 :conditions => q.statement,
347 347 :order => "#{c.sortable} ASC"
348 348 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
349 349 assert !values.empty?
350 350 assert_equal values.sort, values
351 351 end
352 352
353 353 def test_invalid_query_should_raise_query_statement_invalid_error
354 354 q = Query.new
355 355 assert_raise Query::StatementInvalid do
356 356 q.issues(:conditions => "foo = 1")
357 357 end
358 358 end
359 359
360 360 def test_issue_count_by_association_group
361 361 q = Query.new(:name => '_', :group_by => 'assigned_to')
362 362 count_by_group = q.issue_count_by_group
363 363 assert_kind_of Hash, count_by_group
364 364 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
365 365 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
366 366 assert count_by_group.has_key?(User.find(3))
367 367 end
368 368
369 369 def test_issue_count_by_list_custom_field_group
370 370 q = Query.new(:name => '_', :group_by => 'cf_1')
371 371 count_by_group = q.issue_count_by_group
372 372 assert_kind_of Hash, count_by_group
373 373 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
374 374 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
375 375 assert count_by_group.has_key?('MySQL')
376 376 end
377 377
378 378 def test_issue_count_by_date_custom_field_group
379 379 q = Query.new(:name => '_', :group_by => 'cf_8')
380 380 count_by_group = q.issue_count_by_group
381 381 assert_kind_of Hash, count_by_group
382 382 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
383 383 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
384 384 end
385 385
386 386 def test_label_for
387 387 q = Query.new
388 388 assert_equal 'assigned_to', q.label_for('assigned_to_id')
389 389 end
390 390
391 391 def test_editable_by
392 392 admin = User.find(1)
393 393 manager = User.find(2)
394 394 developer = User.find(3)
395 395
396 396 # Public query on project 1
397 397 q = Query.find(1)
398 398 assert q.editable_by?(admin)
399 399 assert q.editable_by?(manager)
400 400 assert !q.editable_by?(developer)
401 401
402 402 # Private query on project 1
403 403 q = Query.find(2)
404 404 assert q.editable_by?(admin)
405 405 assert !q.editable_by?(manager)
406 406 assert q.editable_by?(developer)
407 407
408 408 # Private query for all projects
409 409 q = Query.find(3)
410 410 assert q.editable_by?(admin)
411 411 assert !q.editable_by?(manager)
412 412 assert q.editable_by?(developer)
413 413
414 414 # Public query for all projects
415 415 q = Query.find(4)
416 416 assert q.editable_by?(admin)
417 417 assert !q.editable_by?(manager)
418 418 assert !q.editable_by?(developer)
419 419 end
420
421 def test_visible_scope
422 query_ids = Query.visible(User.anonymous).map(&:id)
423
424 assert query_ids.include?(1), 'public query on public project was not visible'
425 assert query_ids.include?(4), 'public query for all projects was not visible'
426 assert !query_ids.include?(2), 'private query on public project was visible'
427 assert !query_ids.include?(3), 'private query for all projects was visible'
428 assert !query_ids.include?(7), 'public query on private project was visible'
429 end
420 430
421 431 context "#available_filters" do
422 432 setup do
423 433 @query = Query.new(:name => "_")
424 434 end
425 435
426 436 should "include users of visible projects in cross-project view" do
427 437 users = @query.available_filters["assigned_to_id"]
428 438 assert_not_nil users
429 439 assert users[:values].map{|u|u[1]}.include?("3")
430 440 end
431 441
432 442 should "include visible projects in cross-project view" do
433 443 projects = @query.available_filters["project_id"]
434 444 assert_not_nil projects
435 445 assert projects[:values].map{|u|u[1]}.include?("1")
436 446 end
437 447
438 448 context "'member_of_group' filter" do
439 449 should "be present" do
440 450 assert @query.available_filters.keys.include?("member_of_group")
441 451 end
442 452
443 453 should "be an optional list" do
444 454 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
445 455 end
446 456
447 457 should "have a list of the groups as values" do
448 458 Group.destroy_all # No fixtures
449 459 group1 = Group.generate!.reload
450 460 group2 = Group.generate!.reload
451 461
452 462 expected_group_list = [
453 463 [group1.name, group1.id.to_s],
454 464 [group2.name, group2.id.to_s]
455 465 ]
456 466 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
457 467 end
458 468
459 469 end
460 470
461 471 context "'assigned_to_role' filter" do
462 472 should "be present" do
463 473 assert @query.available_filters.keys.include?("assigned_to_role")
464 474 end
465 475
466 476 should "be an optional list" do
467 477 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
468 478 end
469 479
470 480 should "have a list of the Roles as values" do
471 481 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
472 482 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
473 483 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
474 484 end
475 485
476 486 should "not include the built in Roles as values" do
477 487 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
478 488 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
479 489 end
480 490
481 491 end
482 492
483 493 end
484 494
485 495 context "#statement" do
486 496 context "with 'member_of_group' filter" do
487 497 setup do
488 498 Group.destroy_all # No fixtures
489 499 @user_in_group = User.generate!
490 500 @second_user_in_group = User.generate!
491 501 @user_in_group2 = User.generate!
492 502 @user_not_in_group = User.generate!
493 503
494 504 @group = Group.generate!.reload
495 505 @group.users << @user_in_group
496 506 @group.users << @second_user_in_group
497 507
498 508 @group2 = Group.generate!.reload
499 509 @group2.users << @user_in_group2
500 510
501 511 end
502 512
503 513 should "search assigned to for users in the group" do
504 514 @query = Query.new(:name => '_')
505 515 @query.add_filter('member_of_group', '=', [@group.id.to_s])
506 516
507 517 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
508 518 assert_find_issues_with_query_is_successful @query
509 519 end
510 520
511 521 should "search not assigned to any group member (none)" do
512 522 @query = Query.new(:name => '_')
513 523 @query.add_filter('member_of_group', '!*', [''])
514 524
515 525 # Users not in a group
516 526 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}')"
517 527 assert_find_issues_with_query_is_successful @query
518 528 end
519 529
520 530 should "search assigned to any group member (all)" do
521 531 @query = Query.new(:name => '_')
522 532 @query.add_filter('member_of_group', '*', [''])
523 533
524 534 # Only users in a group
525 535 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}')"
526 536 assert_find_issues_with_query_is_successful @query
527 537 end
528 538
529 539 should "return an empty set with = empty group" do
530 540 @empty_group = Group.generate!
531 541 @query = Query.new(:name => '_')
532 542 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
533 543
534 544 assert_equal [], find_issues_with_query(@query)
535 545 end
536 546
537 547 should "return issues with ! empty group" do
538 548 @empty_group = Group.generate!
539 549 @query = Query.new(:name => '_')
540 550 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
541 551
542 552 assert_find_issues_with_query_is_successful @query
543 553 end
544 554 end
545 555
546 556 context "with 'assigned_to_role' filter" do
547 557 setup do
548 558 # No fixtures
549 559 MemberRole.delete_all
550 560 Member.delete_all
551 561 Role.delete_all
552 562
553 563 @manager_role = Role.generate!(:name => 'Manager')
554 564 @developer_role = Role.generate!(:name => 'Developer')
555 565
556 566 @project = Project.generate!
557 567 @manager = User.generate!
558 568 @developer = User.generate!
559 569 @boss = User.generate!
560 570 User.add_to_project(@manager, @project, @manager_role)
561 571 User.add_to_project(@developer, @project, @developer_role)
562 572 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
563 573 end
564 574
565 575 should "search assigned to for users with the Role" do
566 576 @query = Query.new(:name => '_')
567 577 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
568 578
569 579 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
570 580 assert_find_issues_with_query_is_successful @query
571 581 end
572 582
573 583 should "search assigned to for users not assigned to any Role (none)" do
574 584 @query = Query.new(:name => '_')
575 585 @query.add_filter('assigned_to_role', '!*', [''])
576 586
577 587 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}')"
578 588 assert_find_issues_with_query_is_successful @query
579 589 end
580 590
581 591 should "search assigned to for users assigned to any Role (all)" do
582 592 @query = Query.new(:name => '_')
583 593 @query.add_filter('assigned_to_role', '*', [''])
584 594
585 595 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
586 596 assert_find_issues_with_query_is_successful @query
587 597 end
588 598
589 599 should "return an empty set with empty role" do
590 600 @empty_role = Role.generate!
591 601 @query = Query.new(:name => '_')
592 602 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
593 603
594 604 assert_equal [], find_issues_with_query(@query)
595 605 end
596 606
597 607 should "return issues with ! empty role" do
598 608 @empty_role = Role.generate!
599 609 @query = Query.new(:name => '_')
600 610 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
601 611
602 612 assert_find_issues_with_query_is_successful @query
603 613 end
604 614 end
605 615 end
606 616
607 617 end
General Comments 0
You need to be logged in to leave comments. Login now