##// END OF EJS Templates
Merged r14388 (#19842)....
Jean-Philippe Lang -
r14021:b54924d48bae
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,902 +1,904
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 :tree => ["=", "~", "!*", "*"]
220 :tree => ["=", "~", "!*", "*"]
221 }
221 }
222
222
223 class_attribute :available_columns
223 class_attribute :available_columns
224 self.available_columns = []
224 self.available_columns = []
225
225
226 class_attribute :queried_class
226 class_attribute :queried_class
227
227
228 def queried_table_name
228 def queried_table_name
229 @queried_table_name ||= self.class.queried_class.table_name
229 @queried_table_name ||= self.class.queried_class.table_name
230 end
230 end
231
231
232 def initialize(attributes=nil, *args)
232 def initialize(attributes=nil, *args)
233 super attributes
233 super attributes
234 @is_for_all = project.nil?
234 @is_for_all = project.nil?
235 end
235 end
236
236
237 # Builds the query from the given params
237 # Builds the query from the given params
238 def build_from_params(params)
238 def build_from_params(params)
239 if params[:fields] || params[:f]
239 if params[:fields] || params[:f]
240 self.filters = {}
240 self.filters = {}
241 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
241 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
242 else
242 else
243 available_filters.keys.each do |field|
243 available_filters.keys.each do |field|
244 add_short_filter(field, params[field]) if params[field]
244 add_short_filter(field, params[field]) if params[field]
245 end
245 end
246 end
246 end
247 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
247 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
248 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
248 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
249 self
249 self
250 end
250 end
251
251
252 # Builds a new query from the given params and attributes
252 # Builds a new query from the given params and attributes
253 def self.build_from_params(params, attributes={})
253 def self.build_from_params(params, attributes={})
254 new(attributes).build_from_params(params)
254 new(attributes).build_from_params(params)
255 end
255 end
256
256
257 def validate_query_filters
257 def validate_query_filters
258 filters.each_key do |field|
258 filters.each_key do |field|
259 if values_for(field)
259 if values_for(field)
260 case type_for(field)
260 case type_for(field)
261 when :integer
261 when :integer
262 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
262 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
263 when :float
263 when :float
264 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
264 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
265 when :date, :date_past
265 when :date, :date_past
266 case operator_for(field)
266 case operator_for(field)
267 when "=", ">=", "<=", "><"
267 when "=", ">=", "<=", "><"
268 add_filter_error(field, :invalid) if values_for(field).detect {|v|
268 add_filter_error(field, :invalid) if values_for(field).detect {|v|
269 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 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?)
270 }
270 }
271 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
271 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
272 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
272 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
273 end
273 end
274 end
274 end
275 end
275 end
276
276
277 add_filter_error(field, :blank) unless
277 add_filter_error(field, :blank) unless
278 # filter requires one or more values
278 # filter requires one or more values
279 (values_for(field) and !values_for(field).first.blank?) or
279 (values_for(field) and !values_for(field).first.blank?) or
280 # filter doesn't require any value
280 # filter doesn't require any value
281 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
281 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
282 end if filters
282 end if filters
283 end
283 end
284
284
285 def add_filter_error(field, message)
285 def add_filter_error(field, message)
286 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
286 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
287 errors.add(:base, m)
287 errors.add(:base, m)
288 end
288 end
289
289
290 def editable_by?(user)
290 def editable_by?(user)
291 return false unless user
291 return false unless user
292 # Admin can edit them all and regular users can edit their private queries
292 # Admin can edit them all and regular users can edit their private queries
293 return true if user.admin? || (is_private? && self.user_id == user.id)
293 return true if user.admin? || (is_private? && self.user_id == user.id)
294 # Members can not edit public queries that are for all project (only admin is allowed to)
294 # Members can not edit public queries that are for all project (only admin is allowed to)
295 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
295 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
296 end
296 end
297
297
298 def trackers
298 def trackers
299 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
299 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
300 end
300 end
301
301
302 # Returns a hash of localized labels for all filter operators
302 # Returns a hash of localized labels for all filter operators
303 def self.operators_labels
303 def self.operators_labels
304 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
304 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
305 end
305 end
306
306
307 # Returns a representation of the available filters for JSON serialization
307 # Returns a representation of the available filters for JSON serialization
308 def available_filters_as_json
308 def available_filters_as_json
309 json = {}
309 json = {}
310 available_filters.each do |field, options|
310 available_filters.each do |field, options|
311 json[field] = options.slice(:type, :name, :values).stringify_keys
311 json[field] = options.slice(:type, :name, :values).stringify_keys
312 end
312 end
313 json
313 json
314 end
314 end
315
315
316 def all_projects
316 def all_projects
317 @all_projects ||= Project.visible.to_a
317 @all_projects ||= Project.visible.to_a
318 end
318 end
319
319
320 def all_projects_values
320 def all_projects_values
321 return @all_projects_values if @all_projects_values
321 return @all_projects_values if @all_projects_values
322
322
323 values = []
323 values = []
324 Project.project_tree(all_projects) do |p, level|
324 Project.project_tree(all_projects) do |p, level|
325 prefix = (level > 0 ? ('--' * level + ' ') : '')
325 prefix = (level > 0 ? ('--' * level + ' ') : '')
326 values << ["#{prefix}#{p.name}", p.id.to_s]
326 values << ["#{prefix}#{p.name}", p.id.to_s]
327 end
327 end
328 @all_projects_values = values
328 @all_projects_values = values
329 end
329 end
330
330
331 # Adds available filters
331 # Adds available filters
332 def initialize_available_filters
332 def initialize_available_filters
333 # implemented by sub-classes
333 # implemented by sub-classes
334 end
334 end
335 protected :initialize_available_filters
335 protected :initialize_available_filters
336
336
337 # Adds an available filter
337 # Adds an available filter
338 def add_available_filter(field, options)
338 def add_available_filter(field, options)
339 @available_filters ||= ActiveSupport::OrderedHash.new
339 @available_filters ||= ActiveSupport::OrderedHash.new
340 @available_filters[field] = options
340 @available_filters[field] = options
341 @available_filters
341 @available_filters
342 end
342 end
343
343
344 # Removes an available filter
344 # Removes an available filter
345 def delete_available_filter(field)
345 def delete_available_filter(field)
346 if @available_filters
346 if @available_filters
347 @available_filters.delete(field)
347 @available_filters.delete(field)
348 end
348 end
349 end
349 end
350
350
351 # Return a hash of available filters
351 # Return a hash of available filters
352 def available_filters
352 def available_filters
353 unless @available_filters
353 unless @available_filters
354 initialize_available_filters
354 initialize_available_filters
355 @available_filters.each do |field, options|
355 @available_filters.each do |field, options|
356 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
356 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
357 end
357 end
358 end
358 end
359 @available_filters
359 @available_filters
360 end
360 end
361
361
362 def add_filter(field, operator, values=nil)
362 def add_filter(field, operator, values=nil)
363 # values must be an array
363 # values must be an array
364 return unless values.nil? || values.is_a?(Array)
364 return unless values.nil? || values.is_a?(Array)
365 # check if field is defined as an available filter
365 # check if field is defined as an available filter
366 if available_filters.has_key? field
366 if available_filters.has_key? field
367 filter_options = available_filters[field]
367 filter_options = available_filters[field]
368 filters[field] = {:operator => operator, :values => (values || [''])}
368 filters[field] = {:operator => operator, :values => (values || [''])}
369 end
369 end
370 end
370 end
371
371
372 def add_short_filter(field, expression)
372 def add_short_filter(field, expression)
373 return unless expression && available_filters.has_key?(field)
373 return unless expression && available_filters.has_key?(field)
374 field_type = available_filters[field][:type]
374 field_type = available_filters[field][:type]
375 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
375 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
376 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
376 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
377 values = $1
377 values = $1
378 add_filter field, operator, values.present? ? values.split('|') : ['']
378 add_filter field, operator, values.present? ? values.split('|') : ['']
379 end || add_filter(field, '=', expression.split('|'))
379 end || add_filter(field, '=', expression.split('|'))
380 end
380 end
381
381
382 # Add multiple filters using +add_filter+
382 # Add multiple filters using +add_filter+
383 def add_filters(fields, operators, values)
383 def add_filters(fields, operators, values)
384 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
384 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
385 fields.each do |field|
385 fields.each do |field|
386 add_filter(field, operators[field], values && values[field])
386 add_filter(field, operators[field], values && values[field])
387 end
387 end
388 end
388 end
389 end
389 end
390
390
391 def has_filter?(field)
391 def has_filter?(field)
392 filters and filters[field]
392 filters and filters[field]
393 end
393 end
394
394
395 def type_for(field)
395 def type_for(field)
396 available_filters[field][:type] if available_filters.has_key?(field)
396 available_filters[field][:type] if available_filters.has_key?(field)
397 end
397 end
398
398
399 def operator_for(field)
399 def operator_for(field)
400 has_filter?(field) ? filters[field][:operator] : nil
400 has_filter?(field) ? filters[field][:operator] : nil
401 end
401 end
402
402
403 def values_for(field)
403 def values_for(field)
404 has_filter?(field) ? filters[field][:values] : nil
404 has_filter?(field) ? filters[field][:values] : nil
405 end
405 end
406
406
407 def value_for(field, index=0)
407 def value_for(field, index=0)
408 (values_for(field) || [])[index]
408 (values_for(field) || [])[index]
409 end
409 end
410
410
411 def label_for(field)
411 def label_for(field)
412 label = available_filters[field][:name] if available_filters.has_key?(field)
412 label = available_filters[field][:name] if available_filters.has_key?(field)
413 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
413 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
414 end
414 end
415
415
416 def self.add_available_column(column)
416 def self.add_available_column(column)
417 self.available_columns << (column) if column.is_a?(QueryColumn)
417 self.available_columns << (column) if column.is_a?(QueryColumn)
418 end
418 end
419
419
420 # Returns an array of columns that can be used to group the results
420 # Returns an array of columns that can be used to group the results
421 def groupable_columns
421 def groupable_columns
422 available_columns.select {|c| c.groupable}
422 available_columns.select {|c| c.groupable}
423 end
423 end
424
424
425 # Returns a Hash of columns and the key for sorting
425 # Returns a Hash of columns and the key for sorting
426 def sortable_columns
426 def sortable_columns
427 available_columns.inject({}) {|h, column|
427 available_columns.inject({}) {|h, column|
428 h[column.name.to_s] = column.sortable
428 h[column.name.to_s] = column.sortable
429 h
429 h
430 }
430 }
431 end
431 end
432
432
433 def columns
433 def columns
434 # preserve the column_names order
434 # preserve the column_names order
435 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
435 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
436 available_columns.find { |col| col.name == name }
436 available_columns.find { |col| col.name == name }
437 end.compact
437 end.compact
438 available_columns.select(&:frozen?) | cols
438 available_columns.select(&:frozen?) | cols
439 end
439 end
440
440
441 def inline_columns
441 def inline_columns
442 columns.select(&:inline?)
442 columns.select(&:inline?)
443 end
443 end
444
444
445 def block_columns
445 def block_columns
446 columns.reject(&:inline?)
446 columns.reject(&:inline?)
447 end
447 end
448
448
449 def available_inline_columns
449 def available_inline_columns
450 available_columns.select(&:inline?)
450 available_columns.select(&:inline?)
451 end
451 end
452
452
453 def available_block_columns
453 def available_block_columns
454 available_columns.reject(&:inline?)
454 available_columns.reject(&:inline?)
455 end
455 end
456
456
457 def default_columns_names
457 def default_columns_names
458 []
458 []
459 end
459 end
460
460
461 def column_names=(names)
461 def column_names=(names)
462 if names
462 if names
463 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
463 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
464 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
464 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
465 # Set column_names to nil if default columns
465 # Set column_names to nil if default columns
466 if names == default_columns_names
466 if names == default_columns_names
467 names = nil
467 names = nil
468 end
468 end
469 end
469 end
470 write_attribute(:column_names, names)
470 write_attribute(:column_names, names)
471 end
471 end
472
472
473 def has_column?(column)
473 def has_column?(column)
474 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
474 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
475 end
475 end
476
476
477 def has_custom_field_column?
477 def has_custom_field_column?
478 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
478 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
479 end
479 end
480
480
481 def has_default_columns?
481 def has_default_columns?
482 column_names.nil? || column_names.empty?
482 column_names.nil? || column_names.empty?
483 end
483 end
484
484
485 def sort_criteria=(arg)
485 def sort_criteria=(arg)
486 c = []
486 c = []
487 if arg.is_a?(Hash)
487 if arg.is_a?(Hash)
488 arg = arg.keys.sort.collect {|k| arg[k]}
488 arg = arg.keys.sort.collect {|k| arg[k]}
489 end
489 end
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']}
490 if arg
491 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
492 end
491 write_attribute(:sort_criteria, c)
493 write_attribute(:sort_criteria, c)
492 end
494 end
493
495
494 def sort_criteria
496 def sort_criteria
495 read_attribute(:sort_criteria) || []
497 read_attribute(:sort_criteria) || []
496 end
498 end
497
499
498 def sort_criteria_key(arg)
500 def sort_criteria_key(arg)
499 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
501 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
500 end
502 end
501
503
502 def sort_criteria_order(arg)
504 def sort_criteria_order(arg)
503 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
505 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
504 end
506 end
505
507
506 def sort_criteria_order_for(key)
508 def sort_criteria_order_for(key)
507 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
509 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
508 end
510 end
509
511
510 # Returns the SQL sort order that should be prepended for grouping
512 # Returns the SQL sort order that should be prepended for grouping
511 def group_by_sort_order
513 def group_by_sort_order
512 if grouped? && (column = group_by_column)
514 if grouped? && (column = group_by_column)
513 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
515 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
514 column.sortable.is_a?(Array) ?
516 column.sortable.is_a?(Array) ?
515 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
517 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
516 "#{column.sortable} #{order}"
518 "#{column.sortable} #{order}"
517 end
519 end
518 end
520 end
519
521
520 # Returns true if the query is a grouped query
522 # Returns true if the query is a grouped query
521 def grouped?
523 def grouped?
522 !group_by_column.nil?
524 !group_by_column.nil?
523 end
525 end
524
526
525 def group_by_column
527 def group_by_column
526 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
528 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
527 end
529 end
528
530
529 def group_by_statement
531 def group_by_statement
530 group_by_column.try(:groupable)
532 group_by_column.try(:groupable)
531 end
533 end
532
534
533 def project_statement
535 def project_statement
534 project_clauses = []
536 project_clauses = []
535 if project && !project.descendants.active.empty?
537 if project && !project.descendants.active.empty?
536 ids = [project.id]
538 ids = [project.id]
537 if has_filter?("subproject_id")
539 if has_filter?("subproject_id")
538 case operator_for("subproject_id")
540 case operator_for("subproject_id")
539 when '='
541 when '='
540 # include the selected subprojects
542 # include the selected subprojects
541 ids += values_for("subproject_id").each(&:to_i)
543 ids += values_for("subproject_id").each(&:to_i)
542 when '!*'
544 when '!*'
543 # main project only
545 # main project only
544 else
546 else
545 # all subprojects
547 # all subprojects
546 ids += project.descendants.collect(&:id)
548 ids += project.descendants.collect(&:id)
547 end
549 end
548 elsif Setting.display_subprojects_issues?
550 elsif Setting.display_subprojects_issues?
549 ids += project.descendants.collect(&:id)
551 ids += project.descendants.collect(&:id)
550 end
552 end
551 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
553 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
552 elsif project
554 elsif project
553 project_clauses << "#{Project.table_name}.id = %d" % project.id
555 project_clauses << "#{Project.table_name}.id = %d" % project.id
554 end
556 end
555 project_clauses.any? ? project_clauses.join(' AND ') : nil
557 project_clauses.any? ? project_clauses.join(' AND ') : nil
556 end
558 end
557
559
558 def statement
560 def statement
559 # filters clauses
561 # filters clauses
560 filters_clauses = []
562 filters_clauses = []
561 filters.each_key do |field|
563 filters.each_key do |field|
562 next if field == "subproject_id"
564 next if field == "subproject_id"
563 v = values_for(field).clone
565 v = values_for(field).clone
564 next unless v and !v.empty?
566 next unless v and !v.empty?
565 operator = operator_for(field)
567 operator = operator_for(field)
566
568
567 # "me" value substitution
569 # "me" value substitution
568 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
570 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
569 if v.delete("me")
571 if v.delete("me")
570 if User.current.logged?
572 if User.current.logged?
571 v.push(User.current.id.to_s)
573 v.push(User.current.id.to_s)
572 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
574 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
573 else
575 else
574 v.push("0")
576 v.push("0")
575 end
577 end
576 end
578 end
577 end
579 end
578
580
579 if field == 'project_id'
581 if field == 'project_id'
580 if v.delete('mine')
582 if v.delete('mine')
581 v += User.current.memberships.map(&:project_id).map(&:to_s)
583 v += User.current.memberships.map(&:project_id).map(&:to_s)
582 end
584 end
583 end
585 end
584
586
585 if field =~ /cf_(\d+)$/
587 if field =~ /cf_(\d+)$/
586 # custom field
588 # custom field
587 filters_clauses << sql_for_custom_field(field, operator, v, $1)
589 filters_clauses << sql_for_custom_field(field, operator, v, $1)
588 elsif respond_to?("sql_for_#{field}_field")
590 elsif respond_to?("sql_for_#{field}_field")
589 # specific statement
591 # specific statement
590 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
592 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
591 else
593 else
592 # regular field
594 # regular field
593 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
595 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
594 end
596 end
595 end if filters and valid?
597 end if filters and valid?
596
598
597 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
599 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
598 # Excludes results for which the grouped custom field is not visible
600 # Excludes results for which the grouped custom field is not visible
599 filters_clauses << c.custom_field.visibility_by_project_condition
601 filters_clauses << c.custom_field.visibility_by_project_condition
600 end
602 end
601
603
602 filters_clauses << project_statement
604 filters_clauses << project_statement
603 filters_clauses.reject!(&:blank?)
605 filters_clauses.reject!(&:blank?)
604
606
605 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
607 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
606 end
608 end
607
609
608 private
610 private
609
611
610 def sql_for_custom_field(field, operator, value, custom_field_id)
612 def sql_for_custom_field(field, operator, value, custom_field_id)
611 db_table = CustomValue.table_name
613 db_table = CustomValue.table_name
612 db_field = 'value'
614 db_field = 'value'
613 filter = @available_filters[field]
615 filter = @available_filters[field]
614 return nil unless filter
616 return nil unless filter
615 if filter[:field].format.target_class && filter[:field].format.target_class <= User
617 if filter[:field].format.target_class && filter[:field].format.target_class <= User
616 if value.delete('me')
618 if value.delete('me')
617 value.push User.current.id.to_s
619 value.push User.current.id.to_s
618 end
620 end
619 end
621 end
620 not_in = nil
622 not_in = nil
621 if operator == '!'
623 if operator == '!'
622 # Makes ! operator work for custom fields with multiple values
624 # Makes ! operator work for custom fields with multiple values
623 operator = '='
625 operator = '='
624 not_in = 'NOT'
626 not_in = 'NOT'
625 end
627 end
626 customized_key = "id"
628 customized_key = "id"
627 customized_class = queried_class
629 customized_class = queried_class
628 if field =~ /^(.+)\.cf_/
630 if field =~ /^(.+)\.cf_/
629 assoc = $1
631 assoc = $1
630 customized_key = "#{assoc}_id"
632 customized_key = "#{assoc}_id"
631 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
633 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
632 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
634 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
633 end
635 end
634 where = sql_for_field(field, operator, value, db_table, db_field, true)
636 where = sql_for_field(field, operator, value, db_table, db_field, true)
635 if operator =~ /[<>]/
637 if operator =~ /[<>]/
636 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
638 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
637 end
639 end
638 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
640 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
639 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
641 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
640 " 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}" +
642 " 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 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
643 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
642 end
644 end
643
645
644 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
646 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
645 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
647 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
646 sql = ''
648 sql = ''
647 case operator
649 case operator
648 when "="
650 when "="
649 if value.any?
651 if value.any?
650 case type_for(field)
652 case type_for(field)
651 when :date, :date_past
653 when :date, :date_past
652 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
654 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
653 when :integer
655 when :integer
654 if is_custom_filter
656 if is_custom_filter
655 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})"
657 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 else
658 else
657 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
659 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
658 end
660 end
659 when :float
661 when :float
660 if is_custom_filter
662 if is_custom_filter
661 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})"
663 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 else
664 else
663 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
665 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
664 end
666 end
665 else
667 else
666 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
668 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
667 end
669 end
668 else
670 else
669 # IN an empty set
671 # IN an empty set
670 sql = "1=0"
672 sql = "1=0"
671 end
673 end
672 when "!"
674 when "!"
673 if value.any?
675 if value.any?
674 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
676 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 else
677 else
676 # NOT IN an empty set
678 # NOT IN an empty set
677 sql = "1=1"
679 sql = "1=1"
678 end
680 end
679 when "!*"
681 when "!*"
680 sql = "#{db_table}.#{db_field} IS NULL"
682 sql = "#{db_table}.#{db_field} IS NULL"
681 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
683 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
682 when "*"
684 when "*"
683 sql = "#{db_table}.#{db_field} IS NOT NULL"
685 sql = "#{db_table}.#{db_field} IS NOT NULL"
684 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
686 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
685 when ">="
687 when ">="
686 if [:date, :date_past].include?(type_for(field))
688 if [:date, :date_past].include?(type_for(field))
687 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
689 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
688 else
690 else
689 if is_custom_filter
691 if is_custom_filter
690 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})"
692 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 else
693 else
692 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
694 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
693 end
695 end
694 end
696 end
695 when "<="
697 when "<="
696 if [:date, :date_past].include?(type_for(field))
698 if [:date, :date_past].include?(type_for(field))
697 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
699 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
698 else
700 else
699 if is_custom_filter
701 if is_custom_filter
700 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})"
702 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 else
703 else
702 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
704 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
703 end
705 end
704 end
706 end
705 when "><"
707 when "><"
706 if [:date, :date_past].include?(type_for(field))
708 if [:date, :date_past].include?(type_for(field))
707 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
709 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
708 else
710 else
709 if is_custom_filter
711 if is_custom_filter
710 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})"
712 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 else
713 else
712 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
714 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
713 end
715 end
714 end
716 end
715 when "o"
717 when "o"
716 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"
718 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 when "c"
719 when "c"
718 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"
720 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 when "><t-"
721 when "><t-"
720 # between today - n days and today
722 # between today - n days and today
721 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
723 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
722 when ">t-"
724 when ">t-"
723 # >= today - n days
725 # >= today - n days
724 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
726 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
725 when "<t-"
727 when "<t-"
726 # <= today - n days
728 # <= today - n days
727 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
729 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
728 when "t-"
730 when "t-"
729 # = n days in past
731 # = n days in past
730 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
732 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
731 when "><t+"
733 when "><t+"
732 # between today and today + n days
734 # between today and today + n days
733 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
735 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
734 when ">t+"
736 when ">t+"
735 # >= today + n days
737 # >= today + n days
736 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
738 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
737 when "<t+"
739 when "<t+"
738 # <= today + n days
740 # <= today + n days
739 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
741 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
740 when "t+"
742 when "t+"
741 # = today + n days
743 # = today + n days
742 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
744 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
743 when "t"
745 when "t"
744 # = today
746 # = today
745 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
747 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
746 when "ld"
748 when "ld"
747 # = yesterday
749 # = yesterday
748 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
750 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
749 when "w"
751 when "w"
750 # = this week
752 # = this week
751 first_day_of_week = l(:general_first_day_of_week).to_i
753 first_day_of_week = l(:general_first_day_of_week).to_i
752 day_of_week = Date.today.cwday
754 day_of_week = Date.today.cwday
753 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
755 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 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
756 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
755 when "lw"
757 when "lw"
756 # = last week
758 # = last week
757 first_day_of_week = l(:general_first_day_of_week).to_i
759 first_day_of_week = l(:general_first_day_of_week).to_i
758 day_of_week = Date.today.cwday
760 day_of_week = Date.today.cwday
759 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
761 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 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
762 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
761 when "l2w"
763 when "l2w"
762 # = last 2 weeks
764 # = last 2 weeks
763 first_day_of_week = l(:general_first_day_of_week).to_i
765 first_day_of_week = l(:general_first_day_of_week).to_i
764 day_of_week = Date.today.cwday
766 day_of_week = Date.today.cwday
765 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
767 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 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
768 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
767 when "m"
769 when "m"
768 # = this month
770 # = this month
769 date = Date.today
771 date = Date.today
770 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
772 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
771 when "lm"
773 when "lm"
772 # = last month
774 # = last month
773 date = Date.today.prev_month
775 date = Date.today.prev_month
774 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
776 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
775 when "y"
777 when "y"
776 # = this year
778 # = this year
777 date = Date.today
779 date = Date.today
778 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
780 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
779 when "~"
781 when "~"
780 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
782 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
781 when "!~"
783 when "!~"
782 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
784 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
783 else
785 else
784 raise "Unknown query operator #{operator}"
786 raise "Unknown query operator #{operator}"
785 end
787 end
786
788
787 return sql
789 return sql
788 end
790 end
789
791
790 # Adds a filter for the given custom field
792 # Adds a filter for the given custom field
791 def add_custom_field_filter(field, assoc=nil)
793 def add_custom_field_filter(field, assoc=nil)
792 options = field.format.query_filter_options(field, self)
794 options = field.format.query_filter_options(field, self)
793 if field.format.target_class && field.format.target_class <= User
795 if field.format.target_class && field.format.target_class <= User
794 if options[:values].is_a?(Array) && User.current.logged?
796 if options[:values].is_a?(Array) && User.current.logged?
795 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
797 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
796 end
798 end
797 end
799 end
798
800
799 filter_id = "cf_#{field.id}"
801 filter_id = "cf_#{field.id}"
800 filter_name = field.name
802 filter_name = field.name
801 if assoc.present?
803 if assoc.present?
802 filter_id = "#{assoc}.#{filter_id}"
804 filter_id = "#{assoc}.#{filter_id}"
803 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
805 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
804 end
806 end
805 add_available_filter filter_id, options.merge({
807 add_available_filter filter_id, options.merge({
806 :name => filter_name,
808 :name => filter_name,
807 :field => field
809 :field => field
808 })
810 })
809 end
811 end
810
812
811 # Adds filters for the given custom fields scope
813 # Adds filters for the given custom fields scope
812 def add_custom_fields_filters(scope, assoc=nil)
814 def add_custom_fields_filters(scope, assoc=nil)
813 scope.visible.where(:is_filter => true).sorted.each do |field|
815 scope.visible.where(:is_filter => true).sorted.each do |field|
814 add_custom_field_filter(field, assoc)
816 add_custom_field_filter(field, assoc)
815 end
817 end
816 end
818 end
817
819
818 # Adds filters for the given associations custom fields
820 # Adds filters for the given associations custom fields
819 def add_associations_custom_fields_filters(*associations)
821 def add_associations_custom_fields_filters(*associations)
820 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
822 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
821 associations.each do |assoc|
823 associations.each do |assoc|
822 association_klass = queried_class.reflect_on_association(assoc).klass
824 association_klass = queried_class.reflect_on_association(assoc).klass
823 fields_by_class.each do |field_class, fields|
825 fields_by_class.each do |field_class, fields|
824 if field_class.customized_class <= association_klass
826 if field_class.customized_class <= association_klass
825 fields.sort.each do |field|
827 fields.sort.each do |field|
826 add_custom_field_filter(field, assoc)
828 add_custom_field_filter(field, assoc)
827 end
829 end
828 end
830 end
829 end
831 end
830 end
832 end
831 end
833 end
832
834
833 def quoted_time(time, is_custom_filter)
835 def quoted_time(time, is_custom_filter)
834 if is_custom_filter
836 if is_custom_filter
835 # Custom field values are stored as strings in the DB
837 # Custom field values are stored as strings in the DB
836 # using this format that does not depend on DB date representation
838 # using this format that does not depend on DB date representation
837 time.strftime("%Y-%m-%d %H:%M:%S")
839 time.strftime("%Y-%m-%d %H:%M:%S")
838 else
840 else
839 self.class.connection.quoted_date(time)
841 self.class.connection.quoted_date(time)
840 end
842 end
841 end
843 end
842
844
843 # Returns a SQL clause for a date or datetime field.
845 # Returns a SQL clause for a date or datetime field.
844 def date_clause(table, field, from, to, is_custom_filter)
846 def date_clause(table, field, from, to, is_custom_filter)
845 s = []
847 s = []
846 if from
848 if from
847 if from.is_a?(Date)
849 if from.is_a?(Date)
848 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
850 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
849 else
851 else
850 from = from - 1 # second
852 from = from - 1 # second
851 end
853 end
852 if self.class.default_timezone == :utc
854 if self.class.default_timezone == :utc
853 from = from.utc
855 from = from.utc
854 end
856 end
855 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
857 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
856 end
858 end
857 if to
859 if to
858 if to.is_a?(Date)
860 if to.is_a?(Date)
859 to = Time.local(to.year, to.month, to.day).end_of_day
861 to = Time.local(to.year, to.month, to.day).end_of_day
860 end
862 end
861 if self.class.default_timezone == :utc
863 if self.class.default_timezone == :utc
862 to = to.utc
864 to = to.utc
863 end
865 end
864 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
866 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
865 end
867 end
866 s.join(' AND ')
868 s.join(' AND ')
867 end
869 end
868
870
869 # Returns a SQL clause for a date or datetime field using relative dates.
871 # Returns a SQL clause for a date or datetime field using relative dates.
870 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
872 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
871 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
873 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
872 end
874 end
873
875
874 # Returns a Date or Time from the given filter value
876 # Returns a Date or Time from the given filter value
875 def parse_date(arg)
877 def parse_date(arg)
876 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
878 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
877 Time.parse(arg) rescue nil
879 Time.parse(arg) rescue nil
878 else
880 else
879 Date.parse(arg) rescue nil
881 Date.parse(arg) rescue nil
880 end
882 end
881 end
883 end
882
884
883 # Additional joins required for the given sort options
885 # Additional joins required for the given sort options
884 def joins_for_order_statement(order_options)
886 def joins_for_order_statement(order_options)
885 joins = []
887 joins = []
886
888
887 if order_options
889 if order_options
888 if order_options.include?('authors')
890 if order_options.include?('authors')
889 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
891 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
890 end
892 end
891 order_options.scan(/cf_\d+/).uniq.each do |name|
893 order_options.scan(/cf_\d+/).uniq.each do |name|
892 column = available_columns.detect {|c| c.name.to_s == name}
894 column = available_columns.detect {|c| c.name.to_s == name}
893 join = column && column.custom_field.join_for_order_statement
895 join = column && column.custom_field.join_for_order_statement
894 if join
896 if join
895 joins << join
897 joins << join
896 end
898 end
897 end
899 end
898 end
900 end
899
901
900 joins.any? ? joins.join(' ') : nil
902 joins.any? ? joins.join(' ') : nil
901 end
903 end
902 end
904 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,346 +1,355
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 Redmine::SudoMode.disable!
36 Redmine::SudoMode.disable!
37
37
38 class ActionView::TestCase
38 class ActionView::TestCase
39 helper :application
39 helper :application
40 include ApplicationHelper
40 include ApplicationHelper
41 end
41 end
42
42
43 class ActiveSupport::TestCase
43 class ActiveSupport::TestCase
44 include ActionDispatch::TestProcess
44 include ActionDispatch::TestProcess
45
45
46 self.use_transactional_fixtures = true
46 self.use_transactional_fixtures = true
47 self.use_instantiated_fixtures = false
47 self.use_instantiated_fixtures = false
48
48
49 def uploaded_test_file(name, mime)
49 def uploaded_test_file(name, mime)
50 fixture_file_upload("files/#{name}", mime, true)
50 fixture_file_upload("files/#{name}", mime, true)
51 end
51 end
52
52
53 # Mock out a file
53 # Mock out a file
54 def self.mock_file
54 def self.mock_file
55 file = 'a_file.png'
55 file = 'a_file.png'
56 file.stubs(:size).returns(32)
56 file.stubs(:size).returns(32)
57 file.stubs(:original_filename).returns('a_file.png')
57 file.stubs(:original_filename).returns('a_file.png')
58 file.stubs(:content_type).returns('image/png')
58 file.stubs(:content_type).returns('image/png')
59 file.stubs(:read).returns(false)
59 file.stubs(:read).returns(false)
60 file
60 file
61 end
61 end
62
62
63 def mock_file
63 def mock_file
64 self.class.mock_file
64 self.class.mock_file
65 end
65 end
66
66
67 def mock_file_with_options(options={})
67 def mock_file_with_options(options={})
68 file = ''
68 file = ''
69 file.stubs(:size).returns(32)
69 file.stubs(:size).returns(32)
70 original_filename = options[:original_filename] || nil
70 original_filename = options[:original_filename] || nil
71 file.stubs(:original_filename).returns(original_filename)
71 file.stubs(:original_filename).returns(original_filename)
72 content_type = options[:content_type] || nil
72 content_type = options[:content_type] || nil
73 file.stubs(:content_type).returns(content_type)
73 file.stubs(:content_type).returns(content_type)
74 file.stubs(:read).returns(false)
74 file.stubs(:read).returns(false)
75 file
75 file
76 end
76 end
77
77
78 # Use a temporary directory for attachment related tests
78 # Use a temporary directory for attachment related tests
79 def set_tmp_attachments_directory
79 def set_tmp_attachments_directory
80 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
80 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
81 unless File.directory?("#{Rails.root}/tmp/test/attachments")
81 unless File.directory?("#{Rails.root}/tmp/test/attachments")
82 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
82 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
83 end
83 end
84 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
84 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
85 end
85 end
86
86
87 def set_fixtures_attachments_directory
87 def set_fixtures_attachments_directory
88 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
88 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
89 end
89 end
90
90
91 def with_settings(options, &block)
91 def with_settings(options, &block)
92 saved_settings = options.keys.inject({}) do |h, k|
92 saved_settings = options.keys.inject({}) do |h, k|
93 h[k] = case Setting[k]
93 h[k] = case Setting[k]
94 when Symbol, false, true, nil
94 when Symbol, false, true, nil
95 Setting[k]
95 Setting[k]
96 else
96 else
97 Setting[k].dup
97 Setting[k].dup
98 end
98 end
99 h
99 h
100 end
100 end
101 options.each {|k, v| Setting[k] = v}
101 options.each {|k, v| Setting[k] = v}
102 yield
102 yield
103 ensure
103 ensure
104 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
104 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
105 end
105 end
106
106
107 # Yields the block with user as the current user
107 # Yields the block with user as the current user
108 def with_current_user(user, &block)
108 def with_current_user(user, &block)
109 saved_user = User.current
109 saved_user = User.current
110 User.current = user
110 User.current = user
111 yield
111 yield
112 ensure
112 ensure
113 User.current = saved_user
113 User.current = saved_user
114 end
114 end
115
115
116 def with_locale(locale, &block)
116 def with_locale(locale, &block)
117 saved_localed = ::I18n.locale
117 saved_localed = ::I18n.locale
118 ::I18n.locale = locale
118 ::I18n.locale = locale
119 yield
119 yield
120 ensure
120 ensure
121 ::I18n.locale = saved_localed
121 ::I18n.locale = saved_localed
122 end
122 end
123
123
124 def self.ldap_configured?
124 def self.ldap_configured?
125 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
125 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
126 return @test_ldap.bind
126 return @test_ldap.bind
127 rescue Exception => e
127 rescue Exception => e
128 # LDAP is not listening
128 # LDAP is not listening
129 return nil
129 return nil
130 end
130 end
131
131
132 def self.convert_installed?
132 def self.convert_installed?
133 Redmine::Thumbnail.convert_available?
133 Redmine::Thumbnail.convert_available?
134 end
134 end
135
135
136 def convert_installed?
136 def convert_installed?
137 self.class.convert_installed?
137 self.class.convert_installed?
138 end
138 end
139
139
140 # Returns the path to the test +vendor+ repository
140 # Returns the path to the test +vendor+ repository
141 def self.repository_path(vendor)
141 def self.repository_path(vendor)
142 path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
142 path = Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
143 # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
143 # Unlike ruby, JRuby returns Rails.root with backslashes under Windows
144 path.tr("\\", "/")
144 path.tr("\\", "/")
145 end
145 end
146
146
147 # Returns the url of the subversion test repository
147 # Returns the url of the subversion test repository
148 def self.subversion_repository_url
148 def self.subversion_repository_url
149 path = repository_path('subversion')
149 path = repository_path('subversion')
150 path = '/' + path unless path.starts_with?('/')
150 path = '/' + path unless path.starts_with?('/')
151 "file://#{path}"
151 "file://#{path}"
152 end
152 end
153
153
154 # Returns true if the +vendor+ test repository is configured
154 # Returns true if the +vendor+ test repository is configured
155 def self.repository_configured?(vendor)
155 def self.repository_configured?(vendor)
156 File.directory?(repository_path(vendor))
156 File.directory?(repository_path(vendor))
157 end
157 end
158
158
159 def repository_path_hash(arr)
159 def repository_path_hash(arr)
160 hs = {}
160 hs = {}
161 hs[:path] = arr.join("/")
161 hs[:path] = arr.join("/")
162 hs[:param] = arr.join("/")
162 hs[:param] = arr.join("/")
163 hs
163 hs
164 end
164 end
165
165
166 def sqlite?
166 def sqlite?
167 ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
167 ActiveRecord::Base.connection.adapter_name =~ /sqlite/i
168 end
168 end
169
169
170 def mysql?
170 def mysql?
171 ActiveRecord::Base.connection.adapter_name =~ /mysql/i
171 ActiveRecord::Base.connection.adapter_name =~ /mysql/i
172 end
172 end
173
173
174 def postgresql?
174 def postgresql?
175 ActiveRecord::Base.connection.adapter_name =~ /postgresql/i
175 ActiveRecord::Base.connection.adapter_name =~ /postgresql/i
176 end
176 end
177
177
178 def quoted_date(date)
178 def quoted_date(date)
179 date = Date.parse(date) if date.is_a?(String)
179 date = Date.parse(date) if date.is_a?(String)
180 ActiveRecord::Base.connection.quoted_date(date)
180 ActiveRecord::Base.connection.quoted_date(date)
181 end
181 end
182
182
183 # Asserts that a new record for the given class is created
184 # and returns it
185 def new_record(klass, &block)
186 assert_difference "#{klass}.count" do
187 yield
188 end
189 klass.order(:id => :desc).first
190 end
191
183 def assert_save(object)
192 def assert_save(object)
184 saved = object.save
193 saved = object.save
185 message = "#{object.class} could not be saved"
194 message = "#{object.class} could not be saved"
186 errors = object.errors.full_messages.map {|m| "- #{m}"}
195 errors = object.errors.full_messages.map {|m| "- #{m}"}
187 message << ":\n#{errors.join("\n")}" if errors.any?
196 message << ":\n#{errors.join("\n")}" if errors.any?
188 assert_equal true, saved, message
197 assert_equal true, saved, message
189 end
198 end
190
199
191 def assert_select_error(arg)
200 def assert_select_error(arg)
192 assert_select '#errorExplanation', :text => arg
201 assert_select '#errorExplanation', :text => arg
193 end
202 end
194
203
195 def assert_include(expected, s, message=nil)
204 def assert_include(expected, s, message=nil)
196 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
205 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
197 end
206 end
198
207
199 def assert_not_include(expected, s, message=nil)
208 def assert_not_include(expected, s, message=nil)
200 assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
209 assert !s.include?(expected), (message || "\"#{expected}\" found in \"#{s}\"")
201 end
210 end
202
211
203 def assert_select_in(text, *args, &block)
212 def assert_select_in(text, *args, &block)
204 d = Nokogiri::HTML(CGI::unescapeHTML(String.new(text))).root
213 d = Nokogiri::HTML(CGI::unescapeHTML(String.new(text))).root
205 assert_select(d, *args, &block)
214 assert_select(d, *args, &block)
206 end
215 end
207
216
208 def assert_select_email(*args, &block)
217 def assert_select_email(*args, &block)
209 email = ActionMailer::Base.deliveries.last
218 email = ActionMailer::Base.deliveries.last
210 assert_not_nil email
219 assert_not_nil email
211 html_body = email.parts.detect {|part| part.content_type.include?('text/html')}.try(&:body)
220 html_body = email.parts.detect {|part| part.content_type.include?('text/html')}.try(&:body)
212 assert_not_nil html_body
221 assert_not_nil html_body
213 assert_select_in html_body.encoded, *args, &block
222 assert_select_in html_body.encoded, *args, &block
214 end
223 end
215
224
216 def assert_mail_body_match(expected, mail, message=nil)
225 def assert_mail_body_match(expected, mail, message=nil)
217 if expected.is_a?(String)
226 if expected.is_a?(String)
218 assert_include expected, mail_body(mail), message
227 assert_include expected, mail_body(mail), message
219 else
228 else
220 assert_match expected, mail_body(mail), message
229 assert_match expected, mail_body(mail), message
221 end
230 end
222 end
231 end
223
232
224 def assert_mail_body_no_match(expected, mail, message=nil)
233 def assert_mail_body_no_match(expected, mail, message=nil)
225 if expected.is_a?(String)
234 if expected.is_a?(String)
226 assert_not_include expected, mail_body(mail), message
235 assert_not_include expected, mail_body(mail), message
227 else
236 else
228 assert_no_match expected, mail_body(mail), message
237 assert_no_match expected, mail_body(mail), message
229 end
238 end
230 end
239 end
231
240
232 def mail_body(mail)
241 def mail_body(mail)
233 mail.parts.first.body.encoded
242 mail.parts.first.body.encoded
234 end
243 end
235
244
236 # Returns the lft value for a new root issue
245 # Returns the lft value for a new root issue
237 def new_issue_lft
246 def new_issue_lft
238 1
247 1
239 end
248 end
240 end
249 end
241
250
242 module Redmine
251 module Redmine
243 class RoutingTest < ActionDispatch::IntegrationTest
252 class RoutingTest < ActionDispatch::IntegrationTest
244 def should_route(arg)
253 def should_route(arg)
245 arg = arg.dup
254 arg = arg.dup
246 request = arg.keys.detect {|key| key.is_a?(String)}
255 request = arg.keys.detect {|key| key.is_a?(String)}
247 raise ArgumentError unless request
256 raise ArgumentError unless request
248 options = arg.slice!(request)
257 options = arg.slice!(request)
249
258
250 raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
259 raise ArgumentError unless request =~ /\A(GET|POST|PUT|PATCH|DELETE)\s+(.+)\z/
251 method, path = $1.downcase.to_sym, $2
260 method, path = $1.downcase.to_sym, $2
252
261
253 raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
262 raise ArgumentError unless arg.values.first =~ /\A(.+)#(.+)\z/
254 controller, action = $1, $2
263 controller, action = $1, $2
255
264
256 assert_routing(
265 assert_routing(
257 {:method => method, :path => path},
266 {:method => method, :path => path},
258 options.merge(:controller => controller, :action => action)
267 options.merge(:controller => controller, :action => action)
259 )
268 )
260 end
269 end
261 end
270 end
262
271
263 class IntegrationTest < ActionDispatch::IntegrationTest
272 class IntegrationTest < ActionDispatch::IntegrationTest
264 def log_user(login, password)
273 def log_user(login, password)
265 User.anonymous
274 User.anonymous
266 get "/login"
275 get "/login"
267 assert_equal nil, session[:user_id]
276 assert_equal nil, session[:user_id]
268 assert_response :success
277 assert_response :success
269 assert_template "account/login"
278 assert_template "account/login"
270 post "/login", :username => login, :password => password
279 post "/login", :username => login, :password => password
271 assert_equal login, User.find(session[:user_id]).login
280 assert_equal login, User.find(session[:user_id]).login
272 end
281 end
273
282
274 def credentials(user, password=nil)
283 def credentials(user, password=nil)
275 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
284 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
276 end
285 end
277 end
286 end
278
287
279 module ApiTest
288 module ApiTest
280 API_FORMATS = %w(json xml).freeze
289 API_FORMATS = %w(json xml).freeze
281
290
282 # Base class for API tests
291 # Base class for API tests
283 class Base < Redmine::IntegrationTest
292 class Base < Redmine::IntegrationTest
284 def setup
293 def setup
285 Setting.rest_api_enabled = '1'
294 Setting.rest_api_enabled = '1'
286 end
295 end
287
296
288 def teardown
297 def teardown
289 Setting.rest_api_enabled = '0'
298 Setting.rest_api_enabled = '0'
290 end
299 end
291
300
292 # Uploads content using the XML API and returns the attachment token
301 # Uploads content using the XML API and returns the attachment token
293 def xml_upload(content, credentials)
302 def xml_upload(content, credentials)
294 upload('xml', content, credentials)
303 upload('xml', content, credentials)
295 end
304 end
296
305
297 # Uploads content using the JSON API and returns the attachment token
306 # Uploads content using the JSON API and returns the attachment token
298 def json_upload(content, credentials)
307 def json_upload(content, credentials)
299 upload('json', content, credentials)
308 upload('json', content, credentials)
300 end
309 end
301
310
302 def upload(format, content, credentials)
311 def upload(format, content, credentials)
303 set_tmp_attachments_directory
312 set_tmp_attachments_directory
304 assert_difference 'Attachment.count' do
313 assert_difference 'Attachment.count' do
305 post "/uploads.#{format}", content, {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials)
314 post "/uploads.#{format}", content, {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials)
306 assert_response :created
315 assert_response :created
307 end
316 end
308 data = response_data
317 data = response_data
309 assert_kind_of Hash, data['upload']
318 assert_kind_of Hash, data['upload']
310 token = data['upload']['token']
319 token = data['upload']['token']
311 assert_not_nil token
320 assert_not_nil token
312 token
321 token
313 end
322 end
314
323
315 # Parses the response body based on its content type
324 # Parses the response body based on its content type
316 def response_data
325 def response_data
317 unless response.content_type.to_s =~ /^application\/(.+)/
326 unless response.content_type.to_s =~ /^application\/(.+)/
318 raise "Unexpected response type: #{response.content_type}"
327 raise "Unexpected response type: #{response.content_type}"
319 end
328 end
320 format = $1
329 format = $1
321 case format
330 case format
322 when 'xml'
331 when 'xml'
323 Hash.from_xml(response.body)
332 Hash.from_xml(response.body)
324 when 'json'
333 when 'json'
325 ActiveSupport::JSON.decode(response.body)
334 ActiveSupport::JSON.decode(response.body)
326 else
335 else
327 raise "Unknown response format: #{format}"
336 raise "Unknown response format: #{format}"
328 end
337 end
329 end
338 end
330 end
339 end
331
340
332 class Routing < Redmine::RoutingTest
341 class Routing < Redmine::RoutingTest
333 def should_route(arg)
342 def should_route(arg)
334 arg = arg.dup
343 arg = arg.dup
335 request = arg.keys.detect {|key| key.is_a?(String)}
344 request = arg.keys.detect {|key| key.is_a?(String)}
336 raise ArgumentError unless request
345 raise ArgumentError unless request
337 options = arg.slice!(request)
346 options = arg.slice!(request)
338
347
339 API_FORMATS.each do |format|
348 API_FORMATS.each do |format|
340 format_request = request.sub /$/, ".#{format}"
349 format_request = request.sub /$/, ".#{format}"
341 super options.merge(format_request => arg[request], :format => format)
350 super options.merge(format_request => arg[request], :format => format)
342 end
351 end
343 end
352 end
344 end
353 end
345 end
354 end
346 end
355 end
General Comments 0
You need to be logged in to leave comments. Login now