##// END OF EJS Templates
Moved build_query_from_params helper to Query#build_from_params....
Jean-Philippe Lang -
r10739:10998c9bae31
parent child
Show More
@@ -1,107 +1,107
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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
34
35 @query_count = IssueQuery.visible.count
35 @query_count = IssueQuery.visible.count
36 @query_pages = Paginator.new self, @query_count, @limit, params['page']
36 @query_pages = Paginator.new self, @query_count, @limit, params['page']
37 @queries = IssueQuery.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
37 @queries = IssueQuery.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
38
38
39 respond_to do |format|
39 respond_to do |format|
40 format.html { render :nothing => true }
40 format.html { render :nothing => true }
41 format.api
41 format.api
42 end
42 end
43 end
43 end
44
44
45 def new
45 def new
46 @query = IssueQuery.new
46 @query = IssueQuery.new
47 @query.user = User.current
47 @query.user = User.current
48 @query.project = @project
48 @query.project = @project
49 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
49 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
50 build_query_from_params
50 @query.build_from_params(params)
51 end
51 end
52
52
53 def create
53 def create
54 @query = IssueQuery.new(params[:query])
54 @query = IssueQuery.new(params[:query])
55 @query.user = User.current
55 @query.user = User.current
56 @query.project = params[:query_is_for_all] ? nil : @project
56 @query.project = params[:query_is_for_all] ? nil : @project
57 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
57 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
58 build_query_from_params
58 @query.build_from_params(params)
59 @query.column_names = nil if params[:default_columns]
59 @query.column_names = nil if params[:default_columns]
60
60
61 if @query.save
61 if @query.save
62 flash[:notice] = l(:notice_successful_create)
62 flash[:notice] = l(:notice_successful_create)
63 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
63 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
64 else
64 else
65 render :action => 'new', :layout => !request.xhr?
65 render :action => 'new', :layout => !request.xhr?
66 end
66 end
67 end
67 end
68
68
69 def edit
69 def edit
70 end
70 end
71
71
72 def update
72 def update
73 @query.attributes = params[:query]
73 @query.attributes = params[:query]
74 @query.project = nil if params[:query_is_for_all]
74 @query.project = nil if params[:query_is_for_all]
75 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
75 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
76 build_query_from_params
76 @query.build_from_params(params)
77 @query.column_names = nil if params[:default_columns]
77 @query.column_names = nil if params[:default_columns]
78
78
79 if @query.save
79 if @query.save
80 flash[:notice] = l(:notice_successful_update)
80 flash[:notice] = l(:notice_successful_update)
81 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
81 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
82 else
82 else
83 render :action => 'edit'
83 render :action => 'edit'
84 end
84 end
85 end
85 end
86
86
87 def destroy
87 def destroy
88 @query.destroy
88 @query.destroy
89 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
89 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
90 end
90 end
91
91
92 private
92 private
93 def find_query
93 def find_query
94 @query = IssueQuery.find(params[:id])
94 @query = IssueQuery.find(params[:id])
95 @project = @query.project
95 @project = @query.project
96 render_403 unless @query.editable_by?(User.current)
96 render_403 unless @query.editable_by?(User.current)
97 rescue ActiveRecord::RecordNotFound
97 rescue ActiveRecord::RecordNotFound
98 render_404
98 render_404
99 end
99 end
100
100
101 def find_optional_project
101 def find_optional_project
102 @project = Project.find(params[:project_id]) if params[:project_id]
102 @project = Project.find(params[:project_id]) if params[:project_id]
103 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
103 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
104 rescue ActiveRecord::RecordNotFound
104 rescue ActiveRecord::RecordNotFound
105 render_404
105 render_404
106 end
106 end
107 end
107 end
@@ -1,173 +1,160
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 def filters_options_for_select(query)
21 def filters_options_for_select(query)
22 options_for_select(filters_options(query))
22 options_for_select(filters_options(query))
23 end
23 end
24
24
25 def filters_options(query)
25 def filters_options(query)
26 options = [[]]
26 options = [[]]
27 sorted_options = query.available_filters.sort do |a, b|
27 sorted_options = query.available_filters.sort do |a, b|
28 ord = 0
28 ord = 0
29 if !(a[1][:order] == 20 && b[1][:order] == 20)
29 if !(a[1][:order] == 20 && b[1][:order] == 20)
30 ord = a[1][:order] <=> b[1][:order]
30 ord = a[1][:order] <=> b[1][:order]
31 else
31 else
32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
34 if cn != 0
34 if cn != 0
35 ord = cn
35 ord = cn
36 else
36 else
37 f = (a[1][:field] <=> b[1][:field])
37 f = (a[1][:field] <=> b[1][:field])
38 if f != 0
38 if f != 0
39 ord = f
39 ord = f
40 else
40 else
41 # assigned_to or author
41 # assigned_to or author
42 ord = (a[0] <=> b[0])
42 ord = (a[0] <=> b[0])
43 end
43 end
44 end
44 end
45 end
45 end
46 ord
46 ord
47 end
47 end
48 options += sorted_options.map do |field, field_options|
48 options += sorted_options.map do |field, field_options|
49 [field_options[:name], field]
49 [field_options[:name], field]
50 end
50 end
51 end
51 end
52
52
53 def available_block_columns_tags(query)
53 def available_block_columns_tags(query)
54 tags = ''.html_safe
54 tags = ''.html_safe
55 query.available_block_columns.each do |column|
55 query.available_block_columns.each do |column|
56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
57 end
57 end
58 tags
58 tags
59 end
59 end
60
60
61 def column_header(column)
61 def column_header(column)
62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
63 :default_order => column.default_order) :
63 :default_order => column.default_order) :
64 content_tag('th', h(column.caption))
64 content_tag('th', h(column.caption))
65 end
65 end
66
66
67 def column_content(column, issue)
67 def column_content(column, issue)
68 value = column.value(issue)
68 value = column.value(issue)
69 if value.is_a?(Array)
69 if value.is_a?(Array)
70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
71 else
71 else
72 column_value(column, issue, value)
72 column_value(column, issue, value)
73 end
73 end
74 end
74 end
75
75
76 def column_value(column, issue, value)
76 def column_value(column, issue, value)
77 case value.class.name
77 case value.class.name
78 when 'String'
78 when 'String'
79 if column.name == :subject
79 if column.name == :subject
80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
81 elsif column.name == :description
81 elsif column.name == :description
82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
83 else
83 else
84 h(value)
84 h(value)
85 end
85 end
86 when 'Time'
86 when 'Time'
87 format_time(value)
87 format_time(value)
88 when 'Date'
88 when 'Date'
89 format_date(value)
89 format_date(value)
90 when 'Fixnum', 'Float'
90 when 'Fixnum', 'Float'
91 if column.name == :done_ratio
91 if column.name == :done_ratio
92 progress_bar(value, :width => '80px')
92 progress_bar(value, :width => '80px')
93 elsif column.name == :spent_hours
93 elsif column.name == :spent_hours
94 sprintf "%.2f", value
94 sprintf "%.2f", value
95 else
95 else
96 h(value.to_s)
96 h(value.to_s)
97 end
97 end
98 when 'User'
98 when 'User'
99 link_to_user value
99 link_to_user value
100 when 'Project'
100 when 'Project'
101 link_to_project value
101 link_to_project value
102 when 'Version'
102 when 'Version'
103 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
103 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
104 when 'TrueClass'
104 when 'TrueClass'
105 l(:general_text_Yes)
105 l(:general_text_Yes)
106 when 'FalseClass'
106 when 'FalseClass'
107 l(:general_text_No)
107 l(:general_text_No)
108 when 'Issue'
108 when 'Issue'
109 link_to_issue(value, :subject => false)
109 link_to_issue(value, :subject => false)
110 when 'IssueRelation'
110 when 'IssueRelation'
111 other = value.other_issue(issue)
111 other = value.other_issue(issue)
112 content_tag('span',
112 content_tag('span',
113 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
113 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
114 :class => value.css_classes_for(issue))
114 :class => value.css_classes_for(issue))
115 else
115 else
116 h(value)
116 h(value)
117 end
117 end
118 end
118 end
119
119
120 # Retrieve query from session or build a new query
120 # Retrieve query from session or build a new query
121 def retrieve_query
121 def retrieve_query
122 if !params[:query_id].blank?
122 if !params[:query_id].blank?
123 cond = "project_id IS NULL"
123 cond = "project_id IS NULL"
124 cond << " OR project_id = #{@project.id}" if @project
124 cond << " OR project_id = #{@project.id}" if @project
125 @query = IssueQuery.find(params[:query_id], :conditions => cond)
125 @query = IssueQuery.find(params[:query_id], :conditions => cond)
126 raise ::Unauthorized unless @query.visible?
126 raise ::Unauthorized unless @query.visible?
127 @query.project = @project
127 @query.project = @project
128 session[:query] = {:id => @query.id, :project_id => @query.project_id}
128 session[:query] = {:id => @query.id, :project_id => @query.project_id}
129 sort_clear
129 sort_clear
130 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
130 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
131 # Give it a name, required to be valid
131 # Give it a name, required to be valid
132 @query = IssueQuery.new(:name => "_")
132 @query = IssueQuery.new(:name => "_")
133 @query.project = @project
133 @query.project = @project
134 build_query_from_params
134 @query.build_from_params(params)
135 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
135 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
136 else
136 else
137 # retrieve from session
137 # retrieve from session
138 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
138 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
139 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
139 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
140 @query.project = @project
140 @query.project = @project
141 end
141 end
142 end
142 end
143
143
144 def retrieve_query_from_session
144 def retrieve_query_from_session
145 if session[:query]
145 if session[:query]
146 if session[:query][:id]
146 if session[:query][:id]
147 @query = IssueQuery.find_by_id(session[:query][:id])
147 @query = IssueQuery.find_by_id(session[:query][:id])
148 return unless @query
148 return unless @query
149 else
149 else
150 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
150 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
151 end
151 end
152 if session[:query].has_key?(:project_id)
152 if session[:query].has_key?(:project_id)
153 @query.project_id = session[:query][:project_id]
153 @query.project_id = session[:query][:project_id]
154 else
154 else
155 @query.project = @project
155 @query.project = @project
156 end
156 end
157 @query
157 @query
158 end
158 end
159 end
159 end
160
161 def build_query_from_params
162 if params[:fields] || params[:f]
163 @query.filters = {}
164 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
165 else
166 @query.available_filters.keys.each do |field|
167 @query.add_short_filter(field, params[field]) if params[field]
168 end
169 end
170 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
171 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
172 end
173 end
160 end
@@ -1,716 +1,731
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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}"
31 @caption_key = options[:caption] || "field_#{name}"
32 end
32 end
33
33
34 def caption
34 def caption
35 l(@caption_key)
35 l(@caption_key)
36 end
36 end
37
37
38 # Returns true if the column is sortable, otherwise false
38 # Returns true if the column is sortable, otherwise false
39 def sortable?
39 def sortable?
40 !@sortable.nil?
40 !@sortable.nil?
41 end
41 end
42
42
43 def sortable
43 def sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 end
45 end
46
46
47 def inline?
47 def inline?
48 @inline
48 @inline
49 end
49 end
50
50
51 def value(object)
51 def value(object)
52 object.send name
52 object.send name
53 end
53 end
54
54
55 def css_classes
55 def css_classes
56 name
56 name
57 end
57 end
58 end
58 end
59
59
60 class QueryCustomFieldColumn < QueryColumn
60 class QueryCustomFieldColumn < QueryColumn
61
61
62 def initialize(custom_field)
62 def initialize(custom_field)
63 self.name = "cf_#{custom_field.id}".to_sym
63 self.name = "cf_#{custom_field.id}".to_sym
64 self.sortable = custom_field.order_statement || false
64 self.sortable = custom_field.order_statement || false
65 self.groupable = custom_field.group_statement || false
65 self.groupable = custom_field.group_statement || false
66 @inline = true
66 @inline = true
67 @cf = custom_field
67 @cf = custom_field
68 end
68 end
69
69
70 def caption
70 def caption
71 @cf.name
71 @cf.name
72 end
72 end
73
73
74 def custom_field
74 def custom_field
75 @cf
75 @cf
76 end
76 end
77
77
78 def value(object)
78 def value(object)
79 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
79 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
81 end
81 end
82
82
83 def css_classes
83 def css_classes
84 @css_classes ||= "#{name} #{@cf.field_format}"
84 @css_classes ||= "#{name} #{@cf.field_format}"
85 end
85 end
86 end
86 end
87
87
88 class Query < ActiveRecord::Base
88 class Query < ActiveRecord::Base
89 class StatementInvalid < ::ActiveRecord::StatementInvalid
89 class StatementInvalid < ::ActiveRecord::StatementInvalid
90 end
90 end
91
91
92 belongs_to :project
92 belongs_to :project
93 belongs_to :user
93 belongs_to :user
94 serialize :filters
94 serialize :filters
95 serialize :column_names
95 serialize :column_names
96 serialize :sort_criteria, Array
96 serialize :sort_criteria, Array
97
97
98 attr_protected :project_id, :user_id
98 attr_protected :project_id, :user_id
99
99
100 validates_presence_of :name
100 validates_presence_of :name
101 validates_length_of :name, :maximum => 255
101 validates_length_of :name, :maximum => 255
102 validate :validate_query_filters
102 validate :validate_query_filters
103
103
104 class_attribute :operators
104 class_attribute :operators
105 self.operators = {
105 self.operators = {
106 "=" => :label_equals,
106 "=" => :label_equals,
107 "!" => :label_not_equals,
107 "!" => :label_not_equals,
108 "o" => :label_open_issues,
108 "o" => :label_open_issues,
109 "c" => :label_closed_issues,
109 "c" => :label_closed_issues,
110 "!*" => :label_none,
110 "!*" => :label_none,
111 "*" => :label_any,
111 "*" => :label_any,
112 ">=" => :label_greater_or_equal,
112 ">=" => :label_greater_or_equal,
113 "<=" => :label_less_or_equal,
113 "<=" => :label_less_or_equal,
114 "><" => :label_between,
114 "><" => :label_between,
115 "<t+" => :label_in_less_than,
115 "<t+" => :label_in_less_than,
116 ">t+" => :label_in_more_than,
116 ">t+" => :label_in_more_than,
117 "><t+"=> :label_in_the_next_days,
117 "><t+"=> :label_in_the_next_days,
118 "t+" => :label_in,
118 "t+" => :label_in,
119 "t" => :label_today,
119 "t" => :label_today,
120 "w" => :label_this_week,
120 "w" => :label_this_week,
121 ">t-" => :label_less_than_ago,
121 ">t-" => :label_less_than_ago,
122 "<t-" => :label_more_than_ago,
122 "<t-" => :label_more_than_ago,
123 "><t-"=> :label_in_the_past_days,
123 "><t-"=> :label_in_the_past_days,
124 "t-" => :label_ago,
124 "t-" => :label_ago,
125 "~" => :label_contains,
125 "~" => :label_contains,
126 "!~" => :label_not_contains,
126 "!~" => :label_not_contains,
127 "=p" => :label_any_issues_in_project,
127 "=p" => :label_any_issues_in_project,
128 "=!p" => :label_any_issues_not_in_project,
128 "=!p" => :label_any_issues_not_in_project,
129 "!p" => :label_no_issues_in_project
129 "!p" => :label_no_issues_in_project
130 }
130 }
131
131
132 class_attribute :operators_by_filter_type
132 class_attribute :operators_by_filter_type
133 self.operators_by_filter_type = {
133 self.operators_by_filter_type = {
134 :list => [ "=", "!" ],
134 :list => [ "=", "!" ],
135 :list_status => [ "o", "=", "!", "c", "*" ],
135 :list_status => [ "o", "=", "!", "c", "*" ],
136 :list_optional => [ "=", "!", "!*", "*" ],
136 :list_optional => [ "=", "!", "!*", "*" ],
137 :list_subprojects => [ "*", "!*", "=" ],
137 :list_subprojects => [ "*", "!*", "=" ],
138 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
138 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
139 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
139 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
140 :string => [ "=", "~", "!", "!~", "!*", "*" ],
140 :string => [ "=", "~", "!", "!~", "!*", "*" ],
141 :text => [ "~", "!~", "!*", "*" ],
141 :text => [ "~", "!~", "!*", "*" ],
142 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
142 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
143 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
143 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
144 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
144 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
145 }
145 }
146
146
147 class_attribute :available_columns
147 class_attribute :available_columns
148 self.available_columns = []
148 self.available_columns = []
149
149
150 class_attribute :queried_class
150 class_attribute :queried_class
151
151
152 def queried_table_name
152 def queried_table_name
153 @queried_table_name ||= self.class.queried_class.table_name
153 @queried_table_name ||= self.class.queried_class.table_name
154 end
154 end
155
155
156 def initialize(attributes=nil, *args)
156 def initialize(attributes=nil, *args)
157 super attributes
157 super attributes
158 @is_for_all = project.nil?
158 @is_for_all = project.nil?
159 end
159 end
160
160
161 # Builds the query from the given params
162 def build_from_params(params)
163 if params[:fields] || params[:f]
164 self.filters = {}
165 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
166 else
167 available_filters.keys.each do |field|
168 add_short_filter(field, params[field]) if params[field]
169 end
170 end
171 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
172 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
173 self
174 end
175
161 def validate_query_filters
176 def validate_query_filters
162 filters.each_key do |field|
177 filters.each_key do |field|
163 if values_for(field)
178 if values_for(field)
164 case type_for(field)
179 case type_for(field)
165 when :integer
180 when :integer
166 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
167 when :float
182 when :float
168 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
169 when :date, :date_past
184 when :date, :date_past
170 case operator_for(field)
185 case operator_for(field)
171 when "=", ">=", "<=", "><"
186 when "=", ">=", "<=", "><"
172 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
173 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
174 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
175 end
190 end
176 end
191 end
177 end
192 end
178
193
179 add_filter_error(field, :blank) unless
194 add_filter_error(field, :blank) unless
180 # filter requires one or more values
195 # filter requires one or more values
181 (values_for(field) and !values_for(field).first.blank?) or
196 (values_for(field) and !values_for(field).first.blank?) or
182 # filter doesn't require any value
197 # filter doesn't require any value
183 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
184 end if filters
199 end if filters
185 end
200 end
186
201
187 def add_filter_error(field, message)
202 def add_filter_error(field, message)
188 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
189 errors.add(:base, m)
204 errors.add(:base, m)
190 end
205 end
191
206
192 def editable_by?(user)
207 def editable_by?(user)
193 return false unless user
208 return false unless user
194 # Admin can edit them all and regular users can edit their private queries
209 # Admin can edit them all and regular users can edit their private queries
195 return true if user.admin? || (!is_public && self.user_id == user.id)
210 return true if user.admin? || (!is_public && self.user_id == user.id)
196 # Members can not edit public queries that are for all project (only admin is allowed to)
211 # Members can not edit public queries that are for all project (only admin is allowed to)
197 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
212 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
198 end
213 end
199
214
200 def trackers
215 def trackers
201 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
216 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
202 end
217 end
203
218
204 # Returns a hash of localized labels for all filter operators
219 # Returns a hash of localized labels for all filter operators
205 def self.operators_labels
220 def self.operators_labels
206 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
221 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
207 end
222 end
208
223
209 # Returns a representation of the available filters for JSON serialization
224 # Returns a representation of the available filters for JSON serialization
210 def available_filters_as_json
225 def available_filters_as_json
211 json = {}
226 json = {}
212 available_filters.each do |field, options|
227 available_filters.each do |field, options|
213 json[field] = options.slice(:type, :name, :values).stringify_keys
228 json[field] = options.slice(:type, :name, :values).stringify_keys
214 end
229 end
215 json
230 json
216 end
231 end
217
232
218 def all_projects
233 def all_projects
219 @all_projects ||= Project.visible.all
234 @all_projects ||= Project.visible.all
220 end
235 end
221
236
222 def all_projects_values
237 def all_projects_values
223 return @all_projects_values if @all_projects_values
238 return @all_projects_values if @all_projects_values
224
239
225 values = []
240 values = []
226 Project.project_tree(all_projects) do |p, level|
241 Project.project_tree(all_projects) do |p, level|
227 prefix = (level > 0 ? ('--' * level + ' ') : '')
242 prefix = (level > 0 ? ('--' * level + ' ') : '')
228 values << ["#{prefix}#{p.name}", p.id.to_s]
243 values << ["#{prefix}#{p.name}", p.id.to_s]
229 end
244 end
230 @all_projects_values = values
245 @all_projects_values = values
231 end
246 end
232
247
233 def add_filter(field, operator, values)
248 def add_filter(field, operator, values)
234 # values must be an array
249 # values must be an array
235 return unless values.nil? || values.is_a?(Array)
250 return unless values.nil? || values.is_a?(Array)
236 # check if field is defined as an available filter
251 # check if field is defined as an available filter
237 if available_filters.has_key? field
252 if available_filters.has_key? field
238 filter_options = available_filters[field]
253 filter_options = available_filters[field]
239 filters[field] = {:operator => operator, :values => (values || [''])}
254 filters[field] = {:operator => operator, :values => (values || [''])}
240 end
255 end
241 end
256 end
242
257
243 def add_short_filter(field, expression)
258 def add_short_filter(field, expression)
244 return unless expression && available_filters.has_key?(field)
259 return unless expression && available_filters.has_key?(field)
245 field_type = available_filters[field][:type]
260 field_type = available_filters[field][:type]
246 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
261 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
247 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
262 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
248 add_filter field, operator, $1.present? ? $1.split('|') : ['']
263 add_filter field, operator, $1.present? ? $1.split('|') : ['']
249 end || add_filter(field, '=', expression.split('|'))
264 end || add_filter(field, '=', expression.split('|'))
250 end
265 end
251
266
252 # Add multiple filters using +add_filter+
267 # Add multiple filters using +add_filter+
253 def add_filters(fields, operators, values)
268 def add_filters(fields, operators, values)
254 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
269 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
255 fields.each do |field|
270 fields.each do |field|
256 add_filter(field, operators[field], values && values[field])
271 add_filter(field, operators[field], values && values[field])
257 end
272 end
258 end
273 end
259 end
274 end
260
275
261 def has_filter?(field)
276 def has_filter?(field)
262 filters and filters[field]
277 filters and filters[field]
263 end
278 end
264
279
265 def type_for(field)
280 def type_for(field)
266 available_filters[field][:type] if available_filters.has_key?(field)
281 available_filters[field][:type] if available_filters.has_key?(field)
267 end
282 end
268
283
269 def operator_for(field)
284 def operator_for(field)
270 has_filter?(field) ? filters[field][:operator] : nil
285 has_filter?(field) ? filters[field][:operator] : nil
271 end
286 end
272
287
273 def values_for(field)
288 def values_for(field)
274 has_filter?(field) ? filters[field][:values] : nil
289 has_filter?(field) ? filters[field][:values] : nil
275 end
290 end
276
291
277 def value_for(field, index=0)
292 def value_for(field, index=0)
278 (values_for(field) || [])[index]
293 (values_for(field) || [])[index]
279 end
294 end
280
295
281 def label_for(field)
296 def label_for(field)
282 label = available_filters[field][:name] if available_filters.has_key?(field)
297 label = available_filters[field][:name] if available_filters.has_key?(field)
283 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
298 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
284 end
299 end
285
300
286 def self.add_available_column(column)
301 def self.add_available_column(column)
287 self.available_columns << (column) if column.is_a?(QueryColumn)
302 self.available_columns << (column) if column.is_a?(QueryColumn)
288 end
303 end
289
304
290 # Returns an array of columns that can be used to group the results
305 # Returns an array of columns that can be used to group the results
291 def groupable_columns
306 def groupable_columns
292 available_columns.select {|c| c.groupable}
307 available_columns.select {|c| c.groupable}
293 end
308 end
294
309
295 # Returns a Hash of columns and the key for sorting
310 # Returns a Hash of columns and the key for sorting
296 def sortable_columns
311 def sortable_columns
297 available_columns.inject({}) {|h, column|
312 available_columns.inject({}) {|h, column|
298 h[column.name.to_s] = column.sortable
313 h[column.name.to_s] = column.sortable
299 h
314 h
300 }
315 }
301 end
316 end
302
317
303 def columns
318 def columns
304 # preserve the column_names order
319 # preserve the column_names order
305 (has_default_columns? ? default_columns_names : column_names).collect do |name|
320 (has_default_columns? ? default_columns_names : column_names).collect do |name|
306 available_columns.find { |col| col.name == name }
321 available_columns.find { |col| col.name == name }
307 end.compact
322 end.compact
308 end
323 end
309
324
310 def inline_columns
325 def inline_columns
311 columns.select(&:inline?)
326 columns.select(&:inline?)
312 end
327 end
313
328
314 def block_columns
329 def block_columns
315 columns.reject(&:inline?)
330 columns.reject(&:inline?)
316 end
331 end
317
332
318 def available_inline_columns
333 def available_inline_columns
319 available_columns.select(&:inline?)
334 available_columns.select(&:inline?)
320 end
335 end
321
336
322 def available_block_columns
337 def available_block_columns
323 available_columns.reject(&:inline?)
338 available_columns.reject(&:inline?)
324 end
339 end
325
340
326 def default_columns_names
341 def default_columns_names
327 []
342 []
328 end
343 end
329
344
330 def column_names=(names)
345 def column_names=(names)
331 if names
346 if names
332 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
347 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
333 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
348 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
334 # Set column_names to nil if default columns
349 # Set column_names to nil if default columns
335 if names == default_columns_names
350 if names == default_columns_names
336 names = nil
351 names = nil
337 end
352 end
338 end
353 end
339 write_attribute(:column_names, names)
354 write_attribute(:column_names, names)
340 end
355 end
341
356
342 def has_column?(column)
357 def has_column?(column)
343 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
358 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
344 end
359 end
345
360
346 def has_default_columns?
361 def has_default_columns?
347 column_names.nil? || column_names.empty?
362 column_names.nil? || column_names.empty?
348 end
363 end
349
364
350 def sort_criteria=(arg)
365 def sort_criteria=(arg)
351 c = []
366 c = []
352 if arg.is_a?(Hash)
367 if arg.is_a?(Hash)
353 arg = arg.keys.sort.collect {|k| arg[k]}
368 arg = arg.keys.sort.collect {|k| arg[k]}
354 end
369 end
355 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
370 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
356 write_attribute(:sort_criteria, c)
371 write_attribute(:sort_criteria, c)
357 end
372 end
358
373
359 def sort_criteria
374 def sort_criteria
360 read_attribute(:sort_criteria) || []
375 read_attribute(:sort_criteria) || []
361 end
376 end
362
377
363 def sort_criteria_key(arg)
378 def sort_criteria_key(arg)
364 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
379 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
365 end
380 end
366
381
367 def sort_criteria_order(arg)
382 def sort_criteria_order(arg)
368 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
383 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
369 end
384 end
370
385
371 def sort_criteria_order_for(key)
386 def sort_criteria_order_for(key)
372 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
387 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
373 end
388 end
374
389
375 # Returns the SQL sort order that should be prepended for grouping
390 # Returns the SQL sort order that should be prepended for grouping
376 def group_by_sort_order
391 def group_by_sort_order
377 if grouped? && (column = group_by_column)
392 if grouped? && (column = group_by_column)
378 order = sort_criteria_order_for(column.name) || column.default_order
393 order = sort_criteria_order_for(column.name) || column.default_order
379 column.sortable.is_a?(Array) ?
394 column.sortable.is_a?(Array) ?
380 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
395 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
381 "#{column.sortable} #{order}"
396 "#{column.sortable} #{order}"
382 end
397 end
383 end
398 end
384
399
385 # Returns true if the query is a grouped query
400 # Returns true if the query is a grouped query
386 def grouped?
401 def grouped?
387 !group_by_column.nil?
402 !group_by_column.nil?
388 end
403 end
389
404
390 def group_by_column
405 def group_by_column
391 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
406 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
392 end
407 end
393
408
394 def group_by_statement
409 def group_by_statement
395 group_by_column.try(:groupable)
410 group_by_column.try(:groupable)
396 end
411 end
397
412
398 def project_statement
413 def project_statement
399 project_clauses = []
414 project_clauses = []
400 if project && !project.descendants.active.empty?
415 if project && !project.descendants.active.empty?
401 ids = [project.id]
416 ids = [project.id]
402 if has_filter?("subproject_id")
417 if has_filter?("subproject_id")
403 case operator_for("subproject_id")
418 case operator_for("subproject_id")
404 when '='
419 when '='
405 # include the selected subprojects
420 # include the selected subprojects
406 ids += values_for("subproject_id").each(&:to_i)
421 ids += values_for("subproject_id").each(&:to_i)
407 when '!*'
422 when '!*'
408 # main project only
423 # main project only
409 else
424 else
410 # all subprojects
425 # all subprojects
411 ids += project.descendants.collect(&:id)
426 ids += project.descendants.collect(&:id)
412 end
427 end
413 elsif Setting.display_subprojects_issues?
428 elsif Setting.display_subprojects_issues?
414 ids += project.descendants.collect(&:id)
429 ids += project.descendants.collect(&:id)
415 end
430 end
416 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
431 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
417 elsif project
432 elsif project
418 project_clauses << "#{Project.table_name}.id = %d" % project.id
433 project_clauses << "#{Project.table_name}.id = %d" % project.id
419 end
434 end
420 project_clauses.any? ? project_clauses.join(' AND ') : nil
435 project_clauses.any? ? project_clauses.join(' AND ') : nil
421 end
436 end
422
437
423 def statement
438 def statement
424 # filters clauses
439 # filters clauses
425 filters_clauses = []
440 filters_clauses = []
426 filters.each_key do |field|
441 filters.each_key do |field|
427 next if field == "subproject_id"
442 next if field == "subproject_id"
428 v = values_for(field).clone
443 v = values_for(field).clone
429 next unless v and !v.empty?
444 next unless v and !v.empty?
430 operator = operator_for(field)
445 operator = operator_for(field)
431
446
432 # "me" value subsitution
447 # "me" value subsitution
433 if %w(assigned_to_id author_id watcher_id).include?(field)
448 if %w(assigned_to_id author_id watcher_id).include?(field)
434 if v.delete("me")
449 if v.delete("me")
435 if User.current.logged?
450 if User.current.logged?
436 v.push(User.current.id.to_s)
451 v.push(User.current.id.to_s)
437 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
452 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
438 else
453 else
439 v.push("0")
454 v.push("0")
440 end
455 end
441 end
456 end
442 end
457 end
443
458
444 if field == 'project_id'
459 if field == 'project_id'
445 if v.delete('mine')
460 if v.delete('mine')
446 v += User.current.memberships.map(&:project_id).map(&:to_s)
461 v += User.current.memberships.map(&:project_id).map(&:to_s)
447 end
462 end
448 end
463 end
449
464
450 if field =~ /cf_(\d+)$/
465 if field =~ /cf_(\d+)$/
451 # custom field
466 # custom field
452 filters_clauses << sql_for_custom_field(field, operator, v, $1)
467 filters_clauses << sql_for_custom_field(field, operator, v, $1)
453 elsif respond_to?("sql_for_#{field}_field")
468 elsif respond_to?("sql_for_#{field}_field")
454 # specific statement
469 # specific statement
455 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
470 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
456 else
471 else
457 # regular field
472 # regular field
458 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
473 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
459 end
474 end
460 end if filters and valid?
475 end if filters and valid?
461
476
462 filters_clauses << project_statement
477 filters_clauses << project_statement
463 filters_clauses.reject!(&:blank?)
478 filters_clauses.reject!(&:blank?)
464
479
465 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
480 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
466 end
481 end
467
482
468 private
483 private
469
484
470 def sql_for_custom_field(field, operator, value, custom_field_id)
485 def sql_for_custom_field(field, operator, value, custom_field_id)
471 db_table = CustomValue.table_name
486 db_table = CustomValue.table_name
472 db_field = 'value'
487 db_field = 'value'
473 filter = @available_filters[field]
488 filter = @available_filters[field]
474 return nil unless filter
489 return nil unless filter
475 if filter[:format] == 'user'
490 if filter[:format] == 'user'
476 if value.delete('me')
491 if value.delete('me')
477 value.push User.current.id.to_s
492 value.push User.current.id.to_s
478 end
493 end
479 end
494 end
480 not_in = nil
495 not_in = nil
481 if operator == '!'
496 if operator == '!'
482 # Makes ! operator work for custom fields with multiple values
497 # Makes ! operator work for custom fields with multiple values
483 operator = '='
498 operator = '='
484 not_in = 'NOT'
499 not_in = 'NOT'
485 end
500 end
486 customized_key = "id"
501 customized_key = "id"
487 customized_class = queried_class
502 customized_class = queried_class
488 if field =~ /^(.+)\.cf_/
503 if field =~ /^(.+)\.cf_/
489 assoc = $1
504 assoc = $1
490 customized_key = "#{assoc}_id"
505 customized_key = "#{assoc}_id"
491 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
506 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
492 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
507 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
493 end
508 end
494 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} 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} WHERE " +
509 "#{queried_table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} 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} WHERE " +
495 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
510 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
496 end
511 end
497
512
498 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
513 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
499 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
514 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
500 sql = ''
515 sql = ''
501 case operator
516 case operator
502 when "="
517 when "="
503 if value.any?
518 if value.any?
504 case type_for(field)
519 case type_for(field)
505 when :date, :date_past
520 when :date, :date_past
506 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
521 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
507 when :integer
522 when :integer
508 if is_custom_filter
523 if is_custom_filter
509 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
524 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
510 else
525 else
511 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
526 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
512 end
527 end
513 when :float
528 when :float
514 if is_custom_filter
529 if is_custom_filter
515 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
530 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
516 else
531 else
517 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
532 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
518 end
533 end
519 else
534 else
520 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
535 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
521 end
536 end
522 else
537 else
523 # IN an empty set
538 # IN an empty set
524 sql = "1=0"
539 sql = "1=0"
525 end
540 end
526 when "!"
541 when "!"
527 if value.any?
542 if value.any?
528 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
543 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
529 else
544 else
530 # NOT IN an empty set
545 # NOT IN an empty set
531 sql = "1=1"
546 sql = "1=1"
532 end
547 end
533 when "!*"
548 when "!*"
534 sql = "#{db_table}.#{db_field} IS NULL"
549 sql = "#{db_table}.#{db_field} IS NULL"
535 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
550 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
536 when "*"
551 when "*"
537 sql = "#{db_table}.#{db_field} IS NOT NULL"
552 sql = "#{db_table}.#{db_field} IS NOT NULL"
538 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
553 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
539 when ">="
554 when ">="
540 if [:date, :date_past].include?(type_for(field))
555 if [:date, :date_past].include?(type_for(field))
541 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
556 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
542 else
557 else
543 if is_custom_filter
558 if is_custom_filter
544 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
559 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
545 else
560 else
546 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
561 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
547 end
562 end
548 end
563 end
549 when "<="
564 when "<="
550 if [:date, :date_past].include?(type_for(field))
565 if [:date, :date_past].include?(type_for(field))
551 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
566 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
552 else
567 else
553 if is_custom_filter
568 if is_custom_filter
554 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
569 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
555 else
570 else
556 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
571 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
557 end
572 end
558 end
573 end
559 when "><"
574 when "><"
560 if [:date, :date_past].include?(type_for(field))
575 if [:date, :date_past].include?(type_for(field))
561 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
576 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
562 else
577 else
563 if is_custom_filter
578 if is_custom_filter
564 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
579 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
565 else
580 else
566 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
581 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
567 end
582 end
568 end
583 end
569 when "o"
584 when "o"
570 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
585 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
571 when "c"
586 when "c"
572 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
587 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
573 when "><t-"
588 when "><t-"
574 # between today - n days and today
589 # between today - n days and today
575 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
590 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
576 when ">t-"
591 when ">t-"
577 # >= today - n days
592 # >= today - n days
578 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
593 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
579 when "<t-"
594 when "<t-"
580 # <= today - n days
595 # <= today - n days
581 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
596 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
582 when "t-"
597 when "t-"
583 # = n days in past
598 # = n days in past
584 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
599 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
585 when "><t+"
600 when "><t+"
586 # between today and today + n days
601 # between today and today + n days
587 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
602 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
588 when ">t+"
603 when ">t+"
589 # >= today + n days
604 # >= today + n days
590 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
605 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
591 when "<t+"
606 when "<t+"
592 # <= today + n days
607 # <= today + n days
593 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
608 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
594 when "t+"
609 when "t+"
595 # = today + n days
610 # = today + n days
596 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
611 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
597 when "t"
612 when "t"
598 # = today
613 # = today
599 sql = relative_date_clause(db_table, db_field, 0, 0)
614 sql = relative_date_clause(db_table, db_field, 0, 0)
600 when "w"
615 when "w"
601 # = this week
616 # = this week
602 first_day_of_week = l(:general_first_day_of_week).to_i
617 first_day_of_week = l(:general_first_day_of_week).to_i
603 day_of_week = Date.today.cwday
618 day_of_week = Date.today.cwday
604 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
619 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
605 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
620 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
606 when "~"
621 when "~"
607 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
622 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
608 when "!~"
623 when "!~"
609 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
624 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
610 else
625 else
611 raise "Unknown query operator #{operator}"
626 raise "Unknown query operator #{operator}"
612 end
627 end
613
628
614 return sql
629 return sql
615 end
630 end
616
631
617 def add_custom_fields_filters(custom_fields, assoc=nil)
632 def add_custom_fields_filters(custom_fields, assoc=nil)
618 return unless custom_fields.present?
633 return unless custom_fields.present?
619 @available_filters ||= {}
634 @available_filters ||= {}
620
635
621 custom_fields.select(&:is_filter?).each do |field|
636 custom_fields.select(&:is_filter?).each do |field|
622 case field.field_format
637 case field.field_format
623 when "text"
638 when "text"
624 options = { :type => :text, :order => 20 }
639 options = { :type => :text, :order => 20 }
625 when "list"
640 when "list"
626 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
641 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
627 when "date"
642 when "date"
628 options = { :type => :date, :order => 20 }
643 options = { :type => :date, :order => 20 }
629 when "bool"
644 when "bool"
630 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
645 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
631 when "int"
646 when "int"
632 options = { :type => :integer, :order => 20 }
647 options = { :type => :integer, :order => 20 }
633 when "float"
648 when "float"
634 options = { :type => :float, :order => 20 }
649 options = { :type => :float, :order => 20 }
635 when "user", "version"
650 when "user", "version"
636 next unless project
651 next unless project
637 values = field.possible_values_options(project)
652 values = field.possible_values_options(project)
638 if User.current.logged? && field.field_format == 'user'
653 if User.current.logged? && field.field_format == 'user'
639 values.unshift ["<< #{l(:label_me)} >>", "me"]
654 values.unshift ["<< #{l(:label_me)} >>", "me"]
640 end
655 end
641 options = { :type => :list_optional, :values => values, :order => 20}
656 options = { :type => :list_optional, :values => values, :order => 20}
642 else
657 else
643 options = { :type => :string, :order => 20 }
658 options = { :type => :string, :order => 20 }
644 end
659 end
645 filter_id = "cf_#{field.id}"
660 filter_id = "cf_#{field.id}"
646 filter_name = field.name
661 filter_name = field.name
647 if assoc.present?
662 if assoc.present?
648 filter_id = "#{assoc}.#{filter_id}"
663 filter_id = "#{assoc}.#{filter_id}"
649 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
664 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
650 end
665 end
651 @available_filters[filter_id] = options.merge({
666 @available_filters[filter_id] = options.merge({
652 :name => filter_name,
667 :name => filter_name,
653 :format => field.field_format,
668 :format => field.field_format,
654 :field => field
669 :field => field
655 })
670 })
656 end
671 end
657 end
672 end
658
673
659 def add_associations_custom_fields_filters(*associations)
674 def add_associations_custom_fields_filters(*associations)
660 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
675 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
661 associations.each do |assoc|
676 associations.each do |assoc|
662 association_klass = queried_class.reflect_on_association(assoc).klass
677 association_klass = queried_class.reflect_on_association(assoc).klass
663 fields_by_class.each do |field_class, fields|
678 fields_by_class.each do |field_class, fields|
664 if field_class.customized_class <= association_klass
679 if field_class.customized_class <= association_klass
665 add_custom_fields_filters(fields, assoc)
680 add_custom_fields_filters(fields, assoc)
666 end
681 end
667 end
682 end
668 end
683 end
669 end
684 end
670
685
671 # Returns a SQL clause for a date or datetime field.
686 # Returns a SQL clause for a date or datetime field.
672 def date_clause(table, field, from, to)
687 def date_clause(table, field, from, to)
673 s = []
688 s = []
674 if from
689 if from
675 from_yesterday = from - 1
690 from_yesterday = from - 1
676 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
691 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
677 if self.class.default_timezone == :utc
692 if self.class.default_timezone == :utc
678 from_yesterday_time = from_yesterday_time.utc
693 from_yesterday_time = from_yesterday_time.utc
679 end
694 end
680 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
695 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
681 end
696 end
682 if to
697 if to
683 to_time = Time.local(to.year, to.month, to.day)
698 to_time = Time.local(to.year, to.month, to.day)
684 if self.class.default_timezone == :utc
699 if self.class.default_timezone == :utc
685 to_time = to_time.utc
700 to_time = to_time.utc
686 end
701 end
687 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
702 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
688 end
703 end
689 s.join(' AND ')
704 s.join(' AND ')
690 end
705 end
691
706
692 # Returns a SQL clause for a date or datetime field using relative dates.
707 # Returns a SQL clause for a date or datetime field using relative dates.
693 def relative_date_clause(table, field, days_from, days_to)
708 def relative_date_clause(table, field, days_from, days_to)
694 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
709 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
695 end
710 end
696
711
697 # Additional joins required for the given sort options
712 # Additional joins required for the given sort options
698 def joins_for_order_statement(order_options)
713 def joins_for_order_statement(order_options)
699 joins = []
714 joins = []
700
715
701 if order_options
716 if order_options
702 if order_options.include?('authors')
717 if order_options.include?('authors')
703 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
718 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
704 end
719 end
705 order_options.scan(/cf_\d+/).uniq.each do |name|
720 order_options.scan(/cf_\d+/).uniq.each do |name|
706 column = available_columns.detect {|c| c.name.to_s == name}
721 column = available_columns.detect {|c| c.name.to_s == name}
707 join = column && column.custom_field.join_for_order_statement
722 join = column && column.custom_field.join_for_order_statement
708 if join
723 if join
709 joins << join
724 joins << join
710 end
725 end
711 end
726 end
712 end
727 end
713
728
714 joins.any? ? joins.join(' ') : nil
729 joins.any? ? joins.join(' ') : nil
715 end
730 end
716 end
731 end
General Comments 0
You need to be logged in to leave comments. Login now