##// END OF EJS Templates
Merged r14388 (#19842)....
Jean-Philippe Lang -
r14022:9b48fe532cdd
parent child
Show More
@@ -1,121 +1,128
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueriesController < ApplicationController
18 class QueriesController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_query, :except => [:new, :create, :index]
20 before_filter :find_query, :except => [:new, :create, :index]
21 before_filter :find_optional_project, :only => [:new, :create]
21 before_filter :find_optional_project, :only => [:new, :create]
22
22
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 include QueriesHelper
25 include QueriesHelper
26
26
27 def index
27 def index
28 case params[:format]
28 case params[:format]
29 when 'xml', 'json'
29 when 'xml', 'json'
30 @offset, @limit = api_offset_and_limit
30 @offset, @limit = api_offset_and_limit
31 else
31 else
32 @limit = per_page_option
32 @limit = per_page_option
33 end
33 end
34 @query_count = IssueQuery.visible.count
34 @query_count = IssueQuery.visible.count
35 @query_pages = Paginator.new @query_count, @limit, params['page']
35 @query_pages = Paginator.new @query_count, @limit, params['page']
36 @queries = IssueQuery.visible.
36 @queries = IssueQuery.visible.
37 order("#{Query.table_name}.name").
37 order("#{Query.table_name}.name").
38 limit(@limit).
38 limit(@limit).
39 offset(@offset).
39 offset(@offset).
40 to_a
40 to_a
41 respond_to do |format|
41 respond_to do |format|
42 format.html {render_error :status => 406}
42 format.html {render_error :status => 406}
43 format.api
43 format.api
44 end
44 end
45 end
45 end
46
46
47 def new
47 def new
48 @query = IssueQuery.new
48 @query = IssueQuery.new
49 @query.user = User.current
49 @query.user = User.current
50 @query.project = @project
50 @query.project = @project
51 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
52 @query.build_from_params(params)
51 @query.build_from_params(params)
53 end
52 end
54
53
55 def create
54 def create
56 @query = IssueQuery.new(params[:query])
55 @query = IssueQuery.new
57 @query.user = User.current
56 @query.user = User.current
58 @query.project = params[:query_is_for_all] ? nil : @project
57 @query.project = @project
59 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
58 update_query_from_params
60 @query.build_from_params(params)
61 @query.column_names = nil if params[:default_columns]
62
59
63 if @query.save
60 if @query.save
64 flash[:notice] = l(:notice_successful_create)
61 flash[:notice] = l(:notice_successful_create)
65 redirect_to_issues(:query_id => @query)
62 redirect_to_issues(:query_id => @query)
66 else
63 else
67 render :action => 'new', :layout => !request.xhr?
64 render :action => 'new', :layout => !request.xhr?
68 end
65 end
69 end
66 end
70
67
71 def edit
68 def edit
72 end
69 end
73
70
74 def update
71 def update
75 @query.attributes = params[:query]
72 update_query_from_params
76 @query.project = nil if params[:query_is_for_all]
77 @query.visibility = IssueQuery::VISIBILITY_PRIVATE unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
78 @query.build_from_params(params)
79 @query.column_names = nil if params[:default_columns]
80
73
81 if @query.save
74 if @query.save
82 flash[:notice] = l(:notice_successful_update)
75 flash[:notice] = l(:notice_successful_update)
83 redirect_to_issues(:query_id => @query)
76 redirect_to_issues(:query_id => @query)
84 else
77 else
85 render :action => 'edit'
78 render :action => 'edit'
86 end
79 end
87 end
80 end
88
81
89 def destroy
82 def destroy
90 @query.destroy
83 @query.destroy
91 redirect_to_issues(:set_filter => 1)
84 redirect_to_issues(:set_filter => 1)
92 end
85 end
93
86
94 private
87 private
95 def find_query
88 def find_query
96 @query = IssueQuery.find(params[:id])
89 @query = IssueQuery.find(params[:id])
97 @project = @query.project
90 @project = @query.project
98 render_403 unless @query.editable_by?(User.current)
91 render_403 unless @query.editable_by?(User.current)
99 rescue ActiveRecord::RecordNotFound
92 rescue ActiveRecord::RecordNotFound
100 render_404
93 render_404
101 end
94 end
102
95
103 def find_optional_project
96 def find_optional_project
104 @project = Project.find(params[:project_id]) if params[:project_id]
97 @project = Project.find(params[:project_id]) if params[:project_id]
105 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
98 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
106 rescue ActiveRecord::RecordNotFound
99 rescue ActiveRecord::RecordNotFound
107 render_404
100 render_404
108 end
101 end
109
102
103 def update_query_from_params
104 @query.project = params[:query_is_for_all] ? nil : @project
105 @query.build_from_params(params)
106 @query.column_names = nil if params[:default_columns]
107 @query.sort_criteria = params[:query] && params[:query][:sort_criteria]
108 @query.name = params[:query] && params[:query][:name]
109 if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
110 @query.visibility = (params[:query] && params[:query][:visibility]) || IssueQuery::VISIBILITY_PRIVATE
111 else
112 @query.visibility = IssueQuery::VISIBILITY_PRIVATE
113 end
114 @query
115 end
116
110 def redirect_to_issues(options)
117 def redirect_to_issues(options)
111 if params[:gantt]
118 if params[:gantt]
112 if @project
119 if @project
113 redirect_to project_gantt_path(@project, options)
120 redirect_to project_gantt_path(@project, options)
114 else
121 else
115 redirect_to issues_gantt_path(options)
122 redirect_to issues_gantt_path(options)
116 end
123 end
117 else
124 else
118 redirect_to _project_issues_path(@project, options)
125 redirect_to _project_issues_path(@project, options)
119 end
126 end
120 end
127 end
121 end
128 end
@@ -1,901 +1,903
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @inline = options.key?(:inline) ? options[:inline] : true
30 @inline = options.key?(:inline) ? options[:inline] : true
31 @caption_key = options[:caption] || "field_#{name}".to_sym
31 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @frozen = options[:frozen]
32 @frozen = options[:frozen]
33 end
33 end
34
34
35 def caption
35 def caption
36 case @caption_key
36 case @caption_key
37 when Symbol
37 when Symbol
38 l(@caption_key)
38 l(@caption_key)
39 when Proc
39 when Proc
40 @caption_key.call
40 @caption_key.call
41 else
41 else
42 @caption_key
42 @caption_key
43 end
43 end
44 end
44 end
45
45
46 # Returns true if the column is sortable, otherwise false
46 # Returns true if the column is sortable, otherwise false
47 def sortable?
47 def sortable?
48 !@sortable.nil?
48 !@sortable.nil?
49 end
49 end
50
50
51 def sortable
51 def sortable
52 @sortable.is_a?(Proc) ? @sortable.call : @sortable
52 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 end
53 end
54
54
55 def inline?
55 def inline?
56 @inline
56 @inline
57 end
57 end
58
58
59 def frozen?
59 def frozen?
60 @frozen
60 @frozen
61 end
61 end
62
62
63 def value(object)
63 def value(object)
64 object.send name
64 object.send name
65 end
65 end
66
66
67 def value_object(object)
67 def value_object(object)
68 object.send name
68 object.send name
69 end
69 end
70
70
71 def css_classes
71 def css_classes
72 name
72 name
73 end
73 end
74 end
74 end
75
75
76 class QueryCustomFieldColumn < QueryColumn
76 class QueryCustomFieldColumn < QueryColumn
77
77
78 def initialize(custom_field)
78 def initialize(custom_field)
79 self.name = "cf_#{custom_field.id}".to_sym
79 self.name = "cf_#{custom_field.id}".to_sym
80 self.sortable = custom_field.order_statement || false
80 self.sortable = custom_field.order_statement || false
81 self.groupable = custom_field.group_statement || false
81 self.groupable = custom_field.group_statement || false
82 @inline = true
82 @inline = true
83 @cf = custom_field
83 @cf = custom_field
84 end
84 end
85
85
86 def caption
86 def caption
87 @cf.name
87 @cf.name
88 end
88 end
89
89
90 def custom_field
90 def custom_field
91 @cf
91 @cf
92 end
92 end
93
93
94 def value_object(object)
94 def value_object(object)
95 if custom_field.visible_by?(object.project, User.current)
95 if custom_field.visible_by?(object.project, User.current)
96 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
96 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
97 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
97 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
98 else
98 else
99 nil
99 nil
100 end
100 end
101 end
101 end
102
102
103 def value(object)
103 def value(object)
104 raw = value_object(object)
104 raw = value_object(object)
105 if raw.is_a?(Array)
105 if raw.is_a?(Array)
106 raw.map {|r| @cf.cast_value(r.value)}
106 raw.map {|r| @cf.cast_value(r.value)}
107 elsif raw
107 elsif raw
108 @cf.cast_value(raw.value)
108 @cf.cast_value(raw.value)
109 else
109 else
110 nil
110 nil
111 end
111 end
112 end
112 end
113
113
114 def css_classes
114 def css_classes
115 @css_classes ||= "#{name} #{@cf.field_format}"
115 @css_classes ||= "#{name} #{@cf.field_format}"
116 end
116 end
117 end
117 end
118
118
119 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
119 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
120
120
121 def initialize(association, custom_field)
121 def initialize(association, custom_field)
122 super(custom_field)
122 super(custom_field)
123 self.name = "#{association}.cf_#{custom_field.id}".to_sym
123 self.name = "#{association}.cf_#{custom_field.id}".to_sym
124 # TODO: support sorting/grouping by association custom field
124 # TODO: support sorting/grouping by association custom field
125 self.sortable = false
125 self.sortable = false
126 self.groupable = false
126 self.groupable = false
127 @association = association
127 @association = association
128 end
128 end
129
129
130 def value_object(object)
130 def value_object(object)
131 if assoc = object.send(@association)
131 if assoc = object.send(@association)
132 super(assoc)
132 super(assoc)
133 end
133 end
134 end
134 end
135
135
136 def css_classes
136 def css_classes
137 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
137 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
138 end
138 end
139 end
139 end
140
140
141 class Query < ActiveRecord::Base
141 class Query < ActiveRecord::Base
142 class StatementInvalid < ::ActiveRecord::StatementInvalid
142 class StatementInvalid < ::ActiveRecord::StatementInvalid
143 end
143 end
144
144
145 VISIBILITY_PRIVATE = 0
145 VISIBILITY_PRIVATE = 0
146 VISIBILITY_ROLES = 1
146 VISIBILITY_ROLES = 1
147 VISIBILITY_PUBLIC = 2
147 VISIBILITY_PUBLIC = 2
148
148
149 belongs_to :project
149 belongs_to :project
150 belongs_to :user
150 belongs_to :user
151 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
151 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
152 serialize :filters
152 serialize :filters
153 serialize :column_names
153 serialize :column_names
154 serialize :sort_criteria, Array
154 serialize :sort_criteria, Array
155 serialize :options, Hash
155 serialize :options, Hash
156
156
157 attr_protected :project_id, :user_id
157 attr_protected :project_id, :user_id
158
158
159 validates_presence_of :name
159 validates_presence_of :name
160 validates_length_of :name, :maximum => 255
160 validates_length_of :name, :maximum => 255
161 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
161 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
162 validate :validate_query_filters
162 validate :validate_query_filters
163 validate do |query|
163 validate do |query|
164 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
164 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
165 end
165 end
166
166
167 after_save do |query|
167 after_save do |query|
168 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
168 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
169 query.roles.clear
169 query.roles.clear
170 end
170 end
171 end
171 end
172
172
173 class_attribute :operators
173 class_attribute :operators
174 self.operators = {
174 self.operators = {
175 "=" => :label_equals,
175 "=" => :label_equals,
176 "!" => :label_not_equals,
176 "!" => :label_not_equals,
177 "o" => :label_open_issues,
177 "o" => :label_open_issues,
178 "c" => :label_closed_issues,
178 "c" => :label_closed_issues,
179 "!*" => :label_none,
179 "!*" => :label_none,
180 "*" => :label_any,
180 "*" => :label_any,
181 ">=" => :label_greater_or_equal,
181 ">=" => :label_greater_or_equal,
182 "<=" => :label_less_or_equal,
182 "<=" => :label_less_or_equal,
183 "><" => :label_between,
183 "><" => :label_between,
184 "<t+" => :label_in_less_than,
184 "<t+" => :label_in_less_than,
185 ">t+" => :label_in_more_than,
185 ">t+" => :label_in_more_than,
186 "><t+"=> :label_in_the_next_days,
186 "><t+"=> :label_in_the_next_days,
187 "t+" => :label_in,
187 "t+" => :label_in,
188 "t" => :label_today,
188 "t" => :label_today,
189 "ld" => :label_yesterday,
189 "ld" => :label_yesterday,
190 "w" => :label_this_week,
190 "w" => :label_this_week,
191 "lw" => :label_last_week,
191 "lw" => :label_last_week,
192 "l2w" => [:label_last_n_weeks, {:count => 2}],
192 "l2w" => [:label_last_n_weeks, {:count => 2}],
193 "m" => :label_this_month,
193 "m" => :label_this_month,
194 "lm" => :label_last_month,
194 "lm" => :label_last_month,
195 "y" => :label_this_year,
195 "y" => :label_this_year,
196 ">t-" => :label_less_than_ago,
196 ">t-" => :label_less_than_ago,
197 "<t-" => :label_more_than_ago,
197 "<t-" => :label_more_than_ago,
198 "><t-"=> :label_in_the_past_days,
198 "><t-"=> :label_in_the_past_days,
199 "t-" => :label_ago,
199 "t-" => :label_ago,
200 "~" => :label_contains,
200 "~" => :label_contains,
201 "!~" => :label_not_contains,
201 "!~" => :label_not_contains,
202 "=p" => :label_any_issues_in_project,
202 "=p" => :label_any_issues_in_project,
203 "=!p" => :label_any_issues_not_in_project,
203 "=!p" => :label_any_issues_not_in_project,
204 "!p" => :label_no_issues_in_project
204 "!p" => :label_no_issues_in_project
205 }
205 }
206
206
207 class_attribute :operators_by_filter_type
207 class_attribute :operators_by_filter_type
208 self.operators_by_filter_type = {
208 self.operators_by_filter_type = {
209 :list => [ "=", "!" ],
209 :list => [ "=", "!" ],
210 :list_status => [ "o", "=", "!", "c", "*" ],
210 :list_status => [ "o", "=", "!", "c", "*" ],
211 :list_optional => [ "=", "!", "!*", "*" ],
211 :list_optional => [ "=", "!", "!*", "*" ],
212 :list_subprojects => [ "*", "!*", "=" ],
212 :list_subprojects => [ "*", "!*", "=" ],
213 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
213 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
214 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
214 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
215 :string => [ "=", "~", "!", "!~", "!*", "*" ],
215 :string => [ "=", "~", "!", "!~", "!*", "*" ],
216 :text => [ "~", "!~", "!*", "*" ],
216 :text => [ "~", "!~", "!*", "*" ],
217 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
217 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
218 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
218 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
219 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
219 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
220 }
220 }
221
221
222 class_attribute :available_columns
222 class_attribute :available_columns
223 self.available_columns = []
223 self.available_columns = []
224
224
225 class_attribute :queried_class
225 class_attribute :queried_class
226
226
227 def queried_table_name
227 def queried_table_name
228 @queried_table_name ||= self.class.queried_class.table_name
228 @queried_table_name ||= self.class.queried_class.table_name
229 end
229 end
230
230
231 def initialize(attributes=nil, *args)
231 def initialize(attributes=nil, *args)
232 super attributes
232 super attributes
233 @is_for_all = project.nil?
233 @is_for_all = project.nil?
234 end
234 end
235
235
236 # Builds the query from the given params
236 # Builds the query from the given params
237 def build_from_params(params)
237 def build_from_params(params)
238 if params[:fields] || params[:f]
238 if params[:fields] || params[:f]
239 self.filters = {}
239 self.filters = {}
240 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
240 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
241 else
241 else
242 available_filters.keys.each do |field|
242 available_filters.keys.each do |field|
243 add_short_filter(field, params[field]) if params[field]
243 add_short_filter(field, params[field]) if params[field]
244 end
244 end
245 end
245 end
246 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
246 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
247 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
247 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
248 self
248 self
249 end
249 end
250
250
251 # Builds a new query from the given params and attributes
251 # Builds a new query from the given params and attributes
252 def self.build_from_params(params, attributes={})
252 def self.build_from_params(params, attributes={})
253 new(attributes).build_from_params(params)
253 new(attributes).build_from_params(params)
254 end
254 end
255
255
256 def validate_query_filters
256 def validate_query_filters
257 filters.each_key do |field|
257 filters.each_key do |field|
258 if values_for(field)
258 if values_for(field)
259 case type_for(field)
259 case type_for(field)
260 when :integer
260 when :integer
261 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
261 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
262 when :float
262 when :float
263 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
263 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
264 when :date, :date_past
264 when :date, :date_past
265 case operator_for(field)
265 case operator_for(field)
266 when "=", ">=", "<=", "><"
266 when "=", ">=", "<=", "><"
267 add_filter_error(field, :invalid) if values_for(field).detect {|v|
267 add_filter_error(field, :invalid) if values_for(field).detect {|v|
268 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
268 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
269 }
269 }
270 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
270 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
271 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
271 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
272 end
272 end
273 end
273 end
274 end
274 end
275
275
276 add_filter_error(field, :blank) unless
276 add_filter_error(field, :blank) unless
277 # filter requires one or more values
277 # filter requires one or more values
278 (values_for(field) and !values_for(field).first.blank?) or
278 (values_for(field) and !values_for(field).first.blank?) or
279 # filter doesn't require any value
279 # filter doesn't require any value
280 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
280 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
281 end if filters
281 end if filters
282 end
282 end
283
283
284 def add_filter_error(field, message)
284 def add_filter_error(field, message)
285 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
285 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
286 errors.add(:base, m)
286 errors.add(:base, m)
287 end
287 end
288
288
289 def editable_by?(user)
289 def editable_by?(user)
290 return false unless user
290 return false unless user
291 # Admin can edit them all and regular users can edit their private queries
291 # Admin can edit them all and regular users can edit their private queries
292 return true if user.admin? || (is_private? && self.user_id == user.id)
292 return true if user.admin? || (is_private? && self.user_id == user.id)
293 # Members can not edit public queries that are for all project (only admin is allowed to)
293 # Members can not edit public queries that are for all project (only admin is allowed to)
294 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
294 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
295 end
295 end
296
296
297 def trackers
297 def trackers
298 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
298 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
299 end
299 end
300
300
301 # Returns a hash of localized labels for all filter operators
301 # Returns a hash of localized labels for all filter operators
302 def self.operators_labels
302 def self.operators_labels
303 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
303 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
304 end
304 end
305
305
306 # Returns a representation of the available filters for JSON serialization
306 # Returns a representation of the available filters for JSON serialization
307 def available_filters_as_json
307 def available_filters_as_json
308 json = {}
308 json = {}
309 available_filters.each do |field, options|
309 available_filters.each do |field, options|
310 json[field] = options.slice(:type, :name, :values).stringify_keys
310 json[field] = options.slice(:type, :name, :values).stringify_keys
311 end
311 end
312 json
312 json
313 end
313 end
314
314
315 def all_projects
315 def all_projects
316 @all_projects ||= Project.visible.to_a
316 @all_projects ||= Project.visible.to_a
317 end
317 end
318
318
319 def all_projects_values
319 def all_projects_values
320 return @all_projects_values if @all_projects_values
320 return @all_projects_values if @all_projects_values
321
321
322 values = []
322 values = []
323 Project.project_tree(all_projects) do |p, level|
323 Project.project_tree(all_projects) do |p, level|
324 prefix = (level > 0 ? ('--' * level + ' ') : '')
324 prefix = (level > 0 ? ('--' * level + ' ') : '')
325 values << ["#{prefix}#{p.name}", p.id.to_s]
325 values << ["#{prefix}#{p.name}", p.id.to_s]
326 end
326 end
327 @all_projects_values = values
327 @all_projects_values = values
328 end
328 end
329
329
330 # Adds available filters
330 # Adds available filters
331 def initialize_available_filters
331 def initialize_available_filters
332 # implemented by sub-classes
332 # implemented by sub-classes
333 end
333 end
334 protected :initialize_available_filters
334 protected :initialize_available_filters
335
335
336 # Adds an available filter
336 # Adds an available filter
337 def add_available_filter(field, options)
337 def add_available_filter(field, options)
338 @available_filters ||= ActiveSupport::OrderedHash.new
338 @available_filters ||= ActiveSupport::OrderedHash.new
339 @available_filters[field] = options
339 @available_filters[field] = options
340 @available_filters
340 @available_filters
341 end
341 end
342
342
343 # Removes an available filter
343 # Removes an available filter
344 def delete_available_filter(field)
344 def delete_available_filter(field)
345 if @available_filters
345 if @available_filters
346 @available_filters.delete(field)
346 @available_filters.delete(field)
347 end
347 end
348 end
348 end
349
349
350 # Return a hash of available filters
350 # Return a hash of available filters
351 def available_filters
351 def available_filters
352 unless @available_filters
352 unless @available_filters
353 initialize_available_filters
353 initialize_available_filters
354 @available_filters.each do |field, options|
354 @available_filters.each do |field, options|
355 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
355 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
356 end
356 end
357 end
357 end
358 @available_filters
358 @available_filters
359 end
359 end
360
360
361 def add_filter(field, operator, values=nil)
361 def add_filter(field, operator, values=nil)
362 # values must be an array
362 # values must be an array
363 return unless values.nil? || values.is_a?(Array)
363 return unless values.nil? || values.is_a?(Array)
364 # check if field is defined as an available filter
364 # check if field is defined as an available filter
365 if available_filters.has_key? field
365 if available_filters.has_key? field
366 filter_options = available_filters[field]
366 filter_options = available_filters[field]
367 filters[field] = {:operator => operator, :values => (values || [''])}
367 filters[field] = {:operator => operator, :values => (values || [''])}
368 end
368 end
369 end
369 end
370
370
371 def add_short_filter(field, expression)
371 def add_short_filter(field, expression)
372 return unless expression && available_filters.has_key?(field)
372 return unless expression && available_filters.has_key?(field)
373 field_type = available_filters[field][:type]
373 field_type = available_filters[field][:type]
374 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
374 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
375 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
375 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
376 values = $1
376 values = $1
377 add_filter field, operator, values.present? ? values.split('|') : ['']
377 add_filter field, operator, values.present? ? values.split('|') : ['']
378 end || add_filter(field, '=', expression.split('|'))
378 end || add_filter(field, '=', expression.split('|'))
379 end
379 end
380
380
381 # Add multiple filters using +add_filter+
381 # Add multiple filters using +add_filter+
382 def add_filters(fields, operators, values)
382 def add_filters(fields, operators, values)
383 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
383 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
384 fields.each do |field|
384 fields.each do |field|
385 add_filter(field, operators[field], values && values[field])
385 add_filter(field, operators[field], values && values[field])
386 end
386 end
387 end
387 end
388 end
388 end
389
389
390 def has_filter?(field)
390 def has_filter?(field)
391 filters and filters[field]
391 filters and filters[field]
392 end
392 end
393
393
394 def type_for(field)
394 def type_for(field)
395 available_filters[field][:type] if available_filters.has_key?(field)
395 available_filters[field][:type] if available_filters.has_key?(field)
396 end
396 end
397
397
398 def operator_for(field)
398 def operator_for(field)
399 has_filter?(field) ? filters[field][:operator] : nil
399 has_filter?(field) ? filters[field][:operator] : nil
400 end
400 end
401
401
402 def values_for(field)
402 def values_for(field)
403 has_filter?(field) ? filters[field][:values] : nil
403 has_filter?(field) ? filters[field][:values] : nil
404 end
404 end
405
405
406 def value_for(field, index=0)
406 def value_for(field, index=0)
407 (values_for(field) || [])[index]
407 (values_for(field) || [])[index]
408 end
408 end
409
409
410 def label_for(field)
410 def label_for(field)
411 label = available_filters[field][:name] if available_filters.has_key?(field)
411 label = available_filters[field][:name] if available_filters.has_key?(field)
412 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
412 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
413 end
413 end
414
414
415 def self.add_available_column(column)
415 def self.add_available_column(column)
416 self.available_columns << (column) if column.is_a?(QueryColumn)
416 self.available_columns << (column) if column.is_a?(QueryColumn)
417 end
417 end
418
418
419 # Returns an array of columns that can be used to group the results
419 # Returns an array of columns that can be used to group the results
420 def groupable_columns
420 def groupable_columns
421 available_columns.select {|c| c.groupable}
421 available_columns.select {|c| c.groupable}
422 end
422 end
423
423
424 # Returns a Hash of columns and the key for sorting
424 # Returns a Hash of columns and the key for sorting
425 def sortable_columns
425 def sortable_columns
426 available_columns.inject({}) {|h, column|
426 available_columns.inject({}) {|h, column|
427 h[column.name.to_s] = column.sortable
427 h[column.name.to_s] = column.sortable
428 h
428 h
429 }
429 }
430 end
430 end
431
431
432 def columns
432 def columns
433 # preserve the column_names order
433 # preserve the column_names order
434 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
434 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
435 available_columns.find { |col| col.name == name }
435 available_columns.find { |col| col.name == name }
436 end.compact
436 end.compact
437 available_columns.select(&:frozen?) | cols
437 available_columns.select(&:frozen?) | cols
438 end
438 end
439
439
440 def inline_columns
440 def inline_columns
441 columns.select(&:inline?)
441 columns.select(&:inline?)
442 end
442 end
443
443
444 def block_columns
444 def block_columns
445 columns.reject(&:inline?)
445 columns.reject(&:inline?)
446 end
446 end
447
447
448 def available_inline_columns
448 def available_inline_columns
449 available_columns.select(&:inline?)
449 available_columns.select(&:inline?)
450 end
450 end
451
451
452 def available_block_columns
452 def available_block_columns
453 available_columns.reject(&:inline?)
453 available_columns.reject(&:inline?)
454 end
454 end
455
455
456 def default_columns_names
456 def default_columns_names
457 []
457 []
458 end
458 end
459
459
460 def column_names=(names)
460 def column_names=(names)
461 if names
461 if names
462 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
462 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
463 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
463 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
464 # Set column_names to nil if default columns
464 # Set column_names to nil if default columns
465 if names == default_columns_names
465 if names == default_columns_names
466 names = nil
466 names = nil
467 end
467 end
468 end
468 end
469 write_attribute(:column_names, names)
469 write_attribute(:column_names, names)
470 end
470 end
471
471
472 def has_column?(column)
472 def has_column?(column)
473 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
473 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
474 end
474 end
475
475
476 def has_custom_field_column?
476 def has_custom_field_column?
477 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
477 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
478 end
478 end
479
479
480 def has_default_columns?
480 def has_default_columns?
481 column_names.nil? || column_names.empty?
481 column_names.nil? || column_names.empty?
482 end
482 end
483
483
484 def sort_criteria=(arg)
484 def sort_criteria=(arg)
485 c = []
485 c = []
486 if arg.is_a?(Hash)
486 if arg.is_a?(Hash)
487 arg = arg.keys.sort.collect {|k| arg[k]}
487 arg = arg.keys.sort.collect {|k| arg[k]}
488 end
488 end
489 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
489 if arg
490 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
491 end
490 write_attribute(:sort_criteria, c)
492 write_attribute(:sort_criteria, c)
491 end
493 end
492
494
493 def sort_criteria
495 def sort_criteria
494 read_attribute(:sort_criteria) || []
496 read_attribute(:sort_criteria) || []
495 end
497 end
496
498
497 def sort_criteria_key(arg)
499 def sort_criteria_key(arg)
498 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
500 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
499 end
501 end
500
502
501 def sort_criteria_order(arg)
503 def sort_criteria_order(arg)
502 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
504 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
503 end
505 end
504
506
505 def sort_criteria_order_for(key)
507 def sort_criteria_order_for(key)
506 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
508 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
507 end
509 end
508
510
509 # Returns the SQL sort order that should be prepended for grouping
511 # Returns the SQL sort order that should be prepended for grouping
510 def group_by_sort_order
512 def group_by_sort_order
511 if grouped? && (column = group_by_column)
513 if grouped? && (column = group_by_column)
512 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
514 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
513 column.sortable.is_a?(Array) ?
515 column.sortable.is_a?(Array) ?
514 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
516 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
515 "#{column.sortable} #{order}"
517 "#{column.sortable} #{order}"
516 end
518 end
517 end
519 end
518
520
519 # Returns true if the query is a grouped query
521 # Returns true if the query is a grouped query
520 def grouped?
522 def grouped?
521 !group_by_column.nil?
523 !group_by_column.nil?
522 end
524 end
523
525
524 def group_by_column
526 def group_by_column
525 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
527 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
526 end
528 end
527
529
528 def group_by_statement
530 def group_by_statement
529 group_by_column.try(:groupable)
531 group_by_column.try(:groupable)
530 end
532 end
531
533
532 def project_statement
534 def project_statement
533 project_clauses = []
535 project_clauses = []
534 if project && !project.descendants.active.empty?
536 if project && !project.descendants.active.empty?
535 ids = [project.id]
537 ids = [project.id]
536 if has_filter?("subproject_id")
538 if has_filter?("subproject_id")
537 case operator_for("subproject_id")
539 case operator_for("subproject_id")
538 when '='
540 when '='
539 # include the selected subprojects
541 # include the selected subprojects
540 ids += values_for("subproject_id").each(&:to_i)
542 ids += values_for("subproject_id").each(&:to_i)
541 when '!*'
543 when '!*'
542 # main project only
544 # main project only
543 else
545 else
544 # all subprojects
546 # all subprojects
545 ids += project.descendants.collect(&:id)
547 ids += project.descendants.collect(&:id)
546 end
548 end
547 elsif Setting.display_subprojects_issues?
549 elsif Setting.display_subprojects_issues?
548 ids += project.descendants.collect(&:id)
550 ids += project.descendants.collect(&:id)
549 end
551 end
550 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
552 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
551 elsif project
553 elsif project
552 project_clauses << "#{Project.table_name}.id = %d" % project.id
554 project_clauses << "#{Project.table_name}.id = %d" % project.id
553 end
555 end
554 project_clauses.any? ? project_clauses.join(' AND ') : nil
556 project_clauses.any? ? project_clauses.join(' AND ') : nil
555 end
557 end
556
558
557 def statement
559 def statement
558 # filters clauses
560 # filters clauses
559 filters_clauses = []
561 filters_clauses = []
560 filters.each_key do |field|
562 filters.each_key do |field|
561 next if field == "subproject_id"
563 next if field == "subproject_id"
562 v = values_for(field).clone
564 v = values_for(field).clone
563 next unless v and !v.empty?
565 next unless v and !v.empty?
564 operator = operator_for(field)
566 operator = operator_for(field)
565
567
566 # "me" value substitution
568 # "me" value substitution
567 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
569 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
568 if v.delete("me")
570 if v.delete("me")
569 if User.current.logged?
571 if User.current.logged?
570 v.push(User.current.id.to_s)
572 v.push(User.current.id.to_s)
571 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
573 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
572 else
574 else
573 v.push("0")
575 v.push("0")
574 end
576 end
575 end
577 end
576 end
578 end
577
579
578 if field == 'project_id'
580 if field == 'project_id'
579 if v.delete('mine')
581 if v.delete('mine')
580 v += User.current.memberships.map(&:project_id).map(&:to_s)
582 v += User.current.memberships.map(&:project_id).map(&:to_s)
581 end
583 end
582 end
584 end
583
585
584 if field =~ /cf_(\d+)$/
586 if field =~ /cf_(\d+)$/
585 # custom field
587 # custom field
586 filters_clauses << sql_for_custom_field(field, operator, v, $1)
588 filters_clauses << sql_for_custom_field(field, operator, v, $1)
587 elsif respond_to?("sql_for_#{field}_field")
589 elsif respond_to?("sql_for_#{field}_field")
588 # specific statement
590 # specific statement
589 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
591 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
590 else
592 else
591 # regular field
593 # regular field
592 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
594 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
593 end
595 end
594 end if filters and valid?
596 end if filters and valid?
595
597
596 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
598 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
597 # Excludes results for which the grouped custom field is not visible
599 # Excludes results for which the grouped custom field is not visible
598 filters_clauses << c.custom_field.visibility_by_project_condition
600 filters_clauses << c.custom_field.visibility_by_project_condition
599 end
601 end
600
602
601 filters_clauses << project_statement
603 filters_clauses << project_statement
602 filters_clauses.reject!(&:blank?)
604 filters_clauses.reject!(&:blank?)
603
605
604 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
606 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
605 end
607 end
606
608
607 private
609 private
608
610
609 def sql_for_custom_field(field, operator, value, custom_field_id)
611 def sql_for_custom_field(field, operator, value, custom_field_id)
610 db_table = CustomValue.table_name
612 db_table = CustomValue.table_name
611 db_field = 'value'
613 db_field = 'value'
612 filter = @available_filters[field]
614 filter = @available_filters[field]
613 return nil unless filter
615 return nil unless filter
614 if filter[:field].format.target_class && filter[:field].format.target_class <= User
616 if filter[:field].format.target_class && filter[:field].format.target_class <= User
615 if value.delete('me')
617 if value.delete('me')
616 value.push User.current.id.to_s
618 value.push User.current.id.to_s
617 end
619 end
618 end
620 end
619 not_in = nil
621 not_in = nil
620 if operator == '!'
622 if operator == '!'
621 # Makes ! operator work for custom fields with multiple values
623 # Makes ! operator work for custom fields with multiple values
622 operator = '='
624 operator = '='
623 not_in = 'NOT'
625 not_in = 'NOT'
624 end
626 end
625 customized_key = "id"
627 customized_key = "id"
626 customized_class = queried_class
628 customized_class = queried_class
627 if field =~ /^(.+)\.cf_/
629 if field =~ /^(.+)\.cf_/
628 assoc = $1
630 assoc = $1
629 customized_key = "#{assoc}_id"
631 customized_key = "#{assoc}_id"
630 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
632 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
631 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
633 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
632 end
634 end
633 where = sql_for_field(field, operator, value, db_table, db_field, true)
635 where = sql_for_field(field, operator, value, db_table, db_field, true)
634 if operator =~ /[<>]/
636 if operator =~ /[<>]/
635 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
637 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
636 end
638 end
637 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
639 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
638 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
640 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
639 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
641 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
640 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
642 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
641 end
643 end
642
644
643 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
645 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
644 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
646 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
645 sql = ''
647 sql = ''
646 case operator
648 case operator
647 when "="
649 when "="
648 if value.any?
650 if value.any?
649 case type_for(field)
651 case type_for(field)
650 when :date, :date_past
652 when :date, :date_past
651 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
653 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
652 when :integer
654 when :integer
653 if is_custom_filter
655 if is_custom_filter
654 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
656 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
655 else
657 else
656 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
658 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
657 end
659 end
658 when :float
660 when :float
659 if is_custom_filter
661 if is_custom_filter
660 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
662 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
661 else
663 else
662 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
664 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
663 end
665 end
664 else
666 else
665 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
667 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
666 end
668 end
667 else
669 else
668 # IN an empty set
670 # IN an empty set
669 sql = "1=0"
671 sql = "1=0"
670 end
672 end
671 when "!"
673 when "!"
672 if value.any?
674 if value.any?
673 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
675 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
674 else
676 else
675 # NOT IN an empty set
677 # NOT IN an empty set
676 sql = "1=1"
678 sql = "1=1"
677 end
679 end
678 when "!*"
680 when "!*"
679 sql = "#{db_table}.#{db_field} IS NULL"
681 sql = "#{db_table}.#{db_field} IS NULL"
680 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
682 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
681 when "*"
683 when "*"
682 sql = "#{db_table}.#{db_field} IS NOT NULL"
684 sql = "#{db_table}.#{db_field} IS NOT NULL"
683 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
685 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
684 when ">="
686 when ">="
685 if [:date, :date_past].include?(type_for(field))
687 if [:date, :date_past].include?(type_for(field))
686 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
688 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
687 else
689 else
688 if is_custom_filter
690 if is_custom_filter
689 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
691 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
690 else
692 else
691 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
693 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
692 end
694 end
693 end
695 end
694 when "<="
696 when "<="
695 if [:date, :date_past].include?(type_for(field))
697 if [:date, :date_past].include?(type_for(field))
696 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
698 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
697 else
699 else
698 if is_custom_filter
700 if is_custom_filter
699 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
701 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
700 else
702 else
701 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
703 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
702 end
704 end
703 end
705 end
704 when "><"
706 when "><"
705 if [:date, :date_past].include?(type_for(field))
707 if [:date, :date_past].include?(type_for(field))
706 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
708 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
707 else
709 else
708 if is_custom_filter
710 if is_custom_filter
709 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
711 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
710 else
712 else
711 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
713 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
712 end
714 end
713 end
715 end
714 when "o"
716 when "o"
715 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
717 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
716 when "c"
718 when "c"
717 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
719 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
718 when "><t-"
720 when "><t-"
719 # between today - n days and today
721 # between today - n days and today
720 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
722 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
721 when ">t-"
723 when ">t-"
722 # >= today - n days
724 # >= today - n days
723 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
725 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
724 when "<t-"
726 when "<t-"
725 # <= today - n days
727 # <= today - n days
726 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
728 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
727 when "t-"
729 when "t-"
728 # = n days in past
730 # = n days in past
729 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
731 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
730 when "><t+"
732 when "><t+"
731 # between today and today + n days
733 # between today and today + n days
732 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
734 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
733 when ">t+"
735 when ">t+"
734 # >= today + n days
736 # >= today + n days
735 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
737 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
736 when "<t+"
738 when "<t+"
737 # <= today + n days
739 # <= today + n days
738 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
740 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
739 when "t+"
741 when "t+"
740 # = today + n days
742 # = today + n days
741 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
743 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
742 when "t"
744 when "t"
743 # = today
745 # = today
744 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
746 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
745 when "ld"
747 when "ld"
746 # = yesterday
748 # = yesterday
747 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
749 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
748 when "w"
750 when "w"
749 # = this week
751 # = this week
750 first_day_of_week = l(:general_first_day_of_week).to_i
752 first_day_of_week = l(:general_first_day_of_week).to_i
751 day_of_week = Date.today.cwday
753 day_of_week = Date.today.cwday
752 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
754 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
753 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
755 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
754 when "lw"
756 when "lw"
755 # = last week
757 # = last week
756 first_day_of_week = l(:general_first_day_of_week).to_i
758 first_day_of_week = l(:general_first_day_of_week).to_i
757 day_of_week = Date.today.cwday
759 day_of_week = Date.today.cwday
758 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
760 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
759 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
761 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
760 when "l2w"
762 when "l2w"
761 # = last 2 weeks
763 # = last 2 weeks
762 first_day_of_week = l(:general_first_day_of_week).to_i
764 first_day_of_week = l(:general_first_day_of_week).to_i
763 day_of_week = Date.today.cwday
765 day_of_week = Date.today.cwday
764 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
766 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
765 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
767 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
766 when "m"
768 when "m"
767 # = this month
769 # = this month
768 date = Date.today
770 date = Date.today
769 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
771 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
770 when "lm"
772 when "lm"
771 # = last month
773 # = last month
772 date = Date.today.prev_month
774 date = Date.today.prev_month
773 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
775 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
774 when "y"
776 when "y"
775 # = this year
777 # = this year
776 date = Date.today
778 date = Date.today
777 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
779 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
778 when "~"
780 when "~"
779 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
781 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
780 when "!~"
782 when "!~"
781 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
783 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
782 else
784 else
783 raise "Unknown query operator #{operator}"
785 raise "Unknown query operator #{operator}"
784 end
786 end
785
787
786 return sql
788 return sql
787 end
789 end
788
790
789 # Adds a filter for the given custom field
791 # Adds a filter for the given custom field
790 def add_custom_field_filter(field, assoc=nil)
792 def add_custom_field_filter(field, assoc=nil)
791 options = field.format.query_filter_options(field, self)
793 options = field.format.query_filter_options(field, self)
792 if field.format.target_class && field.format.target_class <= User
794 if field.format.target_class && field.format.target_class <= User
793 if options[:values].is_a?(Array) && User.current.logged?
795 if options[:values].is_a?(Array) && User.current.logged?
794 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
796 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
795 end
797 end
796 end
798 end
797
799
798 filter_id = "cf_#{field.id}"
800 filter_id = "cf_#{field.id}"
799 filter_name = field.name
801 filter_name = field.name
800 if assoc.present?
802 if assoc.present?
801 filter_id = "#{assoc}.#{filter_id}"
803 filter_id = "#{assoc}.#{filter_id}"
802 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
804 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
803 end
805 end
804 add_available_filter filter_id, options.merge({
806 add_available_filter filter_id, options.merge({
805 :name => filter_name,
807 :name => filter_name,
806 :field => field
808 :field => field
807 })
809 })
808 end
810 end
809
811
810 # Adds filters for the given custom fields scope
812 # Adds filters for the given custom fields scope
811 def add_custom_fields_filters(scope, assoc=nil)
813 def add_custom_fields_filters(scope, assoc=nil)
812 scope.visible.where(:is_filter => true).sorted.each do |field|
814 scope.visible.where(:is_filter => true).sorted.each do |field|
813 add_custom_field_filter(field, assoc)
815 add_custom_field_filter(field, assoc)
814 end
816 end
815 end
817 end
816
818
817 # Adds filters for the given associations custom fields
819 # Adds filters for the given associations custom fields
818 def add_associations_custom_fields_filters(*associations)
820 def add_associations_custom_fields_filters(*associations)
819 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
821 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
820 associations.each do |assoc|
822 associations.each do |assoc|
821 association_klass = queried_class.reflect_on_association(assoc).klass
823 association_klass = queried_class.reflect_on_association(assoc).klass
822 fields_by_class.each do |field_class, fields|
824 fields_by_class.each do |field_class, fields|
823 if field_class.customized_class <= association_klass
825 if field_class.customized_class <= association_klass
824 fields.sort.each do |field|
826 fields.sort.each do |field|
825 add_custom_field_filter(field, assoc)
827 add_custom_field_filter(field, assoc)
826 end
828 end
827 end
829 end
828 end
830 end
829 end
831 end
830 end
832 end
831
833
832 def quoted_time(time, is_custom_filter)
834 def quoted_time(time, is_custom_filter)
833 if is_custom_filter
835 if is_custom_filter
834 # Custom field values are stored as strings in the DB
836 # Custom field values are stored as strings in the DB
835 # using this format that does not depend on DB date representation
837 # using this format that does not depend on DB date representation
836 time.strftime("%Y-%m-%d %H:%M:%S")
838 time.strftime("%Y-%m-%d %H:%M:%S")
837 else
839 else
838 self.class.connection.quoted_date(time)
840 self.class.connection.quoted_date(time)
839 end
841 end
840 end
842 end
841
843
842 # Returns a SQL clause for a date or datetime field.
844 # Returns a SQL clause for a date or datetime field.
843 def date_clause(table, field, from, to, is_custom_filter)
845 def date_clause(table, field, from, to, is_custom_filter)
844 s = []
846 s = []
845 if from
847 if from
846 if from.is_a?(Date)
848 if from.is_a?(Date)
847 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
849 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
848 else
850 else
849 from = from - 1 # second
851 from = from - 1 # second
850 end
852 end
851 if self.class.default_timezone == :utc
853 if self.class.default_timezone == :utc
852 from = from.utc
854 from = from.utc
853 end
855 end
854 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
856 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
855 end
857 end
856 if to
858 if to
857 if to.is_a?(Date)
859 if to.is_a?(Date)
858 to = Time.local(to.year, to.month, to.day).end_of_day
860 to = Time.local(to.year, to.month, to.day).end_of_day
859 end
861 end
860 if self.class.default_timezone == :utc
862 if self.class.default_timezone == :utc
861 to = to.utc
863 to = to.utc
862 end
864 end
863 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
865 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
864 end
866 end
865 s.join(' AND ')
867 s.join(' AND ')
866 end
868 end
867
869
868 # Returns a SQL clause for a date or datetime field using relative dates.
870 # Returns a SQL clause for a date or datetime field using relative dates.
869 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
871 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
870 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
872 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
871 end
873 end
872
874
873 # Returns a Date or Time from the given filter value
875 # Returns a Date or Time from the given filter value
874 def parse_date(arg)
876 def parse_date(arg)
875 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
877 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
876 Time.parse(arg) rescue nil
878 Time.parse(arg) rescue nil
877 else
879 else
878 Date.parse(arg) rescue nil
880 Date.parse(arg) rescue nil
879 end
881 end
880 end
882 end
881
883
882 # Additional joins required for the given sort options
884 # Additional joins required for the given sort options
883 def joins_for_order_statement(order_options)
885 def joins_for_order_statement(order_options)
884 joins = []
886 joins = []
885
887
886 if order_options
888 if order_options
887 if order_options.include?('authors')
889 if order_options.include?('authors')
888 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
890 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
889 end
891 end
890 order_options.scan(/cf_\d+/).uniq.each do |name|
892 order_options.scan(/cf_\d+/).uniq.each do |name|
891 column = available_columns.detect {|c| c.name.to_s == name}
893 column = available_columns.detect {|c| c.name.to_s == name}
892 join = column && column.custom_field.join_for_order_statement
894 join = column && column.custom_field.join_for_order_statement
893 if join
895 if join
894 joins << join
896 joins << join
895 end
897 end
896 end
898 end
897 end
899 end
898
900
899 joins.any? ? joins.join(' ') : nil
901 joins.any? ? joins.join(' ') : nil
900 end
902 end
901 end
903 end
@@ -1,87 +1,89
1 <%= error_messages_for 'query' %>
1 <%= error_messages_for 'query' %>
2
2
3 <div class="box">
3 <div class="box">
4 <div class="tabular">
4 <div class="tabular">
5 <%= hidden_field_tag 'gantt', '1' if params[:gantt] %>
5 <%= hidden_field_tag 'gantt', '1' if params[:gantt] %>
6
6
7 <p><label for="query_name"><%=l(:field_name)%></label>
7 <p><label for="query_name"><%=l(:field_name)%></label>
8 <%= text_field 'query', 'name', :size => 80 %></p>
8 <%= text_field 'query', 'name', :size => 80 %></p>
9
9
10 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
10 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @query.project) %>
11 <p><label><%=l(:field_visible)%></label>
11 <p><label><%=l(:field_visible)%></label>
12 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PRIVATE %> <%= l(:label_visibility_private) %></label>
12 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PRIVATE %> <%= l(:label_visibility_private) %></label>
13 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PUBLIC %> <%= l(:label_visibility_public) %></label>
13 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label>
14 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label>
14 <% Role.givable.sorted.each do |role| %>
15 <% Role.givable.sorted.each do |role| %>
15 <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label>
16 <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label>
16 <% end %>
17 <% end %>
17 <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PUBLIC %> <%= l(:label_visibility_public) %></label>
18 <%= hidden_field_tag 'query[role_ids][]', '' %>
18 <%= hidden_field_tag 'query[role_ids][]', '' %>
19 </p>
19 </p>
20 <% end %>
20 <% end %>
21
21
22 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
22 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
23 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
23 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :class => (User.current.admin? ? '' : 'disable-unless-private') %></p>
24 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
25
24
26 <% unless params[:gantt] %>
25 <% unless params[:gantt] %>
27 <fieldset><legend><%= l(:label_options) %></legend>
26 <fieldset><legend><%= l(:label_options) %></legend>
28 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
27 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
29 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
28 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
30 :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p>
29 :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p>
31
30
32 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
31 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
33 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
32 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
34
33
35 <p><label><%= l(:button_show) %></label>
34 <p><label><%= l(:button_show) %></label>
36 <%= available_block_columns_tags(@query) %></p>
35 <%= available_block_columns_tags(@query) %></p>
37 </fieldset>
36 </fieldset>
38 <% else %>
37 <% else %>
39 <fieldset><legend><%= l(:label_options) %></legend>
38 <fieldset><legend><%= l(:label_options) %></legend>
40 <p><label><%= l(:button_show) %></label>
39 <p><label><%= l(:button_show) %></label>
41 <label class="inline"><%= check_box_tag "query[draw_relations]", "1", @query.draw_relations %> <%= l(:label_related_issues) %></label>
40 <label class="inline"><%= check_box_tag "query[draw_relations]", "1", @query.draw_relations %> <%= l(:label_related_issues) %></label>
42 <label class="inline"><%= check_box_tag "query[draw_progress_line]", "1", @query.draw_progress_line %> <%= l(:label_gantt_progress_line) %></label>
41 <label class="inline"><%= check_box_tag "query[draw_progress_line]", "1", @query.draw_progress_line %> <%= l(:label_gantt_progress_line) %></label>
43 </p>
42 </p>
44 </fieldset>
43 </fieldset>
45 <% end %>
44 <% end %>
46 </div>
45 </div>
47
46
48 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
47 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
49 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
48 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
50 </fieldset>
49 </fieldset>
51
50
52 <% unless params[:gantt] %>
51 <% unless params[:gantt] %>
53 <fieldset><legend><%= l(:label_sort) %></legend>
52 <fieldset><legend><%= l(:label_sort) %></legend>
54 <% 3.times do |i| %>
53 <% 3.times do |i| %>
55 <%= i+1 %>:
54 <%= i+1 %>:
56 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
55 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
57 l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %>
56 l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %>
58 <%= select_tag("query[sort_criteria][#{i}][]",
57 <%= select_tag("query[sort_criteria][#{i}][]",
59 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)),
58 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)),
60 :id => "query_sort_criteria_attribute_" + i.to_s)%>
59 :id => "query_sort_criteria_attribute_" + i.to_s)%>
61 <%= label_tag "query_sort_criteria_direction_" + i.to_s,
60 <%= label_tag "query_sort_criteria_direction_" + i.to_s,
62 l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
61 l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
63 <%= select_tag("query[sort_criteria][#{i}][]",
62 <%= select_tag("query[sort_criteria][#{i}][]",
64 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
63 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
65 :id => "query_sort_criteria_direction_" + i.to_s) %>
64 :id => "query_sort_criteria_direction_" + i.to_s) %>
66 <br />
65 <br />
67 <% end %>
66 <% end %>
68 </fieldset>
67 </fieldset>
69 <% end %>
68 <% end %>
70
69
71 <% unless params[:gantt] %>
70 <% unless params[:gantt] %>
72 <%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
71 <%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
73 <legend><%= l(:field_column_names) %></legend>
72 <legend><%= l(:field_column_names) %></legend>
74 <%= render_query_columns_selection(query) %>
73 <%= render_query_columns_selection(query) %>
75 <% end %>
74 <% end %>
76 <% end %>
75 <% end %>
77
76
78 </div>
77 </div>
79
78
80 <%= javascript_tag do %>
79 <%= javascript_tag do %>
81 $(document).ready(function(){
80 $(document).ready(function(){
82 $("input[name='query[visibility]']").change(function(){
81 $("input[name='query[visibility]']").change(function(){
83 var checked = $('#query_visibility_1').is(':checked');
82 var roles_checked = $('#query_visibility_1').is(':checked');
84 $("input[name='query[role_ids][]'][type=checkbox]").attr('disabled', !checked);
83 var private_checked = $('#query_visibility_0').is(':checked');
84 $("input[name='query[role_ids][]'][type=checkbox]").attr('disabled', !roles_checked);
85 if (!private_checked) $("input.disable-unless-private").attr('checked', false);
86 $("input.disable-unless-private").attr('disabled', !private_checked);
85 }).trigger('change');
87 }).trigger('change');
86 });
88 });
87 <% end %>
89 <% end %>
@@ -1,297 +1,358
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueriesControllerTest < ActionController::TestCase
20 class QueriesControllerTest < ActionController::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
21 fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules
22
22
23 def setup
23 def setup
24 User.current = nil
24 User.current = nil
25 end
25 end
26
26
27 def test_index
27 def test_index
28 get :index
28 get :index
29 # HTML response not implemented
29 # HTML response not implemented
30 assert_response 406
30 assert_response 406
31 end
31 end
32
32
33 def test_new_project_query
33 def test_new_project_query
34 @request.session[:user_id] = 2
34 @request.session[:user_id] = 2
35 get :new, :project_id => 1
35 get :new, :project_id => 1
36 assert_response :success
36 assert_response :success
37 assert_template 'new'
37 assert_template 'new'
38 assert_select 'input[name=?][value="0"][checked=checked]', 'query[visibility]'
38 assert_select 'input[name=?][value="0"][checked=checked]', 'query[visibility]'
39 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])'
39 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])'
40 assert_select 'select[name=?]', 'c[]' do
40 assert_select 'select[name=?]', 'c[]' do
41 assert_select 'option[value=tracker]'
41 assert_select 'option[value=tracker]'
42 assert_select 'option[value=subject]'
42 assert_select 'option[value=subject]'
43 end
43 end
44 end
44 end
45
45
46 def test_new_global_query
46 def test_new_global_query
47 @request.session[:user_id] = 2
47 @request.session[:user_id] = 2
48 get :new
48 get :new
49 assert_response :success
49 assert_response :success
50 assert_template 'new'
50 assert_template 'new'
51 assert_select 'input[name=?]', 'query[visibility]', 0
51 assert_select 'input[name=?]', 'query[visibility]', 0
52 assert_select 'input[name=query_is_for_all][type=checkbox][checked]:not([disabled])'
52 assert_select 'input[name=query_is_for_all][type=checkbox][checked]:not([disabled])'
53 end
53 end
54
54
55 def test_new_on_invalid_project
55 def test_new_on_invalid_project
56 @request.session[:user_id] = 2
56 @request.session[:user_id] = 2
57 get :new, :project_id => 'invalid'
57 get :new, :project_id => 'invalid'
58 assert_response 404
58 assert_response 404
59 end
59 end
60
60
61 def test_create_project_public_query
61 def test_create_project_public_query
62 @request.session[:user_id] = 2
62 @request.session[:user_id] = 2
63 post :create,
63 post :create,
64 :project_id => 'ecookbook',
64 :project_id => 'ecookbook',
65 :default_columns => '1',
65 :default_columns => '1',
66 :f => ["status_id", "assigned_to_id"],
66 :f => ["status_id", "assigned_to_id"],
67 :op => {"assigned_to_id" => "=", "status_id" => "o"},
67 :op => {"assigned_to_id" => "=", "status_id" => "o"},
68 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
68 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
69 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
69 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
70
70
71 q = Query.find_by_name('test_new_project_public_query')
71 q = Query.find_by_name('test_new_project_public_query')
72 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
72 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
73 assert q.is_public?
73 assert q.is_public?
74 assert q.has_default_columns?
74 assert q.has_default_columns?
75 assert q.valid?
75 assert q.valid?
76 end
76 end
77
77
78 def test_create_project_private_query
78 def test_create_project_private_query
79 @request.session[:user_id] = 3
79 @request.session[:user_id] = 3
80 post :create,
80 post :create,
81 :project_id => 'ecookbook',
81 :project_id => 'ecookbook',
82 :default_columns => '1',
82 :default_columns => '1',
83 :fields => ["status_id", "assigned_to_id"],
83 :fields => ["status_id", "assigned_to_id"],
84 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
84 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
85 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
85 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
86 :query => {"name" => "test_new_project_private_query", "visibility" => "2"}
86 :query => {"name" => "test_new_project_private_query", "visibility" => "0"}
87
87
88 q = Query.find_by_name('test_new_project_private_query')
88 q = Query.find_by_name('test_new_project_private_query')
89 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
89 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
90 assert !q.is_public?
90 assert !q.is_public?
91 assert q.has_default_columns?
91 assert q.has_default_columns?
92 assert q.valid?
92 assert q.valid?
93 end
93 end
94
94
95 def test_create_global_private_query_with_custom_columns
95 def test_create_global_private_query_with_custom_columns
96 @request.session[:user_id] = 3
96 @request.session[:user_id] = 3
97 post :create,
97 post :create,
98 :fields => ["status_id", "assigned_to_id"],
98 :fields => ["status_id", "assigned_to_id"],
99 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
99 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
100 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
100 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
101 :query => {"name" => "test_new_global_private_query", "visibility" => "2"},
101 :query => {"name" => "test_new_global_private_query", "visibility" => "0"},
102 :c => ["", "tracker", "subject", "priority", "category"]
102 :c => ["", "tracker", "subject", "priority", "category"]
103
103
104 q = Query.find_by_name('test_new_global_private_query')
104 q = Query.find_by_name('test_new_global_private_query')
105 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
105 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
106 assert !q.is_public?
106 assert !q.is_public?
107 assert !q.has_default_columns?
107 assert !q.has_default_columns?
108 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
108 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
109 assert q.valid?
109 assert q.valid?
110 end
110 end
111
111
112 def test_create_global_query_with_custom_filters
112 def test_create_global_query_with_custom_filters
113 @request.session[:user_id] = 3
113 @request.session[:user_id] = 3
114 post :create,
114 post :create,
115 :fields => ["assigned_to_id"],
115 :fields => ["assigned_to_id"],
116 :operators => {"assigned_to_id" => "="},
116 :operators => {"assigned_to_id" => "="},
117 :values => { "assigned_to_id" => ["me"]},
117 :values => { "assigned_to_id" => ["me"]},
118 :query => {"name" => "test_new_global_query"}
118 :query => {"name" => "test_new_global_query"}
119
119
120 q = Query.find_by_name('test_new_global_query')
120 q = Query.find_by_name('test_new_global_query')
121 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
121 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
122 assert !q.is_public?
122 assert !q.has_filter?(:status_id)
123 assert !q.has_filter?(:status_id)
123 assert_equal ['assigned_to_id'], q.filters.keys
124 assert_equal ['assigned_to_id'], q.filters.keys
124 assert q.valid?
125 assert q.valid?
125 end
126 end
126
127
127 def test_create_with_sort
128 def test_create_with_sort
128 @request.session[:user_id] = 1
129 @request.session[:user_id] = 1
129 post :create,
130 post :create,
130 :default_columns => '1',
131 :default_columns => '1',
131 :operators => {"status_id" => "o"},
132 :operators => {"status_id" => "o"},
132 :values => {"status_id" => ["1"]},
133 :values => {"status_id" => ["1"]},
133 :query => {:name => "test_new_with_sort",
134 :query => {:name => "test_new_with_sort",
134 :visibility => "2",
135 :visibility => "2",
135 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
136 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
136
137
137 query = Query.find_by_name("test_new_with_sort")
138 query = Query.find_by_name("test_new_with_sort")
138 assert_not_nil query
139 assert_not_nil query
139 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
140 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
140 end
141 end
141
142
142 def test_create_with_failure
143 def test_create_with_failure
143 @request.session[:user_id] = 2
144 @request.session[:user_id] = 2
144 assert_no_difference '::Query.count' do
145 assert_no_difference '::Query.count' do
145 post :create, :project_id => 'ecookbook', :query => {:name => ''}
146 post :create, :project_id => 'ecookbook', :query => {:name => ''}
146 end
147 end
147 assert_response :success
148 assert_response :success
148 assert_template 'new'
149 assert_template 'new'
149 assert_select 'input[name=?]', 'query[name]'
150 assert_select 'input[name=?]', 'query[name]'
150 end
151 end
151
152
152 def test_create_global_query_from_gantt
153 def test_create_global_query_from_gantt
153 @request.session[:user_id] = 1
154 @request.session[:user_id] = 1
154 assert_difference 'IssueQuery.count' do
155 assert_difference 'IssueQuery.count' do
155 post :create,
156 post :create,
156 :gantt => 1,
157 :gantt => 1,
157 :operators => {"status_id" => "o"},
158 :operators => {"status_id" => "o"},
158 :values => {"status_id" => ["1"]},
159 :values => {"status_id" => ["1"]},
159 :query => {:name => "test_create_from_gantt",
160 :query => {:name => "test_create_from_gantt",
160 :draw_relations => '1',
161 :draw_relations => '1',
161 :draw_progress_line => '1'}
162 :draw_progress_line => '1'}
162 assert_response 302
163 assert_response 302
163 end
164 end
164 query = IssueQuery.order('id DESC').first
165 query = IssueQuery.order('id DESC').first
165 assert_redirected_to "/issues/gantt?query_id=#{query.id}"
166 assert_redirected_to "/issues/gantt?query_id=#{query.id}"
166 assert_equal true, query.draw_relations
167 assert_equal true, query.draw_relations
167 assert_equal true, query.draw_progress_line
168 assert_equal true, query.draw_progress_line
168 end
169 end
169
170
170 def test_create_project_query_from_gantt
171 def test_create_project_query_from_gantt
171 @request.session[:user_id] = 1
172 @request.session[:user_id] = 1
172 assert_difference 'IssueQuery.count' do
173 assert_difference 'IssueQuery.count' do
173 post :create,
174 post :create,
174 :project_id => 'ecookbook',
175 :project_id => 'ecookbook',
175 :gantt => 1,
176 :gantt => 1,
176 :operators => {"status_id" => "o"},
177 :operators => {"status_id" => "o"},
177 :values => {"status_id" => ["1"]},
178 :values => {"status_id" => ["1"]},
178 :query => {:name => "test_create_from_gantt",
179 :query => {:name => "test_create_from_gantt",
179 :draw_relations => '0',
180 :draw_relations => '0',
180 :draw_progress_line => '0'}
181 :draw_progress_line => '0'}
181 assert_response 302
182 assert_response 302
182 end
183 end
183 query = IssueQuery.order('id DESC').first
184 query = IssueQuery.order('id DESC').first
184 assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}"
185 assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}"
185 assert_equal false, query.draw_relations
186 assert_equal false, query.draw_relations
186 assert_equal false, query.draw_progress_line
187 assert_equal false, query.draw_progress_line
187 end
188 end
188
189
190 def test_create_project_public_query_should_force_private_without_manage_public_queries_permission
191 @request.session[:user_id] = 3
192 query = new_record(Query) do
193 post :create,
194 :project_id => 'ecookbook',
195 :query => {"name" => "name", "visibility" => "2"}
196 assert_response 302
197 end
198 assert_not_nil query.project
199 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
200 end
201
202 def test_create_global_public_query_should_force_private_without_manage_public_queries_permission
203 @request.session[:user_id] = 3
204 query = new_record(Query) do
205 post :create,
206 :project_id => 'ecookbook', :query_is_for_all => '1',
207 :query => {"name" => "name", "visibility" => "2"}
208 assert_response 302
209 end
210 assert_nil query.project
211 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
212 end
213
214 def test_create_project_public_query_with_manage_public_queries_permission
215 @request.session[:user_id] = 2
216 query = new_record(Query) do
217 post :create,
218 :project_id => 'ecookbook',
219 :query => {"name" => "name", "visibility" => "2"}
220 assert_response 302
221 end
222 assert_not_nil query.project
223 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
224 end
225
226 def test_create_global_public_query_should_force_private_with_manage_public_queries_permission
227 @request.session[:user_id] = 2
228 query = new_record(Query) do
229 post :create,
230 :project_id => 'ecookbook', :query_is_for_all => '1',
231 :query => {"name" => "name", "visibility" => "2"}
232 assert_response 302
233 end
234 assert_nil query.project
235 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
236 end
237
238 def test_create_global_public_query_by_admin
239 @request.session[:user_id] = 1
240 query = new_record(Query) do
241 post :create,
242 :project_id => 'ecookbook', :query_is_for_all => '1',
243 :query => {"name" => "name", "visibility" => "2"}
244 assert_response 302
245 end
246 assert_nil query.project
247 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
248 end
249
189 def test_edit_global_public_query
250 def test_edit_global_public_query
190 @request.session[:user_id] = 1
251 @request.session[:user_id] = 1
191 get :edit, :id => 4
252 get :edit, :id => 4
192 assert_response :success
253 assert_response :success
193 assert_template 'edit'
254 assert_template 'edit'
194 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
255 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
195 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked][disabled=disabled]'
256 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
196 end
257 end
197
258
198 def test_edit_global_private_query
259 def test_edit_global_private_query
199 @request.session[:user_id] = 3
260 @request.session[:user_id] = 3
200 get :edit, :id => 3
261 get :edit, :id => 3
201 assert_response :success
262 assert_response :success
202 assert_template 'edit'
263 assert_template 'edit'
203 assert_select 'input[name=?]', 'query[visibility]', 0
264 assert_select 'input[name=?]', 'query[visibility]', 0
204 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked][disabled=disabled]'
265 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
205 end
266 end
206
267
207 def test_edit_project_private_query
268 def test_edit_project_private_query
208 @request.session[:user_id] = 3
269 @request.session[:user_id] = 3
209 get :edit, :id => 2
270 get :edit, :id => 2
210 assert_response :success
271 assert_response :success
211 assert_template 'edit'
272 assert_template 'edit'
212 assert_select 'input[name=?]', 'query[visibility]', 0
273 assert_select 'input[name=?]', 'query[visibility]', 0
213 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])'
274 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
214 end
275 end
215
276
216 def test_edit_project_public_query
277 def test_edit_project_public_query
217 @request.session[:user_id] = 2
278 @request.session[:user_id] = 2
218 get :edit, :id => 1
279 get :edit, :id => 1
219 assert_response :success
280 assert_response :success
220 assert_template 'edit'
281 assert_template 'edit'
221 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
282 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
222 assert_select 'input[name=query_is_for_all][type=checkbox][disabled=disabled]:not([checked])'
283 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
223 end
284 end
224
285
225 def test_edit_sort_criteria
286 def test_edit_sort_criteria
226 @request.session[:user_id] = 1
287 @request.session[:user_id] = 1
227 get :edit, :id => 5
288 get :edit, :id => 5
228 assert_response :success
289 assert_response :success
229 assert_template 'edit'
290 assert_template 'edit'
230 assert_select 'select[name=?]', 'query[sort_criteria][0][]' do
291 assert_select 'select[name=?]', 'query[sort_criteria][0][]' do
231 assert_select 'option[value=priority][selected=selected]'
292 assert_select 'option[value=priority][selected=selected]'
232 assert_select 'option[value=desc][selected=selected]'
293 assert_select 'option[value=desc][selected=selected]'
233 end
294 end
234 end
295 end
235
296
236 def test_edit_invalid_query
297 def test_edit_invalid_query
237 @request.session[:user_id] = 2
298 @request.session[:user_id] = 2
238 get :edit, :id => 99
299 get :edit, :id => 99
239 assert_response 404
300 assert_response 404
240 end
301 end
241
302
242 def test_udpate_global_private_query
303 def test_udpate_global_private_query
243 @request.session[:user_id] = 3
304 @request.session[:user_id] = 3
244 put :update,
305 put :update,
245 :id => 3,
306 :id => 3,
246 :default_columns => '1',
307 :default_columns => '1',
247 :fields => ["status_id", "assigned_to_id"],
308 :fields => ["status_id", "assigned_to_id"],
248 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
309 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
249 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
310 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
250 :query => {"name" => "test_edit_global_private_query", "visibility" => "2"}
311 :query => {"name" => "test_edit_global_private_query", "visibility" => "2"}
251
312
252 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
313 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
253 q = Query.find_by_name('test_edit_global_private_query')
314 q = Query.find_by_name('test_edit_global_private_query')
254 assert !q.is_public?
315 assert !q.is_public?
255 assert q.has_default_columns?
316 assert q.has_default_columns?
256 assert q.valid?
317 assert q.valid?
257 end
318 end
258
319
259 def test_update_global_public_query
320 def test_update_global_public_query
260 @request.session[:user_id] = 1
321 @request.session[:user_id] = 1
261 put :update,
322 put :update,
262 :id => 4,
323 :id => 4,
263 :default_columns => '1',
324 :default_columns => '1',
264 :fields => ["status_id", "assigned_to_id"],
325 :fields => ["status_id", "assigned_to_id"],
265 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
326 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
266 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
327 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
267 :query => {"name" => "test_edit_global_public_query", "visibility" => "2"}
328 :query => {"name" => "test_edit_global_public_query", "visibility" => "2"}
268
329
269 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
330 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
270 q = Query.find_by_name('test_edit_global_public_query')
331 q = Query.find_by_name('test_edit_global_public_query')
271 assert q.is_public?
332 assert q.is_public?
272 assert q.has_default_columns?
333 assert q.has_default_columns?
273 assert q.valid?
334 assert q.valid?
274 end
335 end
275
336
276 def test_update_with_failure
337 def test_update_with_failure
277 @request.session[:user_id] = 1
338 @request.session[:user_id] = 1
278 put :update, :id => 4, :query => {:name => ''}
339 put :update, :id => 4, :query => {:name => ''}
279 assert_response :success
340 assert_response :success
280 assert_template 'edit'
341 assert_template 'edit'
281 end
342 end
282
343
283 def test_destroy
344 def test_destroy
284 @request.session[:user_id] = 2
345 @request.session[:user_id] = 2
285 delete :destroy, :id => 1
346 delete :destroy, :id => 1
286 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
347 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
287 assert_nil Query.find_by_id(1)
348 assert_nil Query.find_by_id(1)
288 end
349 end
289
350
290 def test_backslash_should_be_escaped_in_filters
351 def test_backslash_should_be_escaped_in_filters
291 @request.session[:user_id] = 2
352 @request.session[:user_id] = 2
292 get :new, :subject => 'foo/bar'
353 get :new, :subject => 'foo/bar'
293 assert_response :success
354 assert_response :success
294 assert_template 'new'
355 assert_template 'new'
295 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
356 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
296 end
357 end
297 end
358 end
@@ -1,344 +1,353
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 if ENV["COVERAGE"]
18 if ENV["COVERAGE"]
19 require 'simplecov'
19 require 'simplecov'
20 require File.expand_path(File.dirname(__FILE__) + "/coverage/html_formatter")
20 require File.expand_path(File.dirname(__FILE__) + "/coverage/html_formatter")
21 SimpleCov.formatter = Redmine::Coverage::HtmlFormatter
21 SimpleCov.formatter = Redmine::Coverage::HtmlFormatter
22 SimpleCov.start 'rails'
22 SimpleCov.start 'rails'
23 end
23 end
24
24
25 ENV["RAILS_ENV"] = "test"
25 ENV["RAILS_ENV"] = "test"
26 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
26 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
27 require 'rails/test_help'
27 require 'rails/test_help'
28 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
28 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
29
29
30 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
30 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
31 include ObjectHelpers
31 include ObjectHelpers
32
32
33 require 'net/ldap'
33 require 'net/ldap'
34 require 'mocha/setup'
34 require 'mocha/setup'
35
35
36 class ActionView::TestCase
36 class ActionView::TestCase
37 helper :application
37 helper :application
38 include ApplicationHelper
38 include ApplicationHelper
39 end
39 end
40
40
41 class ActiveSupport::TestCase
41 class ActiveSupport::TestCase
42 include ActionDispatch::TestProcess
42 include ActionDispatch::TestProcess
43
43
44 self.use_transactional_fixtures = true
44 self.use_transactional_fixtures = true
45 self.use_instantiated_fixtures = false
45 self.use_instantiated_fixtures = false
46
46
47 def uploaded_test_file(name, mime)
47 def uploaded_test_file(name, mime)
48 fixture_file_upload("files/#{name}", mime, true)
48 fixture_file_upload("files/#{name}", mime, true)
49 end
49 end
50
50
51 # Mock out a file
51 # Mock out a file
52 def self.mock_file
52 def self.mock_file
53 file = 'a_file.png'
53 file = 'a_file.png'
54 file.stubs(:size).returns(32)
54 file.stubs(:size).returns(32)
55 file.stubs(:original_filename).returns('a_file.png')
55 file.stubs(:original_filename).returns('a_file.png')
56 file.stubs(:content_type).returns('image/png')
56 file.stubs(:content_type).returns('image/png')
57 file.stubs(:read).returns(false)
57 file.stubs(:read).returns(false)
58 file
58 file
59 end
59 end
60
60
61 def mock_file
61 def mock_file
62 self.class.mock_file
62 self.class.mock_file
63 end
63 end
64
64
65 def mock_file_with_options(options={})
65 def mock_file_with_options(options={})
66 file = ''
66 file = ''
67 file.stubs(:size).returns(32)
67 file.stubs(:size).returns(32)
68 original_filename = options[:original_filename] || nil
68 original_filename = options[:original_filename] || nil
69 file.stubs(:original_filename).returns(original_filename)
69 file.stubs(:original_filename).returns(original_filename)
70 content_type = options[:content_type] || nil
70 content_type = options[:content_type] || nil
71 file.stubs(:content_type).returns(content_type)
71 file.stubs(:content_type).returns(content_type)
72 file.stubs(:read).returns(false)
72 file.stubs(:read).returns(false)
73 file
73 file
74 end
74 end
75
75
76 # Use a temporary directory for attachment related tests
76 # Use a temporary directory for attachment related tests
77 def set_tmp_attachments_directory
77 def set_tmp_attachments_directory
78 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
78 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
79 unless File.directory?("#{Rails.root}/tmp/test/attachments")
79 unless File.directory?("#{Rails.root}/tmp/test/attachments")
80 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
80 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
81 end
81 end
82 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
82 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
83 end
83 end
84
84
85 def set_fixtures_attachments_directory
85 def set_fixtures_attachments_directory
86 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
86 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
87 end
87 end
88
88
89 def with_settings(options, &block)
89 def with_settings(options, &block)
90 saved_settings = options.keys.inject({}) do |h, k|
90 saved_settings = options.keys.inject({}) do |h, k|
91 h[k] = case Setting[k]
91 h[k] = case Setting[k]
92 when Symbol, false, true, nil
92 when Symbol, false, true, nil
93 Setting[k]
93 Setting[k]
94 else
94 else
95 Setting[k].dup
95 Setting[k].dup
96 end
96 end
97 h
97 h
98 end
98 end
99 options.each {|k, v| Setting[k] = v}
99 options.each {|k, v| Setting[k] = v}
100 yield
100 yield
101 ensure
101 ensure
102 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
102 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
103 end
103 end
104
104
105 # Yields the block with user as the current user
105 # Yields the block with user as the current user
106 def with_current_user(user, &block)
106 def with_current_user(user, &block)
107 saved_user = User.current
107 saved_user = User.current
108 User.current = user
108 User.current = user
109 yield
109 yield
110 ensure
110 ensure
111 User.current = saved_user
111 User.current = saved_user
112 end
112 end
113
113
114 def with_locale(locale, &block)
114 def with_locale(locale, &block)
115 saved_localed = ::I18n.locale
115 saved_localed = ::I18n.locale
116 ::I18n.locale = locale
116 ::I18n.locale = locale
117 yield
117 yield
118 ensure
118 ensure
119 ::I18n.locale = saved_localed
119 ::I18n.locale = saved_localed
120 end
120 end
121
121
122 def self.ldap_configured?
122 def self.ldap_configured?
123 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
123 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
124 return @test_ldap.bind
124 return @test_ldap.bind
125 rescue Exception => e
125 rescue Exception => e
126 # LDAP is not listening
126 # LDAP is not listening
127 return nil
127 return nil
128 end
128 end
129
129
130 def self.convert_installed?
130 def self.convert_installed?
131 Redmine::Thumbnail.convert_available?
131 Redmine::Thumbnail.convert_available?
132 end
132 end
133
133
134 def convert_installed?
134 def convert_installed?
135 self.class.convert_installed?
135 self.class.convert_installed?
136 end
136 end
137
137
138 # Returns the path to the test +vendor+ repository
138 # Returns the path to the test +vendor+ repository
139 def self.repository_path(vendor)
139 def self.repository_path(vendor)
140 path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
140 path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
141 # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
141 # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
142 path.tr("\\", "/")
142 path.tr("\\", "/")
143 end
143 end
144
144
145 # Returns the url of the subversion test repository
145 # Returns the url of the subversion test repository
146 def self.subversion_repository_url
146 def self.subversion_repository_url
147 path = repository_path('subversion')
147 path = repository_path('subversion')
148 path = '/' + path unless path.starts_with?('/')
148 path = '/' + path unless path.starts_with?('/')
149 "file://#{path}"
149 "file://#{path}"
150 end
150 end
151
151
152 # Returns true if the +vendor+ test repository is configured
152 # Returns true if the +vendor+ test repository is configured
153 def self.repository_configured?(vendor)
153 def self.repository_configured?(vendor)
154 File.directory?(repository_path(vendor))
154 File.directory?(repository_path(vendor))
155 end
155 end
156
156
157 def repository_path_hash(arr)
157 def repository_path_hash(arr)
158 hs = {}
158 hs = {}
159 hs[:path] = arr.join("/")
159 hs[:path] = arr.join("/")
160 hs[:param] = arr.join("/")
160 hs[:param] = arr.join("/")
161 hs
161 hs
162 end
162 end
163
163
164 def sqlite?
164 def sqlite?
165 ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
165 ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
166 end
166 end
167
167
168 def mysql?
168 def mysql?
169 ActiveRecord::Base.connection.adapter_name =~ /mysql/i
169 ActiveRecord::Base.connection.adapter_name =~ /mysql/i
170 end
170 end
171
171
172 def postgresql?
172 def postgresql?
173 ActiveRecord::Base.connection.adapter_name =~ /postgresql/i
173 ActiveRecord::Base.connection.adapter_name =~ /postgresql/i
174 end
174 end
175
175
176 def quoted_date(date)
176 def quoted_date(date)
177 date = Date.parse(date) if date.is_a?(String)
177 date = Date.parse(date) if date.is_a?(String)
178 ActiveRecord::Base.connection.quoted_date(date)
178 ActiveRecord::Base.connection.quoted_date(date)
179 end
179 end
180
180
181 # Asserts that a new record for the given class is created
182 # and returns it
183 def new_record(klass, &block)
184 assert_difference "#{klass}.count" do
185 yield
186 end
187 klass.order(:id => :desc).first
188 end
189
181 def assert_save(object)
190 def assert_save(object)
182 saved = object.save
191 saved = object.save
183 message = "#{object.class} could not be saved"
192 message = "#{object.class} could not be saved"
184 errors = object.errors.full_messages.map {|m| "- #{m}"}
193 errors = object.errors.full_messages.map {|m| "- #{m}"}
185 message << ":\n#{errors.join("\n")}" if errors.any?
194 message << ":\n#{errors.join("\n")}" if errors.any?
186 assert_equal true, saved, message
195 assert_equal true, saved, message
187 end
196 end
188
197
189 def assert_select_error(arg)
198 def assert_select_error(arg)
190 assert_select '#errorExplanation', :text => arg
199 assert_select '#errorExplanation', :text => arg
191 end
200 end
192
201
193 def assert_include(expected, s, message=nil)
202 def assert_include(expected, s, message=nil)
194 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
203 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
195 end
204 end
196
205
197 def assert_not_include(expected, s, message=nil)
206 def assert_not_include(expected, s, message=nil)
198 assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
207 assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
199 end
208 end
200
209
201 def assert_select_in(text, *args, &block)
210 def assert_select_in(text, *args, &block)
202 d = Nokogiri::HTML(CGI::unescapeHTML(String.new(text))).root
211 d = Nokogiri::HTML(CGI::unescapeHTML(String.new(text))).root
203 assert_select(d, *args, &block)
212 assert_select(d, *args, &block)
204 end
213 end
205
214
206 def assert_select_email(*args, &block)
215 def assert_select_email(*args, &block)
207 email = ActionMailer::Base.deliveries.last
216 email = ActionMailer::Base.deliveries.last
208 assert_not_nil email
217 assert_not_nil email
209 html_body = email.parts.detect {|part| part.content_type.include?('text/html')}.try(&:body)
218 html_body = email.parts.detect {|part| part.content_type.include?('text/html')}.try(&:body)
210 assert_not_nil html_body
219 assert_not_nil html_body
211 assert_select_in html_body.encoded, *args, &block
220 assert_select_in html_body.encoded, *args, &block
212 end
221 end
213
222
214 def assert_mail_body_match(expected, mail, message=nil)
223 def assert_mail_body_match(expected, mail, message=nil)
215 if expected.is_a?(String)
224 if expected.is_a?(String)
216 assert_include expected, mail_body(mail), message
225 assert_include expected, mail_body(mail), message
217 else
226 else
218 assert_match expected, mail_body(mail), message
227 assert_match expected, mail_body(mail), message
219 end
228 end
220 end
229 end
221
230
222 def assert_mail_body_no_match(expected, mail, message=nil)
231 def assert_mail_body_no_match(expected, mail, message=nil)
223 if expected.is_a?(String)
232 if expected.is_a?(String)
224 assert_not_include expected, mail_body(mail), message
233 assert_not_include expected, mail_body(mail), message
225 else
234 else
226 assert_no_match expected, mail_body(mail), message
235 assert_no_match expected, mail_body(mail), message
227 end
236 end
228 end
237 end
229
238
230 def mail_body(mail)
239 def mail_body(mail)
231 mail.parts.first.body.encoded
240 mail.parts.first.body.encoded
232 end
241 end
233
242
234 # Returns the lft value for a new root issue
243 # Returns the lft value for a new root issue
235 def new_issue_lft
244 def new_issue_lft
236 1
245 1
237 end
246 end
238 end
247 end
239
248
240 module Redmine
249 module Redmine
241 class RoutingTest < ActionDispatch::IntegrationTest
250 class RoutingTest < ActionDispatch::IntegrationTest
242 def should_route(arg)
251 def should_route(arg)
243 arg = arg.dup
252 arg = arg.dup
244 request = arg.keys.detect {|key| key.is_a?(String)}
253 request = arg.keys.detect {|key| key.is_a?(String)}
245 raise ArgumentError unless request
254 raise ArgumentError unless request
246 options = arg.slice!(request)
255 options = arg.slice!(request)
247
256
248 raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
257 raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
249 method, path = $1.downcase.to_sym, $2
258 method, path = $1.downcase.to_sym, $2
250
259
251 raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
260 raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
252 controller, action = $1, $2
261 controller, action = $1, $2
253
262
254 assert_routing(
263 assert_routing(
255 {:method => method, :path => path},
264 {:method => method, :path => path},
256 options.merge(:controller => controller, :action => action)
265 options.merge(:controller => controller, :action => action)
257 )
266 )
258 end
267 end
259 end
268 end
260
269
261 class IntegrationTest < ActionDispatch::IntegrationTest
270 class IntegrationTest < ActionDispatch::IntegrationTest
262 def log_user(login, password)
271 def log_user(login, password)
263 User.anonymous
272 User.anonymous
264 get "/login"
273 get "/login"
265 assert_equal nil, session[:user_id]
274 assert_equal nil, session[:user_id]
266 assert_response :success
275 assert_response :success
267 assert_template "account/login"
276 assert_template "account/login"
268 post "/login", :username => login, :password => password
277 post "/login", :username => login, :password => password
269 assert_equal login, User.find(session[:user_id]).login
278 assert_equal login, User.find(session[:user_id]).login
270 end
279 end
271
280
272 def credentials(user, password=nil)
281 def credentials(user, password=nil)
273 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
282 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
274 end
283 end
275 end
284 end
276
285
277 module ApiTest
286 module ApiTest
278 API_FORMATS = %w(json xml).freeze
287 API_FORMATS = %w(json xml).freeze
279
288
280 # Base class for API tests
289 # Base class for API tests
281 class Base < Redmine::IntegrationTest
290 class Base < Redmine::IntegrationTest
282 def setup
291 def setup
283 Setting.rest_api_enabled = '1'
292 Setting.rest_api_enabled = '1'
284 end
293 end
285
294
286 def teardown
295 def teardown
287 Setting.rest_api_enabled = '0'
296 Setting.rest_api_enabled = '0'
288 end
297 end
289
298
290 # Uploads content using the XML API and returns the attachment token
299 # Uploads content using the XML API and returns the attachment token
291 def xml_upload(content, credentials)
300 def xml_upload(content, credentials)
292 upload('xml', content, credentials)
301 upload('xml', content, credentials)
293 end
302 end
294
303
295 # Uploads content using the JSON API and returns the attachment token
304 # Uploads content using the JSON API and returns the attachment token
296 def json_upload(content, credentials)
305 def json_upload(content, credentials)
297 upload('json', content, credentials)
306 upload('json', content, credentials)
298 end
307 end
299
308
300 def upload(format, content, credentials)
309 def upload(format, content, credentials)
301 set_tmp_attachments_directory
310 set_tmp_attachments_directory
302 assert_difference 'Attachment.count' do
311 assert_difference 'Attachment.count' do
303 post "/uploads.#{format}", content, {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials)
312 post "/uploads.#{format}", content, {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials)
304 assert_response :created
313 assert_response :created
305 end
314 end
306 data = response_data
315 data = response_data
307 assert_kind_of Hash, data['upload']
316 assert_kind_of Hash, data['upload']
308 token = data['upload']['token']
317 token = data['upload']['token']
309 assert_not_nil token
318 assert_not_nil token
310 token
319 token
311 end
320 end
312
321
313 # Parses the response body based on its content type
322 # Parses the response body based on its content type
314 def response_data
323 def response_data
315 unless response.content_type.to_s =~ /^application\/(.+)/
324 unless response.content_type.to_s =~ /^application\/(.+)/
316 raise "Unexpected response type: #{response.content_type}"
325 raise "Unexpected response type: #{response.content_type}"
317 end
326 end
318 format = $1
327 format = $1
319 case format
328 case format
320 when 'xml'
329 when 'xml'
321 Hash.from_xml(response.body)
330 Hash.from_xml(response.body)
322 when 'json'
331 when 'json'
323 ActiveSupport::JSON.decode(response.body)
332 ActiveSupport::JSON.decode(response.body)
324 else
333 else
325 raise "Unknown response format: #{format}"
334 raise "Unknown response format: #{format}"
326 end
335 end
327 end
336 end
328 end
337 end
329
338
330 class Routing < Redmine::RoutingTest
339 class Routing < Redmine::RoutingTest
331 def should_route(arg)
340 def should_route(arg)
332 arg = arg.dup
341 arg = arg.dup
333 request = arg.keys.detect {|key| key.is_a?(String)}
342 request = arg.keys.detect {|key| key.is_a?(String)}
334 raise ArgumentError unless request
343 raise ArgumentError unless request
335 options = arg.slice!(request)
344 options = arg.slice!(request)
336
345
337 API_FORMATS.each do |format|
346 API_FORMATS.each do |format|
338 format_request = request.sub /$/, ".#{format}"
347 format_request = request.sub /$/, ".#{format}"
339 super options.merge(format_request => arg[request], :format => format)
348 super options.merge(format_request => arg[request], :format => format)
340 end
349 end
341 end
350 end
342 end
351 end
343 end
352 end
344 end
353 end
General Comments 0
You need to be logged in to leave comments. Login now