##// END OF EJS Templates
Don't preload all query filters (#24787)....
Jean-Philippe Lang -
r15788:fd3c08aaa107
parent child
Show More
@@ -1,146 +1,165
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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_action :find_query, :except => [:new, :create, :index]
20 before_action :find_query, :only => [:edit, :update, :destroy]
21 before_action :find_optional_project, :only => [:new, :create]
21 before_action :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 scope = query_class.visible
34 scope = query_class.visible
35 @query_count = scope.count
35 @query_count = scope.count
36 @query_pages = Paginator.new @query_count, @limit, params['page']
36 @query_pages = Paginator.new @query_count, @limit, params['page']
37 @queries = scope.
37 @queries = scope.
38 order("#{Query.table_name}.name").
38 order("#{Query.table_name}.name").
39 limit(@limit).
39 limit(@limit).
40 offset(@offset).
40 offset(@offset).
41 to_a
41 to_a
42 respond_to do |format|
42 respond_to do |format|
43 format.html {render_error :status => 406}
43 format.html {render_error :status => 406}
44 format.api
44 format.api
45 end
45 end
46 end
46 end
47
47
48 def new
48 def new
49 @query = query_class.new
49 @query = query_class.new
50 @query.user = User.current
50 @query.user = User.current
51 @query.project = @project
51 @query.project = @project
52 @query.build_from_params(params)
52 @query.build_from_params(params)
53 end
53 end
54
54
55 def create
55 def create
56 @query = query_class.new
56 @query = query_class.new
57 @query.user = User.current
57 @query.user = User.current
58 @query.project = @project
58 @query.project = @project
59 update_query_from_params
59 update_query_from_params
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_items(:query_id => @query)
63 redirect_to_items(: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 update_query_from_params
73 update_query_from_params
74
74
75 if @query.save
75 if @query.save
76 flash[:notice] = l(:notice_successful_update)
76 flash[:notice] = l(:notice_successful_update)
77 redirect_to_items(:query_id => @query)
77 redirect_to_items(:query_id => @query)
78 else
78 else
79 render :action => 'edit'
79 render :action => 'edit'
80 end
80 end
81 end
81 end
82
82
83 def destroy
83 def destroy
84 @query.destroy
84 @query.destroy
85 redirect_to_items(:set_filter => 1)
85 redirect_to_items(:set_filter => 1)
86 end
86 end
87
87
88 # Returns the values for a query filter
89 def filter
90 q = query_class.new
91 if params[:project_id].present?
92 q.project = Project.find(params[:project_id])
93 end
94
95 unless User.current.allowed_to?(q.class.view_permission, q.project, :global => true)
96 raise Unauthorized
97 end
98
99 filter = q.available_filters[params[:name].to_s]
100 values = filter ? filter.values : []
101
102 render :json => values
103 rescue ActiveRecord::RecordNotFound
104 render_404
105 end
106
88 private
107 private
89
108
90 def find_query
109 def find_query
91 @query = Query.find(params[:id])
110 @query = Query.find(params[:id])
92 @project = @query.project
111 @project = @query.project
93 render_403 unless @query.editable_by?(User.current)
112 render_403 unless @query.editable_by?(User.current)
94 rescue ActiveRecord::RecordNotFound
113 rescue ActiveRecord::RecordNotFound
95 render_404
114 render_404
96 end
115 end
97
116
98 def find_optional_project
117 def find_optional_project
99 @project = Project.find(params[:project_id]) if params[:project_id]
118 @project = Project.find(params[:project_id]) if params[:project_id]
100 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
119 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
101 rescue ActiveRecord::RecordNotFound
120 rescue ActiveRecord::RecordNotFound
102 render_404
121 render_404
103 end
122 end
104
123
105 def update_query_from_params
124 def update_query_from_params
106 @query.project = params[:query_is_for_all] ? nil : @project
125 @query.project = params[:query_is_for_all] ? nil : @project
107 @query.build_from_params(params)
126 @query.build_from_params(params)
108 @query.column_names = nil if params[:default_columns]
127 @query.column_names = nil if params[:default_columns]
109 @query.sort_criteria = params[:query] && params[:query][:sort_criteria]
128 @query.sort_criteria = params[:query] && params[:query][:sort_criteria]
110 @query.name = params[:query] && params[:query][:name]
129 @query.name = params[:query] && params[:query][:name]
111 if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
130 if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
112 @query.visibility = (params[:query] && params[:query][:visibility]) || Query::VISIBILITY_PRIVATE
131 @query.visibility = (params[:query] && params[:query][:visibility]) || Query::VISIBILITY_PRIVATE
113 @query.role_ids = params[:query] && params[:query][:role_ids]
132 @query.role_ids = params[:query] && params[:query][:role_ids]
114 else
133 else
115 @query.visibility = Query::VISIBILITY_PRIVATE
134 @query.visibility = Query::VISIBILITY_PRIVATE
116 end
135 end
117 @query
136 @query
118 end
137 end
119
138
120 def redirect_to_items(options)
139 def redirect_to_items(options)
121 method = "redirect_to_#{@query.class.name.underscore}"
140 method = "redirect_to_#{@query.class.name.underscore}"
122 send method, options
141 send method, options
123 end
142 end
124
143
125 def redirect_to_issue_query(options)
144 def redirect_to_issue_query(options)
126 if params[:gantt]
145 if params[:gantt]
127 if @project
146 if @project
128 redirect_to project_gantt_path(@project, options)
147 redirect_to project_gantt_path(@project, options)
129 else
148 else
130 redirect_to issues_gantt_path(options)
149 redirect_to issues_gantt_path(options)
131 end
150 end
132 else
151 else
133 redirect_to _project_issues_path(@project, options)
152 redirect_to _project_issues_path(@project, options)
134 end
153 end
135 end
154 end
136
155
137 def redirect_to_time_entry_query(options)
156 def redirect_to_time_entry_query(options)
138 redirect_to _time_entries_path(@project, nil, options)
157 redirect_to _time_entries_path(@project, nil, options)
139 end
158 end
140
159
141 # Returns the Query subclass, IssueQuery by default
160 # Returns the Query subclass, IssueQuery by default
142 # for compatibility with previous behaviour
161 # for compatibility with previous behaviour
143 def query_class
162 def query_class
144 Query.get_subclass(params[:type] || 'IssueQuery')
163 Query.get_subclass(params[:type] || 'IssueQuery')
145 end
164 end
146 end
165 end
@@ -1,549 +1,508
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21 self.view_permission = :view_issues
21 self.view_permission = :view_issues
22
22
23 self.available_columns = [
23 self.available_columns = [
24 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
24 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
25 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
26 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
26 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
27 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
27 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
28 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
28 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
29 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
29 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
30 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
30 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
31 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
31 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
32 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
32 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
33 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
33 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
34 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
34 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
35 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
35 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
36 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
36 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
37 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
37 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
38 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
38 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours", :totalable => true),
39 QueryColumn.new(:total_estimated_hours,
39 QueryColumn.new(:total_estimated_hours,
40 :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
40 :sortable => "COALESCE((SELECT SUM(estimated_hours) FROM #{Issue.table_name} subtasks" +
41 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
41 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
42 :default_order => 'desc'),
42 :default_order => 'desc'),
43 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
43 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
44 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
44 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
45 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
45 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
46 QueryColumn.new(:relations, :caption => :label_related_issues),
46 QueryColumn.new(:relations, :caption => :label_related_issues),
47 QueryColumn.new(:description, :inline => false)
47 QueryColumn.new(:description, :inline => false)
48 ]
48 ]
49
49
50 def initialize(attributes=nil, *args)
50 def initialize(attributes=nil, *args)
51 super attributes
51 super attributes
52 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
52 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
53 end
53 end
54
54
55 def draw_relations
55 def draw_relations
56 r = options[:draw_relations]
56 r = options[:draw_relations]
57 r.nil? || r == '1'
57 r.nil? || r == '1'
58 end
58 end
59
59
60 def draw_relations=(arg)
60 def draw_relations=(arg)
61 options[:draw_relations] = (arg == '0' ? '0' : nil)
61 options[:draw_relations] = (arg == '0' ? '0' : nil)
62 end
62 end
63
63
64 def draw_progress_line
64 def draw_progress_line
65 r = options[:draw_progress_line]
65 r = options[:draw_progress_line]
66 r == '1'
66 r == '1'
67 end
67 end
68
68
69 def draw_progress_line=(arg)
69 def draw_progress_line=(arg)
70 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
70 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
71 end
71 end
72
72
73 def build_from_params(params)
73 def build_from_params(params)
74 super
74 super
75 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
75 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
76 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
76 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
77 self
77 self
78 end
78 end
79
79
80 def initialize_available_filters
80 def initialize_available_filters
81 principals = []
82 subprojects = []
83 versions = []
84 categories = []
85 issue_custom_fields = []
86
87 if project
88 principals += project.principals.visible
89 unless project.leaf?
90 subprojects = project.descendants.visible.to_a
91 principals += Principal.member_of(subprojects).visible
92 end
93 versions = project.shared_versions.to_a
94 categories = project.issue_categories.to_a
95 issue_custom_fields = project.all_issue_custom_fields
96 else
97 if all_projects.any?
98 principals += Principal.member_of(all_projects).visible
99 end
100 versions = Version.visible.where(:sharing => 'system').to_a
101 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
102 end
103 principals.uniq!
104 principals.sort!
105 principals.reject! {|p| p.is_a?(GroupBuiltin)}
106 users = principals.select {|p| p.is_a?(User)}
107
108 add_available_filter "status_id",
81 add_available_filter "status_id",
109 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
82 :type => :list_status, :values => lambda { IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } }
110
83
111 if project.nil?
84 add_available_filter("project_id",
112 project_values = []
85 :type => :list, :values => lambda { project_values }
113 if User.current.logged? && User.current.memberships.any?
86 ) if project.nil?
114 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
115 end
116 project_values += all_projects_values
117 add_available_filter("project_id",
118 :type => :list, :values => project_values
119 ) unless project_values.empty?
120 end
121
87
122 add_available_filter "tracker_id",
88 add_available_filter "tracker_id",
123 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
89 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
90
124 add_available_filter "priority_id",
91 add_available_filter "priority_id",
125 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
92 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
126
93
127 author_values = []
128 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
129 author_values += users.collect{|s| [s.name, s.id.to_s] }
130 add_available_filter("author_id",
94 add_available_filter("author_id",
131 :type => :list, :values => author_values
95 :type => :list, :values => lambda { author_values }
132 ) unless author_values.empty?
96 )
133
97
134 assigned_to_values = []
135 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
136 assigned_to_values += (Setting.issue_group_assignment? ?
137 principals : users).collect{|s| [s.name, s.id.to_s] }
138 add_available_filter("assigned_to_id",
98 add_available_filter("assigned_to_id",
139 :type => :list_optional, :values => assigned_to_values
99 :type => :list_optional, :values => lambda { assigned_to_values }
140 ) unless assigned_to_values.empty?
100 )
141
101
142 group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
143 add_available_filter("member_of_group",
102 add_available_filter("member_of_group",
144 :type => :list_optional, :values => group_values
103 :type => :list_optional, :values => lambda { Group.givable.visible.collect {|g| [g.name, g.id.to_s] } }
145 ) unless group_values.empty?
104 )
146
105
147 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
148 add_available_filter("assigned_to_role",
106 add_available_filter("assigned_to_role",
149 :type => :list_optional, :values => role_values
107 :type => :list_optional, :values => lambda { Role.givable.collect {|r| [r.name, r.id.to_s] } }
150 ) unless role_values.empty?
108 )
151
109
152 add_available_filter "fixed_version_id",
110 add_available_filter "fixed_version_id",
153 :type => :list_optional,
111 :type => :list_optional, :values => lambda { fixed_version_values }
154 :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
155
112
156 add_available_filter "fixed_version.due_date",
113 add_available_filter "fixed_version.due_date",
157 :type => :date,
114 :type => :date,
158 :name => l(:label_attribute_of_fixed_version, :name => l(:field_effective_date))
115 :name => l(:label_attribute_of_fixed_version, :name => l(:field_effective_date))
159
116
160 add_available_filter "fixed_version.status",
117 add_available_filter "fixed_version.status",
161 :type => :list,
118 :type => :list,
162 :name => l(:label_attribute_of_fixed_version, :name => l(:field_status)),
119 :name => l(:label_attribute_of_fixed_version, :name => l(:field_status)),
163 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
120 :values => Version::VERSION_STATUSES.map{|s| [l("version_status_#{s}"), s] }
164
121
165 add_available_filter "category_id",
122 add_available_filter "category_id",
166 :type => :list_optional,
123 :type => :list_optional,
167 :values => categories.collect{|s| [s.name, s.id.to_s] }
124 :values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } } if project
168
125
169 add_available_filter "subject", :type => :text
126 add_available_filter "subject", :type => :text
170 add_available_filter "description", :type => :text
127 add_available_filter "description", :type => :text
171 add_available_filter "created_on", :type => :date_past
128 add_available_filter "created_on", :type => :date_past
172 add_available_filter "updated_on", :type => :date_past
129 add_available_filter "updated_on", :type => :date_past
173 add_available_filter "closed_on", :type => :date_past
130 add_available_filter "closed_on", :type => :date_past
174 add_available_filter "start_date", :type => :date
131 add_available_filter "start_date", :type => :date
175 add_available_filter "due_date", :type => :date
132 add_available_filter "due_date", :type => :date
176 add_available_filter "estimated_hours", :type => :float
133 add_available_filter "estimated_hours", :type => :float
177 add_available_filter "done_ratio", :type => :integer
134 add_available_filter "done_ratio", :type => :integer
178
135
179 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
136 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
180 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
137 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
181 add_available_filter "is_private",
138 add_available_filter "is_private",
182 :type => :list,
139 :type => :list,
183 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
140 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
184 end
141 end
185
142
186 if User.current.logged?
143 if User.current.logged?
187 add_available_filter "watcher_id",
144 add_available_filter "watcher_id",
188 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
145 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
189 end
146 end
190
147
191 if subprojects.any?
148 if project && !project.leaf?
192 add_available_filter "subproject_id",
149 add_available_filter "subproject_id",
193 :type => :list_subprojects,
150 :type => :list_subprojects,
194 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
151 :values => lambda { subproject_values }
195 end
152 end
196
153
154
155 issue_custom_fields = project ? project.all_issue_custom_fields : IssueCustomField.where(:is_for_all => true)
197 add_custom_fields_filters(issue_custom_fields)
156 add_custom_fields_filters(issue_custom_fields)
198
157
199 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
158 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
200
159
201 IssueRelation::TYPES.each do |relation_type, options|
160 IssueRelation::TYPES.each do |relation_type, options|
202 add_available_filter relation_type, :type => :relation, :label => options[:name]
161 add_available_filter relation_type, :type => :relation, :label => options[:name], :values => lambda {all_projects_values}
203 end
162 end
204 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
163 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
205 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
164 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
206
165
207 add_available_filter "issue_id", :type => :integer, :label => :label_issue
166 add_available_filter "issue_id", :type => :integer, :label => :label_issue
208
167
209 Tracker.disabled_core_fields(trackers).each {|field|
168 Tracker.disabled_core_fields(trackers).each {|field|
210 delete_available_filter field
169 delete_available_filter field
211 }
170 }
212 end
171 end
213
172
214 def available_columns
173 def available_columns
215 return @available_columns if @available_columns
174 return @available_columns if @available_columns
216 @available_columns = self.class.available_columns.dup
175 @available_columns = self.class.available_columns.dup
217 @available_columns += (project ?
176 @available_columns += (project ?
218 project.all_issue_custom_fields :
177 project.all_issue_custom_fields :
219 IssueCustomField
178 IssueCustomField
220 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
179 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
221
180
222 if User.current.allowed_to?(:view_time_entries, project, :global => true)
181 if User.current.allowed_to?(:view_time_entries, project, :global => true)
223 index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
182 index = @available_columns.find_index {|column| column.name == :total_estimated_hours}
224 index = (index ? index + 1 : -1)
183 index = (index ? index + 1 : -1)
225 # insert the column after total_estimated_hours or at the end
184 # insert the column after total_estimated_hours or at the end
226 @available_columns.insert index, QueryColumn.new(:spent_hours,
185 @available_columns.insert index, QueryColumn.new(:spent_hours,
227 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
186 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
228 :default_order => 'desc',
187 :default_order => 'desc',
229 :caption => :label_spent_time,
188 :caption => :label_spent_time,
230 :totalable => true
189 :totalable => true
231 )
190 )
232 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
191 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
233 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
192 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
234 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
193 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
235 :default_order => 'desc',
194 :default_order => 'desc',
236 :caption => :label_total_spent_time
195 :caption => :label_total_spent_time
237 )
196 )
238 end
197 end
239
198
240 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
199 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
241 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
200 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
242 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
201 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
243 end
202 end
244
203
245 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
204 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
246 @available_columns.reject! {|column|
205 @available_columns.reject! {|column|
247 disabled_fields.include?(column.name.to_s)
206 disabled_fields.include?(column.name.to_s)
248 }
207 }
249
208
250 @available_columns
209 @available_columns
251 end
210 end
252
211
253 def default_columns_names
212 def default_columns_names
254 @default_columns_names ||= begin
213 @default_columns_names ||= begin
255 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
214 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
256
215
257 project.present? ? default_columns : [:project] | default_columns
216 project.present? ? default_columns : [:project] | default_columns
258 end
217 end
259 end
218 end
260
219
261 def default_totalable_names
220 def default_totalable_names
262 Setting.issue_list_default_totals.map(&:to_sym)
221 Setting.issue_list_default_totals.map(&:to_sym)
263 end
222 end
264
223
265 def base_scope
224 def base_scope
266 Issue.visible.joins(:status, :project).where(statement)
225 Issue.visible.joins(:status, :project).where(statement)
267 end
226 end
268
227
269 # Returns the issue count
228 # Returns the issue count
270 def issue_count
229 def issue_count
271 base_scope.count
230 base_scope.count
272 rescue ::ActiveRecord::StatementInvalid => e
231 rescue ::ActiveRecord::StatementInvalid => e
273 raise StatementInvalid.new(e.message)
232 raise StatementInvalid.new(e.message)
274 end
233 end
275
234
276 # Returns the issue count by group or nil if query is not grouped
235 # Returns the issue count by group or nil if query is not grouped
277 def issue_count_by_group
236 def issue_count_by_group
278 grouped_query do |scope|
237 grouped_query do |scope|
279 scope.count
238 scope.count
280 end
239 end
281 end
240 end
282
241
283 # Returns sum of all the issue's estimated_hours
242 # Returns sum of all the issue's estimated_hours
284 def total_for_estimated_hours(scope)
243 def total_for_estimated_hours(scope)
285 map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
244 map_total(scope.sum(:estimated_hours)) {|t| t.to_f.round(2)}
286 end
245 end
287
246
288 # Returns sum of all the issue's time entries hours
247 # Returns sum of all the issue's time entries hours
289 def total_for_spent_hours(scope)
248 def total_for_spent_hours(scope)
290 total = if group_by_column.try(:name) == :project
249 total = if group_by_column.try(:name) == :project
291 # TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
250 # TODO: remove this when https://github.com/rails/rails/issues/21922 is fixed
292 # We have to do a custom join without the time_entries.project_id column
251 # We have to do a custom join without the time_entries.project_id column
293 # that would trigger a ambiguous column name error
252 # that would trigger a ambiguous column name error
294 scope.joins("JOIN (SELECT issue_id, hours FROM #{TimeEntry.table_name}) AS joined_time_entries ON joined_time_entries.issue_id = #{Issue.table_name}.id").
253 scope.joins("JOIN (SELECT issue_id, hours FROM #{TimeEntry.table_name}) AS joined_time_entries ON joined_time_entries.issue_id = #{Issue.table_name}.id").
295 sum("joined_time_entries.hours")
254 sum("joined_time_entries.hours")
296 else
255 else
297 scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
256 scope.joins(:time_entries).sum("#{TimeEntry.table_name}.hours")
298 end
257 end
299 map_total(total) {|t| t.to_f.round(2)}
258 map_total(total) {|t| t.to_f.round(2)}
300 end
259 end
301
260
302 # Returns the issues
261 # Returns the issues
303 # Valid options are :order, :offset, :limit, :include, :conditions
262 # Valid options are :order, :offset, :limit, :include, :conditions
304 def issues(options={})
263 def issues(options={})
305 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
264 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
306
265
307 scope = Issue.visible.
266 scope = Issue.visible.
308 joins(:status, :project).
267 joins(:status, :project).
309 where(statement).
268 where(statement).
310 includes(([:status, :project] + (options[:include] || [])).uniq).
269 includes(([:status, :project] + (options[:include] || [])).uniq).
311 where(options[:conditions]).
270 where(options[:conditions]).
312 order(order_option).
271 order(order_option).
313 joins(joins_for_order_statement(order_option.join(','))).
272 joins(joins_for_order_statement(order_option.join(','))).
314 limit(options[:limit]).
273 limit(options[:limit]).
315 offset(options[:offset])
274 offset(options[:offset])
316
275
317 scope = scope.preload(:custom_values)
276 scope = scope.preload(:custom_values)
318 if has_column?(:author)
277 if has_column?(:author)
319 scope = scope.preload(:author)
278 scope = scope.preload(:author)
320 end
279 end
321
280
322 issues = scope.to_a
281 issues = scope.to_a
323
282
324 if has_column?(:spent_hours)
283 if has_column?(:spent_hours)
325 Issue.load_visible_spent_hours(issues)
284 Issue.load_visible_spent_hours(issues)
326 end
285 end
327 if has_column?(:total_spent_hours)
286 if has_column?(:total_spent_hours)
328 Issue.load_visible_total_spent_hours(issues)
287 Issue.load_visible_total_spent_hours(issues)
329 end
288 end
330 if has_column?(:relations)
289 if has_column?(:relations)
331 Issue.load_visible_relations(issues)
290 Issue.load_visible_relations(issues)
332 end
291 end
333 issues
292 issues
334 rescue ::ActiveRecord::StatementInvalid => e
293 rescue ::ActiveRecord::StatementInvalid => e
335 raise StatementInvalid.new(e.message)
294 raise StatementInvalid.new(e.message)
336 end
295 end
337
296
338 # Returns the issues ids
297 # Returns the issues ids
339 def issue_ids(options={})
298 def issue_ids(options={})
340 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
299 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
341
300
342 Issue.visible.
301 Issue.visible.
343 joins(:status, :project).
302 joins(:status, :project).
344 where(statement).
303 where(statement).
345 includes(([:status, :project] + (options[:include] || [])).uniq).
304 includes(([:status, :project] + (options[:include] || [])).uniq).
346 references(([:status, :project] + (options[:include] || [])).uniq).
305 references(([:status, :project] + (options[:include] || [])).uniq).
347 where(options[:conditions]).
306 where(options[:conditions]).
348 order(order_option).
307 order(order_option).
349 joins(joins_for_order_statement(order_option.join(','))).
308 joins(joins_for_order_statement(order_option.join(','))).
350 limit(options[:limit]).
309 limit(options[:limit]).
351 offset(options[:offset]).
310 offset(options[:offset]).
352 pluck(:id)
311 pluck(:id)
353 rescue ::ActiveRecord::StatementInvalid => e
312 rescue ::ActiveRecord::StatementInvalid => e
354 raise StatementInvalid.new(e.message)
313 raise StatementInvalid.new(e.message)
355 end
314 end
356
315
357 # Returns the journals
316 # Returns the journals
358 # Valid options are :order, :offset, :limit
317 # Valid options are :order, :offset, :limit
359 def journals(options={})
318 def journals(options={})
360 Journal.visible.
319 Journal.visible.
361 joins(:issue => [:project, :status]).
320 joins(:issue => [:project, :status]).
362 where(statement).
321 where(statement).
363 order(options[:order]).
322 order(options[:order]).
364 limit(options[:limit]).
323 limit(options[:limit]).
365 offset(options[:offset]).
324 offset(options[:offset]).
366 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
325 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
367 to_a
326 to_a
368 rescue ::ActiveRecord::StatementInvalid => e
327 rescue ::ActiveRecord::StatementInvalid => e
369 raise StatementInvalid.new(e.message)
328 raise StatementInvalid.new(e.message)
370 end
329 end
371
330
372 # Returns the versions
331 # Returns the versions
373 # Valid options are :conditions
332 # Valid options are :conditions
374 def versions(options={})
333 def versions(options={})
375 Version.visible.
334 Version.visible.
376 where(project_statement).
335 where(project_statement).
377 where(options[:conditions]).
336 where(options[:conditions]).
378 includes(:project).
337 includes(:project).
379 references(:project).
338 references(:project).
380 to_a
339 to_a
381 rescue ::ActiveRecord::StatementInvalid => e
340 rescue ::ActiveRecord::StatementInvalid => e
382 raise StatementInvalid.new(e.message)
341 raise StatementInvalid.new(e.message)
383 end
342 end
384
343
385 def sql_for_watcher_id_field(field, operator, value)
344 def sql_for_watcher_id_field(field, operator, value)
386 db_table = Watcher.table_name
345 db_table = Watcher.table_name
387 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
346 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
388 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
347 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
389 end
348 end
390
349
391 def sql_for_member_of_group_field(field, operator, value)
350 def sql_for_member_of_group_field(field, operator, value)
392 if operator == '*' # Any group
351 if operator == '*' # Any group
393 groups = Group.givable
352 groups = Group.givable
394 operator = '=' # Override the operator since we want to find by assigned_to
353 operator = '=' # Override the operator since we want to find by assigned_to
395 elsif operator == "!*"
354 elsif operator == "!*"
396 groups = Group.givable
355 groups = Group.givable
397 operator = '!' # Override the operator since we want to find by assigned_to
356 operator = '!' # Override the operator since we want to find by assigned_to
398 else
357 else
399 groups = Group.where(:id => value).to_a
358 groups = Group.where(:id => value).to_a
400 end
359 end
401 groups ||= []
360 groups ||= []
402
361
403 members_of_groups = groups.inject([]) {|user_ids, group|
362 members_of_groups = groups.inject([]) {|user_ids, group|
404 user_ids + group.user_ids + [group.id]
363 user_ids + group.user_ids + [group.id]
405 }.uniq.compact.sort.collect(&:to_s)
364 }.uniq.compact.sort.collect(&:to_s)
406
365
407 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
366 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
408 end
367 end
409
368
410 def sql_for_assigned_to_role_field(field, operator, value)
369 def sql_for_assigned_to_role_field(field, operator, value)
411 case operator
370 case operator
412 when "*", "!*" # Member / Not member
371 when "*", "!*" # Member / Not member
413 sw = operator == "!*" ? 'NOT' : ''
372 sw = operator == "!*" ? 'NOT' : ''
414 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
373 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
415 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
374 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
416 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
375 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
417 when "=", "!"
376 when "=", "!"
418 role_cond = value.any? ?
377 role_cond = value.any? ?
419 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
378 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
420 "1=0"
379 "1=0"
421
380
422 sw = operator == "!" ? 'NOT' : ''
381 sw = operator == "!" ? 'NOT' : ''
423 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
382 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
424 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
383 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
425 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
384 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
426 end
385 end
427 end
386 end
428
387
429 def sql_for_fixed_version_status_field(field, operator, value)
388 def sql_for_fixed_version_status_field(field, operator, value)
430 where = sql_for_field(field, operator, value, Version.table_name, "status")
389 where = sql_for_field(field, operator, value, Version.table_name, "status")
431 version_ids = versions(:conditions => [where]).map(&:id)
390 version_ids = versions(:conditions => [where]).map(&:id)
432
391
433 nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
392 nl = operator == "!" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
434 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
393 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
435 end
394 end
436
395
437 def sql_for_fixed_version_due_date_field(field, operator, value)
396 def sql_for_fixed_version_due_date_field(field, operator, value)
438 where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
397 where = sql_for_field(field, operator, value, Version.table_name, "effective_date")
439 version_ids = versions(:conditions => [where]).map(&:id)
398 version_ids = versions(:conditions => [where]).map(&:id)
440
399
441 nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
400 nl = operator == "!*" ? "#{Issue.table_name}.fixed_version_id IS NULL OR" : ''
442 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
401 "(#{nl} #{sql_for_field("fixed_version_id", "=", version_ids, Issue.table_name, "fixed_version_id")})"
443 end
402 end
444
403
445 def sql_for_is_private_field(field, operator, value)
404 def sql_for_is_private_field(field, operator, value)
446 op = (operator == "=" ? 'IN' : 'NOT IN')
405 op = (operator == "=" ? 'IN' : 'NOT IN')
447 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
406 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
448
407
449 "#{Issue.table_name}.is_private #{op} (#{va})"
408 "#{Issue.table_name}.is_private #{op} (#{va})"
450 end
409 end
451
410
452 def sql_for_parent_id_field(field, operator, value)
411 def sql_for_parent_id_field(field, operator, value)
453 case operator
412 case operator
454 when "="
413 when "="
455 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
414 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
456 when "~"
415 when "~"
457 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
416 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
458 if root_id && lft && rgt
417 if root_id && lft && rgt
459 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
418 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
460 else
419 else
461 "1=0"
420 "1=0"
462 end
421 end
463 when "!*"
422 when "!*"
464 "#{Issue.table_name}.parent_id IS NULL"
423 "#{Issue.table_name}.parent_id IS NULL"
465 when "*"
424 when "*"
466 "#{Issue.table_name}.parent_id IS NOT NULL"
425 "#{Issue.table_name}.parent_id IS NOT NULL"
467 end
426 end
468 end
427 end
469
428
470 def sql_for_child_id_field(field, operator, value)
429 def sql_for_child_id_field(field, operator, value)
471 case operator
430 case operator
472 when "="
431 when "="
473 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
432 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
474 if parent_id
433 if parent_id
475 "#{Issue.table_name}.id = #{parent_id}"
434 "#{Issue.table_name}.id = #{parent_id}"
476 else
435 else
477 "1=0"
436 "1=0"
478 end
437 end
479 when "~"
438 when "~"
480 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
439 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
481 if root_id && lft && rgt
440 if root_id && lft && rgt
482 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
441 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
483 else
442 else
484 "1=0"
443 "1=0"
485 end
444 end
486 when "!*"
445 when "!*"
487 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
446 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
488 when "*"
447 when "*"
489 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
448 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
490 end
449 end
491 end
450 end
492
451
493 def sql_for_issue_id_field(field, operator, value)
452 def sql_for_issue_id_field(field, operator, value)
494 if operator == "="
453 if operator == "="
495 # accepts a comma separated list of ids
454 # accepts a comma separated list of ids
496 ids = value.first.to_s.scan(/\d+/).map(&:to_i)
455 ids = value.first.to_s.scan(/\d+/).map(&:to_i)
497 if ids.present?
456 if ids.present?
498 "#{Issue.table_name}.id IN (#{ids.join(",")})"
457 "#{Issue.table_name}.id IN (#{ids.join(",")})"
499 else
458 else
500 "1=0"
459 "1=0"
501 end
460 end
502 else
461 else
503 sql_for_field("id", operator, value, Issue.table_name, "id")
462 sql_for_field("id", operator, value, Issue.table_name, "id")
504 end
463 end
505 end
464 end
506
465
507 def sql_for_relations(field, operator, value, options={})
466 def sql_for_relations(field, operator, value, options={})
508 relation_options = IssueRelation::TYPES[field]
467 relation_options = IssueRelation::TYPES[field]
509 return relation_options unless relation_options
468 return relation_options unless relation_options
510
469
511 relation_type = field
470 relation_type = field
512 join_column, target_join_column = "issue_from_id", "issue_to_id"
471 join_column, target_join_column = "issue_from_id", "issue_to_id"
513 if relation_options[:reverse] || options[:reverse]
472 if relation_options[:reverse] || options[:reverse]
514 relation_type = relation_options[:reverse] || relation_type
473 relation_type = relation_options[:reverse] || relation_type
515 join_column, target_join_column = target_join_column, join_column
474 join_column, target_join_column = target_join_column, join_column
516 end
475 end
517
476
518 sql = case operator
477 sql = case operator
519 when "*", "!*"
478 when "*", "!*"
520 op = (operator == "*" ? 'IN' : 'NOT IN')
479 op = (operator == "*" ? 'IN' : 'NOT IN')
521 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
480 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
522 when "=", "!"
481 when "=", "!"
523 op = (operator == "=" ? 'IN' : 'NOT IN')
482 op = (operator == "=" ? 'IN' : 'NOT IN')
524 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
483 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
525 when "=p", "=!p", "!p"
484 when "=p", "=!p", "!p"
526 op = (operator == "!p" ? 'NOT IN' : 'IN')
485 op = (operator == "!p" ? 'NOT IN' : 'IN')
527 comp = (operator == "=!p" ? '<>' : '=')
486 comp = (operator == "=!p" ? '<>' : '=')
528 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
487 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
529 when "*o", "!o"
488 when "*o", "!o"
530 op = (operator == "!o" ? 'NOT IN' : 'IN')
489 op = (operator == "!o" ? 'NOT IN' : 'IN')
531 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))"
490 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false}))"
532 end
491 end
533
492
534 if relation_options[:sym] == field && !options[:reverse]
493 if relation_options[:sym] == field && !options[:reverse]
535 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
494 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
536 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
495 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
537 end
496 end
538 "(#{sql})"
497 "(#{sql})"
539 end
498 end
540
499
541 def find_assigned_to_id_filter_values(values)
500 def find_assigned_to_id_filter_values(values)
542 Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
501 Principal.visible.where(:id => values).map {|p| [p.name, p.id.to_s]}
543 end
502 end
544 alias :find_author_id_filter_values :find_assigned_to_id_filter_values
503 alias :find_author_id_filter_values :find_assigned_to_id_filter_values
545
504
546 IssueRelation::TYPES.keys.each do |relation_type|
505 IssueRelation::TYPES.keys.each do |relation_type|
547 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
506 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
548 end
507 end
549 end
508 end
@@ -1,1136 +1,1231
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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, :totalable, :default_order
19 attr_accessor :name, :sortable, :groupable, :totalable, :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.totalable = options[:totalable] || false
29 self.totalable = options[:totalable] || false
30 self.default_order = options[:default_order]
30 self.default_order = options[:default_order]
31 @inline = options.key?(:inline) ? options[:inline] : true
31 @inline = options.key?(:inline) ? options[:inline] : true
32 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 @frozen = options[:frozen]
33 @frozen = options[:frozen]
34 end
34 end
35
35
36 def caption
36 def caption
37 case @caption_key
37 case @caption_key
38 when Symbol
38 when Symbol
39 l(@caption_key)
39 l(@caption_key)
40 when Proc
40 when Proc
41 @caption_key.call
41 @caption_key.call
42 else
42 else
43 @caption_key
43 @caption_key
44 end
44 end
45 end
45 end
46
46
47 # Returns true if the column is sortable, otherwise false
47 # Returns true if the column is sortable, otherwise false
48 def sortable?
48 def sortable?
49 !@sortable.nil?
49 !@sortable.nil?
50 end
50 end
51
51
52 def sortable
52 def sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 end
54 end
55
55
56 def inline?
56 def inline?
57 @inline
57 @inline
58 end
58 end
59
59
60 def frozen?
60 def frozen?
61 @frozen
61 @frozen
62 end
62 end
63
63
64 def value(object)
64 def value(object)
65 object.send name
65 object.send name
66 end
66 end
67
67
68 def value_object(object)
68 def value_object(object)
69 object.send name
69 object.send name
70 end
70 end
71
71
72 def css_classes
72 def css_classes
73 name
73 name
74 end
74 end
75 end
75 end
76
76
77 class QueryAssociationColumn < QueryColumn
77 class QueryAssociationColumn < QueryColumn
78
78
79 def initialize(association, attribute, options={})
79 def initialize(association, attribute, options={})
80 @association = association
80 @association = association
81 @attribute = attribute
81 @attribute = attribute
82 name_with_assoc = "#{association}.#{attribute}".to_sym
82 name_with_assoc = "#{association}.#{attribute}".to_sym
83 super(name_with_assoc, options)
83 super(name_with_assoc, options)
84 end
84 end
85
85
86 def value_object(object)
86 def value_object(object)
87 if assoc = object.send(@association)
87 if assoc = object.send(@association)
88 assoc.send @attribute
88 assoc.send @attribute
89 end
89 end
90 end
90 end
91
91
92 def css_classes
92 def css_classes
93 @css_classes ||= "#{@association}-#{@attribute}"
93 @css_classes ||= "#{@association}-#{@attribute}"
94 end
94 end
95 end
95 end
96
96
97 class QueryCustomFieldColumn < QueryColumn
97 class QueryCustomFieldColumn < QueryColumn
98
98
99 def initialize(custom_field, options={})
99 def initialize(custom_field, options={})
100 self.name = "cf_#{custom_field.id}".to_sym
100 self.name = "cf_#{custom_field.id}".to_sym
101 self.sortable = custom_field.order_statement || false
101 self.sortable = custom_field.order_statement || false
102 self.groupable = custom_field.group_statement || false
102 self.groupable = custom_field.group_statement || false
103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
104 @inline = true
104 @inline = true
105 @cf = custom_field
105 @cf = custom_field
106 end
106 end
107
107
108 def caption
108 def caption
109 @cf.name
109 @cf.name
110 end
110 end
111
111
112 def custom_field
112 def custom_field
113 @cf
113 @cf
114 end
114 end
115
115
116 def value_object(object)
116 def value_object(object)
117 if custom_field.visible_by?(object.project, User.current)
117 if custom_field.visible_by?(object.project, User.current)
118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
120 else
120 else
121 nil
121 nil
122 end
122 end
123 end
123 end
124
124
125 def value(object)
125 def value(object)
126 raw = value_object(object)
126 raw = value_object(object)
127 if raw.is_a?(Array)
127 if raw.is_a?(Array)
128 raw.map {|r| @cf.cast_value(r.value)}
128 raw.map {|r| @cf.cast_value(r.value)}
129 elsif raw
129 elsif raw
130 @cf.cast_value(raw.value)
130 @cf.cast_value(raw.value)
131 else
131 else
132 nil
132 nil
133 end
133 end
134 end
134 end
135
135
136 def css_classes
136 def css_classes
137 @css_classes ||= "#{name} #{@cf.field_format}"
137 @css_classes ||= "#{name} #{@cf.field_format}"
138 end
138 end
139 end
139 end
140
140
141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
142
142
143 def initialize(association, custom_field, options={})
143 def initialize(association, custom_field, options={})
144 super(custom_field, options)
144 super(custom_field, options)
145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
146 # TODO: support sorting/grouping by association custom field
146 # TODO: support sorting/grouping by association custom field
147 self.sortable = false
147 self.sortable = false
148 self.groupable = false
148 self.groupable = false
149 @association = association
149 @association = association
150 end
150 end
151
151
152 def value_object(object)
152 def value_object(object)
153 if assoc = object.send(@association)
153 if assoc = object.send(@association)
154 super(assoc)
154 super(assoc)
155 end
155 end
156 end
156 end
157
157
158 def css_classes
158 def css_classes
159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
160 end
160 end
161 end
161 end
162
162
163 class QueryFilter
164 include Redmine::I18n
165
166 def initialize(field, options)
167 @field = field.to_s
168 @options = options
169 @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
170 # Consider filters with a Proc for values as remote by default
171 @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
172 end
173
174 def [](arg)
175 if arg == :values
176 values
177 else
178 @options[arg]
179 end
180 end
181
182 def values
183 @values ||= begin
184 values = @options[:values]
185 if values.is_a?(Proc)
186 values = values.call
187 end
188 values
189 end
190 end
191
192 def remote
193 @remote
194 end
195 end
196
163 class Query < ActiveRecord::Base
197 class Query < ActiveRecord::Base
164 class StatementInvalid < ::ActiveRecord::StatementInvalid
198 class StatementInvalid < ::ActiveRecord::StatementInvalid
165 end
199 end
166
200
167 include Redmine::SubclassFactory
201 include Redmine::SubclassFactory
168
202
169 VISIBILITY_PRIVATE = 0
203 VISIBILITY_PRIVATE = 0
170 VISIBILITY_ROLES = 1
204 VISIBILITY_ROLES = 1
171 VISIBILITY_PUBLIC = 2
205 VISIBILITY_PUBLIC = 2
172
206
173 belongs_to :project
207 belongs_to :project
174 belongs_to :user
208 belongs_to :user
175 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
209 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
176 serialize :filters
210 serialize :filters
177 serialize :column_names
211 serialize :column_names
178 serialize :sort_criteria, Array
212 serialize :sort_criteria, Array
179 serialize :options, Hash
213 serialize :options, Hash
180
214
181 attr_protected :project_id, :user_id
215 attr_protected :project_id, :user_id
182
216
183 validates_presence_of :name
217 validates_presence_of :name
184 validates_length_of :name, :maximum => 255
218 validates_length_of :name, :maximum => 255
185 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
219 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
186 validate :validate_query_filters
220 validate :validate_query_filters
187 validate do |query|
221 validate do |query|
188 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
222 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
189 end
223 end
190
224
191 after_save do |query|
225 after_save do |query|
192 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
226 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
193 query.roles.clear
227 query.roles.clear
194 end
228 end
195 end
229 end
196
230
197 class_attribute :operators
231 class_attribute :operators
198 self.operators = {
232 self.operators = {
199 "=" => :label_equals,
233 "=" => :label_equals,
200 "!" => :label_not_equals,
234 "!" => :label_not_equals,
201 "o" => :label_open_issues,
235 "o" => :label_open_issues,
202 "c" => :label_closed_issues,
236 "c" => :label_closed_issues,
203 "!*" => :label_none,
237 "!*" => :label_none,
204 "*" => :label_any,
238 "*" => :label_any,
205 ">=" => :label_greater_or_equal,
239 ">=" => :label_greater_or_equal,
206 "<=" => :label_less_or_equal,
240 "<=" => :label_less_or_equal,
207 "><" => :label_between,
241 "><" => :label_between,
208 "<t+" => :label_in_less_than,
242 "<t+" => :label_in_less_than,
209 ">t+" => :label_in_more_than,
243 ">t+" => :label_in_more_than,
210 "><t+"=> :label_in_the_next_days,
244 "><t+"=> :label_in_the_next_days,
211 "t+" => :label_in,
245 "t+" => :label_in,
212 "t" => :label_today,
246 "t" => :label_today,
213 "ld" => :label_yesterday,
247 "ld" => :label_yesterday,
214 "w" => :label_this_week,
248 "w" => :label_this_week,
215 "lw" => :label_last_week,
249 "lw" => :label_last_week,
216 "l2w" => [:label_last_n_weeks, {:count => 2}],
250 "l2w" => [:label_last_n_weeks, {:count => 2}],
217 "m" => :label_this_month,
251 "m" => :label_this_month,
218 "lm" => :label_last_month,
252 "lm" => :label_last_month,
219 "y" => :label_this_year,
253 "y" => :label_this_year,
220 ">t-" => :label_less_than_ago,
254 ">t-" => :label_less_than_ago,
221 "<t-" => :label_more_than_ago,
255 "<t-" => :label_more_than_ago,
222 "><t-"=> :label_in_the_past_days,
256 "><t-"=> :label_in_the_past_days,
223 "t-" => :label_ago,
257 "t-" => :label_ago,
224 "~" => :label_contains,
258 "~" => :label_contains,
225 "!~" => :label_not_contains,
259 "!~" => :label_not_contains,
226 "=p" => :label_any_issues_in_project,
260 "=p" => :label_any_issues_in_project,
227 "=!p" => :label_any_issues_not_in_project,
261 "=!p" => :label_any_issues_not_in_project,
228 "!p" => :label_no_issues_in_project,
262 "!p" => :label_no_issues_in_project,
229 "*o" => :label_any_open_issues,
263 "*o" => :label_any_open_issues,
230 "!o" => :label_no_open_issues
264 "!o" => :label_no_open_issues
231 }
265 }
232
266
233 class_attribute :operators_by_filter_type
267 class_attribute :operators_by_filter_type
234 self.operators_by_filter_type = {
268 self.operators_by_filter_type = {
235 :list => [ "=", "!" ],
269 :list => [ "=", "!" ],
236 :list_status => [ "o", "=", "!", "c", "*" ],
270 :list_status => [ "o", "=", "!", "c", "*" ],
237 :list_optional => [ "=", "!", "!*", "*" ],
271 :list_optional => [ "=", "!", "!*", "*" ],
238 :list_subprojects => [ "*", "!*", "=" ],
272 :list_subprojects => [ "*", "!*", "=" ],
239 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
273 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
240 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
274 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
241 :string => [ "=", "~", "!", "!~", "!*", "*" ],
275 :string => [ "=", "~", "!", "!~", "!*", "*" ],
242 :text => [ "~", "!~", "!*", "*" ],
276 :text => [ "~", "!~", "!*", "*" ],
243 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
277 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
244 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
278 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
245 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
279 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
246 :tree => ["=", "~", "!*", "*"]
280 :tree => ["=", "~", "!*", "*"]
247 }
281 }
248
282
249 class_attribute :available_columns
283 class_attribute :available_columns
250 self.available_columns = []
284 self.available_columns = []
251
285
252 class_attribute :queried_class
286 class_attribute :queried_class
253
287
254 # Permission required to view the queries, set on subclasses.
288 # Permission required to view the queries, set on subclasses.
255 class_attribute :view_permission
289 class_attribute :view_permission
256
290
257 # Scope of queries that are global or on the given project
291 # Scope of queries that are global or on the given project
258 scope :global_or_on_project, lambda {|project|
292 scope :global_or_on_project, lambda {|project|
259 where(:project_id => (project.nil? ? nil : [nil, project.id]))
293 where(:project_id => (project.nil? ? nil : [nil, project.id]))
260 }
294 }
261
295
262 scope :sorted, lambda {order(:name, :id)}
296 scope :sorted, lambda {order(:name, :id)}
263
297
264 # Scope of visible queries, can be used from subclasses only.
298 # Scope of visible queries, can be used from subclasses only.
265 # Unlike other visible scopes, a class methods is used as it
299 # Unlike other visible scopes, a class methods is used as it
266 # let handle inheritance more nicely than scope DSL.
300 # let handle inheritance more nicely than scope DSL.
267 def self.visible(*args)
301 def self.visible(*args)
268 if self == ::Query
302 if self == ::Query
269 # Visibility depends on permissions for each subclass,
303 # Visibility depends on permissions for each subclass,
270 # raise an error if the scope is called from Query (eg. Query.visible)
304 # raise an error if the scope is called from Query (eg. Query.visible)
271 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
305 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
272 end
306 end
273
307
274 user = args.shift || User.current
308 user = args.shift || User.current
275 base = Project.allowed_to_condition(user, view_permission, *args)
309 base = Project.allowed_to_condition(user, view_permission, *args)
276 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
310 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
277 where("#{table_name}.project_id IS NULL OR (#{base})")
311 where("#{table_name}.project_id IS NULL OR (#{base})")
278
312
279 if user.admin?
313 if user.admin?
280 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
314 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
281 elsif user.memberships.any?
315 elsif user.memberships.any?
282 scope.where("#{table_name}.visibility = ?" +
316 scope.where("#{table_name}.visibility = ?" +
283 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
317 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
284 "SELECT DISTINCT q.id FROM #{table_name} q" +
318 "SELECT DISTINCT q.id FROM #{table_name} q" +
285 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
319 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
286 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
320 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
287 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
321 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
288 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
322 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
289 " OR #{table_name}.user_id = ?",
323 " OR #{table_name}.user_id = ?",
290 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
324 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
291 elsif user.logged?
325 elsif user.logged?
292 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
326 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
293 else
327 else
294 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
328 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
295 end
329 end
296 end
330 end
297
331
298 # Returns true if the query is visible to +user+ or the current user.
332 # Returns true if the query is visible to +user+ or the current user.
299 def visible?(user=User.current)
333 def visible?(user=User.current)
300 return true if user.admin?
334 return true if user.admin?
301 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
335 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
302 case visibility
336 case visibility
303 when VISIBILITY_PUBLIC
337 when VISIBILITY_PUBLIC
304 true
338 true
305 when VISIBILITY_ROLES
339 when VISIBILITY_ROLES
306 if project
340 if project
307 (user.roles_for_project(project) & roles).any?
341 (user.roles_for_project(project) & roles).any?
308 else
342 else
309 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
343 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
310 end
344 end
311 else
345 else
312 user == self.user
346 user == self.user
313 end
347 end
314 end
348 end
315
349
316 def is_private?
350 def is_private?
317 visibility == VISIBILITY_PRIVATE
351 visibility == VISIBILITY_PRIVATE
318 end
352 end
319
353
320 def is_public?
354 def is_public?
321 !is_private?
355 !is_private?
322 end
356 end
323
357
324 def queried_table_name
358 def queried_table_name
325 @queried_table_name ||= self.class.queried_class.table_name
359 @queried_table_name ||= self.class.queried_class.table_name
326 end
360 end
327
361
328 def initialize(attributes=nil, *args)
362 def initialize(attributes=nil, *args)
329 super attributes
363 super attributes
330 @is_for_all = project.nil?
364 @is_for_all = project.nil?
331 end
365 end
332
366
333 # Builds the query from the given params
367 # Builds the query from the given params
334 def build_from_params(params)
368 def build_from_params(params)
335 if params[:fields] || params[:f]
369 if params[:fields] || params[:f]
336 self.filters = {}
370 self.filters = {}
337 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
371 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
338 else
372 else
339 available_filters.keys.each do |field|
373 available_filters.keys.each do |field|
340 add_short_filter(field, params[field]) if params[field]
374 add_short_filter(field, params[field]) if params[field]
341 end
375 end
342 end
376 end
343 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
377 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
344 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
378 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
345 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
379 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
346 self
380 self
347 end
381 end
348
382
349 # Builds a new query from the given params and attributes
383 # Builds a new query from the given params and attributes
350 def self.build_from_params(params, attributes={})
384 def self.build_from_params(params, attributes={})
351 new(attributes).build_from_params(params)
385 new(attributes).build_from_params(params)
352 end
386 end
353
387
354 def validate_query_filters
388 def validate_query_filters
355 filters.each_key do |field|
389 filters.each_key do |field|
356 if values_for(field)
390 if values_for(field)
357 case type_for(field)
391 case type_for(field)
358 when :integer
392 when :integer
359 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
393 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
360 when :float
394 when :float
361 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
395 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
362 when :date, :date_past
396 when :date, :date_past
363 case operator_for(field)
397 case operator_for(field)
364 when "=", ">=", "<=", "><"
398 when "=", ">=", "<=", "><"
365 add_filter_error(field, :invalid) if values_for(field).detect {|v|
399 add_filter_error(field, :invalid) if values_for(field).detect {|v|
366 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?)
400 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?)
367 }
401 }
368 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
402 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
369 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
403 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
370 end
404 end
371 end
405 end
372 end
406 end
373
407
374 add_filter_error(field, :blank) unless
408 add_filter_error(field, :blank) unless
375 # filter requires one or more values
409 # filter requires one or more values
376 (values_for(field) and !values_for(field).first.blank?) or
410 (values_for(field) and !values_for(field).first.blank?) or
377 # filter doesn't require any value
411 # filter doesn't require any value
378 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
412 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
379 end if filters
413 end if filters
380 end
414 end
381
415
382 def add_filter_error(field, message)
416 def add_filter_error(field, message)
383 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
417 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
384 errors.add(:base, m)
418 errors.add(:base, m)
385 end
419 end
386
420
387 def editable_by?(user)
421 def editable_by?(user)
388 return false unless user
422 return false unless user
389 # Admin can edit them all and regular users can edit their private queries
423 # Admin can edit them all and regular users can edit their private queries
390 return true if user.admin? || (is_private? && self.user_id == user.id)
424 return true if user.admin? || (is_private? && self.user_id == user.id)
391 # Members can not edit public queries that are for all project (only admin is allowed to)
425 # Members can not edit public queries that are for all project (only admin is allowed to)
392 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
426 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
393 end
427 end
394
428
395 def trackers
429 def trackers
396 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
430 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
397 end
431 end
398
432
399 # Returns a hash of localized labels for all filter operators
433 # Returns a hash of localized labels for all filter operators
400 def self.operators_labels
434 def self.operators_labels
401 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
435 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
402 end
436 end
403
437
404 # Returns a representation of the available filters for JSON serialization
438 # Returns a representation of the available filters for JSON serialization
405 def available_filters_as_json
439 def available_filters_as_json
406 json = {}
440 json = {}
407 available_filters.each do |field, options|
441 available_filters.each do |field, filter|
408 options = options.slice(:type, :name, :values)
442 options = {:type => filter[:type], :name => filter[:name]}
409 if options[:values] && values_for(field)
443 options[:remote] = true if filter.remote
410 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
444
411 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
445 if has_filter?(field) || !filter.remote
412 options[:values] += send(method, missing)
446 options[:values] = filter.values
447 if options[:values] && values_for(field)
448 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
449 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
450 options[:values] += send(method, missing)
451 end
413 end
452 end
414 end
453 end
415 json[field] = options.stringify_keys
454 json[field] = options.stringify_keys
416 end
455 end
417 json
456 json
418 end
457 end
419
458
420 def all_projects
459 def all_projects
421 @all_projects ||= Project.visible.to_a
460 @all_projects ||= Project.visible.to_a
422 end
461 end
423
462
424 def all_projects_values
463 def all_projects_values
425 return @all_projects_values if @all_projects_values
464 return @all_projects_values if @all_projects_values
426
465
427 values = []
466 values = []
428 Project.project_tree(all_projects) do |p, level|
467 Project.project_tree(all_projects) do |p, level|
429 prefix = (level > 0 ? ('--' * level + ' ') : '')
468 prefix = (level > 0 ? ('--' * level + ' ') : '')
430 values << ["#{prefix}#{p.name}", p.id.to_s]
469 values << ["#{prefix}#{p.name}", p.id.to_s]
431 end
470 end
432 @all_projects_values = values
471 @all_projects_values = values
433 end
472 end
434
473
474 def project_values
475 project_values = []
476 if User.current.logged? && User.current.memberships.any?
477 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
478 end
479 project_values += all_projects_values
480 project_values
481 end
482
483 def subproject_values
484 project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
485 end
486
487 def principals
488 @principal ||= begin
489 principals = []
490 if project
491 principals += project.principals.visible
492 unless project.leaf?
493 principals += Principal.member_of(project.descendants.visible).visible
494 end
495 else
496 principals += Principal.member_of(all_projects).visible
497 end
498 principals.uniq!
499 principals.sort!
500 principals.reject! {|p| p.is_a?(GroupBuiltin)}
501 principals
502 end
503 end
504
505 def users
506 principals.select {|p| p.is_a?(User)}
507 end
508
509 def author_values
510 author_values = []
511 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
512 author_values += users.collect{|s| [s.name, s.id.to_s] }
513 author_values
514 end
515
516 def assigned_to_values
517 assigned_to_values = []
518 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
519 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
520 assigned_to_values
521 end
522
523 def fixed_version_values
524 versions = []
525 if project
526 versions = project.shared_versions.to_a
527 else
528 versions = Version.visible.where(:sharing => 'system').to_a
529 end
530 Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
531 end
532
435 # Adds available filters
533 # Adds available filters
436 def initialize_available_filters
534 def initialize_available_filters
437 # implemented by sub-classes
535 # implemented by sub-classes
438 end
536 end
439 protected :initialize_available_filters
537 protected :initialize_available_filters
440
538
441 # Adds an available filter
539 # Adds an available filter
442 def add_available_filter(field, options)
540 def add_available_filter(field, options)
443 @available_filters ||= ActiveSupport::OrderedHash.new
541 @available_filters ||= ActiveSupport::OrderedHash.new
444 @available_filters[field] = options
542 @available_filters[field] = QueryFilter.new(field, options)
445 @available_filters
543 @available_filters
446 end
544 end
447
545
448 # Removes an available filter
546 # Removes an available filter
449 def delete_available_filter(field)
547 def delete_available_filter(field)
450 if @available_filters
548 if @available_filters
451 @available_filters.delete(field)
549 @available_filters.delete(field)
452 end
550 end
453 end
551 end
454
552
455 # Return a hash of available filters
553 # Return a hash of available filters
456 def available_filters
554 def available_filters
457 unless @available_filters
555 unless @available_filters
458 initialize_available_filters
556 initialize_available_filters
459 @available_filters ||= {}
557 @available_filters ||= {}
460 @available_filters.each do |field, options|
461 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
462 end
463 end
558 end
464 @available_filters
559 @available_filters
465 end
560 end
466
561
467 def add_filter(field, operator, values=nil)
562 def add_filter(field, operator, values=nil)
468 # values must be an array
563 # values must be an array
469 return unless values.nil? || values.is_a?(Array)
564 return unless values.nil? || values.is_a?(Array)
470 # check if field is defined as an available filter
565 # check if field is defined as an available filter
471 if available_filters.has_key? field
566 if available_filters.has_key? field
472 filter_options = available_filters[field]
567 filter_options = available_filters[field]
473 filters[field] = {:operator => operator, :values => (values || [''])}
568 filters[field] = {:operator => operator, :values => (values || [''])}
474 end
569 end
475 end
570 end
476
571
477 def add_short_filter(field, expression)
572 def add_short_filter(field, expression)
478 return unless expression && available_filters.has_key?(field)
573 return unless expression && available_filters.has_key?(field)
479 field_type = available_filters[field][:type]
574 field_type = available_filters[field][:type]
480 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
575 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
481 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
576 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
482 values = $1
577 values = $1
483 add_filter field, operator, values.present? ? values.split('|') : ['']
578 add_filter field, operator, values.present? ? values.split('|') : ['']
484 end || add_filter(field, '=', expression.to_s.split('|'))
579 end || add_filter(field, '=', expression.to_s.split('|'))
485 end
580 end
486
581
487 # Add multiple filters using +add_filter+
582 # Add multiple filters using +add_filter+
488 def add_filters(fields, operators, values)
583 def add_filters(fields, operators, values)
489 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
584 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
490 fields.each do |field|
585 fields.each do |field|
491 add_filter(field, operators[field], values && values[field])
586 add_filter(field, operators[field], values && values[field])
492 end
587 end
493 end
588 end
494 end
589 end
495
590
496 def has_filter?(field)
591 def has_filter?(field)
497 filters and filters[field]
592 filters and filters[field]
498 end
593 end
499
594
500 def type_for(field)
595 def type_for(field)
501 available_filters[field][:type] if available_filters.has_key?(field)
596 available_filters[field][:type] if available_filters.has_key?(field)
502 end
597 end
503
598
504 def operator_for(field)
599 def operator_for(field)
505 has_filter?(field) ? filters[field][:operator] : nil
600 has_filter?(field) ? filters[field][:operator] : nil
506 end
601 end
507
602
508 def values_for(field)
603 def values_for(field)
509 has_filter?(field) ? filters[field][:values] : nil
604 has_filter?(field) ? filters[field][:values] : nil
510 end
605 end
511
606
512 def value_for(field, index=0)
607 def value_for(field, index=0)
513 (values_for(field) || [])[index]
608 (values_for(field) || [])[index]
514 end
609 end
515
610
516 def label_for(field)
611 def label_for(field)
517 label = available_filters[field][:name] if available_filters.has_key?(field)
612 label = available_filters[field][:name] if available_filters.has_key?(field)
518 label ||= queried_class.human_attribute_name(field, :default => field)
613 label ||= queried_class.human_attribute_name(field, :default => field)
519 end
614 end
520
615
521 def self.add_available_column(column)
616 def self.add_available_column(column)
522 self.available_columns << (column) if column.is_a?(QueryColumn)
617 self.available_columns << (column) if column.is_a?(QueryColumn)
523 end
618 end
524
619
525 # Returns an array of columns that can be used to group the results
620 # Returns an array of columns that can be used to group the results
526 def groupable_columns
621 def groupable_columns
527 available_columns.select {|c| c.groupable}
622 available_columns.select {|c| c.groupable}
528 end
623 end
529
624
530 # Returns a Hash of columns and the key for sorting
625 # Returns a Hash of columns and the key for sorting
531 def sortable_columns
626 def sortable_columns
532 available_columns.inject({}) {|h, column|
627 available_columns.inject({}) {|h, column|
533 h[column.name.to_s] = column.sortable
628 h[column.name.to_s] = column.sortable
534 h
629 h
535 }
630 }
536 end
631 end
537
632
538 def columns
633 def columns
539 # preserve the column_names order
634 # preserve the column_names order
540 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
635 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
541 available_columns.find { |col| col.name == name }
636 available_columns.find { |col| col.name == name }
542 end.compact
637 end.compact
543 available_columns.select(&:frozen?) | cols
638 available_columns.select(&:frozen?) | cols
544 end
639 end
545
640
546 def inline_columns
641 def inline_columns
547 columns.select(&:inline?)
642 columns.select(&:inline?)
548 end
643 end
549
644
550 def block_columns
645 def block_columns
551 columns.reject(&:inline?)
646 columns.reject(&:inline?)
552 end
647 end
553
648
554 def available_inline_columns
649 def available_inline_columns
555 available_columns.select(&:inline?)
650 available_columns.select(&:inline?)
556 end
651 end
557
652
558 def available_block_columns
653 def available_block_columns
559 available_columns.reject(&:inline?)
654 available_columns.reject(&:inline?)
560 end
655 end
561
656
562 def available_totalable_columns
657 def available_totalable_columns
563 available_columns.select(&:totalable)
658 available_columns.select(&:totalable)
564 end
659 end
565
660
566 def default_columns_names
661 def default_columns_names
567 []
662 []
568 end
663 end
569
664
570 def default_totalable_names
665 def default_totalable_names
571 []
666 []
572 end
667 end
573
668
574 def column_names=(names)
669 def column_names=(names)
575 if names
670 if names
576 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
671 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
577 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
672 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
578 # Set column_names to nil if default columns
673 # Set column_names to nil if default columns
579 if names == default_columns_names
674 if names == default_columns_names
580 names = nil
675 names = nil
581 end
676 end
582 end
677 end
583 write_attribute(:column_names, names)
678 write_attribute(:column_names, names)
584 end
679 end
585
680
586 def has_column?(column)
681 def has_column?(column)
587 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
682 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
588 end
683 end
589
684
590 def has_custom_field_column?
685 def has_custom_field_column?
591 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
686 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
592 end
687 end
593
688
594 def has_default_columns?
689 def has_default_columns?
595 column_names.nil? || column_names.empty?
690 column_names.nil? || column_names.empty?
596 end
691 end
597
692
598 def totalable_columns
693 def totalable_columns
599 names = totalable_names
694 names = totalable_names
600 available_totalable_columns.select {|column| names.include?(column.name)}
695 available_totalable_columns.select {|column| names.include?(column.name)}
601 end
696 end
602
697
603 def totalable_names=(names)
698 def totalable_names=(names)
604 if names
699 if names
605 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
700 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
606 end
701 end
607 options[:totalable_names] = names
702 options[:totalable_names] = names
608 end
703 end
609
704
610 def totalable_names
705 def totalable_names
611 options[:totalable_names] || default_totalable_names || []
706 options[:totalable_names] || default_totalable_names || []
612 end
707 end
613
708
614 def sort_criteria=(arg)
709 def sort_criteria=(arg)
615 c = []
710 c = []
616 if arg.is_a?(Hash)
711 if arg.is_a?(Hash)
617 arg = arg.keys.sort.collect {|k| arg[k]}
712 arg = arg.keys.sort.collect {|k| arg[k]}
618 end
713 end
619 if arg
714 if arg
620 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
715 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
621 end
716 end
622 write_attribute(:sort_criteria, c)
717 write_attribute(:sort_criteria, c)
623 end
718 end
624
719
625 def sort_criteria
720 def sort_criteria
626 read_attribute(:sort_criteria) || []
721 read_attribute(:sort_criteria) || []
627 end
722 end
628
723
629 def sort_criteria_key(arg)
724 def sort_criteria_key(arg)
630 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
725 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
631 end
726 end
632
727
633 def sort_criteria_order(arg)
728 def sort_criteria_order(arg)
634 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
729 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
635 end
730 end
636
731
637 def sort_criteria_order_for(key)
732 def sort_criteria_order_for(key)
638 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
733 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
639 end
734 end
640
735
641 # Returns the SQL sort order that should be prepended for grouping
736 # Returns the SQL sort order that should be prepended for grouping
642 def group_by_sort_order
737 def group_by_sort_order
643 if column = group_by_column
738 if column = group_by_column
644 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
739 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
645 Array(column.sortable).map {|s| "#{s} #{order}"}
740 Array(column.sortable).map {|s| "#{s} #{order}"}
646 end
741 end
647 end
742 end
648
743
649 # Returns true if the query is a grouped query
744 # Returns true if the query is a grouped query
650 def grouped?
745 def grouped?
651 !group_by_column.nil?
746 !group_by_column.nil?
652 end
747 end
653
748
654 def group_by_column
749 def group_by_column
655 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
750 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
656 end
751 end
657
752
658 def group_by_statement
753 def group_by_statement
659 group_by_column.try(:groupable)
754 group_by_column.try(:groupable)
660 end
755 end
661
756
662 def project_statement
757 def project_statement
663 project_clauses = []
758 project_clauses = []
664 if project && !project.descendants.active.empty?
759 if project && !project.descendants.active.empty?
665 if has_filter?("subproject_id")
760 if has_filter?("subproject_id")
666 case operator_for("subproject_id")
761 case operator_for("subproject_id")
667 when '='
762 when '='
668 # include the selected subprojects
763 # include the selected subprojects
669 ids = [project.id] + values_for("subproject_id").each(&:to_i)
764 ids = [project.id] + values_for("subproject_id").each(&:to_i)
670 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
765 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
671 when '!*'
766 when '!*'
672 # main project only
767 # main project only
673 project_clauses << "#{Project.table_name}.id = %d" % project.id
768 project_clauses << "#{Project.table_name}.id = %d" % project.id
674 else
769 else
675 # all subprojects
770 # all subprojects
676 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
771 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
677 end
772 end
678 elsif Setting.display_subprojects_issues?
773 elsif Setting.display_subprojects_issues?
679 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
774 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
680 else
775 else
681 project_clauses << "#{Project.table_name}.id = %d" % project.id
776 project_clauses << "#{Project.table_name}.id = %d" % project.id
682 end
777 end
683 elsif project
778 elsif project
684 project_clauses << "#{Project.table_name}.id = %d" % project.id
779 project_clauses << "#{Project.table_name}.id = %d" % project.id
685 end
780 end
686 project_clauses.any? ? project_clauses.join(' AND ') : nil
781 project_clauses.any? ? project_clauses.join(' AND ') : nil
687 end
782 end
688
783
689 def statement
784 def statement
690 # filters clauses
785 # filters clauses
691 filters_clauses = []
786 filters_clauses = []
692 filters.each_key do |field|
787 filters.each_key do |field|
693 next if field == "subproject_id"
788 next if field == "subproject_id"
694 v = values_for(field).clone
789 v = values_for(field).clone
695 next unless v and !v.empty?
790 next unless v and !v.empty?
696 operator = operator_for(field)
791 operator = operator_for(field)
697
792
698 # "me" value substitution
793 # "me" value substitution
699 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
794 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
700 if v.delete("me")
795 if v.delete("me")
701 if User.current.logged?
796 if User.current.logged?
702 v.push(User.current.id.to_s)
797 v.push(User.current.id.to_s)
703 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
798 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
704 else
799 else
705 v.push("0")
800 v.push("0")
706 end
801 end
707 end
802 end
708 end
803 end
709
804
710 if field == 'project_id'
805 if field == 'project_id'
711 if v.delete('mine')
806 if v.delete('mine')
712 v += User.current.memberships.map(&:project_id).map(&:to_s)
807 v += User.current.memberships.map(&:project_id).map(&:to_s)
713 end
808 end
714 end
809 end
715
810
716 if field =~ /cf_(\d+)$/
811 if field =~ /cf_(\d+)$/
717 # custom field
812 # custom field
718 filters_clauses << sql_for_custom_field(field, operator, v, $1)
813 filters_clauses << sql_for_custom_field(field, operator, v, $1)
719 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
814 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
720 # specific statement
815 # specific statement
721 filters_clauses << send(method, field, operator, v)
816 filters_clauses << send(method, field, operator, v)
722 else
817 else
723 # regular field
818 # regular field
724 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
819 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
725 end
820 end
726 end if filters and valid?
821 end if filters and valid?
727
822
728 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
823 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
729 # Excludes results for which the grouped custom field is not visible
824 # Excludes results for which the grouped custom field is not visible
730 filters_clauses << c.custom_field.visibility_by_project_condition
825 filters_clauses << c.custom_field.visibility_by_project_condition
731 end
826 end
732
827
733 filters_clauses << project_statement
828 filters_clauses << project_statement
734 filters_clauses.reject!(&:blank?)
829 filters_clauses.reject!(&:blank?)
735
830
736 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
831 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
737 end
832 end
738
833
739 # Returns the sum of values for the given column
834 # Returns the sum of values for the given column
740 def total_for(column)
835 def total_for(column)
741 total_with_scope(column, base_scope)
836 total_with_scope(column, base_scope)
742 end
837 end
743
838
744 # Returns a hash of the sum of the given column for each group,
839 # Returns a hash of the sum of the given column for each group,
745 # or nil if the query is not grouped
840 # or nil if the query is not grouped
746 def total_by_group_for(column)
841 def total_by_group_for(column)
747 grouped_query do |scope|
842 grouped_query do |scope|
748 total_with_scope(column, scope)
843 total_with_scope(column, scope)
749 end
844 end
750 end
845 end
751
846
752 def totals
847 def totals
753 totals = totalable_columns.map {|column| [column, total_for(column)]}
848 totals = totalable_columns.map {|column| [column, total_for(column)]}
754 yield totals if block_given?
849 yield totals if block_given?
755 totals
850 totals
756 end
851 end
757
852
758 def totals_by_group
853 def totals_by_group
759 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
854 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
760 yield totals if block_given?
855 yield totals if block_given?
761 totals
856 totals
762 end
857 end
763
858
764 private
859 private
765
860
766 def grouped_query(&block)
861 def grouped_query(&block)
767 r = nil
862 r = nil
768 if grouped?
863 if grouped?
769 begin
864 begin
770 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
865 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
771 r = yield base_group_scope
866 r = yield base_group_scope
772 rescue ActiveRecord::RecordNotFound
867 rescue ActiveRecord::RecordNotFound
773 r = {nil => yield(base_scope)}
868 r = {nil => yield(base_scope)}
774 end
869 end
775 c = group_by_column
870 c = group_by_column
776 if c.is_a?(QueryCustomFieldColumn)
871 if c.is_a?(QueryCustomFieldColumn)
777 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
872 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
778 end
873 end
779 end
874 end
780 r
875 r
781 rescue ::ActiveRecord::StatementInvalid => e
876 rescue ::ActiveRecord::StatementInvalid => e
782 raise StatementInvalid.new(e.message)
877 raise StatementInvalid.new(e.message)
783 end
878 end
784
879
785 def total_with_scope(column, scope)
880 def total_with_scope(column, scope)
786 unless column.is_a?(QueryColumn)
881 unless column.is_a?(QueryColumn)
787 column = column.to_sym
882 column = column.to_sym
788 column = available_totalable_columns.detect {|c| c.name == column}
883 column = available_totalable_columns.detect {|c| c.name == column}
789 end
884 end
790 if column.is_a?(QueryCustomFieldColumn)
885 if column.is_a?(QueryCustomFieldColumn)
791 custom_field = column.custom_field
886 custom_field = column.custom_field
792 send "total_for_custom_field", custom_field, scope
887 send "total_for_custom_field", custom_field, scope
793 else
888 else
794 send "total_for_#{column.name}", scope
889 send "total_for_#{column.name}", scope
795 end
890 end
796 rescue ::ActiveRecord::StatementInvalid => e
891 rescue ::ActiveRecord::StatementInvalid => e
797 raise StatementInvalid.new(e.message)
892 raise StatementInvalid.new(e.message)
798 end
893 end
799
894
800 def base_scope
895 def base_scope
801 raise "unimplemented"
896 raise "unimplemented"
802 end
897 end
803
898
804 def base_group_scope
899 def base_group_scope
805 base_scope.
900 base_scope.
806 joins(joins_for_order_statement(group_by_statement)).
901 joins(joins_for_order_statement(group_by_statement)).
807 group(group_by_statement)
902 group(group_by_statement)
808 end
903 end
809
904
810 def total_for_custom_field(custom_field, scope, &block)
905 def total_for_custom_field(custom_field, scope, &block)
811 total = custom_field.format.total_for_scope(custom_field, scope)
906 total = custom_field.format.total_for_scope(custom_field, scope)
812 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
907 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
813 total
908 total
814 end
909 end
815
910
816 def map_total(total, &block)
911 def map_total(total, &block)
817 if total.is_a?(Hash)
912 if total.is_a?(Hash)
818 total.keys.each {|k| total[k] = yield total[k]}
913 total.keys.each {|k| total[k] = yield total[k]}
819 else
914 else
820 total = yield total
915 total = yield total
821 end
916 end
822 total
917 total
823 end
918 end
824
919
825 def sql_for_custom_field(field, operator, value, custom_field_id)
920 def sql_for_custom_field(field, operator, value, custom_field_id)
826 db_table = CustomValue.table_name
921 db_table = CustomValue.table_name
827 db_field = 'value'
922 db_field = 'value'
828 filter = @available_filters[field]
923 filter = @available_filters[field]
829 return nil unless filter
924 return nil unless filter
830 if filter[:field].format.target_class && filter[:field].format.target_class <= User
925 if filter[:field].format.target_class && filter[:field].format.target_class <= User
831 if value.delete('me')
926 if value.delete('me')
832 value.push User.current.id.to_s
927 value.push User.current.id.to_s
833 end
928 end
834 end
929 end
835 not_in = nil
930 not_in = nil
836 if operator == '!'
931 if operator == '!'
837 # Makes ! operator work for custom fields with multiple values
932 # Makes ! operator work for custom fields with multiple values
838 operator = '='
933 operator = '='
839 not_in = 'NOT'
934 not_in = 'NOT'
840 end
935 end
841 customized_key = "id"
936 customized_key = "id"
842 customized_class = queried_class
937 customized_class = queried_class
843 if field =~ /^(.+)\.cf_/
938 if field =~ /^(.+)\.cf_/
844 assoc = $1
939 assoc = $1
845 customized_key = "#{assoc}_id"
940 customized_key = "#{assoc}_id"
846 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
941 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
847 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
942 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
848 end
943 end
849 where = sql_for_field(field, operator, value, db_table, db_field, true)
944 where = sql_for_field(field, operator, value, db_table, db_field, true)
850 if operator =~ /[<>]/
945 if operator =~ /[<>]/
851 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
946 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
852 end
947 end
853 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
948 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
854 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
949 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
855 " 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}" +
950 " 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}" +
856 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
951 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
857 end
952 end
858
953
859 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
954 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
860 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
955 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
861 sql = ''
956 sql = ''
862 case operator
957 case operator
863 when "="
958 when "="
864 if value.any?
959 if value.any?
865 case type_for(field)
960 case type_for(field)
866 when :date, :date_past
961 when :date, :date_past
867 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
962 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
868 when :integer
963 when :integer
869 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
964 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
870 if int_values.present?
965 if int_values.present?
871 if is_custom_filter
966 if is_custom_filter
872 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)) IN (#{int_values}))"
967 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)) IN (#{int_values}))"
873 else
968 else
874 sql = "#{db_table}.#{db_field} IN (#{int_values})"
969 sql = "#{db_table}.#{db_field} IN (#{int_values})"
875 end
970 end
876 else
971 else
877 sql = "1=0"
972 sql = "1=0"
878 end
973 end
879 when :float
974 when :float
880 if is_custom_filter
975 if is_custom_filter
881 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})"
976 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})"
882 else
977 else
883 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
978 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
884 end
979 end
885 else
980 else
886 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
981 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
887 end
982 end
888 else
983 else
889 # IN an empty set
984 # IN an empty set
890 sql = "1=0"
985 sql = "1=0"
891 end
986 end
892 when "!"
987 when "!"
893 if value.any?
988 if value.any?
894 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
989 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
895 else
990 else
896 # NOT IN an empty set
991 # NOT IN an empty set
897 sql = "1=1"
992 sql = "1=1"
898 end
993 end
899 when "!*"
994 when "!*"
900 sql = "#{db_table}.#{db_field} IS NULL"
995 sql = "#{db_table}.#{db_field} IS NULL"
901 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
996 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
902 when "*"
997 when "*"
903 sql = "#{db_table}.#{db_field} IS NOT NULL"
998 sql = "#{db_table}.#{db_field} IS NOT NULL"
904 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
999 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
905 when ">="
1000 when ">="
906 if [:date, :date_past].include?(type_for(field))
1001 if [:date, :date_past].include?(type_for(field))
907 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
1002 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
908 else
1003 else
909 if is_custom_filter
1004 if is_custom_filter
910 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})"
1005 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})"
911 else
1006 else
912 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
1007 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
913 end
1008 end
914 end
1009 end
915 when "<="
1010 when "<="
916 if [:date, :date_past].include?(type_for(field))
1011 if [:date, :date_past].include?(type_for(field))
917 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
1012 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
918 else
1013 else
919 if is_custom_filter
1014 if is_custom_filter
920 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})"
1015 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})"
921 else
1016 else
922 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
1017 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
923 end
1018 end
924 end
1019 end
925 when "><"
1020 when "><"
926 if [:date, :date_past].include?(type_for(field))
1021 if [:date, :date_past].include?(type_for(field))
927 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
1022 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
928 else
1023 else
929 if is_custom_filter
1024 if is_custom_filter
930 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})"
1025 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})"
931 else
1026 else
932 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
1027 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
933 end
1028 end
934 end
1029 end
935 when "o"
1030 when "o"
936 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"
1031 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"
937 when "c"
1032 when "c"
938 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"
1033 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"
939 when "><t-"
1034 when "><t-"
940 # between today - n days and today
1035 # between today - n days and today
941 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
1036 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
942 when ">t-"
1037 when ">t-"
943 # >= today - n days
1038 # >= today - n days
944 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
1039 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
945 when "<t-"
1040 when "<t-"
946 # <= today - n days
1041 # <= today - n days
947 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
1042 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
948 when "t-"
1043 when "t-"
949 # = n days in past
1044 # = n days in past
950 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
1045 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
951 when "><t+"
1046 when "><t+"
952 # between today and today + n days
1047 # between today and today + n days
953 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
1048 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
954 when ">t+"
1049 when ">t+"
955 # >= today + n days
1050 # >= today + n days
956 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
1051 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
957 when "<t+"
1052 when "<t+"
958 # <= today + n days
1053 # <= today + n days
959 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
1054 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
960 when "t+"
1055 when "t+"
961 # = today + n days
1056 # = today + n days
962 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
1057 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
963 when "t"
1058 when "t"
964 # = today
1059 # = today
965 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
1060 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
966 when "ld"
1061 when "ld"
967 # = yesterday
1062 # = yesterday
968 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
1063 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
969 when "w"
1064 when "w"
970 # = this week
1065 # = this week
971 first_day_of_week = l(:general_first_day_of_week).to_i
1066 first_day_of_week = l(:general_first_day_of_week).to_i
972 day_of_week = User.current.today.cwday
1067 day_of_week = User.current.today.cwday
973 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1068 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
974 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
1069 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
975 when "lw"
1070 when "lw"
976 # = last week
1071 # = last week
977 first_day_of_week = l(:general_first_day_of_week).to_i
1072 first_day_of_week = l(:general_first_day_of_week).to_i
978 day_of_week = User.current.today.cwday
1073 day_of_week = User.current.today.cwday
979 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1074 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
980 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
1075 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
981 when "l2w"
1076 when "l2w"
982 # = last 2 weeks
1077 # = last 2 weeks
983 first_day_of_week = l(:general_first_day_of_week).to_i
1078 first_day_of_week = l(:general_first_day_of_week).to_i
984 day_of_week = User.current.today.cwday
1079 day_of_week = User.current.today.cwday
985 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1080 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
986 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
1081 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
987 when "m"
1082 when "m"
988 # = this month
1083 # = this month
989 date = User.current.today
1084 date = User.current.today
990 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1085 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
991 when "lm"
1086 when "lm"
992 # = last month
1087 # = last month
993 date = User.current.today.prev_month
1088 date = User.current.today.prev_month
994 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1089 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
995 when "y"
1090 when "y"
996 # = this year
1091 # = this year
997 date = User.current.today
1092 date = User.current.today
998 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
1093 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
999 when "~"
1094 when "~"
1000 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1095 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1001 when "!~"
1096 when "!~"
1002 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1097 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1003 else
1098 else
1004 raise "Unknown query operator #{operator}"
1099 raise "Unknown query operator #{operator}"
1005 end
1100 end
1006
1101
1007 return sql
1102 return sql
1008 end
1103 end
1009
1104
1010 # Returns a SQL LIKE statement with wildcards
1105 # Returns a SQL LIKE statement with wildcards
1011 def sql_contains(db_field, value, match=true)
1106 def sql_contains(db_field, value, match=true)
1012 queried_class.send :sanitize_sql_for_conditions,
1107 queried_class.send :sanitize_sql_for_conditions,
1013 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1108 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1014 end
1109 end
1015
1110
1016 # Adds a filter for the given custom field
1111 # Adds a filter for the given custom field
1017 def add_custom_field_filter(field, assoc=nil)
1112 def add_custom_field_filter(field, assoc=nil)
1018 options = field.query_filter_options(self)
1113 options = field.query_filter_options(self)
1019 if field.format.target_class && field.format.target_class <= User
1114 if field.format.target_class && field.format.target_class <= User
1020 if options[:values].is_a?(Array) && User.current.logged?
1115 if options[:values].is_a?(Array) && User.current.logged?
1021 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
1116 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
1022 end
1117 end
1023 end
1118 end
1024
1119
1025 filter_id = "cf_#{field.id}"
1120 filter_id = "cf_#{field.id}"
1026 filter_name = field.name
1121 filter_name = field.name
1027 if assoc.present?
1122 if assoc.present?
1028 filter_id = "#{assoc}.#{filter_id}"
1123 filter_id = "#{assoc}.#{filter_id}"
1029 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1124 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1030 end
1125 end
1031 add_available_filter filter_id, options.merge({
1126 add_available_filter filter_id, options.merge({
1032 :name => filter_name,
1127 :name => filter_name,
1033 :field => field
1128 :field => field
1034 })
1129 })
1035 end
1130 end
1036
1131
1037 # Adds filters for the given custom fields scope
1132 # Adds filters for the given custom fields scope
1038 def add_custom_fields_filters(scope, assoc=nil)
1133 def add_custom_fields_filters(scope, assoc=nil)
1039 scope.visible.where(:is_filter => true).sorted.each do |field|
1134 scope.visible.where(:is_filter => true).sorted.each do |field|
1040 add_custom_field_filter(field, assoc)
1135 add_custom_field_filter(field, assoc)
1041 end
1136 end
1042 end
1137 end
1043
1138
1044 # Adds filters for the given associations custom fields
1139 # Adds filters for the given associations custom fields
1045 def add_associations_custom_fields_filters(*associations)
1140 def add_associations_custom_fields_filters(*associations)
1046 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1141 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1047 associations.each do |assoc|
1142 associations.each do |assoc|
1048 association_klass = queried_class.reflect_on_association(assoc).klass
1143 association_klass = queried_class.reflect_on_association(assoc).klass
1049 fields_by_class.each do |field_class, fields|
1144 fields_by_class.each do |field_class, fields|
1050 if field_class.customized_class <= association_klass
1145 if field_class.customized_class <= association_klass
1051 fields.sort.each do |field|
1146 fields.sort.each do |field|
1052 add_custom_field_filter(field, assoc)
1147 add_custom_field_filter(field, assoc)
1053 end
1148 end
1054 end
1149 end
1055 end
1150 end
1056 end
1151 end
1057 end
1152 end
1058
1153
1059 def quoted_time(time, is_custom_filter)
1154 def quoted_time(time, is_custom_filter)
1060 if is_custom_filter
1155 if is_custom_filter
1061 # Custom field values are stored as strings in the DB
1156 # Custom field values are stored as strings in the DB
1062 # using this format that does not depend on DB date representation
1157 # using this format that does not depend on DB date representation
1063 time.strftime("%Y-%m-%d %H:%M:%S")
1158 time.strftime("%Y-%m-%d %H:%M:%S")
1064 else
1159 else
1065 self.class.connection.quoted_date(time)
1160 self.class.connection.quoted_date(time)
1066 end
1161 end
1067 end
1162 end
1068
1163
1069 def date_for_user_time_zone(y, m, d)
1164 def date_for_user_time_zone(y, m, d)
1070 if tz = User.current.time_zone
1165 if tz = User.current.time_zone
1071 tz.local y, m, d
1166 tz.local y, m, d
1072 else
1167 else
1073 Time.local y, m, d
1168 Time.local y, m, d
1074 end
1169 end
1075 end
1170 end
1076
1171
1077 # Returns a SQL clause for a date or datetime field.
1172 # Returns a SQL clause for a date or datetime field.
1078 def date_clause(table, field, from, to, is_custom_filter)
1173 def date_clause(table, field, from, to, is_custom_filter)
1079 s = []
1174 s = []
1080 if from
1175 if from
1081 if from.is_a?(Date)
1176 if from.is_a?(Date)
1082 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1177 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1083 else
1178 else
1084 from = from - 1 # second
1179 from = from - 1 # second
1085 end
1180 end
1086 if self.class.default_timezone == :utc
1181 if self.class.default_timezone == :utc
1087 from = from.utc
1182 from = from.utc
1088 end
1183 end
1089 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1184 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1090 end
1185 end
1091 if to
1186 if to
1092 if to.is_a?(Date)
1187 if to.is_a?(Date)
1093 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1188 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1094 end
1189 end
1095 if self.class.default_timezone == :utc
1190 if self.class.default_timezone == :utc
1096 to = to.utc
1191 to = to.utc
1097 end
1192 end
1098 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1193 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1099 end
1194 end
1100 s.join(' AND ')
1195 s.join(' AND ')
1101 end
1196 end
1102
1197
1103 # Returns a SQL clause for a date or datetime field using relative dates.
1198 # Returns a SQL clause for a date or datetime field using relative dates.
1104 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1199 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1105 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1200 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1106 end
1201 end
1107
1202
1108 # Returns a Date or Time from the given filter value
1203 # Returns a Date or Time from the given filter value
1109 def parse_date(arg)
1204 def parse_date(arg)
1110 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1205 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1111 Time.parse(arg) rescue nil
1206 Time.parse(arg) rescue nil
1112 else
1207 else
1113 Date.parse(arg) rescue nil
1208 Date.parse(arg) rescue nil
1114 end
1209 end
1115 end
1210 end
1116
1211
1117 # Additional joins required for the given sort options
1212 # Additional joins required for the given sort options
1118 def joins_for_order_statement(order_options)
1213 def joins_for_order_statement(order_options)
1119 joins = []
1214 joins = []
1120
1215
1121 if order_options
1216 if order_options
1122 if order_options.include?('authors')
1217 if order_options.include?('authors')
1123 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1218 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1124 end
1219 end
1125 order_options.scan(/cf_\d+/).uniq.each do |name|
1220 order_options.scan(/cf_\d+/).uniq.each do |name|
1126 column = available_columns.detect {|c| c.name.to_s == name}
1221 column = available_columns.detect {|c| c.name.to_s == name}
1127 join = column && column.custom_field.join_for_order_statement
1222 join = column && column.custom_field.join_for_order_statement
1128 if join
1223 if join
1129 joins << join
1224 joins << join
1130 end
1225 end
1131 end
1226 end
1132 end
1227 end
1133
1228
1134 joins.any? ? joins.join(' ') : nil
1229 joins.any? ? joins.join(' ') : nil
1135 end
1230 end
1136 end
1231 end
@@ -1,250 +1,223
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TimeEntryQuery < Query
18 class TimeEntryQuery < Query
19
19
20 self.queried_class = TimeEntry
20 self.queried_class = TimeEntry
21 self.view_permission = :view_time_entries
21 self.view_permission = :view_time_entries
22
22
23 self.available_columns = [
23 self.available_columns = [
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
30 QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
30 QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
31 QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
31 QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
32 QueryColumn.new(:comments),
32 QueryColumn.new(:comments),
33 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
33 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
34 ]
34 ]
35
35
36 def initialize(attributes=nil, *args)
36 def initialize(attributes=nil, *args)
37 super attributes
37 super attributes
38 self.filters ||= {}
38 self.filters ||= {}
39 add_filter('spent_on', '*') unless filters.present?
39 add_filter('spent_on', '*') unless filters.present?
40 end
40 end
41
41
42 def initialize_available_filters
42 def initialize_available_filters
43 add_available_filter "spent_on", :type => :date_past
43 add_available_filter "spent_on", :type => :date_past
44
44
45 principals = []
45 add_available_filter("project_id",
46 versions = []
46 :type => :list, :values => lambda { project_values }
47 if project
47 ) if project.nil?
48 principals += project.principals.visible.sort
48
49 unless project.leaf?
49 if project && !project.leaf?
50 subprojects = project.descendants.visible.to_a
50 add_available_filter "subproject_id",
51 if subprojects.any?
51 :type => :list_subprojects,
52 add_available_filter "subproject_id",
52 :values => lambda { subproject_values }
53 :type => :list_subprojects,
54 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
55 principals += Principal.member_of(subprojects).visible
56 end
57 end
58 versions = project.shared_versions.to_a
59 else
60 if all_projects.any?
61 # members of visible projects
62 principals += Principal.member_of(all_projects).visible
63 # project filter
64 project_values = []
65 if User.current.logged? && User.current.memberships.any?
66 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
67 end
68 project_values += all_projects_values
69 add_available_filter("project_id",
70 :type => :list, :values => project_values
71 ) unless project_values.empty?
72 end
73 end
53 end
74
54
75 add_available_filter("issue_id", :type => :tree, :label => :label_issue)
55 add_available_filter("issue_id", :type => :tree, :label => :label_issue)
76 add_available_filter("issue.tracker_id",
56 add_available_filter("issue.tracker_id",
77 :type => :list,
57 :type => :list,
78 :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
58 :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
79 :values => Tracker.sorted.map {|t| [t.name, t.id.to_s]})
59 :values => lambda { Tracker.sorted.map {|t| [t.name, t.id.to_s]} })
80 add_available_filter("issue.status_id",
60 add_available_filter("issue.status_id",
81 :type => :list,
61 :type => :list,
82 :name => l("label_attribute_of_issue", :name => l(:field_status)),
62 :name => l("label_attribute_of_issue", :name => l(:field_status)),
83 :values => IssueStatus.sorted.map {|s| [s.name, s.id.to_s]})
63 :values => lambda { IssueStatus.sorted.map {|s| [s.name, s.id.to_s]} })
84 add_available_filter("issue.fixed_version_id",
64 add_available_filter("issue.fixed_version_id",
85 :type => :list,
65 :type => :list,
86 :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
66 :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
87 :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] })
67 :values => lambda { fixed_version_values }) if project
88
89 principals.uniq!
90 principals.sort!
91 users = principals.select {|p| p.is_a?(User)}
92
68
93 users_values = []
94 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
95 users_values += users.collect{|s| [s.name, s.id.to_s] }
96 add_available_filter("user_id",
69 add_available_filter("user_id",
97 :type => :list_optional, :values => users_values
70 :type => :list_optional, :values => lambda { author_values }
98 ) unless users_values.empty?
71 )
99
72
100 activities = (project ? project.activities : TimeEntryActivity.shared)
73 activities = (project ? project.activities : TimeEntryActivity.shared)
101 add_available_filter("activity_id",
74 add_available_filter("activity_id",
102 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
75 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
103 ) unless activities.empty?
76 )
104
77
105 add_available_filter "comments", :type => :text
78 add_available_filter "comments", :type => :text
106 add_available_filter "hours", :type => :float
79 add_available_filter "hours", :type => :float
107
80
108 add_custom_fields_filters(TimeEntryCustomField)
81 add_custom_fields_filters(TimeEntryCustomField)
109 add_associations_custom_fields_filters :project
82 add_associations_custom_fields_filters :project
110 add_custom_fields_filters(issue_custom_fields, :issue)
83 add_custom_fields_filters(issue_custom_fields, :issue)
111 add_associations_custom_fields_filters :user
84 add_associations_custom_fields_filters :user
112 end
85 end
113
86
114 def available_columns
87 def available_columns
115 return @available_columns if @available_columns
88 return @available_columns if @available_columns
116 @available_columns = self.class.available_columns.dup
89 @available_columns = self.class.available_columns.dup
117 @available_columns += TimeEntryCustomField.visible.
90 @available_columns += TimeEntryCustomField.visible.
118 map {|cf| QueryCustomFieldColumn.new(cf) }
91 map {|cf| QueryCustomFieldColumn.new(cf) }
119 @available_columns += issue_custom_fields.visible.
92 @available_columns += issue_custom_fields.visible.
120 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
93 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
121 @available_columns
94 @available_columns
122 end
95 end
123
96
124 def default_columns_names
97 def default_columns_names
125 @default_columns_names ||= begin
98 @default_columns_names ||= begin
126 default_columns = [:spent_on, :user, :activity, :issue, :comments, :hours]
99 default_columns = [:spent_on, :user, :activity, :issue, :comments, :hours]
127
100
128 project.present? ? default_columns : [:project] | default_columns
101 project.present? ? default_columns : [:project] | default_columns
129 end
102 end
130 end
103 end
131
104
132 def default_totalable_names
105 def default_totalable_names
133 [:hours]
106 [:hours]
134 end
107 end
135
108
136 def base_scope
109 def base_scope
137 TimeEntry.visible.
110 TimeEntry.visible.
138 joins(:project, :user).
111 joins(:project, :user).
139 joins("LEFT OUTER JOIN issues ON issues.id = time_entries.issue_id").
112 joins("LEFT OUTER JOIN issues ON issues.id = time_entries.issue_id").
140 where(statement)
113 where(statement)
141 end
114 end
142
115
143 def results_scope(options={})
116 def results_scope(options={})
144 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
117 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
145
118
146 base_scope.
119 base_scope.
147 order(order_option).
120 order(order_option).
148 joins(joins_for_order_statement(order_option.join(','))).
121 joins(joins_for_order_statement(order_option.join(','))).
149 includes(:activity).
122 includes(:activity).
150 references(:activity)
123 references(:activity)
151 end
124 end
152
125
153 # Returns sum of all the spent hours
126 # Returns sum of all the spent hours
154 def total_for_hours(scope)
127 def total_for_hours(scope)
155 map_total(scope.sum(:hours)) {|t| t.to_f.round(2)}
128 map_total(scope.sum(:hours)) {|t| t.to_f.round(2)}
156 end
129 end
157
130
158 def sql_for_issue_id_field(field, operator, value)
131 def sql_for_issue_id_field(field, operator, value)
159 case operator
132 case operator
160 when "="
133 when "="
161 "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
134 "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
162 when "~"
135 when "~"
163 issue = Issue.where(:id => value.first.to_i).first
136 issue = Issue.where(:id => value.first.to_i).first
164 if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
137 if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
165 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
138 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
166 else
139 else
167 "1=0"
140 "1=0"
168 end
141 end
169 when "!*"
142 when "!*"
170 "#{TimeEntry.table_name}.issue_id IS NULL"
143 "#{TimeEntry.table_name}.issue_id IS NULL"
171 when "*"
144 when "*"
172 "#{TimeEntry.table_name}.issue_id IS NOT NULL"
145 "#{TimeEntry.table_name}.issue_id IS NOT NULL"
173 end
146 end
174 end
147 end
175
148
176 def sql_for_issue_fixed_version_id_field(field, operator, value)
149 def sql_for_issue_fixed_version_id_field(field, operator, value)
177 issue_ids = Issue.where(:fixed_version_id => value.first.to_i).pluck(:id)
150 issue_ids = Issue.where(:fixed_version_id => value.first.to_i).pluck(:id)
178 case operator
151 case operator
179 when "="
152 when "="
180 if issue_ids.any?
153 if issue_ids.any?
181 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
154 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
182 else
155 else
183 "1=0"
156 "1=0"
184 end
157 end
185 when "!"
158 when "!"
186 if issue_ids.any?
159 if issue_ids.any?
187 "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
160 "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
188 else
161 else
189 "1=1"
162 "1=1"
190 end
163 end
191 end
164 end
192 end
165 end
193
166
194 def sql_for_activity_id_field(field, operator, value)
167 def sql_for_activity_id_field(field, operator, value)
195 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
168 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
196 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
169 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
197 ids = value.map(&:to_i).join(',')
170 ids = value.map(&:to_i).join(',')
198 table_name = Enumeration.table_name
171 table_name = Enumeration.table_name
199 if operator == '='
172 if operator == '='
200 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
173 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
201 else
174 else
202 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
175 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
203 end
176 end
204 end
177 end
205
178
206 def sql_for_issue_tracker_id_field(field, operator, value)
179 def sql_for_issue_tracker_id_field(field, operator, value)
207 sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
180 sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
208 end
181 end
209
182
210 def sql_for_issue_status_id_field(field, operator, value)
183 def sql_for_issue_status_id_field(field, operator, value)
211 sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
184 sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
212 end
185 end
213
186
214 # Accepts :from/:to params as shortcut filters
187 # Accepts :from/:to params as shortcut filters
215 def build_from_params(params)
188 def build_from_params(params)
216 super
189 super
217 if params[:from].present? && params[:to].present?
190 if params[:from].present? && params[:to].present?
218 add_filter('spent_on', '><', [params[:from], params[:to]])
191 add_filter('spent_on', '><', [params[:from], params[:to]])
219 elsif params[:from].present?
192 elsif params[:from].present?
220 add_filter('spent_on', '>=', [params[:from]])
193 add_filter('spent_on', '>=', [params[:from]])
221 elsif params[:to].present?
194 elsif params[:to].present?
222 add_filter('spent_on', '<=', [params[:to]])
195 add_filter('spent_on', '<=', [params[:to]])
223 end
196 end
224 self
197 self
225 end
198 end
226
199
227 def joins_for_order_statement(order_options)
200 def joins_for_order_statement(order_options)
228 joins = [super]
201 joins = [super]
229
202
230 if order_options
203 if order_options
231 if order_options.include?('issue_statuses')
204 if order_options.include?('issue_statuses')
232 joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
205 joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
233 end
206 end
234 if order_options.include?('trackers')
207 if order_options.include?('trackers')
235 joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
208 joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
236 end
209 end
237 end
210 end
238
211
239 joins.compact!
212 joins.compact!
240 joins.any? ? joins.join(' ') : nil
213 joins.any? ? joins.join(' ') : nil
241 end
214 end
242
215
243 def issue_custom_fields
216 def issue_custom_fields
244 if project
217 if project
245 project.all_issue_custom_fields
218 project.all_issue_custom_fields
246 else
219 else
247 IssueCustomField.where(:is_for_all => true)
220 IssueCustomField.where(:is_for_all => true)
248 end
221 end
249 end
222 end
250 end
223 end
@@ -1,24 +1,26
1 <%= javascript_tag do %>
1 <%= javascript_tag do %>
2 var operatorLabels = <%= raw_json Query.operators_labels %>;
2 var operatorLabels = <%= raw_json Query.operators_labels %>;
3 var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
3 var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
4 var availableFilters = <%= raw_json query.available_filters_as_json %>;
4 var availableFilters = <%= raw_json query.available_filters_as_json %>;
5 var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
5 var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
6 var allProjects = <%= raw_json query.all_projects_values %>;
6
7 var filtersUrl = <%= raw_json queries_filter_path(:project_id => @query.project.try(:id), :type => @query.type) %>;
8
7 $(document).ready(function(){
9 $(document).ready(function(){
8 initFilters();
10 initFilters();
9 <% query.filters.each do |field, options| %>
11 <% query.filters.each do |field, options| %>
10 addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>);
12 addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>);
11 <% end %>
13 <% end %>
12 });
14 });
13 <% end %>
15 <% end %>
14
16
15 <table id="filters-table">
17 <table id="filters-table">
16 </table>
18 </table>
17
19
18 <div class="add-filter">
20 <div class="add-filter">
19 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
21 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
20 <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
22 <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
21 </div>
23 </div>
22
24
23 <%= hidden_field_tag 'f[]', '' %>
25 <%= hidden_field_tag 'f[]', '' %>
24 <% include_calendar_headers_tags %>
26 <% include_calendar_headers_tags %>
@@ -1,388 +1,389
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 Rails.application.routes.draw do
18 Rails.application.routes.draw do
19 root :to => 'welcome#index', :as => 'home'
19 root :to => 'welcome#index', :as => 'home'
20
20
21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
21 match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
22 match 'logout', :to => 'account#logout', :as => 'signout', :via => [:get, :post]
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 match 'account/activate', :to => 'account#activate', :via => :get
25 match 'account/activate', :to => 'account#activate', :via => :get
26 get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email'
26 get 'account/activation_email', :to => 'account#activation_email', :as => 'activation_email'
27
27
28 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch]
28 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news', :via => [:get, :post, :put, :patch]
29 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch]
29 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue', :via => [:get, :post, :put, :patch]
30 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch]
30 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue', :via => [:get, :post, :put, :patch]
31 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch]
31 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue', :via => [:get, :post, :put, :patch]
32
32
33 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
34 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
35
35
36 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
36 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post], :as => 'new_board_message'
37 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
38 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
39 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
40
40
41 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
41 post 'boards/:board_id/topics/preview', :to => 'messages#preview', :as => 'preview_board_message'
42 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
43 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
44 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
45
45
46 # Misc issue routes. TODO: move into resources
46 # Misc issue routes. TODO: move into resources
47 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
48 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
48 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu', :via => [:get, :post]
49 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
49 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes', :via => :get
50 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
51
51
52 resources :journals, :only => [:edit, :update] do
52 resources :journals, :only => [:edit, :update] do
53 member do
53 member do
54 get 'diff'
54 get 'diff'
55 end
55 end
56 end
56 end
57
57
58 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
58 get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
59 get '/issues/gantt', :to => 'gantts#show'
59 get '/issues/gantt', :to => 'gantts#show'
60
60
61 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
61 get '/projects/:project_id/issues/calendar', :to => 'calendars#show', :as => 'project_calendar'
62 get '/issues/calendar', :to => 'calendars#show'
62 get '/issues/calendar', :to => 'calendars#show'
63
63
64 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
64 get 'projects/:id/issues/report', :to => 'reports#issue_report', :as => 'project_issues_report'
65 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
65 get 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :as => 'project_issues_report_details'
66
66
67 get '/issues/imports/new', :to => 'imports#new', :as => 'new_issues_import'
67 get '/issues/imports/new', :to => 'imports#new', :as => 'new_issues_import'
68 post '/imports', :to => 'imports#create', :as => 'imports'
68 post '/imports', :to => 'imports#create', :as => 'imports'
69 get '/imports/:id', :to => 'imports#show', :as => 'import'
69 get '/imports/:id', :to => 'imports#show', :as => 'import'
70 match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings'
70 match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings'
71 match '/imports/:id/mapping', :to => 'imports#mapping', :via => [:get, :post], :as => 'import_mapping'
71 match '/imports/:id/mapping', :to => 'imports#mapping', :via => [:get, :post], :as => 'import_mapping'
72 match '/imports/:id/run', :to => 'imports#run', :via => [:get, :post], :as => 'import_run'
72 match '/imports/:id/run', :to => 'imports#run', :via => [:get, :post], :as => 'import_run'
73
73
74 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
74 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
75 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
75 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
76 match 'my/page', :controller => 'my', :action => 'page', :via => :get
76 match 'my/page', :controller => 'my', :action => 'page', :via => :get
77 post 'my/page', :to => 'my#update_page'
77 post 'my/page', :to => 'my#update_page'
78 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
78 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
79 get 'my/api_key', :to => 'my#show_api_key', :as => 'my_api_key'
79 get 'my/api_key', :to => 'my#show_api_key', :as => 'my_api_key'
80 post 'my/api_key', :to => 'my#reset_api_key'
80 post 'my/api_key', :to => 'my#reset_api_key'
81 post 'my/rss_key', :to => 'my#reset_rss_key', :as => 'my_rss_key'
81 post 'my/rss_key', :to => 'my#reset_rss_key', :as => 'my_rss_key'
82 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
82 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
83 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
83 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
84 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
84 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
85 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
85 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
86 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
86 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
87
87
88 resources :users do
88 resources :users do
89 resources :memberships, :controller => 'principal_memberships'
89 resources :memberships, :controller => 'principal_memberships'
90 resources :email_addresses, :only => [:index, :create, :update, :destroy]
90 resources :email_addresses, :only => [:index, :create, :update, :destroy]
91 end
91 end
92
92
93 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
93 post 'watchers/watch', :to => 'watchers#watch', :as => 'watch'
94 delete 'watchers/watch', :to => 'watchers#unwatch'
94 delete 'watchers/watch', :to => 'watchers#unwatch'
95 get 'watchers/new', :to => 'watchers#new', :as => 'new_watchers'
95 get 'watchers/new', :to => 'watchers#new', :as => 'new_watchers'
96 post 'watchers', :to => 'watchers#create'
96 post 'watchers', :to => 'watchers#create'
97 post 'watchers/append', :to => 'watchers#append'
97 post 'watchers/append', :to => 'watchers#append'
98 delete 'watchers', :to => 'watchers#destroy'
98 delete 'watchers', :to => 'watchers#destroy'
99 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
99 get 'watchers/autocomplete_for_user', :to => 'watchers#autocomplete_for_user'
100 # Specific routes for issue watchers API
100 # Specific routes for issue watchers API
101 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
101 post 'issues/:object_id/watchers', :to => 'watchers#create', :object_type => 'issue'
102 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
102 delete 'issues/:object_id/watchers/:user_id' => 'watchers#destroy', :object_type => 'issue'
103
103
104 resources :projects do
104 resources :projects do
105 member do
105 member do
106 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
106 get 'settings(/:tab)', :action => 'settings', :as => 'settings'
107 post 'modules'
107 post 'modules'
108 post 'archive'
108 post 'archive'
109 post 'unarchive'
109 post 'unarchive'
110 post 'close'
110 post 'close'
111 post 'reopen'
111 post 'reopen'
112 match 'copy', :via => [:get, :post]
112 match 'copy', :via => [:get, :post]
113 end
113 end
114
114
115 shallow do
115 shallow do
116 resources :memberships, :controller => 'members' do
116 resources :memberships, :controller => 'members' do
117 collection do
117 collection do
118 get 'autocomplete'
118 get 'autocomplete'
119 end
119 end
120 end
120 end
121 end
121 end
122
122
123 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
123 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
124
124
125 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
125 get 'issues/:copy_from/copy', :to => 'issues#new', :as => 'copy_issue'
126 resources :issues, :only => [:index, :new, :create]
126 resources :issues, :only => [:index, :new, :create]
127 # Used when updating the form of a new issue
127 # Used when updating the form of a new issue
128 post 'issues/new', :to => 'issues#new'
128 post 'issues/new', :to => 'issues#new'
129
129
130 resources :files, :only => [:index, :new, :create]
130 resources :files, :only => [:index, :new, :create]
131
131
132 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
132 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
133 collection do
133 collection do
134 put 'close_completed'
134 put 'close_completed'
135 end
135 end
136 end
136 end
137 get 'versions.:format', :to => 'versions#index'
137 get 'versions.:format', :to => 'versions#index'
138 get 'roadmap', :to => 'versions#index', :format => false
138 get 'roadmap', :to => 'versions#index', :format => false
139 get 'versions', :to => 'versions#index'
139 get 'versions', :to => 'versions#index'
140
140
141 resources :news, :except => [:show, :edit, :update, :destroy]
141 resources :news, :except => [:show, :edit, :update, :destroy]
142 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
142 resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do
143 get 'report', :on => :collection
143 get 'report', :on => :collection
144 end
144 end
145 resources :queries, :only => [:new, :create]
145 resources :queries, :only => [:new, :create]
146 shallow do
146 shallow do
147 resources :issue_categories
147 resources :issue_categories
148 end
148 end
149 resources :documents, :except => [:show, :edit, :update, :destroy]
149 resources :documents, :except => [:show, :edit, :update, :destroy]
150 resources :boards
150 resources :boards
151 shallow do
151 shallow do
152 resources :repositories, :except => [:index, :show] do
152 resources :repositories, :except => [:index, :show] do
153 member do
153 member do
154 match 'committers', :via => [:get, :post]
154 match 'committers', :via => [:get, :post]
155 end
155 end
156 end
156 end
157 end
157 end
158
158
159 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
159 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
160 resources :wiki, :except => [:index, :create], :as => 'wiki_page' do
160 resources :wiki, :except => [:index, :create], :as => 'wiki_page' do
161 member do
161 member do
162 get 'rename'
162 get 'rename'
163 post 'rename'
163 post 'rename'
164 get 'history'
164 get 'history'
165 get 'diff'
165 get 'diff'
166 match 'preview', :via => [:post, :put, :patch]
166 match 'preview', :via => [:post, :put, :patch]
167 post 'protect'
167 post 'protect'
168 post 'add_attachment'
168 post 'add_attachment'
169 end
169 end
170 collection do
170 collection do
171 get 'export'
171 get 'export'
172 get 'date_index'
172 get 'date_index'
173 post 'new'
173 post 'new'
174 end
174 end
175 end
175 end
176 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
176 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
177 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
177 get 'wiki/:id/:version', :to => 'wiki#show', :constraints => {:version => /\d+/}
178 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
178 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
179 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
179 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
180 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
180 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
181 end
181 end
182
182
183 resources :issues do
183 resources :issues do
184 member do
184 member do
185 # Used when updating the form of an existing issue
185 # Used when updating the form of an existing issue
186 patch 'edit', :to => 'issues#edit'
186 patch 'edit', :to => 'issues#edit'
187 end
187 end
188 collection do
188 collection do
189 match 'bulk_edit', :via => [:get, :post]
189 match 'bulk_edit', :via => [:get, :post]
190 post 'bulk_update'
190 post 'bulk_update'
191 end
191 end
192 resources :time_entries, :controller => 'timelog', :only => [:new, :create]
192 resources :time_entries, :controller => 'timelog', :only => [:new, :create]
193 shallow do
193 shallow do
194 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
194 resources :relations, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
195 end
195 end
196 end
196 end
197 # Used when updating the form of a new issue outside a project
197 # Used when updating the form of a new issue outside a project
198 post '/issues/new', :to => 'issues#new'
198 post '/issues/new', :to => 'issues#new'
199 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
199 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
200
200
201 resources :queries, :except => [:show]
201 resources :queries, :except => [:show]
202 get '/queries/filter', :to => 'queries#filter', :as => 'queries_filter'
202
203
203 resources :news, :only => [:index, :show, :edit, :update, :destroy]
204 resources :news, :only => [:index, :show, :edit, :update, :destroy]
204 match '/news/:id/comments', :to => 'comments#create', :via => :post
205 match '/news/:id/comments', :to => 'comments#create', :via => :post
205 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
206 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
206
207
207 resources :versions, :only => [:show, :edit, :update, :destroy] do
208 resources :versions, :only => [:show, :edit, :update, :destroy] do
208 post 'status_by', :on => :member
209 post 'status_by', :on => :member
209 end
210 end
210
211
211 resources :documents, :only => [:show, :edit, :update, :destroy] do
212 resources :documents, :only => [:show, :edit, :update, :destroy] do
212 post 'add_attachment', :on => :member
213 post 'add_attachment', :on => :member
213 end
214 end
214
215
215 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
216 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu, :via => [:get, :post]
216
217
217 resources :time_entries, :controller => 'timelog', :except => :destroy do
218 resources :time_entries, :controller => 'timelog', :except => :destroy do
218 member do
219 member do
219 # Used when updating the edit form of an existing time entry
220 # Used when updating the edit form of an existing time entry
220 patch 'edit', :to => 'timelog#edit'
221 patch 'edit', :to => 'timelog#edit'
221 end
222 end
222 collection do
223 collection do
223 get 'report'
224 get 'report'
224 get 'bulk_edit'
225 get 'bulk_edit'
225 post 'bulk_update'
226 post 'bulk_update'
226 end
227 end
227 end
228 end
228 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
229 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
229 # TODO: delete /time_entries for bulk deletion
230 # TODO: delete /time_entries for bulk deletion
230 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
231 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
231 # Used to update the new time entry form
232 # Used to update the new time entry form
232 post '/time_entries/new', :to => 'timelog#new'
233 post '/time_entries/new', :to => 'timelog#new'
233
234
234 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
235 get 'projects/:id/activity', :to => 'activities#index', :as => :project_activity
235 get 'activity', :to => 'activities#index'
236 get 'activity', :to => 'activities#index'
236
237
237 # repositories routes
238 # repositories routes
238 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
239 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
239 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
240 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
240
241
241 get 'projects/:id/repository/:repository_id/changes(/*path)',
242 get 'projects/:id/repository/:repository_id/changes(/*path)',
242 :to => 'repositories#changes',
243 :to => 'repositories#changes',
243 :format => false
244 :format => false
244
245
245 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
246 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
246 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
247 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
247 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
248 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
248 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
249 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
249 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
250 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
250 %w(browse show entry raw annotate diff).each do |action|
251 %w(browse show entry raw annotate diff).each do |action|
251 get "projects/:id/repository/:repository_id/revisions/:rev/#{action}(/*path)",
252 get "projects/:id/repository/:repository_id/revisions/:rev/#{action}(/*path)",
252 :controller => 'repositories',
253 :controller => 'repositories',
253 :action => action,
254 :action => action,
254 :format => false,
255 :format => false,
255 :constraints => {:rev => /[a-z0-9\.\-_]+/}
256 :constraints => {:rev => /[a-z0-9\.\-_]+/}
256 end
257 end
257
258
258 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
259 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
259 get 'projects/:id/repository/graph', :to => 'repositories#graph'
260 get 'projects/:id/repository/graph', :to => 'repositories#graph'
260
261
261 get 'projects/:id/repository/changes(/*path)',
262 get 'projects/:id/repository/changes(/*path)',
262 :to => 'repositories#changes',
263 :to => 'repositories#changes',
263 :format => false
264 :format => false
264
265
265 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
266 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
266 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
267 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
267 get 'projects/:id/repository/revision', :to => 'repositories#revision'
268 get 'projects/:id/repository/revision', :to => 'repositories#revision'
268 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
269 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
269 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
270 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
270 %w(browse show entry raw annotate diff).each do |action|
271 %w(browse show entry raw annotate diff).each do |action|
271 get "projects/:id/repository/revisions/:rev/#{action}(/*path)",
272 get "projects/:id/repository/revisions/:rev/#{action}(/*path)",
272 :controller => 'repositories',
273 :controller => 'repositories',
273 :action => action,
274 :action => action,
274 :format => false,
275 :format => false,
275 :constraints => {:rev => /[a-z0-9\.\-_]+/}
276 :constraints => {:rev => /[a-z0-9\.\-_]+/}
276 end
277 end
277 %w(browse entry raw changes annotate diff).each do |action|
278 %w(browse entry raw changes annotate diff).each do |action|
278 get "projects/:id/repository/:repository_id/#{action}(/*path)",
279 get "projects/:id/repository/:repository_id/#{action}(/*path)",
279 :controller => 'repositories',
280 :controller => 'repositories',
280 :action => action,
281 :action => action,
281 :format => false
282 :format => false
282 end
283 end
283 %w(browse entry raw changes annotate diff).each do |action|
284 %w(browse entry raw changes annotate diff).each do |action|
284 get "projects/:id/repository/#{action}(/*path)",
285 get "projects/:id/repository/#{action}(/*path)",
285 :controller => 'repositories',
286 :controller => 'repositories',
286 :action => action,
287 :action => action,
287 :format => false
288 :format => false
288 end
289 end
289
290
290 get 'projects/:id/repository/:repository_id/show/*path', :to => 'repositories#show', :format => false
291 get 'projects/:id/repository/:repository_id/show/*path', :to => 'repositories#show', :format => false
291 get 'projects/:id/repository/show/*path', :to => 'repositories#show', :format => false
292 get 'projects/:id/repository/show/*path', :to => 'repositories#show', :format => false
292
293
293 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
294 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
294 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
295 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
295
296
296 # additional routes for having the file name at the end of url
297 # additional routes for having the file name at the end of url
297 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
298 get 'attachments/:id/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'named_attachment'
298 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
299 get 'attachments/download/:id/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'download_named_attachment'
299 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
300 get 'attachments/download/:id', :to => 'attachments#download', :id => /\d+/
300 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
301 get 'attachments/thumbnail/:id(/:size)', :to => 'attachments#thumbnail', :id => /\d+/, :size => /\d+/, :as => 'thumbnail'
301 resources :attachments, :only => [:show, :update, :destroy]
302 resources :attachments, :only => [:show, :update, :destroy]
302 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit_all', :as => :object_attachments_edit
303 get 'attachments/:object_type/:object_id/edit', :to => 'attachments#edit_all', :as => :object_attachments_edit
303 patch 'attachments/:object_type/:object_id', :to => 'attachments#update_all', :as => :object_attachments
304 patch 'attachments/:object_type/:object_id', :to => 'attachments#update_all', :as => :object_attachments
304
305
305 resources :groups do
306 resources :groups do
306 resources :memberships, :controller => 'principal_memberships'
307 resources :memberships, :controller => 'principal_memberships'
307 member do
308 member do
308 get 'autocomplete_for_user'
309 get 'autocomplete_for_user'
309 end
310 end
310 end
311 end
311
312
312 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
313 get 'groups/:id/users/new', :to => 'groups#new_users', :id => /\d+/, :as => 'new_group_users'
313 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
314 post 'groups/:id/users', :to => 'groups#add_users', :id => /\d+/, :as => 'group_users'
314 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
315 delete 'groups/:id/users/:user_id', :to => 'groups#remove_user', :id => /\d+/, :as => 'group_user'
315
316
316 resources :trackers, :except => :show do
317 resources :trackers, :except => :show do
317 collection do
318 collection do
318 match 'fields', :via => [:get, :post]
319 match 'fields', :via => [:get, :post]
319 end
320 end
320 end
321 end
321 resources :issue_statuses, :except => :show do
322 resources :issue_statuses, :except => :show do
322 collection do
323 collection do
323 post 'update_issue_done_ratio'
324 post 'update_issue_done_ratio'
324 end
325 end
325 end
326 end
326 resources :custom_fields, :except => :show do
327 resources :custom_fields, :except => :show do
327 resources :enumerations, :controller => 'custom_field_enumerations', :except => [:show, :new, :edit]
328 resources :enumerations, :controller => 'custom_field_enumerations', :except => [:show, :new, :edit]
328 put 'enumerations', :to => 'custom_field_enumerations#update_each'
329 put 'enumerations', :to => 'custom_field_enumerations#update_each'
329 end
330 end
330 resources :roles do
331 resources :roles do
331 collection do
332 collection do
332 match 'permissions', :via => [:get, :post]
333 match 'permissions', :via => [:get, :post]
333 end
334 end
334 end
335 end
335 resources :enumerations, :except => :show
336 resources :enumerations, :except => :show
336 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
337 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
337
338
338 get 'projects/:id/search', :controller => 'search', :action => 'index'
339 get 'projects/:id/search', :controller => 'search', :action => 'index'
339 get 'search', :controller => 'search', :action => 'index'
340 get 'search', :controller => 'search', :action => 'index'
340
341
341
342
342 get 'mail_handler', :to => 'mail_handler#new'
343 get 'mail_handler', :to => 'mail_handler#new'
343 post 'mail_handler', :to => 'mail_handler#index'
344 post 'mail_handler', :to => 'mail_handler#index'
344
345
345 get 'admin', :to => 'admin#index'
346 get 'admin', :to => 'admin#index'
346 get 'admin/projects', :to => 'admin#projects'
347 get 'admin/projects', :to => 'admin#projects'
347 get 'admin/plugins', :to => 'admin#plugins'
348 get 'admin/plugins', :to => 'admin#plugins'
348 get 'admin/info', :to => 'admin#info'
349 get 'admin/info', :to => 'admin#info'
349 post 'admin/test_email', :to => 'admin#test_email', :as => 'test_email'
350 post 'admin/test_email', :to => 'admin#test_email', :as => 'test_email'
350 post 'admin/default_configuration', :to => 'admin#default_configuration'
351 post 'admin/default_configuration', :to => 'admin#default_configuration'
351
352
352 resources :auth_sources do
353 resources :auth_sources do
353 member do
354 member do
354 get 'test_connection', :as => 'try_connection'
355 get 'test_connection', :as => 'try_connection'
355 end
356 end
356 collection do
357 collection do
357 get 'autocomplete_for_new_user'
358 get 'autocomplete_for_new_user'
358 end
359 end
359 end
360 end
360
361
361 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
362 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
362 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
363 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
363 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
364 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
364 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
365 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
365 match 'settings', :controller => 'settings', :action => 'index', :via => :get
366 match 'settings', :controller => 'settings', :action => 'index', :via => :get
366 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
367 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
367 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
368 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post], :as => 'plugin_settings'
368
369
369 match 'sys/projects', :to => 'sys#projects', :via => :get
370 match 'sys/projects', :to => 'sys#projects', :via => :get
370 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
371 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
371 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
372 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => [:get, :post]
372
373
373 match 'uploads', :to => 'attachments#upload', :via => :post
374 match 'uploads', :to => 'attachments#upload', :via => :post
374
375
375 get 'robots.txt', :to => 'welcome#robots'
376 get 'robots.txt', :to => 'welcome#robots'
376
377
377 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
378 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
378 file = File.join(plugin_dir, "config/routes.rb")
379 file = File.join(plugin_dir, "config/routes.rb")
379 if File.exists?(file)
380 if File.exists?(file)
380 begin
381 begin
381 instance_eval File.read(file)
382 instance_eval File.read(file)
382 rescue Exception => e
383 rescue Exception => e
383 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
384 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
384 exit 1
385 exit 1
385 end
386 end
386 end
387 end
387 end
388 end
388 end
389 end
@@ -1,852 +1,864
1 /* Redmine - project management software
1 /* Redmine - project management software
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3
3
4 function checkAll(id, checked) {
4 function checkAll(id, checked) {
5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
6 }
6 }
7
7
8 function toggleCheckboxesBySelector(selector) {
8 function toggleCheckboxesBySelector(selector) {
9 var all_checked = true;
9 var all_checked = true;
10 $(selector).each(function(index) {
10 $(selector).each(function(index) {
11 if (!$(this).is(':checked')) { all_checked = false; }
11 if (!$(this).is(':checked')) { all_checked = false; }
12 });
12 });
13 $(selector).prop('checked', !all_checked);
13 $(selector).prop('checked', !all_checked);
14 }
14 }
15
15
16 function showAndScrollTo(id, focus) {
16 function showAndScrollTo(id, focus) {
17 $('#'+id).show();
17 $('#'+id).show();
18 if (focus !== null) {
18 if (focus !== null) {
19 $('#'+focus).focus();
19 $('#'+focus).focus();
20 }
20 }
21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
22 }
22 }
23
23
24 function toggleRowGroup(el) {
24 function toggleRowGroup(el) {
25 var tr = $(el).parents('tr').first();
25 var tr = $(el).parents('tr').first();
26 var n = tr.next();
26 var n = tr.next();
27 tr.toggleClass('open');
27 tr.toggleClass('open');
28 while (n.length && !n.hasClass('group')) {
28 while (n.length && !n.hasClass('group')) {
29 n.toggle();
29 n.toggle();
30 n = n.next('tr');
30 n = n.next('tr');
31 }
31 }
32 }
32 }
33
33
34 function collapseAllRowGroups(el) {
34 function collapseAllRowGroups(el) {
35 var tbody = $(el).parents('tbody').first();
35 var tbody = $(el).parents('tbody').first();
36 tbody.children('tr').each(function(index) {
36 tbody.children('tr').each(function(index) {
37 if ($(this).hasClass('group')) {
37 if ($(this).hasClass('group')) {
38 $(this).removeClass('open');
38 $(this).removeClass('open');
39 } else {
39 } else {
40 $(this).hide();
40 $(this).hide();
41 }
41 }
42 });
42 });
43 }
43 }
44
44
45 function expandAllRowGroups(el) {
45 function expandAllRowGroups(el) {
46 var tbody = $(el).parents('tbody').first();
46 var tbody = $(el).parents('tbody').first();
47 tbody.children('tr').each(function(index) {
47 tbody.children('tr').each(function(index) {
48 if ($(this).hasClass('group')) {
48 if ($(this).hasClass('group')) {
49 $(this).addClass('open');
49 $(this).addClass('open');
50 } else {
50 } else {
51 $(this).show();
51 $(this).show();
52 }
52 }
53 });
53 });
54 }
54 }
55
55
56 function toggleAllRowGroups(el) {
56 function toggleAllRowGroups(el) {
57 var tr = $(el).parents('tr').first();
57 var tr = $(el).parents('tr').first();
58 if (tr.hasClass('open')) {
58 if (tr.hasClass('open')) {
59 collapseAllRowGroups(el);
59 collapseAllRowGroups(el);
60 } else {
60 } else {
61 expandAllRowGroups(el);
61 expandAllRowGroups(el);
62 }
62 }
63 }
63 }
64
64
65 function toggleFieldset(el) {
65 function toggleFieldset(el) {
66 var fieldset = $(el).parents('fieldset').first();
66 var fieldset = $(el).parents('fieldset').first();
67 fieldset.toggleClass('collapsed');
67 fieldset.toggleClass('collapsed');
68 fieldset.children('div').toggle();
68 fieldset.children('div').toggle();
69 }
69 }
70
70
71 function hideFieldset(el) {
71 function hideFieldset(el) {
72 var fieldset = $(el).parents('fieldset').first();
72 var fieldset = $(el).parents('fieldset').first();
73 fieldset.toggleClass('collapsed');
73 fieldset.toggleClass('collapsed');
74 fieldset.children('div').hide();
74 fieldset.children('div').hide();
75 }
75 }
76
76
77 // columns selection
77 // columns selection
78 function moveOptions(theSelFrom, theSelTo) {
78 function moveOptions(theSelFrom, theSelTo) {
79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
80 }
80 }
81
81
82 function moveOptionUp(theSel) {
82 function moveOptionUp(theSel) {
83 $(theSel).find('option:selected').each(function(){
83 $(theSel).find('option:selected').each(function(){
84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
85 });
85 });
86 }
86 }
87
87
88 function moveOptionTop(theSel) {
88 function moveOptionTop(theSel) {
89 $(theSel).find('option:selected').detach().prependTo($(theSel));
89 $(theSel).find('option:selected').detach().prependTo($(theSel));
90 }
90 }
91
91
92 function moveOptionDown(theSel) {
92 function moveOptionDown(theSel) {
93 $($(theSel).find('option:selected').get().reverse()).each(function(){
93 $($(theSel).find('option:selected').get().reverse()).each(function(){
94 $(this).next(':not(:selected)').detach().insertBefore($(this));
94 $(this).next(':not(:selected)').detach().insertBefore($(this));
95 });
95 });
96 }
96 }
97
97
98 function moveOptionBottom(theSel) {
98 function moveOptionBottom(theSel) {
99 $(theSel).find('option:selected').detach().appendTo($(theSel));
99 $(theSel).find('option:selected').detach().appendTo($(theSel));
100 }
100 }
101
101
102 function initFilters() {
102 function initFilters() {
103 $('#add_filter_select').change(function() {
103 $('#add_filter_select').change(function() {
104 addFilter($(this).val(), '', []);
104 addFilter($(this).val(), '', []);
105 });
105 });
106 $('#filters-table td.field input[type=checkbox]').each(function() {
106 $('#filters-table td.field input[type=checkbox]').each(function() {
107 toggleFilter($(this).val());
107 toggleFilter($(this).val());
108 });
108 });
109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
110 toggleFilter($(this).val());
110 toggleFilter($(this).val());
111 });
111 });
112 $('#filters-table').on('click', '.toggle-multiselect', function() {
112 $('#filters-table').on('click', '.toggle-multiselect', function() {
113 toggleMultiSelect($(this).siblings('select'));
113 toggleMultiSelect($(this).siblings('select'));
114 });
114 });
115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
116 if (e.keyCode == 13) $(this).closest('form').submit();
116 if (e.keyCode == 13) $(this).closest('form').submit();
117 });
117 });
118 }
118 }
119
119
120 function addFilter(field, operator, values) {
120 function addFilter(field, operator, values) {
121 var fieldId = field.replace('.', '_');
121 var fieldId = field.replace('.', '_');
122 var tr = $('#tr_'+fieldId);
122 var tr = $('#tr_'+fieldId);
123
124 var filterOptions = availableFilters[field];
125 if (!filterOptions) return;
126
127 if (filterOptions['remote'] && filterOptions['values'] == null) {
128 $.getJSON(filtersUrl, {'name': field}).done(function(data) {
129 filterOptions['values'] = data;
130 addFilter(field, operator, values) ;
131 });
132 return;
133 }
134
123 if (tr.length > 0) {
135 if (tr.length > 0) {
124 tr.show();
136 tr.show();
125 } else {
137 } else {
126 buildFilterRow(field, operator, values);
138 buildFilterRow(field, operator, values);
127 }
139 }
128 $('#cb_'+fieldId).prop('checked', true);
140 $('#cb_'+fieldId).prop('checked', true);
129 toggleFilter(field);
141 toggleFilter(field);
130 $('#add_filter_select').val('').find('option').each(function() {
142 $('#add_filter_select').val('').find('option').each(function() {
131 if ($(this).attr('value') == field) {
143 if ($(this).attr('value') == field) {
132 $(this).attr('disabled', true);
144 $(this).attr('disabled', true);
133 }
145 }
134 });
146 });
135 }
147 }
136
148
137 function buildFilterRow(field, operator, values) {
149 function buildFilterRow(field, operator, values, loadedValues) {
138 var fieldId = field.replace('.', '_');
150 var fieldId = field.replace('.', '_');
139 var filterTable = $("#filters-table");
151 var filterTable = $("#filters-table");
140 var filterOptions = availableFilters[field];
152 var filterOptions = availableFilters[field];
141 if (!filterOptions) return;
153 if (!filterOptions) return;
142 var operators = operatorByType[filterOptions['type']];
154 var operators = operatorByType[filterOptions['type']];
143 var filterValues = filterOptions['values'];
155 var filterValues = filterOptions['values'];
144 var i, select;
156 var i, select;
145
157
146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
158 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
159 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
160 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
149 '<td class="values"></td>'
161 '<td class="values"></td>'
150 );
162 );
151 filterTable.append(tr);
163 filterTable.append(tr);
152
164
153 select = tr.find('td.operator select');
165 select = tr.find('td.operator select');
154 for (i = 0; i < operators.length; i++) {
166 for (i = 0; i < operators.length; i++) {
155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
167 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
156 if (operators[i] == operator) { option.attr('selected', true); }
168 if (operators[i] == operator) { option.attr('selected', true); }
157 select.append(option);
169 select.append(option);
158 }
170 }
159 select.change(function(){ toggleOperator(field); });
171 select.change(function(){ toggleOperator(field); });
160
172
161 switch (filterOptions['type']) {
173 switch (filterOptions['type']) {
162 case "list":
174 case "list":
163 case "list_optional":
175 case "list_optional":
164 case "list_status":
176 case "list_status":
165 case "list_subprojects":
177 case "list_subprojects":
166 tr.find('td.values').append(
178 tr.find('td.values').append(
167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
179 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
180 ' <span class="toggle-multiselect">&nbsp;</span></span>'
169 );
181 );
170 select = tr.find('td.values select');
182 select = tr.find('td.values select');
171 if (values.length > 1) { select.attr('multiple', true); }
183 if (values.length > 1) { select.attr('multiple', true); }
172 for (i = 0; i < filterValues.length; i++) {
184 for (i = 0; i < filterValues.length; i++) {
173 var filterValue = filterValues[i];
185 var filterValue = filterValues[i];
174 var option = $('<option>');
186 var option = $('<option>');
175 if ($.isArray(filterValue)) {
187 if ($.isArray(filterValue)) {
176 option.val(filterValue[1]).text(filterValue[0]);
188 option.val(filterValue[1]).text(filterValue[0]);
177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
189 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
178 if (filterValue.length == 3) {
190 if (filterValue.length == 3) {
179 var optgroup = select.find('optgroup').filter(function(){return $(this).attr('label') == filterValue[2]});
191 var optgroup = select.find('optgroup').filter(function(){return $(this).attr('label') == filterValue[2]});
180 if (!optgroup.length) {optgroup = $('<optgroup>').attr('label', filterValue[2]);}
192 if (!optgroup.length) {optgroup = $('<optgroup>').attr('label', filterValue[2]);}
181 option = optgroup.append(option);
193 option = optgroup.append(option);
182 }
194 }
183 } else {
195 } else {
184 option.val(filterValue).text(filterValue);
196 option.val(filterValue).text(filterValue);
185 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
197 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
186 }
198 }
187 select.append(option);
199 select.append(option);
188 }
200 }
189 break;
201 break;
190 case "date":
202 case "date":
191 case "date_past":
203 case "date_past":
192 tr.find('td.values').append(
204 tr.find('td.values').append(
193 '<span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
205 '<span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
194 ' <span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
206 ' <span style="display:none;"><input type="date" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
195 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
207 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
196 );
208 );
197 $('#values_'+fieldId+'_1').val(values[0]).datepickerFallback(datepickerOptions);
209 $('#values_'+fieldId+'_1').val(values[0]).datepickerFallback(datepickerOptions);
198 $('#values_'+fieldId+'_2').val(values[1]).datepickerFallback(datepickerOptions);
210 $('#values_'+fieldId+'_2').val(values[1]).datepickerFallback(datepickerOptions);
199 $('#values_'+fieldId).val(values[0]);
211 $('#values_'+fieldId).val(values[0]);
200 break;
212 break;
201 case "string":
213 case "string":
202 case "text":
214 case "text":
203 tr.find('td.values').append(
215 tr.find('td.values').append(
204 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
216 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
205 );
217 );
206 $('#values_'+fieldId).val(values[0]);
218 $('#values_'+fieldId).val(values[0]);
207 break;
219 break;
208 case "relation":
220 case "relation":
209 tr.find('td.values').append(
221 tr.find('td.values').append(
210 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
211 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
223 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
212 );
224 );
213 $('#values_'+fieldId).val(values[0]);
225 $('#values_'+fieldId).val(values[0]);
214 select = tr.find('td.values select');
226 select = tr.find('td.values select');
215 for (i = 0; i < allProjects.length; i++) {
227 for (i = 0; i < filterValues.length; i++) {
216 var filterValue = allProjects[i];
228 var filterValue = filterValues[i];
217 var option = $('<option>');
229 var option = $('<option>');
218 option.val(filterValue[1]).text(filterValue[0]);
230 option.val(filterValue[1]).text(filterValue[0]);
219 if (values[0] == filterValue[1]) { option.attr('selected', true); }
231 if (values[0] == filterValue[1]) { option.attr('selected', true); }
220 select.append(option);
232 select.append(option);
221 }
233 }
222 break;
234 break;
223 case "integer":
235 case "integer":
224 case "float":
236 case "float":
225 case "tree":
237 case "tree":
226 tr.find('td.values').append(
238 tr.find('td.values').append(
227 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="14" class="value" /></span>' +
239 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="14" class="value" /></span>' +
228 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="14" class="value" /></span>'
240 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="14" class="value" /></span>'
229 );
241 );
230 $('#values_'+fieldId+'_1').val(values[0]);
242 $('#values_'+fieldId+'_1').val(values[0]);
231 $('#values_'+fieldId+'_2').val(values[1]);
243 $('#values_'+fieldId+'_2').val(values[1]);
232 break;
244 break;
233 }
245 }
234 }
246 }
235
247
236 function toggleFilter(field) {
248 function toggleFilter(field) {
237 var fieldId = field.replace('.', '_');
249 var fieldId = field.replace('.', '_');
238 if ($('#cb_' + fieldId).is(':checked')) {
250 if ($('#cb_' + fieldId).is(':checked')) {
239 $("#operators_" + fieldId).show().removeAttr('disabled');
251 $("#operators_" + fieldId).show().removeAttr('disabled');
240 toggleOperator(field);
252 toggleOperator(field);
241 } else {
253 } else {
242 $("#operators_" + fieldId).hide().attr('disabled', true);
254 $("#operators_" + fieldId).hide().attr('disabled', true);
243 enableValues(field, []);
255 enableValues(field, []);
244 }
256 }
245 }
257 }
246
258
247 function enableValues(field, indexes) {
259 function enableValues(field, indexes) {
248 var fieldId = field.replace('.', '_');
260 var fieldId = field.replace('.', '_');
249 $('#tr_'+fieldId+' td.values .value').each(function(index) {
261 $('#tr_'+fieldId+' td.values .value').each(function(index) {
250 if ($.inArray(index, indexes) >= 0) {
262 if ($.inArray(index, indexes) >= 0) {
251 $(this).removeAttr('disabled');
263 $(this).removeAttr('disabled');
252 $(this).parents('span').first().show();
264 $(this).parents('span').first().show();
253 } else {
265 } else {
254 $(this).val('');
266 $(this).val('');
255 $(this).attr('disabled', true);
267 $(this).attr('disabled', true);
256 $(this).parents('span').first().hide();
268 $(this).parents('span').first().hide();
257 }
269 }
258
270
259 if ($(this).hasClass('group')) {
271 if ($(this).hasClass('group')) {
260 $(this).addClass('open');
272 $(this).addClass('open');
261 } else {
273 } else {
262 $(this).show();
274 $(this).show();
263 }
275 }
264 });
276 });
265 }
277 }
266
278
267 function toggleOperator(field) {
279 function toggleOperator(field) {
268 var fieldId = field.replace('.', '_');
280 var fieldId = field.replace('.', '_');
269 var operator = $("#operators_" + fieldId);
281 var operator = $("#operators_" + fieldId);
270 switch (operator.val()) {
282 switch (operator.val()) {
271 case "!*":
283 case "!*":
272 case "*":
284 case "*":
273 case "t":
285 case "t":
274 case "ld":
286 case "ld":
275 case "w":
287 case "w":
276 case "lw":
288 case "lw":
277 case "l2w":
289 case "l2w":
278 case "m":
290 case "m":
279 case "lm":
291 case "lm":
280 case "y":
292 case "y":
281 case "o":
293 case "o":
282 case "c":
294 case "c":
283 case "*o":
295 case "*o":
284 case "!o":
296 case "!o":
285 enableValues(field, []);
297 enableValues(field, []);
286 break;
298 break;
287 case "><":
299 case "><":
288 enableValues(field, [0,1]);
300 enableValues(field, [0,1]);
289 break;
301 break;
290 case "<t+":
302 case "<t+":
291 case ">t+":
303 case ">t+":
292 case "><t+":
304 case "><t+":
293 case "t+":
305 case "t+":
294 case ">t-":
306 case ">t-":
295 case "<t-":
307 case "<t-":
296 case "><t-":
308 case "><t-":
297 case "t-":
309 case "t-":
298 enableValues(field, [2]);
310 enableValues(field, [2]);
299 break;
311 break;
300 case "=p":
312 case "=p":
301 case "=!p":
313 case "=!p":
302 case "!p":
314 case "!p":
303 enableValues(field, [1]);
315 enableValues(field, [1]);
304 break;
316 break;
305 default:
317 default:
306 enableValues(field, [0]);
318 enableValues(field, [0]);
307 break;
319 break;
308 }
320 }
309 }
321 }
310
322
311 function toggleMultiSelect(el) {
323 function toggleMultiSelect(el) {
312 if (el.attr('multiple')) {
324 if (el.attr('multiple')) {
313 el.removeAttr('multiple');
325 el.removeAttr('multiple');
314 el.attr('size', 1);
326 el.attr('size', 1);
315 } else {
327 } else {
316 el.attr('multiple', true);
328 el.attr('multiple', true);
317 if (el.children().length > 10)
329 if (el.children().length > 10)
318 el.attr('size', 10);
330 el.attr('size', 10);
319 else
331 else
320 el.attr('size', 4);
332 el.attr('size', 4);
321 }
333 }
322 }
334 }
323
335
324 function showTab(name, url) {
336 function showTab(name, url) {
325 $('#tab-content-' + name).parent().find('.tab-content').hide();
337 $('#tab-content-' + name).parent().find('.tab-content').hide();
326 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
338 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
327 $('#tab-content-' + name).show();
339 $('#tab-content-' + name).show();
328 $('#tab-' + name).addClass('selected');
340 $('#tab-' + name).addClass('selected');
329 //replaces current URL with the "href" attribute of the current link
341 //replaces current URL with the "href" attribute of the current link
330 //(only triggered if supported by browser)
342 //(only triggered if supported by browser)
331 if ("replaceState" in window.history) {
343 if ("replaceState" in window.history) {
332 window.history.replaceState(null, document.title, url);
344 window.history.replaceState(null, document.title, url);
333 }
345 }
334 return false;
346 return false;
335 }
347 }
336
348
337 function moveTabRight(el) {
349 function moveTabRight(el) {
338 var lis = $(el).parents('div.tabs').first().find('ul').children();
350 var lis = $(el).parents('div.tabs').first().find('ul').children();
339 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
351 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
340 var tabsWidth = 0;
352 var tabsWidth = 0;
341 var i = 0;
353 var i = 0;
342 lis.each(function() {
354 lis.each(function() {
343 if ($(this).is(':visible')) {
355 if ($(this).is(':visible')) {
344 tabsWidth += $(this).outerWidth(true);
356 tabsWidth += $(this).outerWidth(true);
345 }
357 }
346 });
358 });
347 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
359 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
348 $(el).siblings('.tab-left').removeClass('disabled');
360 $(el).siblings('.tab-left').removeClass('disabled');
349 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
361 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
350 var w = lis.eq(i).width();
362 var w = lis.eq(i).width();
351 lis.eq(i).hide();
363 lis.eq(i).hide();
352 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
364 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
353 $(el).addClass('disabled');
365 $(el).addClass('disabled');
354 }
366 }
355 }
367 }
356
368
357 function moveTabLeft(el) {
369 function moveTabLeft(el) {
358 var lis = $(el).parents('div.tabs').first().find('ul').children();
370 var lis = $(el).parents('div.tabs').first().find('ul').children();
359 var i = 0;
371 var i = 0;
360 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
372 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
361 if (i > 0) {
373 if (i > 0) {
362 lis.eq(i-1).show();
374 lis.eq(i-1).show();
363 $(el).siblings('.tab-right').removeClass('disabled');
375 $(el).siblings('.tab-right').removeClass('disabled');
364 }
376 }
365 if (i <= 1) {
377 if (i <= 1) {
366 $(el).addClass('disabled');
378 $(el).addClass('disabled');
367 }
379 }
368 }
380 }
369
381
370 function displayTabsButtons() {
382 function displayTabsButtons() {
371 var lis;
383 var lis;
372 var tabsWidth;
384 var tabsWidth;
373 var el;
385 var el;
374 var numHidden;
386 var numHidden;
375 $('div.tabs').each(function() {
387 $('div.tabs').each(function() {
376 el = $(this);
388 el = $(this);
377 lis = el.find('ul').children();
389 lis = el.find('ul').children();
378 tabsWidth = 0;
390 tabsWidth = 0;
379 numHidden = 0;
391 numHidden = 0;
380 lis.each(function(){
392 lis.each(function(){
381 if ($(this).is(':visible')) {
393 if ($(this).is(':visible')) {
382 tabsWidth += $(this).outerWidth(true);
394 tabsWidth += $(this).outerWidth(true);
383 } else {
395 } else {
384 numHidden++;
396 numHidden++;
385 }
397 }
386 });
398 });
387 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
399 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
388 if ((tabsWidth < el.width() - bw) && (lis.length === 0 || lis.first().is(':visible'))) {
400 if ((tabsWidth < el.width() - bw) && (lis.length === 0 || lis.first().is(':visible'))) {
389 el.find('div.tabs-buttons').hide();
401 el.find('div.tabs-buttons').hide();
390 } else {
402 } else {
391 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
403 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
392 }
404 }
393 });
405 });
394 }
406 }
395
407
396 function setPredecessorFieldsVisibility() {
408 function setPredecessorFieldsVisibility() {
397 var relationType = $('#relation_relation_type');
409 var relationType = $('#relation_relation_type');
398 if (relationType.val() == "precedes" || relationType.val() == "follows") {
410 if (relationType.val() == "precedes" || relationType.val() == "follows") {
399 $('#predecessor_fields').show();
411 $('#predecessor_fields').show();
400 } else {
412 } else {
401 $('#predecessor_fields').hide();
413 $('#predecessor_fields').hide();
402 }
414 }
403 }
415 }
404
416
405 function showModal(id, width, title) {
417 function showModal(id, width, title) {
406 var el = $('#'+id).first();
418 var el = $('#'+id).first();
407 if (el.length === 0 || el.is(':visible')) {return;}
419 if (el.length === 0 || el.is(':visible')) {return;}
408 if (!title) title = el.find('h3.title').text();
420 if (!title) title = el.find('h3.title').text();
409 // moves existing modals behind the transparent background
421 // moves existing modals behind the transparent background
410 $(".modal").zIndex(99);
422 $(".modal").zIndex(99);
411 el.dialog({
423 el.dialog({
412 width: width,
424 width: width,
413 modal: true,
425 modal: true,
414 resizable: false,
426 resizable: false,
415 dialogClass: 'modal',
427 dialogClass: 'modal',
416 title: title
428 title: title
417 }).on('dialogclose', function(){
429 }).on('dialogclose', function(){
418 $(".modal").zIndex(101);
430 $(".modal").zIndex(101);
419 });
431 });
420 el.find("input[type=text], input[type=submit]").first().focus();
432 el.find("input[type=text], input[type=submit]").first().focus();
421 }
433 }
422
434
423 function hideModal(el) {
435 function hideModal(el) {
424 var modal;
436 var modal;
425 if (el) {
437 if (el) {
426 modal = $(el).parents('.ui-dialog-content');
438 modal = $(el).parents('.ui-dialog-content');
427 } else {
439 } else {
428 modal = $('#ajax-modal');
440 modal = $('#ajax-modal');
429 }
441 }
430 modal.dialog("close");
442 modal.dialog("close");
431 }
443 }
432
444
433 function submitPreview(url, form, target) {
445 function submitPreview(url, form, target) {
434 $.ajax({
446 $.ajax({
435 url: url,
447 url: url,
436 type: 'post',
448 type: 'post',
437 data: $('#'+form).serialize(),
449 data: $('#'+form).serialize(),
438 success: function(data){
450 success: function(data){
439 $('#'+target).html(data);
451 $('#'+target).html(data);
440 }
452 }
441 });
453 });
442 }
454 }
443
455
444 function collapseScmEntry(id) {
456 function collapseScmEntry(id) {
445 $('.'+id).each(function() {
457 $('.'+id).each(function() {
446 if ($(this).hasClass('open')) {
458 if ($(this).hasClass('open')) {
447 collapseScmEntry($(this).attr('id'));
459 collapseScmEntry($(this).attr('id'));
448 }
460 }
449 $(this).hide();
461 $(this).hide();
450 });
462 });
451 $('#'+id).removeClass('open');
463 $('#'+id).removeClass('open');
452 }
464 }
453
465
454 function expandScmEntry(id) {
466 function expandScmEntry(id) {
455 $('.'+id).each(function() {
467 $('.'+id).each(function() {
456 $(this).show();
468 $(this).show();
457 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
469 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
458 expandScmEntry($(this).attr('id'));
470 expandScmEntry($(this).attr('id'));
459 }
471 }
460 });
472 });
461 $('#'+id).addClass('open');
473 $('#'+id).addClass('open');
462 }
474 }
463
475
464 function scmEntryClick(id, url) {
476 function scmEntryClick(id, url) {
465 var el = $('#'+id);
477 var el = $('#'+id);
466 if (el.hasClass('open')) {
478 if (el.hasClass('open')) {
467 collapseScmEntry(id);
479 collapseScmEntry(id);
468 el.addClass('collapsed');
480 el.addClass('collapsed');
469 return false;
481 return false;
470 } else if (el.hasClass('loaded')) {
482 } else if (el.hasClass('loaded')) {
471 expandScmEntry(id);
483 expandScmEntry(id);
472 el.removeClass('collapsed');
484 el.removeClass('collapsed');
473 return false;
485 return false;
474 }
486 }
475 if (el.hasClass('loading')) {
487 if (el.hasClass('loading')) {
476 return false;
488 return false;
477 }
489 }
478 el.addClass('loading');
490 el.addClass('loading');
479 $.ajax({
491 $.ajax({
480 url: url,
492 url: url,
481 success: function(data) {
493 success: function(data) {
482 el.after(data);
494 el.after(data);
483 el.addClass('open').addClass('loaded').removeClass('loading');
495 el.addClass('open').addClass('loaded').removeClass('loading');
484 }
496 }
485 });
497 });
486 return true;
498 return true;
487 }
499 }
488
500
489 function randomKey(size) {
501 function randomKey(size) {
490 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
502 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
491 var key = '';
503 var key = '';
492 for (var i = 0; i < size; i++) {
504 for (var i = 0; i < size; i++) {
493 key += chars.charAt(Math.floor(Math.random() * chars.length));
505 key += chars.charAt(Math.floor(Math.random() * chars.length));
494 }
506 }
495 return key;
507 return key;
496 }
508 }
497
509
498 function updateIssueFrom(url, el) {
510 function updateIssueFrom(url, el) {
499 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
511 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
500 $(this).data('valuebeforeupdate', $(this).val());
512 $(this).data('valuebeforeupdate', $(this).val());
501 });
513 });
502 if (el) {
514 if (el) {
503 $("#form_update_triggered_by").val($(el).attr('id'));
515 $("#form_update_triggered_by").val($(el).attr('id'));
504 }
516 }
505 return $.ajax({
517 return $.ajax({
506 url: url,
518 url: url,
507 type: 'post',
519 type: 'post',
508 data: $('#issue-form').serialize()
520 data: $('#issue-form').serialize()
509 });
521 });
510 }
522 }
511
523
512 function replaceIssueFormWith(html){
524 function replaceIssueFormWith(html){
513 var replacement = $(html);
525 var replacement = $(html);
514 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
526 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
515 var object_id = $(this).attr('id');
527 var object_id = $(this).attr('id');
516 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
528 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
517 replacement.find('#'+object_id).val($(this).val());
529 replacement.find('#'+object_id).val($(this).val());
518 }
530 }
519 });
531 });
520 $('#all_attributes').empty();
532 $('#all_attributes').empty();
521 $('#all_attributes').prepend(replacement);
533 $('#all_attributes').prepend(replacement);
522 }
534 }
523
535
524 function updateBulkEditFrom(url) {
536 function updateBulkEditFrom(url) {
525 $.ajax({
537 $.ajax({
526 url: url,
538 url: url,
527 type: 'post',
539 type: 'post',
528 data: $('#bulk_edit_form').serialize()
540 data: $('#bulk_edit_form').serialize()
529 });
541 });
530 }
542 }
531
543
532 function observeAutocompleteField(fieldId, url, options) {
544 function observeAutocompleteField(fieldId, url, options) {
533 $(document).ready(function() {
545 $(document).ready(function() {
534 $('#'+fieldId).autocomplete($.extend({
546 $('#'+fieldId).autocomplete($.extend({
535 source: url,
547 source: url,
536 minLength: 2,
548 minLength: 2,
537 position: {collision: "flipfit"},
549 position: {collision: "flipfit"},
538 search: function(){$('#'+fieldId).addClass('ajax-loading');},
550 search: function(){$('#'+fieldId).addClass('ajax-loading');},
539 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
551 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
540 }, options));
552 }, options));
541 $('#'+fieldId).addClass('autocomplete');
553 $('#'+fieldId).addClass('autocomplete');
542 });
554 });
543 }
555 }
544
556
545 function observeSearchfield(fieldId, targetId, url) {
557 function observeSearchfield(fieldId, targetId, url) {
546 $('#'+fieldId).each(function() {
558 $('#'+fieldId).each(function() {
547 var $this = $(this);
559 var $this = $(this);
548 $this.addClass('autocomplete');
560 $this.addClass('autocomplete');
549 $this.attr('data-value-was', $this.val());
561 $this.attr('data-value-was', $this.val());
550 var check = function() {
562 var check = function() {
551 var val = $this.val();
563 var val = $this.val();
552 if ($this.attr('data-value-was') != val){
564 if ($this.attr('data-value-was') != val){
553 $this.attr('data-value-was', val);
565 $this.attr('data-value-was', val);
554 $.ajax({
566 $.ajax({
555 url: url,
567 url: url,
556 type: 'get',
568 type: 'get',
557 data: {q: $this.val()},
569 data: {q: $this.val()},
558 success: function(data){ if(targetId) $('#'+targetId).html(data); },
570 success: function(data){ if(targetId) $('#'+targetId).html(data); },
559 beforeSend: function(){ $this.addClass('ajax-loading'); },
571 beforeSend: function(){ $this.addClass('ajax-loading'); },
560 complete: function(){ $this.removeClass('ajax-loading'); }
572 complete: function(){ $this.removeClass('ajax-loading'); }
561 });
573 });
562 }
574 }
563 };
575 };
564 var reset = function() {
576 var reset = function() {
565 if (timer) {
577 if (timer) {
566 clearInterval(timer);
578 clearInterval(timer);
567 timer = setInterval(check, 300);
579 timer = setInterval(check, 300);
568 }
580 }
569 };
581 };
570 var timer = setInterval(check, 300);
582 var timer = setInterval(check, 300);
571 $this.bind('keyup click mousemove', reset);
583 $this.bind('keyup click mousemove', reset);
572 });
584 });
573 }
585 }
574
586
575 $(document).ready(function(){
587 $(document).ready(function(){
576 $(".drdn .autocomplete").val('');
588 $(".drdn .autocomplete").val('');
577
589
578 $(".drdn-trigger").click(function(e){
590 $(".drdn-trigger").click(function(e){
579 var drdn = $(this).closest(".drdn");
591 var drdn = $(this).closest(".drdn");
580 if (drdn.hasClass("expanded")) {
592 if (drdn.hasClass("expanded")) {
581 drdn.removeClass("expanded");
593 drdn.removeClass("expanded");
582 } else {
594 } else {
583 $(".drdn").removeClass("expanded");
595 $(".drdn").removeClass("expanded");
584 drdn.addClass("expanded");
596 drdn.addClass("expanded");
585 if (!isMobile()) {
597 if (!isMobile()) {
586 drdn.find(".autocomplete").focus();
598 drdn.find(".autocomplete").focus();
587 }
599 }
588 e.stopPropagation();
600 e.stopPropagation();
589 }
601 }
590 });
602 });
591 $(document).click(function(e){
603 $(document).click(function(e){
592 if ($(e.target).closest(".drdn").length < 1) {
604 if ($(e.target).closest(".drdn").length < 1) {
593 $(".drdn.expanded").removeClass("expanded");
605 $(".drdn.expanded").removeClass("expanded");
594 }
606 }
595 });
607 });
596
608
597 observeSearchfield('projects-quick-search', null, $('#projects-quick-search').data('automcomplete-url'));
609 observeSearchfield('projects-quick-search', null, $('#projects-quick-search').data('automcomplete-url'));
598
610
599 $(".drdn-content").keydown(function(event){
611 $(".drdn-content").keydown(function(event){
600 var items = $(this).find(".drdn-items");
612 var items = $(this).find(".drdn-items");
601 var focused = items.find("a:focus");
613 var focused = items.find("a:focus");
602 switch (event.which) {
614 switch (event.which) {
603 case 40: //down
615 case 40: //down
604 if (focused.length > 0) {
616 if (focused.length > 0) {
605 focused.nextAll("a").first().focus();;
617 focused.nextAll("a").first().focus();;
606 } else {
618 } else {
607 items.find("a").first().focus();;
619 items.find("a").first().focus();;
608 }
620 }
609 event.preventDefault();
621 event.preventDefault();
610 break;
622 break;
611 case 38: //up
623 case 38: //up
612 if (focused.length > 0) {
624 if (focused.length > 0) {
613 var prev = focused.prevAll("a");
625 var prev = focused.prevAll("a");
614 if (prev.length > 0) {
626 if (prev.length > 0) {
615 prev.first().focus();
627 prev.first().focus();
616 } else {
628 } else {
617 $(this).find(".autocomplete").focus();
629 $(this).find(".autocomplete").focus();
618 }
630 }
619 event.preventDefault();
631 event.preventDefault();
620 }
632 }
621 break;
633 break;
622 case 35: //end
634 case 35: //end
623 if (focused.length > 0) {
635 if (focused.length > 0) {
624 focused.nextAll("a").last().focus();
636 focused.nextAll("a").last().focus();
625 event.preventDefault();
637 event.preventDefault();
626 }
638 }
627 break;
639 break;
628 case 36: //home
640 case 36: //home
629 if (focused.length > 0) {
641 if (focused.length > 0) {
630 focused.prevAll("a").last().focus();
642 focused.prevAll("a").last().focus();
631 event.preventDefault();
643 event.preventDefault();
632 }
644 }
633 break;
645 break;
634 }
646 }
635 });
647 });
636 });
648 });
637
649
638 function beforeShowDatePicker(input, inst) {
650 function beforeShowDatePicker(input, inst) {
639 var default_date = null;
651 var default_date = null;
640 switch ($(input).attr("id")) {
652 switch ($(input).attr("id")) {
641 case "issue_start_date" :
653 case "issue_start_date" :
642 if ($("#issue_due_date").size() > 0) {
654 if ($("#issue_due_date").size() > 0) {
643 default_date = $("#issue_due_date").val();
655 default_date = $("#issue_due_date").val();
644 }
656 }
645 break;
657 break;
646 case "issue_due_date" :
658 case "issue_due_date" :
647 if ($("#issue_start_date").size() > 0) {
659 if ($("#issue_start_date").size() > 0) {
648 var start_date = $("#issue_start_date").val();
660 var start_date = $("#issue_start_date").val();
649 if (start_date != "") {
661 if (start_date != "") {
650 start_date = new Date(Date.parse(start_date));
662 start_date = new Date(Date.parse(start_date));
651 if (start_date > new Date()) {
663 if (start_date > new Date()) {
652 default_date = $("#issue_start_date").val();
664 default_date = $("#issue_start_date").val();
653 }
665 }
654 }
666 }
655 }
667 }
656 break;
668 break;
657 }
669 }
658 $(input).datepickerFallback("option", "defaultDate", default_date);
670 $(input).datepickerFallback("option", "defaultDate", default_date);
659 }
671 }
660
672
661 (function($){
673 (function($){
662 $.fn.positionedItems = function(sortableOptions, options){
674 $.fn.positionedItems = function(sortableOptions, options){
663 var settings = $.extend({
675 var settings = $.extend({
664 firstPosition: 1
676 firstPosition: 1
665 }, options );
677 }, options );
666
678
667 return this.sortable($.extend({
679 return this.sortable($.extend({
668 axis: 'y',
680 axis: 'y',
669 handle: ".sort-handle",
681 handle: ".sort-handle",
670 helper: function(event, ui){
682 helper: function(event, ui){
671 ui.children('td').each(function(){
683 ui.children('td').each(function(){
672 $(this).width($(this).width());
684 $(this).width($(this).width());
673 });
685 });
674 return ui;
686 return ui;
675 },
687 },
676 update: function(event, ui) {
688 update: function(event, ui) {
677 var sortable = $(this);
689 var sortable = $(this);
678 var handle = ui.item.find(".sort-handle").addClass("ajax-loading");
690 var handle = ui.item.find(".sort-handle").addClass("ajax-loading");
679 var url = handle.data("reorder-url");
691 var url = handle.data("reorder-url");
680 var param = handle.data("reorder-param");
692 var param = handle.data("reorder-param");
681 var data = {};
693 var data = {};
682 data[param] = {position: ui.item.index() + settings['firstPosition']};
694 data[param] = {position: ui.item.index() + settings['firstPosition']};
683 $.ajax({
695 $.ajax({
684 url: url,
696 url: url,
685 type: 'put',
697 type: 'put',
686 dataType: 'script',
698 dataType: 'script',
687 data: data,
699 data: data,
688 success: function(data){
700 success: function(data){
689 sortable.children(":even").removeClass("even").addClass("odd");
701 sortable.children(":even").removeClass("even").addClass("odd");
690 sortable.children(":odd").removeClass("odd").addClass("even");
702 sortable.children(":odd").removeClass("odd").addClass("even");
691 },
703 },
692 error: function(jqXHR, textStatus, errorThrown){
704 error: function(jqXHR, textStatus, errorThrown){
693 alert(jqXHR.status);
705 alert(jqXHR.status);
694 sortable.sortable("cancel");
706 sortable.sortable("cancel");
695 },
707 },
696 complete: function(jqXHR, textStatus, errorThrown){
708 complete: function(jqXHR, textStatus, errorThrown){
697 handle.removeClass("ajax-loading");
709 handle.removeClass("ajax-loading");
698 }
710 }
699 });
711 });
700 },
712 },
701 }, sortableOptions));
713 }, sortableOptions));
702 }
714 }
703 }( jQuery ));
715 }( jQuery ));
704
716
705 function initMyPageSortable(list, url) {
717 function initMyPageSortable(list, url) {
706 $('#list-'+list).sortable({
718 $('#list-'+list).sortable({
707 connectWith: '.block-receiver',
719 connectWith: '.block-receiver',
708 tolerance: 'pointer',
720 tolerance: 'pointer',
709 update: function(){
721 update: function(){
710 $.ajax({
722 $.ajax({
711 url: url,
723 url: url,
712 type: 'post',
724 type: 'post',
713 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
725 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
714 });
726 });
715 }
727 }
716 });
728 });
717 $("#list-top, #list-left, #list-right").disableSelection();
729 $("#list-top, #list-left, #list-right").disableSelection();
718 }
730 }
719
731
720 var warnLeavingUnsavedMessage;
732 var warnLeavingUnsavedMessage;
721 function warnLeavingUnsaved(message) {
733 function warnLeavingUnsaved(message) {
722 warnLeavingUnsavedMessage = message;
734 warnLeavingUnsavedMessage = message;
723 $(document).on('submit', 'form', function(){
735 $(document).on('submit', 'form', function(){
724 $('textarea').removeData('changed');
736 $('textarea').removeData('changed');
725 });
737 });
726 $(document).on('change', 'textarea', function(){
738 $(document).on('change', 'textarea', function(){
727 $(this).data('changed', 'changed');
739 $(this).data('changed', 'changed');
728 });
740 });
729 window.onbeforeunload = function(){
741 window.onbeforeunload = function(){
730 var warn = false;
742 var warn = false;
731 $('textarea').blur().each(function(){
743 $('textarea').blur().each(function(){
732 if ($(this).data('changed')) {
744 if ($(this).data('changed')) {
733 warn = true;
745 warn = true;
734 }
746 }
735 });
747 });
736 if (warn) {return warnLeavingUnsavedMessage;}
748 if (warn) {return warnLeavingUnsavedMessage;}
737 };
749 };
738 }
750 }
739
751
740 function setupAjaxIndicator() {
752 function setupAjaxIndicator() {
741 $(document).bind('ajaxSend', function(event, xhr, settings) {
753 $(document).bind('ajaxSend', function(event, xhr, settings) {
742 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
754 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
743 $('#ajax-indicator').show();
755 $('#ajax-indicator').show();
744 }
756 }
745 });
757 });
746 $(document).bind('ajaxStop', function() {
758 $(document).bind('ajaxStop', function() {
747 $('#ajax-indicator').hide();
759 $('#ajax-indicator').hide();
748 });
760 });
749 }
761 }
750
762
751 function setupTabs() {
763 function setupTabs() {
752 if($('.tabs').length > 0) {
764 if($('.tabs').length > 0) {
753 displayTabsButtons();
765 displayTabsButtons();
754 $(window).resize(displayTabsButtons);
766 $(window).resize(displayTabsButtons);
755 }
767 }
756 }
768 }
757
769
758 function hideOnLoad() {
770 function hideOnLoad() {
759 $('.hol').hide();
771 $('.hol').hide();
760 }
772 }
761
773
762 function addFormObserversForDoubleSubmit() {
774 function addFormObserversForDoubleSubmit() {
763 $('form[method=post]').each(function() {
775 $('form[method=post]').each(function() {
764 if (!$(this).hasClass('multiple-submit')) {
776 if (!$(this).hasClass('multiple-submit')) {
765 $(this).submit(function(form_submission) {
777 $(this).submit(function(form_submission) {
766 if ($(form_submission.target).attr('data-submitted')) {
778 if ($(form_submission.target).attr('data-submitted')) {
767 form_submission.preventDefault();
779 form_submission.preventDefault();
768 } else {
780 } else {
769 $(form_submission.target).attr('data-submitted', true);
781 $(form_submission.target).attr('data-submitted', true);
770 }
782 }
771 });
783 });
772 }
784 }
773 });
785 });
774 }
786 }
775
787
776 function defaultFocus(){
788 function defaultFocus(){
777 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
789 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
778 $('#content input[type=text], #content textarea').first().focus();
790 $('#content input[type=text], #content textarea').first().focus();
779 }
791 }
780 }
792 }
781
793
782 function blockEventPropagation(event) {
794 function blockEventPropagation(event) {
783 event.stopPropagation();
795 event.stopPropagation();
784 event.preventDefault();
796 event.preventDefault();
785 }
797 }
786
798
787 function toggleDisabledOnChange() {
799 function toggleDisabledOnChange() {
788 var checked = $(this).is(':checked');
800 var checked = $(this).is(':checked');
789 $($(this).data('disables')).attr('disabled', checked);
801 $($(this).data('disables')).attr('disabled', checked);
790 $($(this).data('enables')).attr('disabled', !checked);
802 $($(this).data('enables')).attr('disabled', !checked);
791 $($(this).data('shows')).toggle(checked);
803 $($(this).data('shows')).toggle(checked);
792 }
804 }
793 function toggleDisabledInit() {
805 function toggleDisabledInit() {
794 $('input[data-disables], input[data-enables], input[data-shows]').each(toggleDisabledOnChange);
806 $('input[data-disables], input[data-enables], input[data-shows]').each(toggleDisabledOnChange);
795 }
807 }
796
808
797 function toggleNewObjectDropdown() {
809 function toggleNewObjectDropdown() {
798 var dropdown = $('#new-object + ul.menu-children');
810 var dropdown = $('#new-object + ul.menu-children');
799 if(dropdown.hasClass('visible')){
811 if(dropdown.hasClass('visible')){
800 dropdown.removeClass('visible');
812 dropdown.removeClass('visible');
801 }else{
813 }else{
802 dropdown.addClass('visible');
814 dropdown.addClass('visible');
803 }
815 }
804 }
816 }
805
817
806 (function ( $ ) {
818 (function ( $ ) {
807
819
808 // detect if native date input is supported
820 // detect if native date input is supported
809 var nativeDateInputSupported = true;
821 var nativeDateInputSupported = true;
810
822
811 var input = document.createElement('input');
823 var input = document.createElement('input');
812 input.setAttribute('type','date');
824 input.setAttribute('type','date');
813 if (input.type === 'text') {
825 if (input.type === 'text') {
814 nativeDateInputSupported = false;
826 nativeDateInputSupported = false;
815 }
827 }
816
828
817 var notADateValue = 'not-a-date';
829 var notADateValue = 'not-a-date';
818 input.setAttribute('value', notADateValue);
830 input.setAttribute('value', notADateValue);
819 if (input.value === notADateValue) {
831 if (input.value === notADateValue) {
820 nativeDateInputSupported = false;
832 nativeDateInputSupported = false;
821 }
833 }
822
834
823 $.fn.datepickerFallback = function( options ) {
835 $.fn.datepickerFallback = function( options ) {
824 if (nativeDateInputSupported) {
836 if (nativeDateInputSupported) {
825 return this;
837 return this;
826 } else {
838 } else {
827 return this.datepicker( options );
839 return this.datepicker( options );
828 }
840 }
829 };
841 };
830 }( jQuery ));
842 }( jQuery ));
831
843
832 $(document).ready(function(){
844 $(document).ready(function(){
833 $('#content').on('change', 'input[data-disables], input[data-enables], input[data-shows]', toggleDisabledOnChange);
845 $('#content').on('change', 'input[data-disables], input[data-enables], input[data-shows]', toggleDisabledOnChange);
834 toggleDisabledInit();
846 toggleDisabledInit();
835 });
847 });
836
848
837 function keepAnchorOnSignIn(form){
849 function keepAnchorOnSignIn(form){
838 var hash = decodeURIComponent(self.document.location.hash);
850 var hash = decodeURIComponent(self.document.location.hash);
839 if (hash) {
851 if (hash) {
840 if (hash.indexOf("#") === -1) {
852 if (hash.indexOf("#") === -1) {
841 hash = "#" + hash;
853 hash = "#" + hash;
842 }
854 }
843 form.action = form.action + hash;
855 form.action = form.action + hash;
844 }
856 }
845 return true;
857 return true;
846 }
858 }
847
859
848 $(document).ready(setupAjaxIndicator);
860 $(document).ready(setupAjaxIndicator);
849 $(document).ready(hideOnLoad);
861 $(document).ready(hideOnLoad);
850 $(document).ready(addFormObserversForDoubleSubmit);
862 $(document).ready(addFormObserversForDoubleSubmit);
851 $(document).ready(defaultFocus);
863 $(document).ready(defaultFocus);
852 $(document).ready(setupTabs);
864 $(document).ready(setupTabs);
@@ -1,400 +1,425
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 < Redmine::ControllerTest
20 class QueriesControllerTest < Redmine::ControllerTest
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, :enabled_modules,
22 :users, :email_addresses,
23 :members, :member_roles, :roles,
24 :trackers, :issue_statuses, :issue_categories, :enumerations, :versions,
25 :issues, :custom_fields, :custom_values,
26 :queries
22
27
23 def setup
28 def setup
24 User.current = nil
29 User.current = nil
25 end
30 end
26
31
27 def test_index
32 def test_index
28 get :index
33 get :index
29 # HTML response not implemented
34 # HTML response not implemented
30 assert_response 406
35 assert_response 406
31 end
36 end
32
37
33 def test_new_project_query
38 def test_new_project_query
34 @request.session[:user_id] = 2
39 @request.session[:user_id] = 2
35 get :new, :project_id => 1
40 get :new, :project_id => 1
36 assert_response :success
41 assert_response :success
37
42
38 assert_select 'input[name=?][value="0"][checked=checked]', 'query[visibility]'
43 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])'
44 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked]):not([disabled])'
40 assert_select 'select[name=?]', 'c[]' do
45 assert_select 'select[name=?]', 'c[]' do
41 assert_select 'option[value=tracker]'
46 assert_select 'option[value=tracker]'
42 assert_select 'option[value=subject]'
47 assert_select 'option[value=subject]'
43 end
48 end
44 end
49 end
45
50
46 def test_new_global_query
51 def test_new_global_query
47 @request.session[:user_id] = 2
52 @request.session[:user_id] = 2
48 get :new
53 get :new
49 assert_response :success
54 assert_response :success
50
55
51 assert_select 'input[name=?]', 'query[visibility]', 0
56 assert_select 'input[name=?]', 'query[visibility]', 0
52 assert_select 'input[name=query_is_for_all][type=checkbox][checked]:not([disabled])'
57 assert_select 'input[name=query_is_for_all][type=checkbox][checked]:not([disabled])'
53 end
58 end
54
59
55 def test_new_on_invalid_project
60 def test_new_on_invalid_project
56 @request.session[:user_id] = 2
61 @request.session[:user_id] = 2
57 get :new, :project_id => 'invalid'
62 get :new, :project_id => 'invalid'
58 assert_response 404
63 assert_response 404
59 end
64 end
60
65
61 def test_new_time_entry_query
66 def test_new_time_entry_query
62 @request.session[:user_id] = 2
67 @request.session[:user_id] = 2
63 get :new, :project_id => 1, :type => 'TimeEntryQuery'
68 get :new, :project_id => 1, :type => 'TimeEntryQuery'
64 assert_response :success
69 assert_response :success
65 assert_select 'input[name=type][value=?]', 'TimeEntryQuery'
70 assert_select 'input[name=type][value=?]', 'TimeEntryQuery'
66 end
71 end
67
72
68 def test_create_project_public_query
73 def test_create_project_public_query
69 @request.session[:user_id] = 2
74 @request.session[:user_id] = 2
70 post :create,
75 post :create,
71 :project_id => 'ecookbook',
76 :project_id => 'ecookbook',
72 :default_columns => '1',
77 :default_columns => '1',
73 :f => ["status_id", "assigned_to_id"],
78 :f => ["status_id", "assigned_to_id"],
74 :op => {"assigned_to_id" => "=", "status_id" => "o"},
79 :op => {"assigned_to_id" => "=", "status_id" => "o"},
75 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
80 :v => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
76 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
81 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
77
82
78 q = Query.find_by_name('test_new_project_public_query')
83 q = Query.find_by_name('test_new_project_public_query')
79 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
84 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
80 assert q.is_public?
85 assert q.is_public?
81 assert q.has_default_columns?
86 assert q.has_default_columns?
82 assert q.valid?
87 assert q.valid?
83 end
88 end
84
89
85 def test_create_project_private_query
90 def test_create_project_private_query
86 @request.session[:user_id] = 3
91 @request.session[:user_id] = 3
87 post :create,
92 post :create,
88 :project_id => 'ecookbook',
93 :project_id => 'ecookbook',
89 :default_columns => '1',
94 :default_columns => '1',
90 :fields => ["status_id", "assigned_to_id"],
95 :fields => ["status_id", "assigned_to_id"],
91 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
96 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
92 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
97 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
93 :query => {"name" => "test_new_project_private_query", "visibility" => "0"}
98 :query => {"name" => "test_new_project_private_query", "visibility" => "0"}
94
99
95 q = Query.find_by_name('test_new_project_private_query')
100 q = Query.find_by_name('test_new_project_private_query')
96 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
101 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
97 assert !q.is_public?
102 assert !q.is_public?
98 assert q.has_default_columns?
103 assert q.has_default_columns?
99 assert q.valid?
104 assert q.valid?
100 end
105 end
101
106
102 def test_create_project_roles_query
107 def test_create_project_roles_query
103 @request.session[:user_id] = 2
108 @request.session[:user_id] = 2
104 post :create,
109 post :create,
105 :project_id => 'ecookbook',
110 :project_id => 'ecookbook',
106 :default_columns => '1',
111 :default_columns => '1',
107 :fields => ["status_id", "assigned_to_id"],
112 :fields => ["status_id", "assigned_to_id"],
108 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
113 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
109 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
114 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
110 :query => {"name" => "test_create_project_roles_query", "visibility" => "1", "role_ids" => ["1", "2", ""]}
115 :query => {"name" => "test_create_project_roles_query", "visibility" => "1", "role_ids" => ["1", "2", ""]}
111
116
112 q = Query.find_by_name('test_create_project_roles_query')
117 q = Query.find_by_name('test_create_project_roles_query')
113 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
118 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :query_id => q
114 assert_equal Query::VISIBILITY_ROLES, q.visibility
119 assert_equal Query::VISIBILITY_ROLES, q.visibility
115 assert_equal [1, 2], q.roles.ids.sort
120 assert_equal [1, 2], q.roles.ids.sort
116 end
121 end
117
122
118 def test_create_global_private_query_with_custom_columns
123 def test_create_global_private_query_with_custom_columns
119 @request.session[:user_id] = 3
124 @request.session[:user_id] = 3
120 post :create,
125 post :create,
121 :fields => ["status_id", "assigned_to_id"],
126 :fields => ["status_id", "assigned_to_id"],
122 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
127 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
123 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
128 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
124 :query => {"name" => "test_new_global_private_query", "visibility" => "0"},
129 :query => {"name" => "test_new_global_private_query", "visibility" => "0"},
125 :c => ["", "tracker", "subject", "priority", "category"]
130 :c => ["", "tracker", "subject", "priority", "category"]
126
131
127 q = Query.find_by_name('test_new_global_private_query')
132 q = Query.find_by_name('test_new_global_private_query')
128 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
133 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
129 assert !q.is_public?
134 assert !q.is_public?
130 assert !q.has_default_columns?
135 assert !q.has_default_columns?
131 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
136 assert_equal [:id, :tracker, :subject, :priority, :category], q.columns.collect {|c| c.name}
132 assert q.valid?
137 assert q.valid?
133 end
138 end
134
139
135 def test_create_global_query_with_custom_filters
140 def test_create_global_query_with_custom_filters
136 @request.session[:user_id] = 3
141 @request.session[:user_id] = 3
137 post :create,
142 post :create,
138 :fields => ["assigned_to_id"],
143 :fields => ["assigned_to_id"],
139 :operators => {"assigned_to_id" => "="},
144 :operators => {"assigned_to_id" => "="},
140 :values => { "assigned_to_id" => ["me"]},
145 :values => { "assigned_to_id" => ["me"]},
141 :query => {"name" => "test_new_global_query"}
146 :query => {"name" => "test_new_global_query"}
142
147
143 q = Query.find_by_name('test_new_global_query')
148 q = Query.find_by_name('test_new_global_query')
144 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
149 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => nil, :query_id => q
145 assert !q.is_public?
150 assert !q.is_public?
146 assert !q.has_filter?(:status_id)
151 assert !q.has_filter?(:status_id)
147 assert_equal ['assigned_to_id'], q.filters.keys
152 assert_equal ['assigned_to_id'], q.filters.keys
148 assert q.valid?
153 assert q.valid?
149 end
154 end
150
155
151 def test_create_with_sort
156 def test_create_with_sort
152 @request.session[:user_id] = 1
157 @request.session[:user_id] = 1
153 post :create,
158 post :create,
154 :default_columns => '1',
159 :default_columns => '1',
155 :operators => {"status_id" => "o"},
160 :operators => {"status_id" => "o"},
156 :values => {"status_id" => ["1"]},
161 :values => {"status_id" => ["1"]},
157 :query => {:name => "test_new_with_sort",
162 :query => {:name => "test_new_with_sort",
158 :visibility => "2",
163 :visibility => "2",
159 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
164 :sort_criteria => {"0" => ["due_date", "desc"], "1" => ["tracker", ""]}}
160
165
161 query = Query.find_by_name("test_new_with_sort")
166 query = Query.find_by_name("test_new_with_sort")
162 assert_not_nil query
167 assert_not_nil query
163 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
168 assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
164 end
169 end
165
170
166 def test_create_with_failure
171 def test_create_with_failure
167 @request.session[:user_id] = 2
172 @request.session[:user_id] = 2
168 assert_no_difference '::Query.count' do
173 assert_no_difference '::Query.count' do
169 post :create, :project_id => 'ecookbook', :query => {:name => ''}
174 post :create, :project_id => 'ecookbook', :query => {:name => ''}
170 end
175 end
171 assert_response :success
176 assert_response :success
172
177
173 assert_select 'input[name=?]', 'query[name]'
178 assert_select 'input[name=?]', 'query[name]'
174 end
179 end
175
180
176 def test_create_global_query_from_gantt
181 def test_create_global_query_from_gantt
177 @request.session[:user_id] = 1
182 @request.session[:user_id] = 1
178 assert_difference 'IssueQuery.count' do
183 assert_difference 'IssueQuery.count' do
179 post :create,
184 post :create,
180 :gantt => 1,
185 :gantt => 1,
181 :operators => {"status_id" => "o"},
186 :operators => {"status_id" => "o"},
182 :values => {"status_id" => ["1"]},
187 :values => {"status_id" => ["1"]},
183 :query => {:name => "test_create_from_gantt",
188 :query => {:name => "test_create_from_gantt",
184 :draw_relations => '1',
189 :draw_relations => '1',
185 :draw_progress_line => '1'}
190 :draw_progress_line => '1'}
186 assert_response 302
191 assert_response 302
187 end
192 end
188 query = IssueQuery.order('id DESC').first
193 query = IssueQuery.order('id DESC').first
189 assert_redirected_to "/issues/gantt?query_id=#{query.id}"
194 assert_redirected_to "/issues/gantt?query_id=#{query.id}"
190 assert_equal true, query.draw_relations
195 assert_equal true, query.draw_relations
191 assert_equal true, query.draw_progress_line
196 assert_equal true, query.draw_progress_line
192 end
197 end
193
198
194 def test_create_project_query_from_gantt
199 def test_create_project_query_from_gantt
195 @request.session[:user_id] = 1
200 @request.session[:user_id] = 1
196 assert_difference 'IssueQuery.count' do
201 assert_difference 'IssueQuery.count' do
197 post :create,
202 post :create,
198 :project_id => 'ecookbook',
203 :project_id => 'ecookbook',
199 :gantt => 1,
204 :gantt => 1,
200 :operators => {"status_id" => "o"},
205 :operators => {"status_id" => "o"},
201 :values => {"status_id" => ["1"]},
206 :values => {"status_id" => ["1"]},
202 :query => {:name => "test_create_from_gantt",
207 :query => {:name => "test_create_from_gantt",
203 :draw_relations => '0',
208 :draw_relations => '0',
204 :draw_progress_line => '0'}
209 :draw_progress_line => '0'}
205 assert_response 302
210 assert_response 302
206 end
211 end
207 query = IssueQuery.order('id DESC').first
212 query = IssueQuery.order('id DESC').first
208 assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}"
213 assert_redirected_to "/projects/ecookbook/issues/gantt?query_id=#{query.id}"
209 assert_equal false, query.draw_relations
214 assert_equal false, query.draw_relations
210 assert_equal false, query.draw_progress_line
215 assert_equal false, query.draw_progress_line
211 end
216 end
212
217
213 def test_create_project_public_query_should_force_private_without_manage_public_queries_permission
218 def test_create_project_public_query_should_force_private_without_manage_public_queries_permission
214 @request.session[:user_id] = 3
219 @request.session[:user_id] = 3
215 query = new_record(Query) do
220 query = new_record(Query) do
216 post :create,
221 post :create,
217 :project_id => 'ecookbook',
222 :project_id => 'ecookbook',
218 :query => {"name" => "name", "visibility" => "2"}
223 :query => {"name" => "name", "visibility" => "2"}
219 assert_response 302
224 assert_response 302
220 end
225 end
221 assert_not_nil query.project
226 assert_not_nil query.project
222 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
227 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
223 end
228 end
224
229
225 def test_create_global_public_query_should_force_private_without_manage_public_queries_permission
230 def test_create_global_public_query_should_force_private_without_manage_public_queries_permission
226 @request.session[:user_id] = 3
231 @request.session[:user_id] = 3
227 query = new_record(Query) do
232 query = new_record(Query) do
228 post :create,
233 post :create,
229 :project_id => 'ecookbook', :query_is_for_all => '1',
234 :project_id => 'ecookbook', :query_is_for_all => '1',
230 :query => {"name" => "name", "visibility" => "2"}
235 :query => {"name" => "name", "visibility" => "2"}
231 assert_response 302
236 assert_response 302
232 end
237 end
233 assert_nil query.project
238 assert_nil query.project
234 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
239 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
235 end
240 end
236
241
237 def test_create_project_public_query_with_manage_public_queries_permission
242 def test_create_project_public_query_with_manage_public_queries_permission
238 @request.session[:user_id] = 2
243 @request.session[:user_id] = 2
239 query = new_record(Query) do
244 query = new_record(Query) do
240 post :create,
245 post :create,
241 :project_id => 'ecookbook',
246 :project_id => 'ecookbook',
242 :query => {"name" => "name", "visibility" => "2"}
247 :query => {"name" => "name", "visibility" => "2"}
243 assert_response 302
248 assert_response 302
244 end
249 end
245 assert_not_nil query.project
250 assert_not_nil query.project
246 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
251 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
247 end
252 end
248
253
249 def test_create_global_public_query_should_force_private_with_manage_public_queries_permission
254 def test_create_global_public_query_should_force_private_with_manage_public_queries_permission
250 @request.session[:user_id] = 2
255 @request.session[:user_id] = 2
251 query = new_record(Query) do
256 query = new_record(Query) do
252 post :create,
257 post :create,
253 :project_id => 'ecookbook', :query_is_for_all => '1',
258 :project_id => 'ecookbook', :query_is_for_all => '1',
254 :query => {"name" => "name", "visibility" => "2"}
259 :query => {"name" => "name", "visibility" => "2"}
255 assert_response 302
260 assert_response 302
256 end
261 end
257 assert_nil query.project
262 assert_nil query.project
258 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
263 assert_equal Query::VISIBILITY_PRIVATE, query.visibility
259 end
264 end
260
265
261 def test_create_global_public_query_by_admin
266 def test_create_global_public_query_by_admin
262 @request.session[:user_id] = 1
267 @request.session[:user_id] = 1
263 query = new_record(Query) do
268 query = new_record(Query) do
264 post :create,
269 post :create,
265 :project_id => 'ecookbook', :query_is_for_all => '1',
270 :project_id => 'ecookbook', :query_is_for_all => '1',
266 :query => {"name" => "name", "visibility" => "2"}
271 :query => {"name" => "name", "visibility" => "2"}
267 assert_response 302
272 assert_response 302
268 end
273 end
269 assert_nil query.project
274 assert_nil query.project
270 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
275 assert_equal Query::VISIBILITY_PUBLIC, query.visibility
271 end
276 end
272
277
273 def test_create_project_public_time_entry_query
278 def test_create_project_public_time_entry_query
274 @request.session[:user_id] = 2
279 @request.session[:user_id] = 2
275
280
276 q = new_record(TimeEntryQuery) do
281 q = new_record(TimeEntryQuery) do
277 post :create,
282 post :create,
278 :project_id => 'ecookbook',
283 :project_id => 'ecookbook',
279 :type => 'TimeEntryQuery',
284 :type => 'TimeEntryQuery',
280 :default_columns => '1',
285 :default_columns => '1',
281 :f => ["spent_on"],
286 :f => ["spent_on"],
282 :op => {"spent_on" => "="},
287 :op => {"spent_on" => "="},
283 :v => { "spent_on" => ["2016-07-14"]},
288 :v => { "spent_on" => ["2016-07-14"]},
284 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
289 :query => {"name" => "test_new_project_public_query", "visibility" => "2"}
285 end
290 end
286
291
287 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => 'ecookbook', :query_id => q.id
292 assert_redirected_to :controller => 'timelog', :action => 'index', :project_id => 'ecookbook', :query_id => q.id
288 assert q.is_public?
293 assert q.is_public?
289 assert q.has_default_columns?
294 assert q.has_default_columns?
290 assert q.valid?
295 assert q.valid?
291 end
296 end
292
297
293 def test_edit_global_public_query
298 def test_edit_global_public_query
294 @request.session[:user_id] = 1
299 @request.session[:user_id] = 1
295 get :edit, :id => 4
300 get :edit, :id => 4
296 assert_response :success
301 assert_response :success
297
302
298 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
303 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
299 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
304 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
300 end
305 end
301
306
302 def test_edit_global_private_query
307 def test_edit_global_private_query
303 @request.session[:user_id] = 3
308 @request.session[:user_id] = 3
304 get :edit, :id => 3
309 get :edit, :id => 3
305 assert_response :success
310 assert_response :success
306
311
307 assert_select 'input[name=?]', 'query[visibility]', 0
312 assert_select 'input[name=?]', 'query[visibility]', 0
308 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
313 assert_select 'input[name=query_is_for_all][type=checkbox][checked=checked]'
309 end
314 end
310
315
311 def test_edit_project_private_query
316 def test_edit_project_private_query
312 @request.session[:user_id] = 3
317 @request.session[:user_id] = 3
313 get :edit, :id => 2
318 get :edit, :id => 2
314 assert_response :success
319 assert_response :success
315
320
316 assert_select 'input[name=?]', 'query[visibility]', 0
321 assert_select 'input[name=?]', 'query[visibility]', 0
317 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
322 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
318 end
323 end
319
324
320 def test_edit_project_public_query
325 def test_edit_project_public_query
321 @request.session[:user_id] = 2
326 @request.session[:user_id] = 2
322 get :edit, :id => 1
327 get :edit, :id => 1
323 assert_response :success
328 assert_response :success
324
329
325 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
330 assert_select 'input[name=?][value="2"][checked=checked]', 'query[visibility]'
326 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
331 assert_select 'input[name=query_is_for_all][type=checkbox]:not([checked])'
327 end
332 end
328
333
329 def test_edit_sort_criteria
334 def test_edit_sort_criteria
330 @request.session[:user_id] = 1
335 @request.session[:user_id] = 1
331 get :edit, :id => 5
336 get :edit, :id => 5
332 assert_response :success
337 assert_response :success
333
338
334 assert_select 'select[name=?]', 'query[sort_criteria][0][]' do
339 assert_select 'select[name=?]', 'query[sort_criteria][0][]' do
335 assert_select 'option[value=priority][selected=selected]'
340 assert_select 'option[value=priority][selected=selected]'
336 assert_select 'option[value=desc][selected=selected]'
341 assert_select 'option[value=desc][selected=selected]'
337 end
342 end
338 end
343 end
339
344
340 def test_edit_invalid_query
345 def test_edit_invalid_query
341 @request.session[:user_id] = 2
346 @request.session[:user_id] = 2
342 get :edit, :id => 99
347 get :edit, :id => 99
343 assert_response 404
348 assert_response 404
344 end
349 end
345
350
346 def test_udpate_global_private_query
351 def test_udpate_global_private_query
347 @request.session[:user_id] = 3
352 @request.session[:user_id] = 3
348 put :update,
353 put :update,
349 :id => 3,
354 :id => 3,
350 :default_columns => '1',
355 :default_columns => '1',
351 :fields => ["status_id", "assigned_to_id"],
356 :fields => ["status_id", "assigned_to_id"],
352 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
357 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
353 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
358 :values => { "assigned_to_id" => ["me"], "status_id" => ["1"]},
354 :query => {"name" => "test_edit_global_private_query", "visibility" => "2"}
359 :query => {"name" => "test_edit_global_private_query", "visibility" => "2"}
355
360
356 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
361 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 3
357 q = Query.find_by_name('test_edit_global_private_query')
362 q = Query.find_by_name('test_edit_global_private_query')
358 assert !q.is_public?
363 assert !q.is_public?
359 assert q.has_default_columns?
364 assert q.has_default_columns?
360 assert q.valid?
365 assert q.valid?
361 end
366 end
362
367
363 def test_update_global_public_query
368 def test_update_global_public_query
364 @request.session[:user_id] = 1
369 @request.session[:user_id] = 1
365 put :update,
370 put :update,
366 :id => 4,
371 :id => 4,
367 :default_columns => '1',
372 :default_columns => '1',
368 :fields => ["status_id", "assigned_to_id"],
373 :fields => ["status_id", "assigned_to_id"],
369 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
374 :operators => {"assigned_to_id" => "=", "status_id" => "o"},
370 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
375 :values => { "assigned_to_id" => ["1"], "status_id" => ["1"]},
371 :query => {"name" => "test_edit_global_public_query", "visibility" => "2"}
376 :query => {"name" => "test_edit_global_public_query", "visibility" => "2"}
372
377
373 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
378 assert_redirected_to :controller => 'issues', :action => 'index', :query_id => 4
374 q = Query.find_by_name('test_edit_global_public_query')
379 q = Query.find_by_name('test_edit_global_public_query')
375 assert q.is_public?
380 assert q.is_public?
376 assert q.has_default_columns?
381 assert q.has_default_columns?
377 assert q.valid?
382 assert q.valid?
378 end
383 end
379
384
380 def test_update_with_failure
385 def test_update_with_failure
381 @request.session[:user_id] = 1
386 @request.session[:user_id] = 1
382 put :update, :id => 4, :query => {:name => ''}
387 put :update, :id => 4, :query => {:name => ''}
383 assert_response :success
388 assert_response :success
384 assert_select_error /Name cannot be blank/
389 assert_select_error /Name cannot be blank/
385 end
390 end
386
391
387 def test_destroy
392 def test_destroy
388 @request.session[:user_id] = 2
393 @request.session[:user_id] = 2
389 delete :destroy, :id => 1
394 delete :destroy, :id => 1
390 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
395 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil
391 assert_nil Query.find_by_id(1)
396 assert_nil Query.find_by_id(1)
392 end
397 end
393
398
394 def test_backslash_should_be_escaped_in_filters
399 def test_backslash_should_be_escaped_in_filters
395 @request.session[:user_id] = 2
400 @request.session[:user_id] = 2
396 get :new, :subject => 'foo/bar'
401 get :new, :subject => 'foo/bar'
397 assert_response :success
402 assert_response :success
398 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
403 assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body
399 end
404 end
405
406 def test_filter_with_project_id_should_return_filter_values
407 @request.session[:user_id] = 2
408 get :filter, :project_id => 1, :name => 'fixed_version_id'
409
410 assert_response :success
411 assert_equal 'application/json', response.content_type
412 json = ActiveSupport::JSON.decode(response.body)
413 assert_include ["eCookbook - 2.0", "3", "open"], json
414 end
415
416 def test_filter_without_project_id_should_return_filter_values
417 @request.session[:user_id] = 2
418 get :filter, :name => 'fixed_version_id'
419
420 assert_response :success
421 assert_equal 'application/json', response.content_type
422 json = ActiveSupport::JSON.decode(response.body)
423 assert_include ["OnlineStore - Systemwide visible version", "7", "open"], json
424 end
400 end
425 end
@@ -1,34 +1,35
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 RoutingQueriesTest < Redmine::RoutingTest
20 class RoutingQueriesTest < Redmine::RoutingTest
21 def test_queries
21 def test_queries
22 should_route 'GET /queries/new' => 'queries#new'
22 should_route 'GET /queries/new' => 'queries#new'
23 should_route 'POST /queries' => 'queries#create'
23 should_route 'POST /queries' => 'queries#create'
24 should_route 'GET /queries/filter' => 'queries#filter'
24
25
25 should_route 'GET /queries/1/edit' => 'queries#edit', :id => '1'
26 should_route 'GET /queries/1/edit' => 'queries#edit', :id => '1'
26 should_route 'PUT /queries/1' => 'queries#update', :id => '1'
27 should_route 'PUT /queries/1' => 'queries#update', :id => '1'
27 should_route 'DELETE /queries/1' => 'queries#destroy', :id => '1'
28 should_route 'DELETE /queries/1' => 'queries#destroy', :id => '1'
28 end
29 end
29
30
30 def test_queries_scoped_under_project
31 def test_queries_scoped_under_project
31 should_route 'GET /projects/foo/queries/new' => 'queries#new', :project_id => 'foo'
32 should_route 'GET /projects/foo/queries/new' => 'queries#new', :project_id => 'foo'
32 should_route 'POST /projects/foo/queries' => 'queries#create', :project_id => 'foo'
33 should_route 'POST /projects/foo/queries' => 'queries#create', :project_id => 'foo'
33 end
34 end
34 end
35 end
@@ -1,1813 +1,1835
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 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 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class QueryTest < ActiveSupport::TestCase
22 class QueryTest < ActiveSupport::TestCase
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 fixtures :projects, :enabled_modules, :users, :members,
25 fixtures :projects, :enabled_modules, :users, :members,
26 :member_roles, :roles, :trackers, :issue_statuses,
26 :member_roles, :roles, :trackers, :issue_statuses,
27 :issue_categories, :enumerations, :issues,
27 :issue_categories, :enumerations, :issues,
28 :watchers, :custom_fields, :custom_values, :versions,
28 :watchers, :custom_fields, :custom_values, :versions,
29 :queries,
29 :queries,
30 :projects_trackers,
30 :projects_trackers,
31 :custom_fields_trackers,
31 :custom_fields_trackers,
32 :workflows
32 :workflows
33
33
34 def setup
34 def setup
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_query_with_roles_visibility_should_validate_roles
38 def test_query_with_roles_visibility_should_validate_roles
39 set_language_if_valid 'en'
39 set_language_if_valid 'en'
40 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
40 query = IssueQuery.new(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES)
41 assert !query.save
41 assert !query.save
42 assert_include "Roles cannot be blank", query.errors.full_messages
42 assert_include "Roles cannot be blank", query.errors.full_messages
43 query.role_ids = [1, 2]
43 query.role_ids = [1, 2]
44 assert query.save
44 assert query.save
45 end
45 end
46
46
47 def test_changing_roles_visibility_should_clear_roles
47 def test_changing_roles_visibility_should_clear_roles
48 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
48 query = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1, 2])
49 assert_equal 2, query.roles.count
49 assert_equal 2, query.roles.count
50
50
51 query.visibility = IssueQuery::VISIBILITY_PUBLIC
51 query.visibility = IssueQuery::VISIBILITY_PUBLIC
52 query.save!
52 query.save!
53 assert_equal 0, query.roles.count
53 assert_equal 0, query.roles.count
54 end
54 end
55
55
56 def test_available_filters_should_be_ordered
56 def test_available_filters_should_be_ordered
57 set_language_if_valid 'en'
57 set_language_if_valid 'en'
58 query = IssueQuery.new
58 query = IssueQuery.new
59 assert_equal 0, query.available_filters.keys.index('status_id')
59 assert_equal 0, query.available_filters.keys.index('status_id')
60 expected_order = [
60 expected_order = [
61 "Status",
61 "Status",
62 "Project",
62 "Project",
63 "Tracker",
63 "Tracker",
64 "Priority"
64 "Priority"
65 ]
65 ]
66 assert_equal expected_order,
66 assert_equal expected_order,
67 (query.available_filters.values.map{|v| v[:name]} & expected_order)
67 (query.available_filters.values.map{|v| v[:name]} & expected_order)
68 end
68 end
69
69
70 def test_available_filters_with_custom_fields_should_be_ordered
70 def test_available_filters_with_custom_fields_should_be_ordered
71 set_language_if_valid 'en'
71 set_language_if_valid 'en'
72 UserCustomField.create!(
72 UserCustomField.create!(
73 :name => 'order test', :field_format => 'string',
73 :name => 'order test', :field_format => 'string',
74 :is_for_all => true, :is_filter => true
74 :is_for_all => true, :is_filter => true
75 )
75 )
76 query = IssueQuery.new
76 query = IssueQuery.new
77 expected_order = [
77 expected_order = [
78 "Searchable field",
78 "Searchable field",
79 "Database",
79 "Database",
80 "Project's Development status",
80 "Project's Development status",
81 "Author's order test",
81 "Author's order test",
82 "Assignee's order test"
82 "Assignee's order test"
83 ]
83 ]
84 assert_equal expected_order,
84 assert_equal expected_order,
85 (query.available_filters.values.map{|v| v[:name]} & expected_order)
85 (query.available_filters.values.map{|v| v[:name]} & expected_order)
86 end
86 end
87
87
88 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
88 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
89 query = IssueQuery.new(:project => nil, :name => '_')
89 query = IssueQuery.new(:project => nil, :name => '_')
90 assert query.available_filters.has_key?('cf_1')
90 assert query.available_filters.has_key?('cf_1')
91 assert !query.available_filters.has_key?('cf_3')
91 assert !query.available_filters.has_key?('cf_3')
92 end
92 end
93
93
94 def test_system_shared_versions_should_be_available_in_global_queries
94 def test_system_shared_versions_should_be_available_in_global_queries
95 Version.find(2).update_attribute :sharing, 'system'
95 Version.find(2).update_attribute :sharing, 'system'
96 query = IssueQuery.new(:project => nil, :name => '_')
96 query = IssueQuery.new(:project => nil, :name => '_')
97 assert query.available_filters.has_key?('fixed_version_id')
97 assert query.available_filters.has_key?('fixed_version_id')
98 assert query.available_filters['fixed_version_id'][:values].detect {|v| v[1] == '2'}
98 assert query.available_filters['fixed_version_id'][:values].detect {|v| v[1] == '2'}
99 end
99 end
100
100
101 def test_project_filter_in_global_queries
101 def test_project_filter_in_global_queries
102 query = IssueQuery.new(:project => nil, :name => '_')
102 query = IssueQuery.new(:project => nil, :name => '_')
103 project_filter = query.available_filters["project_id"]
103 project_filter = query.available_filters["project_id"]
104 assert_not_nil project_filter
104 assert_not_nil project_filter
105 project_ids = project_filter[:values].map{|p| p[1]}
105 project_ids = project_filter[:values].map{|p| p[1]}
106 assert project_ids.include?("1") #public project
106 assert project_ids.include?("1") #public project
107 assert !project_ids.include?("2") #private project user cannot see
107 assert !project_ids.include?("2") #private project user cannot see
108 end
108 end
109
109
110 def test_available_filters_should_not_include_fields_disabled_on_all_trackers
110 def test_available_filters_should_not_include_fields_disabled_on_all_trackers
111 Tracker.all.each do |tracker|
111 Tracker.all.each do |tracker|
112 tracker.core_fields = Tracker::CORE_FIELDS - ['start_date']
112 tracker.core_fields = Tracker::CORE_FIELDS - ['start_date']
113 tracker.save!
113 tracker.save!
114 end
114 end
115
115
116 query = IssueQuery.new(:name => '_')
116 query = IssueQuery.new(:name => '_')
117 assert_include 'due_date', query.available_filters
117 assert_include 'due_date', query.available_filters
118 assert_not_include 'start_date', query.available_filters
118 assert_not_include 'start_date', query.available_filters
119 end
119 end
120
120
121 def test_filter_values_without_project_should_be_arrays
122 q = IssueQuery.new
123 assert_nil q.project
124
125 q.available_filters.each do |name, filter|
126 values = filter.values
127 assert (values.nil? || values.is_a?(Array)),
128 "#values for #{name} filter returned a #{values.class.name}"
129 end
130 end
131
132 def test_filter_values_with_project_should_be_arrays
133 q = IssueQuery.new(:project => Project.find(1))
134 assert_not_nil q.project
135
136 q.available_filters.each do |name, filter|
137 values = filter.values
138 assert (values.nil? || values.is_a?(Array)),
139 "#values for #{name} filter returned a #{values.class.name}"
140 end
141 end
142
121 def find_issues_with_query(query)
143 def find_issues_with_query(query)
122 Issue.joins(:status, :tracker, :project, :priority).where(
144 Issue.joins(:status, :tracker, :project, :priority).where(
123 query.statement
145 query.statement
124 ).to_a
146 ).to_a
125 end
147 end
126
148
127 def assert_find_issues_with_query_is_successful(query)
149 def assert_find_issues_with_query_is_successful(query)
128 assert_nothing_raised do
150 assert_nothing_raised do
129 find_issues_with_query(query)
151 find_issues_with_query(query)
130 end
152 end
131 end
153 end
132
154
133 def assert_query_statement_includes(query, condition)
155 def assert_query_statement_includes(query, condition)
134 assert_include condition, query.statement
156 assert_include condition, query.statement
135 end
157 end
136
158
137 def assert_query_result(expected, query)
159 def assert_query_result(expected, query)
138 assert_nothing_raised do
160 assert_nothing_raised do
139 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
161 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
140 assert_equal expected.size, query.issue_count
162 assert_equal expected.size, query.issue_count
141 end
163 end
142 end
164 end
143
165
144 def test_query_should_allow_shared_versions_for_a_project_query
166 def test_query_should_allow_shared_versions_for_a_project_query
145 subproject_version = Version.find(4)
167 subproject_version = Version.find(4)
146 query = IssueQuery.new(:project => Project.find(1), :name => '_')
168 query = IssueQuery.new(:project => Project.find(1), :name => '_')
147 filter = query.available_filters["fixed_version_id"]
169 filter = query.available_filters["fixed_version_id"]
148 assert_not_nil filter
170 assert_not_nil filter
149 assert_include subproject_version.id.to_s, filter[:values].map(&:second)
171 assert_include subproject_version.id.to_s, filter[:values].map(&:second)
150 end
172 end
151
173
152 def test_query_with_multiple_custom_fields
174 def test_query_with_multiple_custom_fields
153 query = IssueQuery.find(1)
175 query = IssueQuery.find(1)
154 assert query.valid?
176 assert query.valid?
155 issues = find_issues_with_query(query)
177 issues = find_issues_with_query(query)
156 assert_equal 1, issues.length
178 assert_equal 1, issues.length
157 assert_equal Issue.find(3), issues.first
179 assert_equal Issue.find(3), issues.first
158 end
180 end
159
181
160 def test_operator_none
182 def test_operator_none
161 query = IssueQuery.new(:project => Project.find(1), :name => '_')
183 query = IssueQuery.new(:project => Project.find(1), :name => '_')
162 query.add_filter('fixed_version_id', '!*', [''])
184 query.add_filter('fixed_version_id', '!*', [''])
163 query.add_filter('cf_1', '!*', [''])
185 query.add_filter('cf_1', '!*', [''])
164 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
186 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
165 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
187 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
166 find_issues_with_query(query)
188 find_issues_with_query(query)
167 end
189 end
168
190
169 def test_operator_none_for_integer
191 def test_operator_none_for_integer
170 query = IssueQuery.new(:project => Project.find(1), :name => '_')
192 query = IssueQuery.new(:project => Project.find(1), :name => '_')
171 query.add_filter('estimated_hours', '!*', [''])
193 query.add_filter('estimated_hours', '!*', [''])
172 issues = find_issues_with_query(query)
194 issues = find_issues_with_query(query)
173 assert !issues.empty?
195 assert !issues.empty?
174 assert issues.all? {|i| !i.estimated_hours}
196 assert issues.all? {|i| !i.estimated_hours}
175 end
197 end
176
198
177 def test_operator_none_for_date
199 def test_operator_none_for_date
178 query = IssueQuery.new(:project => Project.find(1), :name => '_')
200 query = IssueQuery.new(:project => Project.find(1), :name => '_')
179 query.add_filter('start_date', '!*', [''])
201 query.add_filter('start_date', '!*', [''])
180 issues = find_issues_with_query(query)
202 issues = find_issues_with_query(query)
181 assert !issues.empty?
203 assert !issues.empty?
182 assert issues.all? {|i| i.start_date.nil?}
204 assert issues.all? {|i| i.start_date.nil?}
183 end
205 end
184
206
185 def test_operator_none_for_string_custom_field
207 def test_operator_none_for_string_custom_field
186 CustomField.find(2).update_attribute :default_value, ""
208 CustomField.find(2).update_attribute :default_value, ""
187 query = IssueQuery.new(:project => Project.find(1), :name => '_')
209 query = IssueQuery.new(:project => Project.find(1), :name => '_')
188 query.add_filter('cf_2', '!*', [''])
210 query.add_filter('cf_2', '!*', [''])
189 assert query.has_filter?('cf_2')
211 assert query.has_filter?('cf_2')
190 issues = find_issues_with_query(query)
212 issues = find_issues_with_query(query)
191 assert !issues.empty?
213 assert !issues.empty?
192 assert issues.all? {|i| i.custom_field_value(2).blank?}
214 assert issues.all? {|i| i.custom_field_value(2).blank?}
193 end
215 end
194
216
195 def test_operator_all
217 def test_operator_all
196 query = IssueQuery.new(:project => Project.find(1), :name => '_')
218 query = IssueQuery.new(:project => Project.find(1), :name => '_')
197 query.add_filter('fixed_version_id', '*', [''])
219 query.add_filter('fixed_version_id', '*', [''])
198 query.add_filter('cf_1', '*', [''])
220 query.add_filter('cf_1', '*', [''])
199 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
221 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
200 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
222 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
201 find_issues_with_query(query)
223 find_issues_with_query(query)
202 end
224 end
203
225
204 def test_operator_all_for_date
226 def test_operator_all_for_date
205 query = IssueQuery.new(:project => Project.find(1), :name => '_')
227 query = IssueQuery.new(:project => Project.find(1), :name => '_')
206 query.add_filter('start_date', '*', [''])
228 query.add_filter('start_date', '*', [''])
207 issues = find_issues_with_query(query)
229 issues = find_issues_with_query(query)
208 assert !issues.empty?
230 assert !issues.empty?
209 assert issues.all? {|i| i.start_date.present?}
231 assert issues.all? {|i| i.start_date.present?}
210 end
232 end
211
233
212 def test_operator_all_for_string_custom_field
234 def test_operator_all_for_string_custom_field
213 query = IssueQuery.new(:project => Project.find(1), :name => '_')
235 query = IssueQuery.new(:project => Project.find(1), :name => '_')
214 query.add_filter('cf_2', '*', [''])
236 query.add_filter('cf_2', '*', [''])
215 assert query.has_filter?('cf_2')
237 assert query.has_filter?('cf_2')
216 issues = find_issues_with_query(query)
238 issues = find_issues_with_query(query)
217 assert !issues.empty?
239 assert !issues.empty?
218 assert issues.all? {|i| i.custom_field_value(2).present?}
240 assert issues.all? {|i| i.custom_field_value(2).present?}
219 end
241 end
220
242
221 def test_numeric_filter_should_not_accept_non_numeric_values
243 def test_numeric_filter_should_not_accept_non_numeric_values
222 query = IssueQuery.new(:name => '_')
244 query = IssueQuery.new(:name => '_')
223 query.add_filter('estimated_hours', '=', ['a'])
245 query.add_filter('estimated_hours', '=', ['a'])
224
246
225 assert query.has_filter?('estimated_hours')
247 assert query.has_filter?('estimated_hours')
226 assert !query.valid?
248 assert !query.valid?
227 end
249 end
228
250
229 def test_operator_is_on_float
251 def test_operator_is_on_float
230 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
252 Issue.where(:id => 2).update_all("estimated_hours = 171.2")
231 query = IssueQuery.new(:name => '_')
253 query = IssueQuery.new(:name => '_')
232 query.add_filter('estimated_hours', '=', ['171.20'])
254 query.add_filter('estimated_hours', '=', ['171.20'])
233 issues = find_issues_with_query(query)
255 issues = find_issues_with_query(query)
234 assert_equal 1, issues.size
256 assert_equal 1, issues.size
235 assert_equal 2, issues.first.id
257 assert_equal 2, issues.first.id
236 end
258 end
237
259
238 def test_operator_is_on_issue_id_should_accept_comma_separated_values
260 def test_operator_is_on_issue_id_should_accept_comma_separated_values
239 query = IssueQuery.new(:name => '_')
261 query = IssueQuery.new(:name => '_')
240 query.add_filter("issue_id", '=', ['1,3'])
262 query.add_filter("issue_id", '=', ['1,3'])
241 issues = find_issues_with_query(query)
263 issues = find_issues_with_query(query)
242 assert_equal 2, issues.size
264 assert_equal 2, issues.size
243 assert_equal [1,3], issues.map(&:id).sort
265 assert_equal [1,3], issues.map(&:id).sort
244 end
266 end
245
267
246 def test_operator_between_on_issue_id_should_return_range
268 def test_operator_between_on_issue_id_should_return_range
247 query = IssueQuery.new(:name => '_')
269 query = IssueQuery.new(:name => '_')
248 query.add_filter("issue_id", '><', ['2','3'])
270 query.add_filter("issue_id", '><', ['2','3'])
249 issues = find_issues_with_query(query)
271 issues = find_issues_with_query(query)
250 assert_equal 2, issues.size
272 assert_equal 2, issues.size
251 assert_equal [2,3], issues.map(&:id).sort
273 assert_equal [2,3], issues.map(&:id).sort
252 end
274 end
253
275
254 def test_operator_is_on_integer_custom_field
276 def test_operator_is_on_integer_custom_field
255 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
277 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
256 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
257 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
258 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
280 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
259
281
260 query = IssueQuery.new(:name => '_')
282 query = IssueQuery.new(:name => '_')
261 query.add_filter("cf_#{f.id}", '=', ['12'])
283 query.add_filter("cf_#{f.id}", '=', ['12'])
262 issues = find_issues_with_query(query)
284 issues = find_issues_with_query(query)
263 assert_equal 1, issues.size
285 assert_equal 1, issues.size
264 assert_equal 2, issues.first.id
286 assert_equal 2, issues.first.id
265 end
287 end
266
288
267 def test_operator_is_on_integer_custom_field_should_accept_negative_value
289 def test_operator_is_on_integer_custom_field_should_accept_negative_value
268 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
290 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true, :trackers => Tracker.all)
269 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
291 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
270 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
292 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
271 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
293 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
272
294
273 query = IssueQuery.new(:name => '_')
295 query = IssueQuery.new(:name => '_')
274 query.add_filter("cf_#{f.id}", '=', ['-12'])
296 query.add_filter("cf_#{f.id}", '=', ['-12'])
275 assert query.valid?
297 assert query.valid?
276 issues = find_issues_with_query(query)
298 issues = find_issues_with_query(query)
277 assert_equal 1, issues.size
299 assert_equal 1, issues.size
278 assert_equal 2, issues.first.id
300 assert_equal 2, issues.first.id
279 end
301 end
280
302
281 def test_operator_is_on_float_custom_field
303 def test_operator_is_on_float_custom_field
282 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
304 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
283 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
305 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
284 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
306 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
285 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
307 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
286
308
287 query = IssueQuery.new(:name => '_')
309 query = IssueQuery.new(:name => '_')
288 query.add_filter("cf_#{f.id}", '=', ['12.7'])
310 query.add_filter("cf_#{f.id}", '=', ['12.7'])
289 issues = find_issues_with_query(query)
311 issues = find_issues_with_query(query)
290 assert_equal 1, issues.size
312 assert_equal 1, issues.size
291 assert_equal 2, issues.first.id
313 assert_equal 2, issues.first.id
292 end
314 end
293
315
294 def test_operator_is_on_float_custom_field_should_accept_negative_value
316 def test_operator_is_on_float_custom_field_should_accept_negative_value
295 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
317 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
296 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
318 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
297 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
319 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
298 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
320 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
299
321
300 query = IssueQuery.new(:name => '_')
322 query = IssueQuery.new(:name => '_')
301 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
323 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
302 assert query.valid?
324 assert query.valid?
303 issues = find_issues_with_query(query)
325 issues = find_issues_with_query(query)
304 assert_equal 1, issues.size
326 assert_equal 1, issues.size
305 assert_equal 2, issues.first.id
327 assert_equal 2, issues.first.id
306 end
328 end
307
329
308 def test_operator_is_on_multi_list_custom_field
330 def test_operator_is_on_multi_list_custom_field
309 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
331 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
310 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
332 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
333 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
312 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
334 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
313 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
335 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
314
336
315 query = IssueQuery.new(:name => '_')
337 query = IssueQuery.new(:name => '_')
316 query.add_filter("cf_#{f.id}", '=', ['value1'])
338 query.add_filter("cf_#{f.id}", '=', ['value1'])
317 issues = find_issues_with_query(query)
339 issues = find_issues_with_query(query)
318 assert_equal [1, 3], issues.map(&:id).sort
340 assert_equal [1, 3], issues.map(&:id).sort
319
341
320 query = IssueQuery.new(:name => '_')
342 query = IssueQuery.new(:name => '_')
321 query.add_filter("cf_#{f.id}", '=', ['value2'])
343 query.add_filter("cf_#{f.id}", '=', ['value2'])
322 issues = find_issues_with_query(query)
344 issues = find_issues_with_query(query)
323 assert_equal [1], issues.map(&:id).sort
345 assert_equal [1], issues.map(&:id).sort
324 end
346 end
325
347
326 def test_operator_is_not_on_multi_list_custom_field
348 def test_operator_is_not_on_multi_list_custom_field
327 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
349 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
328 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
350 :possible_values => ['value1', 'value2', 'value3'], :multiple => true, :trackers => Tracker.all)
329 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
351 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
330 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
352 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
331 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
353 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
332
354
333 query = IssueQuery.new(:name => '_')
355 query = IssueQuery.new(:name => '_')
334 query.add_filter("cf_#{f.id}", '!', ['value1'])
356 query.add_filter("cf_#{f.id}", '!', ['value1'])
335 issues = find_issues_with_query(query)
357 issues = find_issues_with_query(query)
336 assert !issues.map(&:id).include?(1)
358 assert !issues.map(&:id).include?(1)
337 assert !issues.map(&:id).include?(3)
359 assert !issues.map(&:id).include?(3)
338
360
339 query = IssueQuery.new(:name => '_')
361 query = IssueQuery.new(:name => '_')
340 query.add_filter("cf_#{f.id}", '!', ['value2'])
362 query.add_filter("cf_#{f.id}", '!', ['value2'])
341 issues = find_issues_with_query(query)
363 issues = find_issues_with_query(query)
342 assert !issues.map(&:id).include?(1)
364 assert !issues.map(&:id).include?(1)
343 assert issues.map(&:id).include?(3)
365 assert issues.map(&:id).include?(3)
344 end
366 end
345
367
346 def test_operator_is_on_string_custom_field_with_utf8_value
368 def test_operator_is_on_string_custom_field_with_utf8_value
347 f = IssueCustomField.create!(:name => 'filter', :field_format => 'string', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
369 f = IssueCustomField.create!(:name => 'filter', :field_format => 'string', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
348 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'KiÑ»ƒm')
370 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'KiÑ»ƒm')
349
371
350 query = IssueQuery.new(:name => '_')
372 query = IssueQuery.new(:name => '_')
351 query.add_filter("cf_#{f.id}", '=', ['KiÑ»ƒm'])
373 query.add_filter("cf_#{f.id}", '=', ['KiÑ»ƒm'])
352 issues = find_issues_with_query(query)
374 issues = find_issues_with_query(query)
353 assert_equal [1], issues.map(&:id).sort
375 assert_equal [1], issues.map(&:id).sort
354 end
376 end
355
377
356 def test_operator_is_on_is_private_field
378 def test_operator_is_on_is_private_field
357 # is_private filter only available for those who can set issues private
379 # is_private filter only available for those who can set issues private
358 User.current = User.find(2)
380 User.current = User.find(2)
359
381
360 query = IssueQuery.new(:name => '_')
382 query = IssueQuery.new(:name => '_')
361 assert query.available_filters.key?('is_private')
383 assert query.available_filters.key?('is_private')
362
384
363 query.add_filter("is_private", '=', ['1'])
385 query.add_filter("is_private", '=', ['1'])
364 issues = find_issues_with_query(query)
386 issues = find_issues_with_query(query)
365 assert issues.any?
387 assert issues.any?
366 assert_nil issues.detect {|issue| !issue.is_private?}
388 assert_nil issues.detect {|issue| !issue.is_private?}
367 ensure
389 ensure
368 User.current = nil
390 User.current = nil
369 end
391 end
370
392
371 def test_operator_is_not_on_is_private_field
393 def test_operator_is_not_on_is_private_field
372 # is_private filter only available for those who can set issues private
394 # is_private filter only available for those who can set issues private
373 User.current = User.find(2)
395 User.current = User.find(2)
374
396
375 query = IssueQuery.new(:name => '_')
397 query = IssueQuery.new(:name => '_')
376 assert query.available_filters.key?('is_private')
398 assert query.available_filters.key?('is_private')
377
399
378 query.add_filter("is_private", '!', ['1'])
400 query.add_filter("is_private", '!', ['1'])
379 issues = find_issues_with_query(query)
401 issues = find_issues_with_query(query)
380 assert issues.any?
402 assert issues.any?
381 assert_nil issues.detect {|issue| issue.is_private?}
403 assert_nil issues.detect {|issue| issue.is_private?}
382 ensure
404 ensure
383 User.current = nil
405 User.current = nil
384 end
406 end
385
407
386 def test_operator_greater_than
408 def test_operator_greater_than
387 query = IssueQuery.new(:project => Project.find(1), :name => '_')
409 query = IssueQuery.new(:project => Project.find(1), :name => '_')
388 query.add_filter('done_ratio', '>=', ['40'])
410 query.add_filter('done_ratio', '>=', ['40'])
389 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
411 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
390 find_issues_with_query(query)
412 find_issues_with_query(query)
391 end
413 end
392
414
393 def test_operator_greater_than_a_float
415 def test_operator_greater_than_a_float
394 query = IssueQuery.new(:project => Project.find(1), :name => '_')
416 query = IssueQuery.new(:project => Project.find(1), :name => '_')
395 query.add_filter('estimated_hours', '>=', ['40.5'])
417 query.add_filter('estimated_hours', '>=', ['40.5'])
396 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
418 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
397 find_issues_with_query(query)
419 find_issues_with_query(query)
398 end
420 end
399
421
400 def test_operator_greater_than_on_int_custom_field
422 def test_operator_greater_than_on_int_custom_field
401 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
423 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
402 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
424 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
403 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
425 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
404 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
426 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
405
427
406 query = IssueQuery.new(:project => Project.find(1), :name => '_')
428 query = IssueQuery.new(:project => Project.find(1), :name => '_')
407 query.add_filter("cf_#{f.id}", '>=', ['8'])
429 query.add_filter("cf_#{f.id}", '>=', ['8'])
408 issues = find_issues_with_query(query)
430 issues = find_issues_with_query(query)
409 assert_equal 1, issues.size
431 assert_equal 1, issues.size
410 assert_equal 2, issues.first.id
432 assert_equal 2, issues.first.id
411 end
433 end
412
434
413 def test_operator_lesser_than
435 def test_operator_lesser_than
414 query = IssueQuery.new(:project => Project.find(1), :name => '_')
436 query = IssueQuery.new(:project => Project.find(1), :name => '_')
415 query.add_filter('done_ratio', '<=', ['30'])
437 query.add_filter('done_ratio', '<=', ['30'])
416 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
438 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
417 find_issues_with_query(query)
439 find_issues_with_query(query)
418 end
440 end
419
441
420 def test_operator_lesser_than_on_custom_field
442 def test_operator_lesser_than_on_custom_field
421 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
443 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
422 query = IssueQuery.new(:project => Project.find(1), :name => '_')
444 query = IssueQuery.new(:project => Project.find(1), :name => '_')
423 query.add_filter("cf_#{f.id}", '<=', ['30'])
445 query.add_filter("cf_#{f.id}", '<=', ['30'])
424 assert_match /CAST.+ <= 30\.0/, query.statement
446 assert_match /CAST.+ <= 30\.0/, query.statement
425 find_issues_with_query(query)
447 find_issues_with_query(query)
426 end
448 end
427
449
428 def test_operator_lesser_than_on_date_custom_field
450 def test_operator_lesser_than_on_date_custom_field
429 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
451 f = IssueCustomField.create!(:name => 'filter', :field_format => 'date', :is_filter => true, :is_for_all => true, :trackers => Tracker.all)
430 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
452 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '2013-04-11')
431 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
453 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '2013-05-14')
432 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
454 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
433
455
434 query = IssueQuery.new(:project => Project.find(1), :name => '_')
456 query = IssueQuery.new(:project => Project.find(1), :name => '_')
435 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
457 query.add_filter("cf_#{f.id}", '<=', ['2013-05-01'])
436 issue_ids = find_issues_with_query(query).map(&:id)
458 issue_ids = find_issues_with_query(query).map(&:id)
437 assert_include 1, issue_ids
459 assert_include 1, issue_ids
438 assert_not_include 2, issue_ids
460 assert_not_include 2, issue_ids
439 assert_not_include 3, issue_ids
461 assert_not_include 3, issue_ids
440 end
462 end
441
463
442 def test_operator_between
464 def test_operator_between
443 query = IssueQuery.new(:project => Project.find(1), :name => '_')
465 query = IssueQuery.new(:project => Project.find(1), :name => '_')
444 query.add_filter('done_ratio', '><', ['30', '40'])
466 query.add_filter('done_ratio', '><', ['30', '40'])
445 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
467 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
446 find_issues_with_query(query)
468 find_issues_with_query(query)
447 end
469 end
448
470
449 def test_operator_between_on_custom_field
471 def test_operator_between_on_custom_field
450 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
472 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
451 query = IssueQuery.new(:project => Project.find(1), :name => '_')
473 query = IssueQuery.new(:project => Project.find(1), :name => '_')
452 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
474 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
453 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
475 assert_match /CAST.+ BETWEEN 30.0 AND 40.0/, query.statement
454 find_issues_with_query(query)
476 find_issues_with_query(query)
455 end
477 end
456
478
457 def test_date_filter_should_not_accept_non_date_values
479 def test_date_filter_should_not_accept_non_date_values
458 query = IssueQuery.new(:name => '_')
480 query = IssueQuery.new(:name => '_')
459 query.add_filter('created_on', '=', ['a'])
481 query.add_filter('created_on', '=', ['a'])
460
482
461 assert query.has_filter?('created_on')
483 assert query.has_filter?('created_on')
462 assert !query.valid?
484 assert !query.valid?
463 end
485 end
464
486
465 def test_date_filter_should_not_accept_invalid_date_values
487 def test_date_filter_should_not_accept_invalid_date_values
466 query = IssueQuery.new(:name => '_')
488 query = IssueQuery.new(:name => '_')
467 query.add_filter('created_on', '=', ['2011-01-34'])
489 query.add_filter('created_on', '=', ['2011-01-34'])
468
490
469 assert query.has_filter?('created_on')
491 assert query.has_filter?('created_on')
470 assert !query.valid?
492 assert !query.valid?
471 end
493 end
472
494
473 def test_relative_date_filter_should_not_accept_non_integer_values
495 def test_relative_date_filter_should_not_accept_non_integer_values
474 query = IssueQuery.new(:name => '_')
496 query = IssueQuery.new(:name => '_')
475 query.add_filter('created_on', '>t-', ['a'])
497 query.add_filter('created_on', '>t-', ['a'])
476
498
477 assert query.has_filter?('created_on')
499 assert query.has_filter?('created_on')
478 assert !query.valid?
500 assert !query.valid?
479 end
501 end
480
502
481 def test_operator_date_equals
503 def test_operator_date_equals
482 query = IssueQuery.new(:name => '_')
504 query = IssueQuery.new(:name => '_')
483 query.add_filter('due_date', '=', ['2011-07-10'])
505 query.add_filter('due_date', '=', ['2011-07-10'])
484 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/,
506 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/,
485 query.statement
507 query.statement
486 find_issues_with_query(query)
508 find_issues_with_query(query)
487 end
509 end
488
510
489 def test_operator_date_lesser_than
511 def test_operator_date_lesser_than
490 query = IssueQuery.new(:name => '_')
512 query = IssueQuery.new(:name => '_')
491 query.add_filter('due_date', '<=', ['2011-07-10'])
513 query.add_filter('due_date', '<=', ['2011-07-10'])
492 assert_match /issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/, query.statement
514 assert_match /issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?/, query.statement
493 find_issues_with_query(query)
515 find_issues_with_query(query)
494 end
516 end
495
517
496 def test_operator_date_lesser_than_with_timestamp
518 def test_operator_date_lesser_than_with_timestamp
497 query = IssueQuery.new(:name => '_')
519 query = IssueQuery.new(:name => '_')
498 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
520 query.add_filter('updated_on', '<=', ['2011-07-10T19:13:52'])
499 assert_match /issues\.updated_on <= '#{quoted_date "2011-07-10"} 19:13:52/, query.statement
521 assert_match /issues\.updated_on <= '#{quoted_date "2011-07-10"} 19:13:52/, query.statement
500 find_issues_with_query(query)
522 find_issues_with_query(query)
501 end
523 end
502
524
503 def test_operator_date_greater_than
525 def test_operator_date_greater_than
504 query = IssueQuery.new(:name => '_')
526 query = IssueQuery.new(:name => '_')
505 query.add_filter('due_date', '>=', ['2011-07-10'])
527 query.add_filter('due_date', '>=', ['2011-07-10'])
506 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?'/, query.statement
528 assert_match /issues\.due_date > '#{quoted_date "2011-07-09"} 23:59:59(\.\d+)?'/, query.statement
507 find_issues_with_query(query)
529 find_issues_with_query(query)
508 end
530 end
509
531
510 def test_operator_date_greater_than_with_timestamp
532 def test_operator_date_greater_than_with_timestamp
511 query = IssueQuery.new(:name => '_')
533 query = IssueQuery.new(:name => '_')
512 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
534 query.add_filter('updated_on', '>=', ['2011-07-10T19:13:52'])
513 assert_match /issues\.updated_on > '#{quoted_date "2011-07-10"} 19:13:51(\.0+)?'/, query.statement
535 assert_match /issues\.updated_on > '#{quoted_date "2011-07-10"} 19:13:51(\.0+)?'/, query.statement
514 find_issues_with_query(query)
536 find_issues_with_query(query)
515 end
537 end
516
538
517 def test_operator_date_between
539 def test_operator_date_between
518 query = IssueQuery.new(:name => '_')
540 query = IssueQuery.new(:name => '_')
519 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
541 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
520 assert_match /issues\.due_date > '#{quoted_date "2011-06-22"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?'/,
542 assert_match /issues\.due_date > '#{quoted_date "2011-06-22"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-07-10"} 23:59:59(\.\d+)?'/,
521 query.statement
543 query.statement
522 find_issues_with_query(query)
544 find_issues_with_query(query)
523 end
545 end
524
546
525 def test_operator_in_more_than
547 def test_operator_in_more_than
526 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
548 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
527 query = IssueQuery.new(:project => Project.find(1), :name => '_')
549 query = IssueQuery.new(:project => Project.find(1), :name => '_')
528 query.add_filter('due_date', '>t+', ['15'])
550 query.add_filter('due_date', '>t+', ['15'])
529 issues = find_issues_with_query(query)
551 issues = find_issues_with_query(query)
530 assert !issues.empty?
552 assert !issues.empty?
531 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
553 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
532 end
554 end
533
555
534 def test_operator_in_less_than
556 def test_operator_in_less_than
535 query = IssueQuery.new(:project => Project.find(1), :name => '_')
557 query = IssueQuery.new(:project => Project.find(1), :name => '_')
536 query.add_filter('due_date', '<t+', ['15'])
558 query.add_filter('due_date', '<t+', ['15'])
537 issues = find_issues_with_query(query)
559 issues = find_issues_with_query(query)
538 assert !issues.empty?
560 assert !issues.empty?
539 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
561 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
540 end
562 end
541
563
542 def test_operator_in_the_next_days
564 def test_operator_in_the_next_days
543 query = IssueQuery.new(:project => Project.find(1), :name => '_')
565 query = IssueQuery.new(:project => Project.find(1), :name => '_')
544 query.add_filter('due_date', '><t+', ['15'])
566 query.add_filter('due_date', '><t+', ['15'])
545 issues = find_issues_with_query(query)
567 issues = find_issues_with_query(query)
546 assert !issues.empty?
568 assert !issues.empty?
547 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
569 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
548 end
570 end
549
571
550 def test_operator_less_than_ago
572 def test_operator_less_than_ago
551 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
573 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
552 query = IssueQuery.new(:project => Project.find(1), :name => '_')
574 query = IssueQuery.new(:project => Project.find(1), :name => '_')
553 query.add_filter('due_date', '>t-', ['3'])
575 query.add_filter('due_date', '>t-', ['3'])
554 issues = find_issues_with_query(query)
576 issues = find_issues_with_query(query)
555 assert !issues.empty?
577 assert !issues.empty?
556 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
578 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
557 end
579 end
558
580
559 def test_operator_in_the_past_days
581 def test_operator_in_the_past_days
560 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
582 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
561 query = IssueQuery.new(:project => Project.find(1), :name => '_')
583 query = IssueQuery.new(:project => Project.find(1), :name => '_')
562 query.add_filter('due_date', '><t-', ['3'])
584 query.add_filter('due_date', '><t-', ['3'])
563 issues = find_issues_with_query(query)
585 issues = find_issues_with_query(query)
564 assert !issues.empty?
586 assert !issues.empty?
565 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
587 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
566 end
588 end
567
589
568 def test_operator_more_than_ago
590 def test_operator_more_than_ago
569 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
591 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
570 query = IssueQuery.new(:project => Project.find(1), :name => '_')
592 query = IssueQuery.new(:project => Project.find(1), :name => '_')
571 query.add_filter('due_date', '<t-', ['10'])
593 query.add_filter('due_date', '<t-', ['10'])
572 assert query.statement.include?("#{Issue.table_name}.due_date <=")
594 assert query.statement.include?("#{Issue.table_name}.due_date <=")
573 issues = find_issues_with_query(query)
595 issues = find_issues_with_query(query)
574 assert !issues.empty?
596 assert !issues.empty?
575 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
597 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
576 end
598 end
577
599
578 def test_operator_in
600 def test_operator_in
579 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
601 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
580 query = IssueQuery.new(:project => Project.find(1), :name => '_')
602 query = IssueQuery.new(:project => Project.find(1), :name => '_')
581 query.add_filter('due_date', 't+', ['2'])
603 query.add_filter('due_date', 't+', ['2'])
582 issues = find_issues_with_query(query)
604 issues = find_issues_with_query(query)
583 assert !issues.empty?
605 assert !issues.empty?
584 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
606 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
585 end
607 end
586
608
587 def test_operator_ago
609 def test_operator_ago
588 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
610 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
589 query = IssueQuery.new(:project => Project.find(1), :name => '_')
611 query = IssueQuery.new(:project => Project.find(1), :name => '_')
590 query.add_filter('due_date', 't-', ['3'])
612 query.add_filter('due_date', 't-', ['3'])
591 issues = find_issues_with_query(query)
613 issues = find_issues_with_query(query)
592 assert !issues.empty?
614 assert !issues.empty?
593 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
615 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
594 end
616 end
595
617
596 def test_operator_today
618 def test_operator_today
597 query = IssueQuery.new(:project => Project.find(1), :name => '_')
619 query = IssueQuery.new(:project => Project.find(1), :name => '_')
598 query.add_filter('due_date', 't', [''])
620 query.add_filter('due_date', 't', [''])
599 issues = find_issues_with_query(query)
621 issues = find_issues_with_query(query)
600 assert !issues.empty?
622 assert !issues.empty?
601 issues.each {|issue| assert_equal Date.today, issue.due_date}
623 issues.each {|issue| assert_equal Date.today, issue.due_date}
602 end
624 end
603
625
604 def test_operator_date_periods
626 def test_operator_date_periods
605 %w(t ld w lw l2w m lm y).each do |operator|
627 %w(t ld w lw l2w m lm y).each do |operator|
606 query = IssueQuery.new(:name => '_')
628 query = IssueQuery.new(:name => '_')
607 query.add_filter('due_date', operator, [''])
629 query.add_filter('due_date', operator, [''])
608 assert query.valid?
630 assert query.valid?
609 assert query.issues
631 assert query.issues
610 end
632 end
611 end
633 end
612
634
613 def test_operator_datetime_periods
635 def test_operator_datetime_periods
614 %w(t ld w lw l2w m lm y).each do |operator|
636 %w(t ld w lw l2w m lm y).each do |operator|
615 query = IssueQuery.new(:name => '_')
637 query = IssueQuery.new(:name => '_')
616 query.add_filter('created_on', operator, [''])
638 query.add_filter('created_on', operator, [''])
617 assert query.valid?
639 assert query.valid?
618 assert query.issues
640 assert query.issues
619 end
641 end
620 end
642 end
621
643
622 def test_operator_contains
644 def test_operator_contains
623 issue = Issue.generate!(:subject => 'AbCdEfG')
645 issue = Issue.generate!(:subject => 'AbCdEfG')
624
646
625 query = IssueQuery.new(:name => '_')
647 query = IssueQuery.new(:name => '_')
626 query.add_filter('subject', '~', ['cdeF'])
648 query.add_filter('subject', '~', ['cdeF'])
627 result = find_issues_with_query(query)
649 result = find_issues_with_query(query)
628 assert_include issue, result
650 assert_include issue, result
629 result.each {|issue| assert issue.subject.downcase.include?('cdef') }
651 result.each {|issue| assert issue.subject.downcase.include?('cdef') }
630 end
652 end
631
653
632 def test_operator_contains_with_utf8_string
654 def test_operator_contains_with_utf8_string
633 issue = Issue.generate!(:subject => 'Subject contains Kiểm')
655 issue = Issue.generate!(:subject => 'Subject contains Kiểm')
634
656
635 query = IssueQuery.new(:name => '_')
657 query = IssueQuery.new(:name => '_')
636 query.add_filter('subject', '~', ['Kiểm'])
658 query.add_filter('subject', '~', ['Kiểm'])
637 result = find_issues_with_query(query)
659 result = find_issues_with_query(query)
638 assert_include issue, result
660 assert_include issue, result
639 assert_equal 1, result.size
661 assert_equal 1, result.size
640 end
662 end
641
663
642 def test_operator_does_not_contain
664 def test_operator_does_not_contain
643 issue = Issue.generate!(:subject => 'AbCdEfG')
665 issue = Issue.generate!(:subject => 'AbCdEfG')
644
666
645 query = IssueQuery.new(:name => '_')
667 query = IssueQuery.new(:name => '_')
646 query.add_filter('subject', '!~', ['cdeF'])
668 query.add_filter('subject', '!~', ['cdeF'])
647 result = find_issues_with_query(query)
669 result = find_issues_with_query(query)
648 assert_not_include issue, result
670 assert_not_include issue, result
649 end
671 end
650
672
651 def test_range_for_this_week_with_week_starting_on_monday
673 def test_range_for_this_week_with_week_starting_on_monday
652 I18n.locale = :fr
674 I18n.locale = :fr
653 assert_equal '1', I18n.t(:general_first_day_of_week)
675 assert_equal '1', I18n.t(:general_first_day_of_week)
654
676
655 Date.stubs(:today).returns(Date.parse('2011-04-29'))
677 Date.stubs(:today).returns(Date.parse('2011-04-29'))
656
678
657 query = IssueQuery.new(:project => Project.find(1), :name => '_')
679 query = IssueQuery.new(:project => Project.find(1), :name => '_')
658 query.add_filter('due_date', 'w', [''])
680 query.add_filter('due_date', 'w', [''])
659 assert_match /issues\.due_date > '#{quoted_date "2011-04-24"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-05-01"} 23:59:59(\.\d+)?/,
681 assert_match /issues\.due_date > '#{quoted_date "2011-04-24"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-05-01"} 23:59:59(\.\d+)?/,
660 query.statement
682 query.statement
661 I18n.locale = :en
683 I18n.locale = :en
662 end
684 end
663
685
664 def test_range_for_this_week_with_week_starting_on_sunday
686 def test_range_for_this_week_with_week_starting_on_sunday
665 I18n.locale = :en
687 I18n.locale = :en
666 assert_equal '7', I18n.t(:general_first_day_of_week)
688 assert_equal '7', I18n.t(:general_first_day_of_week)
667
689
668 Date.stubs(:today).returns(Date.parse('2011-04-29'))
690 Date.stubs(:today).returns(Date.parse('2011-04-29'))
669
691
670 query = IssueQuery.new(:project => Project.find(1), :name => '_')
692 query = IssueQuery.new(:project => Project.find(1), :name => '_')
671 query.add_filter('due_date', 'w', [''])
693 query.add_filter('due_date', 'w', [''])
672 assert_match /issues\.due_date > '#{quoted_date "2011-04-23"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-04-30"} 23:59:59(\.\d+)?/,
694 assert_match /issues\.due_date > '#{quoted_date "2011-04-23"} 23:59:59(\.\d+)?' AND issues\.due_date <= '#{quoted_date "2011-04-30"} 23:59:59(\.\d+)?/,
673 query.statement
695 query.statement
674 end
696 end
675
697
676 def test_filter_assigned_to_me
698 def test_filter_assigned_to_me
677 user = User.find(2)
699 user = User.find(2)
678 group = Group.find(10)
700 group = Group.find(10)
679 group.users << user
701 group.users << user
680 other_group = Group.find(11)
702 other_group = Group.find(11)
681 Member.create!(:project_id => 1, :principal => group, :role_ids => [1])
703 Member.create!(:project_id => 1, :principal => group, :role_ids => [1])
682 Member.create!(:project_id => 1, :principal => other_group, :role_ids => [1])
704 Member.create!(:project_id => 1, :principal => other_group, :role_ids => [1])
683 User.current = user
705 User.current = user
684
706
685 with_settings :issue_group_assignment => '1' do
707 with_settings :issue_group_assignment => '1' do
686 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
708 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
687 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
709 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
688 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => other_group)
710 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => other_group)
689
711
690 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
712 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
691 result = query.issues
713 result = query.issues
692 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
714 assert_equal Issue.visible.where(:assigned_to_id => ([2] + user.reload.group_ids)).sort_by(&:id), result.sort_by(&:id)
693
715
694 assert result.include?(i1)
716 assert result.include?(i1)
695 assert result.include?(i2)
717 assert result.include?(i2)
696 assert !result.include?(i3)
718 assert !result.include?(i3)
697 end
719 end
698 end
720 end
699
721
700 def test_user_custom_field_filtered_on_me
722 def test_user_custom_field_filtered_on_me
701 User.current = User.find(2)
723 User.current = User.find(2)
702 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
724 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
703 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
725 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
704 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
726 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
705
727
706 query = IssueQuery.new(:name => '_', :project => Project.find(1))
728 query = IssueQuery.new(:name => '_', :project => Project.find(1))
707 filter = query.available_filters["cf_#{cf.id}"]
729 filter = query.available_filters["cf_#{cf.id}"]
708 assert_not_nil filter
730 assert_not_nil filter
709 assert_include 'me', filter[:values].map{|v| v[1]}
731 assert_include 'me', filter[:values].map{|v| v[1]}
710
732
711 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
733 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
712 result = query.issues
734 result = query.issues
713 assert_equal 1, result.size
735 assert_equal 1, result.size
714 assert_equal issue1, result.first
736 assert_equal issue1, result.first
715 end
737 end
716
738
717 def test_filter_on_me_by_anonymous_user
739 def test_filter_on_me_by_anonymous_user
718 User.current = nil
740 User.current = nil
719 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
741 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
720 assert_equal [], query.issues
742 assert_equal [], query.issues
721 end
743 end
722
744
723 def test_filter_my_projects
745 def test_filter_my_projects
724 User.current = User.find(2)
746 User.current = User.find(2)
725 query = IssueQuery.new(:name => '_')
747 query = IssueQuery.new(:name => '_')
726 filter = query.available_filters['project_id']
748 filter = query.available_filters['project_id']
727 assert_not_nil filter
749 assert_not_nil filter
728 assert_include 'mine', filter[:values].map{|v| v[1]}
750 assert_include 'mine', filter[:values].map{|v| v[1]}
729
751
730 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
752 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
731 result = query.issues
753 result = query.issues
732 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
754 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
733 end
755 end
734
756
735 def test_filter_watched_issues
757 def test_filter_watched_issues
736 User.current = User.find(1)
758 User.current = User.find(1)
737 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
759 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
738 result = find_issues_with_query(query)
760 result = find_issues_with_query(query)
739 assert_not_nil result
761 assert_not_nil result
740 assert !result.empty?
762 assert !result.empty?
741 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
763 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
742 User.current = nil
764 User.current = nil
743 end
765 end
744
766
745 def test_filter_unwatched_issues
767 def test_filter_unwatched_issues
746 User.current = User.find(1)
768 User.current = User.find(1)
747 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
769 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
748 result = find_issues_with_query(query)
770 result = find_issues_with_query(query)
749 assert_not_nil result
771 assert_not_nil result
750 assert !result.empty?
772 assert !result.empty?
751 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
773 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
752 User.current = nil
774 User.current = nil
753 end
775 end
754
776
755 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
777 def test_filter_on_custom_field_should_ignore_projects_with_field_disabled
756 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_for_all => false, :is_filter => true)
778 field = IssueCustomField.generate!(:trackers => Tracker.all, :project_ids => [1, 3, 4], :is_for_all => false, :is_filter => true)
757 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
779 Issue.generate!(:project_id => 3, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
758 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
780 Issue.generate!(:project_id => 4, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
759
781
760 query = IssueQuery.new(:name => '_', :project => Project.find(1))
782 query = IssueQuery.new(:name => '_', :project => Project.find(1))
761 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
783 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
762 assert_equal 2, find_issues_with_query(query).size
784 assert_equal 2, find_issues_with_query(query).size
763
785
764 field.project_ids = [1, 3] # Disable the field for project 4
786 field.project_ids = [1, 3] # Disable the field for project 4
765 field.save!
787 field.save!
766 assert_equal 1, find_issues_with_query(query).size
788 assert_equal 1, find_issues_with_query(query).size
767 end
789 end
768
790
769 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
791 def test_filter_on_custom_field_should_ignore_trackers_with_field_disabled
770 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
792 field = IssueCustomField.generate!(:tracker_ids => [1, 2], :is_for_all => true, :is_filter => true)
771 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
793 Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id.to_s => 'Foo'})
772 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
794 Issue.generate!(:project_id => 1, :tracker_id => 2, :custom_field_values => {field.id.to_s => 'Foo'})
773
795
774 query = IssueQuery.new(:name => '_', :project => Project.find(1))
796 query = IssueQuery.new(:name => '_', :project => Project.find(1))
775 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
797 query.filters = {"cf_#{field.id}" => {:operator => '=', :values => ['Foo']}}
776 assert_equal 2, find_issues_with_query(query).size
798 assert_equal 2, find_issues_with_query(query).size
777
799
778 field.tracker_ids = [1] # Disable the field for tracker 2
800 field.tracker_ids = [1] # Disable the field for tracker 2
779 field.save!
801 field.save!
780 assert_equal 1, find_issues_with_query(query).size
802 assert_equal 1, find_issues_with_query(query).size
781 end
803 end
782
804
783 def test_filter_on_project_custom_field
805 def test_filter_on_project_custom_field
784 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
806 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
785 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
807 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
786 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
808 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
787
809
788 query = IssueQuery.new(:name => '_')
810 query = IssueQuery.new(:name => '_')
789 filter_name = "project.cf_#{field.id}"
811 filter_name = "project.cf_#{field.id}"
790 assert_include filter_name, query.available_filters.keys
812 assert_include filter_name, query.available_filters.keys
791 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
813 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
792 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
814 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
793 end
815 end
794
816
795 def test_filter_on_author_custom_field
817 def test_filter_on_author_custom_field
796 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
818 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
797 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
819 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
798
820
799 query = IssueQuery.new(:name => '_')
821 query = IssueQuery.new(:name => '_')
800 filter_name = "author.cf_#{field.id}"
822 filter_name = "author.cf_#{field.id}"
801 assert_include filter_name, query.available_filters.keys
823 assert_include filter_name, query.available_filters.keys
802 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
824 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
803 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
825 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
804 end
826 end
805
827
806 def test_filter_on_assigned_to_custom_field
828 def test_filter_on_assigned_to_custom_field
807 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
829 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
808 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
830 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
809
831
810 query = IssueQuery.new(:name => '_')
832 query = IssueQuery.new(:name => '_')
811 filter_name = "assigned_to.cf_#{field.id}"
833 filter_name = "assigned_to.cf_#{field.id}"
812 assert_include filter_name, query.available_filters.keys
834 assert_include filter_name, query.available_filters.keys
813 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
835 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
814 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
836 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
815 end
837 end
816
838
817 def test_filter_on_fixed_version_custom_field
839 def test_filter_on_fixed_version_custom_field
818 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
840 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
819 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
841 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
820
842
821 query = IssueQuery.new(:name => '_')
843 query = IssueQuery.new(:name => '_')
822 filter_name = "fixed_version.cf_#{field.id}"
844 filter_name = "fixed_version.cf_#{field.id}"
823 assert_include filter_name, query.available_filters.keys
845 assert_include filter_name, query.available_filters.keys
824 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
846 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
825 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
847 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
826 end
848 end
827
849
828 def test_filter_on_fixed_version_due_date
850 def test_filter_on_fixed_version_due_date
829 query = IssueQuery.new(:name => '_')
851 query = IssueQuery.new(:name => '_')
830 filter_name = "fixed_version.due_date"
852 filter_name = "fixed_version.due_date"
831 assert_include filter_name, query.available_filters.keys
853 assert_include filter_name, query.available_filters.keys
832 query.filters = {filter_name => {:operator => '=', :values => [20.day.from_now.to_date.to_s(:db)]}}
854 query.filters = {filter_name => {:operator => '=', :values => [20.day.from_now.to_date.to_s(:db)]}}
833 issues = find_issues_with_query(query)
855 issues = find_issues_with_query(query)
834 assert_equal [2], issues.map(&:fixed_version_id).uniq.sort
856 assert_equal [2], issues.map(&:fixed_version_id).uniq.sort
835 assert_equal [2, 12], issues.map(&:id).sort
857 assert_equal [2, 12], issues.map(&:id).sort
836
858
837 query = IssueQuery.new(:name => '_')
859 query = IssueQuery.new(:name => '_')
838 query.filters = {filter_name => {:operator => '>=', :values => [21.day.from_now.to_date.to_s(:db)]}}
860 query.filters = {filter_name => {:operator => '>=', :values => [21.day.from_now.to_date.to_s(:db)]}}
839 assert_equal 0, find_issues_with_query(query).size
861 assert_equal 0, find_issues_with_query(query).size
840 end
862 end
841
863
842 def test_filter_on_fixed_version_status
864 def test_filter_on_fixed_version_status
843 query = IssueQuery.new(:name => '_')
865 query = IssueQuery.new(:name => '_')
844 filter_name = "fixed_version.status"
866 filter_name = "fixed_version.status"
845 assert_include filter_name, query.available_filters.keys
867 assert_include filter_name, query.available_filters.keys
846 query.filters = {filter_name => {:operator => '=', :values => ['closed']}}
868 query.filters = {filter_name => {:operator => '=', :values => ['closed']}}
847 issues = find_issues_with_query(query)
869 issues = find_issues_with_query(query)
848
870
849 assert_equal [1], issues.map(&:fixed_version_id).sort
871 assert_equal [1], issues.map(&:fixed_version_id).sort
850 assert_equal [11], issues.map(&:id).sort
872 assert_equal [11], issues.map(&:id).sort
851
873
852 # "is not" operator should include issues without target version
874 # "is not" operator should include issues without target version
853 query = IssueQuery.new(:name => '_')
875 query = IssueQuery.new(:name => '_')
854 query.filters = {filter_name => {:operator => '!', :values => ['open', 'closed', 'locked']}, "project_id" => {:operator => '=', :values => [1]}}
876 query.filters = {filter_name => {:operator => '!', :values => ['open', 'closed', 'locked']}, "project_id" => {:operator => '=', :values => [1]}}
855 assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
877 assert_equal [1, 3, 7, 8], find_issues_with_query(query).map(&:id).uniq.sort
856 end
878 end
857
879
858 def test_filter_on_relations_with_a_specific_issue
880 def test_filter_on_relations_with_a_specific_issue
859 IssueRelation.delete_all
881 IssueRelation.delete_all
860 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
882 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
861 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
883 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
862
884
863 query = IssueQuery.new(:name => '_')
885 query = IssueQuery.new(:name => '_')
864 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
886 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
865 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
887 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
866
888
867 query = IssueQuery.new(:name => '_')
889 query = IssueQuery.new(:name => '_')
868 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
890 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
869 assert_equal [1], find_issues_with_query(query).map(&:id).sort
891 assert_equal [1], find_issues_with_query(query).map(&:id).sort
870 end
892 end
871
893
872 def test_filter_on_relations_with_any_issues_in_a_project
894 def test_filter_on_relations_with_any_issues_in_a_project
873 IssueRelation.delete_all
895 IssueRelation.delete_all
874 with_settings :cross_project_issue_relations => '1' do
896 with_settings :cross_project_issue_relations => '1' do
875 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
897 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
876 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
898 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
877 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
899 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
878 end
900 end
879
901
880 query = IssueQuery.new(:name => '_')
902 query = IssueQuery.new(:name => '_')
881 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
903 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
882 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
904 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
883
905
884 query = IssueQuery.new(:name => '_')
906 query = IssueQuery.new(:name => '_')
885 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
907 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
886 assert_equal [1], find_issues_with_query(query).map(&:id).sort
908 assert_equal [1], find_issues_with_query(query).map(&:id).sort
887
909
888 query = IssueQuery.new(:name => '_')
910 query = IssueQuery.new(:name => '_')
889 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
911 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
890 assert_equal [], find_issues_with_query(query).map(&:id).sort
912 assert_equal [], find_issues_with_query(query).map(&:id).sort
891 end
913 end
892
914
893 def test_filter_on_relations_with_any_issues_not_in_a_project
915 def test_filter_on_relations_with_any_issues_not_in_a_project
894 IssueRelation.delete_all
916 IssueRelation.delete_all
895 with_settings :cross_project_issue_relations => '1' do
917 with_settings :cross_project_issue_relations => '1' do
896 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
918 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
897 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
919 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
898 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
920 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
899 end
921 end
900
922
901 query = IssueQuery.new(:name => '_')
923 query = IssueQuery.new(:name => '_')
902 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
924 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
903 assert_equal [1], find_issues_with_query(query).map(&:id).sort
925 assert_equal [1], find_issues_with_query(query).map(&:id).sort
904 end
926 end
905
927
906 def test_filter_on_relations_with_no_issues_in_a_project
928 def test_filter_on_relations_with_no_issues_in_a_project
907 IssueRelation.delete_all
929 IssueRelation.delete_all
908 with_settings :cross_project_issue_relations => '1' do
930 with_settings :cross_project_issue_relations => '1' do
909 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
931 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
910 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
932 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
911 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
933 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
912 end
934 end
913
935
914 query = IssueQuery.new(:name => '_')
936 query = IssueQuery.new(:name => '_')
915 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
937 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
916 ids = find_issues_with_query(query).map(&:id).sort
938 ids = find_issues_with_query(query).map(&:id).sort
917 assert_include 2, ids
939 assert_include 2, ids
918 assert_not_include 1, ids
940 assert_not_include 1, ids
919 assert_not_include 3, ids
941 assert_not_include 3, ids
920 end
942 end
921
943
922 def test_filter_on_relations_with_any_open_issues
944 def test_filter_on_relations_with_any_open_issues
923 IssueRelation.delete_all
945 IssueRelation.delete_all
924 # Issue 1 is blocked by 8, which is closed
946 # Issue 1 is blocked by 8, which is closed
925 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
947 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
926 # Issue 2 is blocked by 3, which is open
948 # Issue 2 is blocked by 3, which is open
927 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
949 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
928
950
929 query = IssueQuery.new(:name => '_')
951 query = IssueQuery.new(:name => '_')
930 query.filters = {"blocked" => {:operator => "*o", :values => ['']}}
952 query.filters = {"blocked" => {:operator => "*o", :values => ['']}}
931 ids = find_issues_with_query(query).map(&:id)
953 ids = find_issues_with_query(query).map(&:id)
932 assert_equal [], ids & [1]
954 assert_equal [], ids & [1]
933 assert_include 2, ids
955 assert_include 2, ids
934 end
956 end
935
957
936 def test_filter_on_relations_with_no_open_issues
958 def test_filter_on_relations_with_no_open_issues
937 IssueRelation.delete_all
959 IssueRelation.delete_all
938 # Issue 1 is blocked by 8, which is closed
960 # Issue 1 is blocked by 8, which is closed
939 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
961 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(1), :issue_to => Issue.find(8))
940 # Issue 2 is blocked by 3, which is open
962 # Issue 2 is blocked by 3, which is open
941 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
963 IssueRelation.create!(:relation_type => "blocked", :issue_from => Issue.find(2), :issue_to => Issue.find(3))
942
964
943 query = IssueQuery.new(:name => '_')
965 query = IssueQuery.new(:name => '_')
944 query.filters = {"blocked" => {:operator => "!o", :values => ['']}}
966 query.filters = {"blocked" => {:operator => "!o", :values => ['']}}
945 ids = find_issues_with_query(query).map(&:id)
967 ids = find_issues_with_query(query).map(&:id)
946 assert_equal [], ids & [2]
968 assert_equal [], ids & [2]
947 assert_include 1, ids
969 assert_include 1, ids
948 end
970 end
949
971
950 def test_filter_on_relations_with_no_issues
972 def test_filter_on_relations_with_no_issues
951 IssueRelation.delete_all
973 IssueRelation.delete_all
952 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
974 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
953 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
975 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
954
976
955 query = IssueQuery.new(:name => '_')
977 query = IssueQuery.new(:name => '_')
956 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
978 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
957 ids = find_issues_with_query(query).map(&:id)
979 ids = find_issues_with_query(query).map(&:id)
958 assert_equal [], ids & [1, 2, 3]
980 assert_equal [], ids & [1, 2, 3]
959 assert_include 4, ids
981 assert_include 4, ids
960 end
982 end
961
983
962 def test_filter_on_relations_with_any_issues
984 def test_filter_on_relations_with_any_issues
963 IssueRelation.delete_all
985 IssueRelation.delete_all
964 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
986 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
965 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
987 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
966
988
967 query = IssueQuery.new(:name => '_')
989 query = IssueQuery.new(:name => '_')
968 query.filters = {"relates" => {:operator => '*', :values => ['']}}
990 query.filters = {"relates" => {:operator => '*', :values => ['']}}
969 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
991 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
970 end
992 end
971
993
972 def test_filter_on_relations_should_not_ignore_other_filter
994 def test_filter_on_relations_should_not_ignore_other_filter
973 issue = Issue.generate!
995 issue = Issue.generate!
974 issue1 = Issue.generate!(:status_id => 1)
996 issue1 = Issue.generate!(:status_id => 1)
975 issue2 = Issue.generate!(:status_id => 2)
997 issue2 = Issue.generate!(:status_id => 2)
976 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
998 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue1)
977 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
999 IssueRelation.create!(:relation_type => "relates", :issue_from => issue, :issue_to => issue2)
978
1000
979 query = IssueQuery.new(:name => '_')
1001 query = IssueQuery.new(:name => '_')
980 query.filters = {
1002 query.filters = {
981 "status_id" => {:operator => '=', :values => ['1']},
1003 "status_id" => {:operator => '=', :values => ['1']},
982 "relates" => {:operator => '=', :values => [issue.id.to_s]}
1004 "relates" => {:operator => '=', :values => [issue.id.to_s]}
983 }
1005 }
984 assert_equal [issue1], find_issues_with_query(query)
1006 assert_equal [issue1], find_issues_with_query(query)
985 end
1007 end
986
1008
987 def test_filter_on_parent
1009 def test_filter_on_parent
988 Issue.delete_all
1010 Issue.delete_all
989 parent = Issue.generate_with_descendants!
1011 parent = Issue.generate_with_descendants!
990
1012
991
1013
992 query = IssueQuery.new(:name => '_')
1014 query = IssueQuery.new(:name => '_')
993 query.filters = {"parent_id" => {:operator => '=', :values => [parent.id.to_s]}}
1015 query.filters = {"parent_id" => {:operator => '=', :values => [parent.id.to_s]}}
994 assert_equal parent.children.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1016 assert_equal parent.children.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
995
1017
996 query.filters = {"parent_id" => {:operator => '~', :values => [parent.id.to_s]}}
1018 query.filters = {"parent_id" => {:operator => '~', :values => [parent.id.to_s]}}
997 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1019 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
998
1020
999 query.filters = {"parent_id" => {:operator => '*', :values => ['']}}
1021 query.filters = {"parent_id" => {:operator => '*', :values => ['']}}
1000 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1022 assert_equal parent.descendants.map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1001
1023
1002 query.filters = {"parent_id" => {:operator => '!*', :values => ['']}}
1024 query.filters = {"parent_id" => {:operator => '!*', :values => ['']}}
1003 assert_equal [parent.id], find_issues_with_query(query).map(&:id).sort
1025 assert_equal [parent.id], find_issues_with_query(query).map(&:id).sort
1004 end
1026 end
1005
1027
1006 def test_filter_on_invalid_parent_should_return_no_results
1028 def test_filter_on_invalid_parent_should_return_no_results
1007 query = IssueQuery.new(:name => '_')
1029 query = IssueQuery.new(:name => '_')
1008 query.filters = {"parent_id" => {:operator => '=', :values => '99999999999'}}
1030 query.filters = {"parent_id" => {:operator => '=', :values => '99999999999'}}
1009 assert_equal [], find_issues_with_query(query).map(&:id).sort
1031 assert_equal [], find_issues_with_query(query).map(&:id).sort
1010
1032
1011 query.filters = {"parent_id" => {:operator => '~', :values => '99999999999'}}
1033 query.filters = {"parent_id" => {:operator => '~', :values => '99999999999'}}
1012 assert_equal [], find_issues_with_query(query)
1034 assert_equal [], find_issues_with_query(query)
1013 end
1035 end
1014
1036
1015 def test_filter_on_child
1037 def test_filter_on_child
1016 Issue.delete_all
1038 Issue.delete_all
1017 parent = Issue.generate_with_descendants!
1039 parent = Issue.generate_with_descendants!
1018 child, leaf = parent.children.sort_by(&:id)
1040 child, leaf = parent.children.sort_by(&:id)
1019 grandchild = child.children.first
1041 grandchild = child.children.first
1020
1042
1021
1043
1022 query = IssueQuery.new(:name => '_')
1044 query = IssueQuery.new(:name => '_')
1023 query.filters = {"child_id" => {:operator => '=', :values => [grandchild.id.to_s]}}
1045 query.filters = {"child_id" => {:operator => '=', :values => [grandchild.id.to_s]}}
1024 assert_equal [child.id], find_issues_with_query(query).map(&:id).sort
1046 assert_equal [child.id], find_issues_with_query(query).map(&:id).sort
1025
1047
1026 query.filters = {"child_id" => {:operator => '~', :values => [grandchild.id.to_s]}}
1048 query.filters = {"child_id" => {:operator => '~', :values => [grandchild.id.to_s]}}
1027 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1049 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1028
1050
1029 query.filters = {"child_id" => {:operator => '*', :values => ['']}}
1051 query.filters = {"child_id" => {:operator => '*', :values => ['']}}
1030 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1052 assert_equal [parent, child].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1031
1053
1032 query.filters = {"child_id" => {:operator => '!*', :values => ['']}}
1054 query.filters = {"child_id" => {:operator => '!*', :values => ['']}}
1033 assert_equal [grandchild, leaf].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1055 assert_equal [grandchild, leaf].map(&:id).sort, find_issues_with_query(query).map(&:id).sort
1034 end
1056 end
1035
1057
1036 def test_filter_on_invalid_child_should_return_no_results
1058 def test_filter_on_invalid_child_should_return_no_results
1037 query = IssueQuery.new(:name => '_')
1059 query = IssueQuery.new(:name => '_')
1038 query.filters = {"child_id" => {:operator => '=', :values => '99999999999'}}
1060 query.filters = {"child_id" => {:operator => '=', :values => '99999999999'}}
1039 assert_equal [], find_issues_with_query(query)
1061 assert_equal [], find_issues_with_query(query)
1040
1062
1041 query.filters = {"child_id" => {:operator => '~', :values => '99999999999'}}
1063 query.filters = {"child_id" => {:operator => '~', :values => '99999999999'}}
1042 assert_equal [].map(&:id).sort, find_issues_with_query(query)
1064 assert_equal [].map(&:id).sort, find_issues_with_query(query)
1043 end
1065 end
1044
1066
1045 def test_statement_should_be_nil_with_no_filters
1067 def test_statement_should_be_nil_with_no_filters
1046 q = IssueQuery.new(:name => '_')
1068 q = IssueQuery.new(:name => '_')
1047 q.filters = {}
1069 q.filters = {}
1048
1070
1049 assert q.valid?
1071 assert q.valid?
1050 assert_nil q.statement
1072 assert_nil q.statement
1051 end
1073 end
1052
1074
1053 def test_available_filters_as_json_should_include_missing_assigned_to_id_values
1075 def test_available_filters_as_json_should_include_missing_assigned_to_id_values
1054 user = User.generate!
1076 user = User.generate!
1055 with_current_user User.find(1) do
1077 with_current_user User.find(1) do
1056 q = IssueQuery.new
1078 q = IssueQuery.new
1057 q.filters = {"assigned_to_id" => {:operator => '=', :values => user.id.to_s}}
1079 q.filters = {"assigned_to_id" => {:operator => '=', :values => user.id.to_s}}
1058
1080
1059 filters = q.available_filters_as_json
1081 filters = q.available_filters_as_json
1060 assert_include [user.name, user.id.to_s], filters['assigned_to_id']['values']
1082 assert_include [user.name, user.id.to_s], filters['assigned_to_id']['values']
1061 end
1083 end
1062 end
1084 end
1063
1085
1064 def test_available_filters_as_json_should_include_missing_author_id_values
1086 def test_available_filters_as_json_should_include_missing_author_id_values
1065 user = User.generate!
1087 user = User.generate!
1066 with_current_user User.find(1) do
1088 with_current_user User.find(1) do
1067 q = IssueQuery.new
1089 q = IssueQuery.new
1068 q.filters = {"author_id" => {:operator => '=', :values => user.id.to_s}}
1090 q.filters = {"author_id" => {:operator => '=', :values => user.id.to_s}}
1069
1091
1070 filters = q.available_filters_as_json
1092 filters = q.available_filters_as_json
1071 assert_include [user.name, user.id.to_s], filters['author_id']['values']
1093 assert_include [user.name, user.id.to_s], filters['author_id']['values']
1072 end
1094 end
1073 end
1095 end
1074
1096
1075 def test_default_columns
1097 def test_default_columns
1076 q = IssueQuery.new
1098 q = IssueQuery.new
1077 assert q.columns.any?
1099 assert q.columns.any?
1078 assert q.inline_columns.any?
1100 assert q.inline_columns.any?
1079 assert q.block_columns.empty?
1101 assert q.block_columns.empty?
1080 end
1102 end
1081
1103
1082 def test_set_column_names
1104 def test_set_column_names
1083 q = IssueQuery.new
1105 q = IssueQuery.new
1084 q.column_names = ['tracker', :subject, '', 'unknonw_column']
1106 q.column_names = ['tracker', :subject, '', 'unknonw_column']
1085 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
1107 assert_equal [:id, :tracker, :subject], q.columns.collect {|c| c.name}
1086 end
1108 end
1087
1109
1088 def test_has_column_should_accept_a_column_name
1110 def test_has_column_should_accept_a_column_name
1089 q = IssueQuery.new
1111 q = IssueQuery.new
1090 q.column_names = ['tracker', :subject]
1112 q.column_names = ['tracker', :subject]
1091 assert q.has_column?(:tracker)
1113 assert q.has_column?(:tracker)
1092 assert !q.has_column?(:category)
1114 assert !q.has_column?(:category)
1093 end
1115 end
1094
1116
1095 def test_has_column_should_accept_a_column
1117 def test_has_column_should_accept_a_column
1096 q = IssueQuery.new
1118 q = IssueQuery.new
1097 q.column_names = ['tracker', :subject]
1119 q.column_names = ['tracker', :subject]
1098
1120
1099 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
1121 tracker_column = q.available_columns.detect {|c| c.name==:tracker}
1100 assert_kind_of QueryColumn, tracker_column
1122 assert_kind_of QueryColumn, tracker_column
1101 category_column = q.available_columns.detect {|c| c.name==:category}
1123 category_column = q.available_columns.detect {|c| c.name==:category}
1102 assert_kind_of QueryColumn, category_column
1124 assert_kind_of QueryColumn, category_column
1103
1125
1104 assert q.has_column?(tracker_column)
1126 assert q.has_column?(tracker_column)
1105 assert !q.has_column?(category_column)
1127 assert !q.has_column?(category_column)
1106 end
1128 end
1107
1129
1108 def test_inline_and_block_columns
1130 def test_inline_and_block_columns
1109 q = IssueQuery.new
1131 q = IssueQuery.new
1110 q.column_names = ['subject', 'description', 'tracker']
1132 q.column_names = ['subject', 'description', 'tracker']
1111
1133
1112 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
1134 assert_equal [:id, :subject, :tracker], q.inline_columns.map(&:name)
1113 assert_equal [:description], q.block_columns.map(&:name)
1135 assert_equal [:description], q.block_columns.map(&:name)
1114 end
1136 end
1115
1137
1116 def test_custom_field_columns_should_be_inline
1138 def test_custom_field_columns_should_be_inline
1117 q = IssueQuery.new
1139 q = IssueQuery.new
1118 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
1140 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
1119 assert columns.any?
1141 assert columns.any?
1120 assert_nil columns.detect {|column| !column.inline?}
1142 assert_nil columns.detect {|column| !column.inline?}
1121 end
1143 end
1122
1144
1123 def test_query_should_preload_spent_hours
1145 def test_query_should_preload_spent_hours
1124 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
1146 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
1125 assert q.has_column?(:spent_hours)
1147 assert q.has_column?(:spent_hours)
1126 issues = q.issues
1148 issues = q.issues
1127 assert_not_nil issues.first.instance_variable_get("@spent_hours")
1149 assert_not_nil issues.first.instance_variable_get("@spent_hours")
1128 end
1150 end
1129
1151
1130 def test_groupable_columns_should_include_custom_fields
1152 def test_groupable_columns_should_include_custom_fields
1131 q = IssueQuery.new
1153 q = IssueQuery.new
1132 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1154 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1133 assert_not_nil column
1155 assert_not_nil column
1134 assert_kind_of QueryCustomFieldColumn, column
1156 assert_kind_of QueryCustomFieldColumn, column
1135 end
1157 end
1136
1158
1137 def test_groupable_columns_should_not_include_multi_custom_fields
1159 def test_groupable_columns_should_not_include_multi_custom_fields
1138 field = CustomField.find(1)
1160 field = CustomField.find(1)
1139 field.update_attribute :multiple, true
1161 field.update_attribute :multiple, true
1140
1162
1141 q = IssueQuery.new
1163 q = IssueQuery.new
1142 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1164 column = q.groupable_columns.detect {|c| c.name == :cf_1}
1143 assert_nil column
1165 assert_nil column
1144 end
1166 end
1145
1167
1146 def test_groupable_columns_should_include_user_custom_fields
1168 def test_groupable_columns_should_include_user_custom_fields
1147 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
1169 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
1148
1170
1149 q = IssueQuery.new
1171 q = IssueQuery.new
1150 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1172 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1151 end
1173 end
1152
1174
1153 def test_groupable_columns_should_include_version_custom_fields
1175 def test_groupable_columns_should_include_version_custom_fields
1154 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
1176 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
1155
1177
1156 q = IssueQuery.new
1178 q = IssueQuery.new
1157 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1179 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
1158 end
1180 end
1159
1181
1160 def test_grouped_with_valid_column
1182 def test_grouped_with_valid_column
1161 q = IssueQuery.new(:group_by => 'status')
1183 q = IssueQuery.new(:group_by => 'status')
1162 assert q.grouped?
1184 assert q.grouped?
1163 assert_not_nil q.group_by_column
1185 assert_not_nil q.group_by_column
1164 assert_equal :status, q.group_by_column.name
1186 assert_equal :status, q.group_by_column.name
1165 assert_not_nil q.group_by_statement
1187 assert_not_nil q.group_by_statement
1166 assert_equal 'status', q.group_by_statement
1188 assert_equal 'status', q.group_by_statement
1167 end
1189 end
1168
1190
1169 def test_grouped_with_invalid_column
1191 def test_grouped_with_invalid_column
1170 q = IssueQuery.new(:group_by => 'foo')
1192 q = IssueQuery.new(:group_by => 'foo')
1171 assert !q.grouped?
1193 assert !q.grouped?
1172 assert_nil q.group_by_column
1194 assert_nil q.group_by_column
1173 assert_nil q.group_by_statement
1195 assert_nil q.group_by_statement
1174 end
1196 end
1175
1197
1176 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
1198 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
1177 with_settings :user_format => 'lastname_comma_firstname' do
1199 with_settings :user_format => 'lastname_comma_firstname' do
1178 q = IssueQuery.new
1200 q = IssueQuery.new
1179 assert q.sortable_columns.has_key?('assigned_to')
1201 assert q.sortable_columns.has_key?('assigned_to')
1180 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
1202 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
1181 end
1203 end
1182 end
1204 end
1183
1205
1184 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
1206 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
1185 with_settings :user_format => 'lastname_comma_firstname' do
1207 with_settings :user_format => 'lastname_comma_firstname' do
1186 q = IssueQuery.new
1208 q = IssueQuery.new
1187 assert q.sortable_columns.has_key?('author')
1209 assert q.sortable_columns.has_key?('author')
1188 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
1210 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
1189 end
1211 end
1190 end
1212 end
1191
1213
1192 def test_sortable_columns_should_include_custom_field
1214 def test_sortable_columns_should_include_custom_field
1193 q = IssueQuery.new
1215 q = IssueQuery.new
1194 assert q.sortable_columns['cf_1']
1216 assert q.sortable_columns['cf_1']
1195 end
1217 end
1196
1218
1197 def test_sortable_columns_should_not_include_multi_custom_field
1219 def test_sortable_columns_should_not_include_multi_custom_field
1198 field = CustomField.find(1)
1220 field = CustomField.find(1)
1199 field.update_attribute :multiple, true
1221 field.update_attribute :multiple, true
1200
1222
1201 q = IssueQuery.new
1223 q = IssueQuery.new
1202 assert !q.sortable_columns['cf_1']
1224 assert !q.sortable_columns['cf_1']
1203 end
1225 end
1204
1226
1205 def test_default_sort
1227 def test_default_sort
1206 q = IssueQuery.new
1228 q = IssueQuery.new
1207 assert_equal [], q.sort_criteria
1229 assert_equal [], q.sort_criteria
1208 end
1230 end
1209
1231
1210 def test_set_sort_criteria_with_hash
1232 def test_set_sort_criteria_with_hash
1211 q = IssueQuery.new
1233 q = IssueQuery.new
1212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
1234 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
1213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1235 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1214 end
1236 end
1215
1237
1216 def test_set_sort_criteria_with_array
1238 def test_set_sort_criteria_with_array
1217 q = IssueQuery.new
1239 q = IssueQuery.new
1218 q.sort_criteria = [['priority', 'desc'], 'tracker']
1240 q.sort_criteria = [['priority', 'desc'], 'tracker']
1219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1241 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1220 end
1242 end
1221
1243
1222 def test_create_query_with_sort
1244 def test_create_query_with_sort
1223 q = IssueQuery.new(:name => 'Sorted')
1245 q = IssueQuery.new(:name => 'Sorted')
1224 q.sort_criteria = [['priority', 'desc'], 'tracker']
1246 q.sort_criteria = [['priority', 'desc'], 'tracker']
1225 assert q.save
1247 assert q.save
1226 q.reload
1248 q.reload
1227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1249 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
1228 end
1250 end
1229
1251
1230 def test_sort_by_string_custom_field_asc
1252 def test_sort_by_string_custom_field_asc
1231 q = IssueQuery.new
1253 q = IssueQuery.new
1232 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1254 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1233 assert c
1255 assert c
1234 assert c.sortable
1256 assert c.sortable
1235 issues = q.issues(:order => "#{c.sortable} ASC")
1257 issues = q.issues(:order => "#{c.sortable} ASC")
1236 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1258 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1237 assert !values.empty?
1259 assert !values.empty?
1238 assert_equal values.sort, values
1260 assert_equal values.sort, values
1239 end
1261 end
1240
1262
1241 def test_sort_by_string_custom_field_desc
1263 def test_sort_by_string_custom_field_desc
1242 q = IssueQuery.new
1264 q = IssueQuery.new
1243 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1265 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
1244 assert c
1266 assert c
1245 assert c.sortable
1267 assert c.sortable
1246 issues = q.issues(:order => "#{c.sortable} DESC")
1268 issues = q.issues(:order => "#{c.sortable} DESC")
1247 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1269 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
1248 assert !values.empty?
1270 assert !values.empty?
1249 assert_equal values.sort.reverse, values
1271 assert_equal values.sort.reverse, values
1250 end
1272 end
1251
1273
1252 def test_sort_by_float_custom_field_asc
1274 def test_sort_by_float_custom_field_asc
1253 q = IssueQuery.new
1275 q = IssueQuery.new
1254 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1276 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
1255 assert c
1277 assert c
1256 assert c.sortable
1278 assert c.sortable
1257 issues = q.issues(:order => "#{c.sortable} ASC")
1279 issues = q.issues(:order => "#{c.sortable} ASC")
1258 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1280 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
1259 assert !values.empty?
1281 assert !values.empty?
1260 assert_equal values.sort, values
1282 assert_equal values.sort, values
1261 end
1283 end
1262
1284
1263 def test_set_totalable_names
1285 def test_set_totalable_names
1264 q = IssueQuery.new
1286 q = IssueQuery.new
1265 q.totalable_names = ['estimated_hours', :spent_hours, '']
1287 q.totalable_names = ['estimated_hours', :spent_hours, '']
1266 assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1288 assert_equal [:estimated_hours, :spent_hours], q.totalable_columns.map(&:name)
1267 end
1289 end
1268
1290
1269 def test_totalable_columns_should_default_to_settings
1291 def test_totalable_columns_should_default_to_settings
1270 with_settings :issue_list_default_totals => ['estimated_hours'] do
1292 with_settings :issue_list_default_totals => ['estimated_hours'] do
1271 q = IssueQuery.new
1293 q = IssueQuery.new
1272 assert_equal [:estimated_hours], q.totalable_columns.map(&:name)
1294 assert_equal [:estimated_hours], q.totalable_columns.map(&:name)
1273 end
1295 end
1274 end
1296 end
1275
1297
1276 def test_available_totalable_columns_should_include_estimated_hours
1298 def test_available_totalable_columns_should_include_estimated_hours
1277 q = IssueQuery.new
1299 q = IssueQuery.new
1278 assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1300 assert_include :estimated_hours, q.available_totalable_columns.map(&:name)
1279 end
1301 end
1280
1302
1281 def test_available_totalable_columns_should_include_spent_hours
1303 def test_available_totalable_columns_should_include_spent_hours
1282 User.current = User.find(1)
1304 User.current = User.find(1)
1283
1305
1284 q = IssueQuery.new
1306 q = IssueQuery.new
1285 assert_include :spent_hours, q.available_totalable_columns.map(&:name)
1307 assert_include :spent_hours, q.available_totalable_columns.map(&:name)
1286 end
1308 end
1287
1309
1288 def test_available_totalable_columns_should_include_int_custom_field
1310 def test_available_totalable_columns_should_include_int_custom_field
1289 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1311 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1290 q = IssueQuery.new
1312 q = IssueQuery.new
1291 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1313 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1292 end
1314 end
1293
1315
1294 def test_available_totalable_columns_should_include_float_custom_field
1316 def test_available_totalable_columns_should_include_float_custom_field
1295 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1317 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1296 q = IssueQuery.new
1318 q = IssueQuery.new
1297 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1319 assert_include "cf_#{field.id}".to_sym, q.available_totalable_columns.map(&:name)
1298 end
1320 end
1299
1321
1300 def test_total_for_estimated_hours
1322 def test_total_for_estimated_hours
1301 Issue.delete_all
1323 Issue.delete_all
1302 Issue.generate!(:estimated_hours => 5.5)
1324 Issue.generate!(:estimated_hours => 5.5)
1303 Issue.generate!(:estimated_hours => 1.1)
1325 Issue.generate!(:estimated_hours => 1.1)
1304 Issue.generate!
1326 Issue.generate!
1305
1327
1306 q = IssueQuery.new
1328 q = IssueQuery.new
1307 assert_equal 6.6, q.total_for(:estimated_hours)
1329 assert_equal 6.6, q.total_for(:estimated_hours)
1308 end
1330 end
1309
1331
1310 def test_total_by_group_for_estimated_hours
1332 def test_total_by_group_for_estimated_hours
1311 Issue.delete_all
1333 Issue.delete_all
1312 Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
1334 Issue.generate!(:estimated_hours => 5.5, :assigned_to_id => 2)
1313 Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
1335 Issue.generate!(:estimated_hours => 1.1, :assigned_to_id => 3)
1314 Issue.generate!(:estimated_hours => 3.5)
1336 Issue.generate!(:estimated_hours => 3.5)
1315
1337
1316 q = IssueQuery.new(:group_by => 'assigned_to')
1338 q = IssueQuery.new(:group_by => 'assigned_to')
1317 assert_equal(
1339 assert_equal(
1318 {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1340 {nil => 3.5, User.find(2) => 5.5, User.find(3) => 1.1},
1319 q.total_by_group_for(:estimated_hours)
1341 q.total_by_group_for(:estimated_hours)
1320 )
1342 )
1321 end
1343 end
1322
1344
1323 def test_total_for_spent_hours
1345 def test_total_for_spent_hours
1324 TimeEntry.delete_all
1346 TimeEntry.delete_all
1325 TimeEntry.generate!(:hours => 5.5)
1347 TimeEntry.generate!(:hours => 5.5)
1326 TimeEntry.generate!(:hours => 1.1)
1348 TimeEntry.generate!(:hours => 1.1)
1327
1349
1328 q = IssueQuery.new
1350 q = IssueQuery.new
1329 assert_equal 6.6, q.total_for(:spent_hours)
1351 assert_equal 6.6, q.total_for(:spent_hours)
1330 end
1352 end
1331
1353
1332 def test_total_by_group_for_spent_hours
1354 def test_total_by_group_for_spent_hours
1333 TimeEntry.delete_all
1355 TimeEntry.delete_all
1334 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1356 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1335 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1357 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1336 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1358 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1337 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1359 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1338
1360
1339 q = IssueQuery.new(:group_by => 'assigned_to')
1361 q = IssueQuery.new(:group_by => 'assigned_to')
1340 assert_equal(
1362 assert_equal(
1341 {User.find(2) => 5.5, User.find(3) => 1.1},
1363 {User.find(2) => 5.5, User.find(3) => 1.1},
1342 q.total_by_group_for(:spent_hours)
1364 q.total_by_group_for(:spent_hours)
1343 )
1365 )
1344 end
1366 end
1345
1367
1346 def test_total_by_project_group_for_spent_hours
1368 def test_total_by_project_group_for_spent_hours
1347 TimeEntry.delete_all
1369 TimeEntry.delete_all
1348 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1370 TimeEntry.generate!(:hours => 5.5, :issue_id => 1)
1349 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1371 TimeEntry.generate!(:hours => 1.1, :issue_id => 2)
1350 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1372 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1351 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1373 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1352
1374
1353 q = IssueQuery.new(:group_by => 'project')
1375 q = IssueQuery.new(:group_by => 'project')
1354 assert_equal(
1376 assert_equal(
1355 {Project.find(1) => 6.6},
1377 {Project.find(1) => 6.6},
1356 q.total_by_group_for(:spent_hours)
1378 q.total_by_group_for(:spent_hours)
1357 )
1379 )
1358 end
1380 end
1359
1381
1360 def test_total_for_int_custom_field
1382 def test_total_for_int_custom_field
1361 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1383 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1362 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1384 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1363 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1385 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1364 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1386 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1365
1387
1366 q = IssueQuery.new
1388 q = IssueQuery.new
1367 assert_equal 9, q.total_for("cf_#{field.id}")
1389 assert_equal 9, q.total_for("cf_#{field.id}")
1368 end
1390 end
1369
1391
1370 def test_total_by_group_for_int_custom_field
1392 def test_total_by_group_for_int_custom_field
1371 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1393 field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
1372 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1394 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
1373 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1395 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1374 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1396 Issue.where(:id => 1).update_all(:assigned_to_id => 2)
1375 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1397 Issue.where(:id => 2).update_all(:assigned_to_id => 3)
1376
1398
1377 q = IssueQuery.new(:group_by => 'assigned_to')
1399 q = IssueQuery.new(:group_by => 'assigned_to')
1378 assert_equal(
1400 assert_equal(
1379 {User.find(2) => 2, User.find(3) => 7},
1401 {User.find(2) => 2, User.find(3) => 7},
1380 q.total_by_group_for("cf_#{field.id}")
1402 q.total_by_group_for("cf_#{field.id}")
1381 )
1403 )
1382 end
1404 end
1383
1405
1384 def test_total_for_float_custom_field
1406 def test_total_for_float_custom_field
1385 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1407 field = IssueCustomField.generate!(:field_format => 'float', :is_for_all => true)
1386 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
1408 CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2.3')
1387 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1409 CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')
1388 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1410 CustomValue.create!(:customized => Issue.find(3), :custom_field => field, :value => '')
1389
1411
1390 q = IssueQuery.new
1412 q = IssueQuery.new
1391 assert_equal 9.3, q.total_for("cf_#{field.id}")
1413 assert_equal 9.3, q.total_for("cf_#{field.id}")
1392 end
1414 end
1393
1415
1394 def test_invalid_query_should_raise_query_statement_invalid_error
1416 def test_invalid_query_should_raise_query_statement_invalid_error
1395 q = IssueQuery.new
1417 q = IssueQuery.new
1396 assert_raise Query::StatementInvalid do
1418 assert_raise Query::StatementInvalid do
1397 q.issues(:conditions => "foo = 1")
1419 q.issues(:conditions => "foo = 1")
1398 end
1420 end
1399 end
1421 end
1400
1422
1401 def test_issue_count
1423 def test_issue_count
1402 q = IssueQuery.new(:name => '_')
1424 q = IssueQuery.new(:name => '_')
1403 issue_count = q.issue_count
1425 issue_count = q.issue_count
1404 assert_equal q.issues.size, issue_count
1426 assert_equal q.issues.size, issue_count
1405 end
1427 end
1406
1428
1407 def test_issue_count_with_archived_issues
1429 def test_issue_count_with_archived_issues
1408 p = Project.generate! do |project|
1430 p = Project.generate! do |project|
1409 project.status = Project::STATUS_ARCHIVED
1431 project.status = Project::STATUS_ARCHIVED
1410 end
1432 end
1411 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1433 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
1412 assert !i.visible?
1434 assert !i.visible?
1413
1435
1414 test_issue_count
1436 test_issue_count
1415 end
1437 end
1416
1438
1417 def test_issue_count_by_association_group
1439 def test_issue_count_by_association_group
1418 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1440 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1419 count_by_group = q.issue_count_by_group
1441 count_by_group = q.issue_count_by_group
1420 assert_kind_of Hash, count_by_group
1442 assert_kind_of Hash, count_by_group
1421 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1443 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1422 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1444 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1423 assert count_by_group.has_key?(User.find(3))
1445 assert count_by_group.has_key?(User.find(3))
1424 end
1446 end
1425
1447
1426 def test_issue_count_by_list_custom_field_group
1448 def test_issue_count_by_list_custom_field_group
1427 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1449 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
1428 count_by_group = q.issue_count_by_group
1450 count_by_group = q.issue_count_by_group
1429 assert_kind_of Hash, count_by_group
1451 assert_kind_of Hash, count_by_group
1430 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1452 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1431 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1453 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1432 assert count_by_group.has_key?('MySQL')
1454 assert count_by_group.has_key?('MySQL')
1433 end
1455 end
1434
1456
1435 def test_issue_count_by_date_custom_field_group
1457 def test_issue_count_by_date_custom_field_group
1436 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1458 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
1437 count_by_group = q.issue_count_by_group
1459 count_by_group = q.issue_count_by_group
1438 assert_kind_of Hash, count_by_group
1460 assert_kind_of Hash, count_by_group
1439 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1461 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
1440 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1462 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
1441 end
1463 end
1442
1464
1443 def test_issue_count_with_nil_group_only
1465 def test_issue_count_with_nil_group_only
1444 Issue.update_all("assigned_to_id = NULL")
1466 Issue.update_all("assigned_to_id = NULL")
1445
1467
1446 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1468 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
1447 count_by_group = q.issue_count_by_group
1469 count_by_group = q.issue_count_by_group
1448 assert_kind_of Hash, count_by_group
1470 assert_kind_of Hash, count_by_group
1449 assert_equal 1, count_by_group.keys.size
1471 assert_equal 1, count_by_group.keys.size
1450 assert_nil count_by_group.keys.first
1472 assert_nil count_by_group.keys.first
1451 end
1473 end
1452
1474
1453 def test_issue_ids
1475 def test_issue_ids
1454 q = IssueQuery.new(:name => '_')
1476 q = IssueQuery.new(:name => '_')
1455 order = "issues.subject, issues.id"
1477 order = "issues.subject, issues.id"
1456 issues = q.issues(:order => order)
1478 issues = q.issues(:order => order)
1457 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1479 assert_equal issues.map(&:id), q.issue_ids(:order => order)
1458 end
1480 end
1459
1481
1460 def test_label_for
1482 def test_label_for
1461 set_language_if_valid 'en'
1483 set_language_if_valid 'en'
1462 q = IssueQuery.new
1484 q = IssueQuery.new
1463 assert_equal 'Assignee', q.label_for('assigned_to_id')
1485 assert_equal 'Assignee', q.label_for('assigned_to_id')
1464 end
1486 end
1465
1487
1466 def test_label_for_fr
1488 def test_label_for_fr
1467 set_language_if_valid 'fr'
1489 set_language_if_valid 'fr'
1468 q = IssueQuery.new
1490 q = IssueQuery.new
1469 assert_equal "Assign\xc3\xa9 \xc3\xa0".force_encoding('UTF-8'), q.label_for('assigned_to_id')
1491 assert_equal "Assign\xc3\xa9 \xc3\xa0".force_encoding('UTF-8'), q.label_for('assigned_to_id')
1470 end
1492 end
1471
1493
1472 def test_editable_by
1494 def test_editable_by
1473 admin = User.find(1)
1495 admin = User.find(1)
1474 manager = User.find(2)
1496 manager = User.find(2)
1475 developer = User.find(3)
1497 developer = User.find(3)
1476
1498
1477 # Public query on project 1
1499 # Public query on project 1
1478 q = IssueQuery.find(1)
1500 q = IssueQuery.find(1)
1479 assert q.editable_by?(admin)
1501 assert q.editable_by?(admin)
1480 assert q.editable_by?(manager)
1502 assert q.editable_by?(manager)
1481 assert !q.editable_by?(developer)
1503 assert !q.editable_by?(developer)
1482
1504
1483 # Private query on project 1
1505 # Private query on project 1
1484 q = IssueQuery.find(2)
1506 q = IssueQuery.find(2)
1485 assert q.editable_by?(admin)
1507 assert q.editable_by?(admin)
1486 assert !q.editable_by?(manager)
1508 assert !q.editable_by?(manager)
1487 assert q.editable_by?(developer)
1509 assert q.editable_by?(developer)
1488
1510
1489 # Private query for all projects
1511 # Private query for all projects
1490 q = IssueQuery.find(3)
1512 q = IssueQuery.find(3)
1491 assert q.editable_by?(admin)
1513 assert q.editable_by?(admin)
1492 assert !q.editable_by?(manager)
1514 assert !q.editable_by?(manager)
1493 assert q.editable_by?(developer)
1515 assert q.editable_by?(developer)
1494
1516
1495 # Public query for all projects
1517 # Public query for all projects
1496 q = IssueQuery.find(4)
1518 q = IssueQuery.find(4)
1497 assert q.editable_by?(admin)
1519 assert q.editable_by?(admin)
1498 assert !q.editable_by?(manager)
1520 assert !q.editable_by?(manager)
1499 assert !q.editable_by?(developer)
1521 assert !q.editable_by?(developer)
1500 end
1522 end
1501
1523
1502 def test_visible_scope
1524 def test_visible_scope
1503 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1525 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1504
1526
1505 assert query_ids.include?(1), 'public query on public project was not visible'
1527 assert query_ids.include?(1), 'public query on public project was not visible'
1506 assert query_ids.include?(4), 'public query for all projects was not visible'
1528 assert query_ids.include?(4), 'public query for all projects was not visible'
1507 assert !query_ids.include?(2), 'private query on public project was visible'
1529 assert !query_ids.include?(2), 'private query on public project was visible'
1508 assert !query_ids.include?(3), 'private query for all projects was visible'
1530 assert !query_ids.include?(3), 'private query for all projects was visible'
1509 assert !query_ids.include?(7), 'public query on private project was visible'
1531 assert !query_ids.include?(7), 'public query on private project was visible'
1510 end
1532 end
1511
1533
1512 def test_query_with_public_visibility_should_be_visible_to_anyone
1534 def test_query_with_public_visibility_should_be_visible_to_anyone
1513 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1535 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
1514
1536
1515 assert q.visible?(User.anonymous)
1537 assert q.visible?(User.anonymous)
1516 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1538 assert IssueQuery.visible(User.anonymous).find_by_id(q.id)
1517
1539
1518 assert q.visible?(User.find(7))
1540 assert q.visible?(User.find(7))
1519 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1541 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1520
1542
1521 assert q.visible?(User.find(2))
1543 assert q.visible?(User.find(2))
1522 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1544 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1523
1545
1524 assert q.visible?(User.find(1))
1546 assert q.visible?(User.find(1))
1525 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1547 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1526 end
1548 end
1527
1549
1528 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1550 def test_query_with_roles_visibility_should_be_visible_to_user_with_role
1529 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1551 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_ROLES, :role_ids => [1,2])
1530
1552
1531 assert !q.visible?(User.anonymous)
1553 assert !q.visible?(User.anonymous)
1532 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1554 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1533
1555
1534 assert !q.visible?(User.find(7))
1556 assert !q.visible?(User.find(7))
1535 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1557 assert_nil IssueQuery.visible(User.find(7)).find_by_id(q.id)
1536
1558
1537 assert q.visible?(User.find(2))
1559 assert q.visible?(User.find(2))
1538 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1560 assert IssueQuery.visible(User.find(2)).find_by_id(q.id)
1539
1561
1540 assert q.visible?(User.find(1))
1562 assert q.visible?(User.find(1))
1541 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1563 assert IssueQuery.visible(User.find(1)).find_by_id(q.id)
1542 end
1564 end
1543
1565
1544 def test_query_with_private_visibility_should_be_visible_to_owner
1566 def test_query_with_private_visibility_should_be_visible_to_owner
1545 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1567 q = IssueQuery.create!(:name => 'Query', :visibility => IssueQuery::VISIBILITY_PRIVATE, :user => User.find(7))
1546
1568
1547 assert !q.visible?(User.anonymous)
1569 assert !q.visible?(User.anonymous)
1548 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1570 assert_nil IssueQuery.visible(User.anonymous).find_by_id(q.id)
1549
1571
1550 assert q.visible?(User.find(7))
1572 assert q.visible?(User.find(7))
1551 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1573 assert IssueQuery.visible(User.find(7)).find_by_id(q.id)
1552
1574
1553 assert !q.visible?(User.find(2))
1575 assert !q.visible?(User.find(2))
1554 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1576 assert_nil IssueQuery.visible(User.find(2)).find_by_id(q.id)
1555
1577
1556 assert q.visible?(User.find(1))
1578 assert q.visible?(User.find(1))
1557 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1579 assert_nil IssueQuery.visible(User.find(1)).find_by_id(q.id)
1558 end
1580 end
1559
1581
1560 test "#available_filters should include users of visible projects in cross-project view" do
1582 test "#available_filters should include users of visible projects in cross-project view" do
1561 users = IssueQuery.new.available_filters["assigned_to_id"]
1583 users = IssueQuery.new.available_filters["assigned_to_id"]
1562 assert_not_nil users
1584 assert_not_nil users
1563 assert users[:values].map{|u|u[1]}.include?("3")
1585 assert users[:values].map{|u|u[1]}.include?("3")
1564 end
1586 end
1565
1587
1566 test "#available_filters should include users of subprojects" do
1588 test "#available_filters should include users of subprojects" do
1567 user1 = User.generate!
1589 user1 = User.generate!
1568 user2 = User.generate!
1590 user2 = User.generate!
1569 project = Project.find(1)
1591 project = Project.find(1)
1570 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1592 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1571
1593
1572 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1594 users = IssueQuery.new(:project => project).available_filters["assigned_to_id"]
1573 assert_not_nil users
1595 assert_not_nil users
1574 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1596 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1575 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1597 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1576 end
1598 end
1577
1599
1578 test "#available_filters should include visible projects in cross-project view" do
1600 test "#available_filters should include visible projects in cross-project view" do
1579 projects = IssueQuery.new.available_filters["project_id"]
1601 projects = IssueQuery.new.available_filters["project_id"]
1580 assert_not_nil projects
1602 assert_not_nil projects
1581 assert projects[:values].map{|u|u[1]}.include?("1")
1603 assert projects[:values].map{|u|u[1]}.include?("1")
1582 end
1604 end
1583
1605
1584 test "#available_filters should include 'member_of_group' filter" do
1606 test "#available_filters should include 'member_of_group' filter" do
1585 query = IssueQuery.new
1607 query = IssueQuery.new
1586 assert query.available_filters.keys.include?("member_of_group")
1608 assert query.available_filters.keys.include?("member_of_group")
1587 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1609 assert_equal :list_optional, query.available_filters["member_of_group"][:type]
1588 assert query.available_filters["member_of_group"][:values].present?
1610 assert query.available_filters["member_of_group"][:values].present?
1589 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1611 assert_equal Group.givable.sort.map {|g| [g.name, g.id.to_s]},
1590 query.available_filters["member_of_group"][:values].sort
1612 query.available_filters["member_of_group"][:values].sort
1591 end
1613 end
1592
1614
1593 test "#available_filters should include 'assigned_to_role' filter" do
1615 test "#available_filters should include 'assigned_to_role' filter" do
1594 query = IssueQuery.new
1616 query = IssueQuery.new
1595 assert query.available_filters.keys.include?("assigned_to_role")
1617 assert query.available_filters.keys.include?("assigned_to_role")
1596 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1618 assert_equal :list_optional, query.available_filters["assigned_to_role"][:type]
1597
1619
1598 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1620 assert query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1599 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1621 assert query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1600 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1622 assert query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1601
1623
1602 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1624 assert ! query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1603 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1625 assert ! query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1604 end
1626 end
1605
1627
1606 def test_available_filters_should_include_custom_field_according_to_user_visibility
1628 def test_available_filters_should_include_custom_field_according_to_user_visibility
1607 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1629 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1608 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1630 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1609
1631
1610 with_current_user User.find(3) do
1632 with_current_user User.find(3) do
1611 query = IssueQuery.new
1633 query = IssueQuery.new
1612 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1634 assert_include "cf_#{visible_field.id}", query.available_filters.keys
1613 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1635 assert_not_include "cf_#{hidden_field.id}", query.available_filters.keys
1614 end
1636 end
1615 end
1637 end
1616
1638
1617 def test_available_columns_should_include_custom_field_according_to_user_visibility
1639 def test_available_columns_should_include_custom_field_according_to_user_visibility
1618 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1640 visible_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => true)
1619 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1641 hidden_field = IssueCustomField.generate!(:is_for_all => true, :is_filter => true, :visible => false, :role_ids => [1])
1620
1642
1621 with_current_user User.find(3) do
1643 with_current_user User.find(3) do
1622 query = IssueQuery.new
1644 query = IssueQuery.new
1623 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1645 assert_include :"cf_#{visible_field.id}", query.available_columns.map(&:name)
1624 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1646 assert_not_include :"cf_#{hidden_field.id}", query.available_columns.map(&:name)
1625 end
1647 end
1626 end
1648 end
1627
1649
1628 def setup_member_of_group
1650 def setup_member_of_group
1629 Group.destroy_all # No fixtures
1651 Group.destroy_all # No fixtures
1630 @user_in_group = User.generate!
1652 @user_in_group = User.generate!
1631 @second_user_in_group = User.generate!
1653 @second_user_in_group = User.generate!
1632 @user_in_group2 = User.generate!
1654 @user_in_group2 = User.generate!
1633 @user_not_in_group = User.generate!
1655 @user_not_in_group = User.generate!
1634
1656
1635 @group = Group.generate!.reload
1657 @group = Group.generate!.reload
1636 @group.users << @user_in_group
1658 @group.users << @user_in_group
1637 @group.users << @second_user_in_group
1659 @group.users << @second_user_in_group
1638
1660
1639 @group2 = Group.generate!.reload
1661 @group2 = Group.generate!.reload
1640 @group2.users << @user_in_group2
1662 @group2.users << @user_in_group2
1641
1663
1642 @query = IssueQuery.new(:name => '_')
1664 @query = IssueQuery.new(:name => '_')
1643 end
1665 end
1644
1666
1645 test "member_of_group filter should search assigned to for users in the group" do
1667 test "member_of_group filter should search assigned to for users in the group" do
1646 setup_member_of_group
1668 setup_member_of_group
1647 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1669 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1648
1670
1649 assert_find_issues_with_query_is_successful @query
1671 assert_find_issues_with_query_is_successful @query
1650 end
1672 end
1651
1673
1652 test "member_of_group filter should search not assigned to any group member (none)" do
1674 test "member_of_group filter should search not assigned to any group member (none)" do
1653 setup_member_of_group
1675 setup_member_of_group
1654 @query.add_filter('member_of_group', '!*', [''])
1676 @query.add_filter('member_of_group', '!*', [''])
1655
1677
1656 assert_find_issues_with_query_is_successful @query
1678 assert_find_issues_with_query_is_successful @query
1657 end
1679 end
1658
1680
1659 test "member_of_group filter should search assigned to any group member (all)" do
1681 test "member_of_group filter should search assigned to any group member (all)" do
1660 setup_member_of_group
1682 setup_member_of_group
1661 @query.add_filter('member_of_group', '*', [''])
1683 @query.add_filter('member_of_group', '*', [''])
1662
1684
1663 assert_find_issues_with_query_is_successful @query
1685 assert_find_issues_with_query_is_successful @query
1664 end
1686 end
1665
1687
1666 test "member_of_group filter should return an empty set with = empty group" do
1688 test "member_of_group filter should return an empty set with = empty group" do
1667 setup_member_of_group
1689 setup_member_of_group
1668 @empty_group = Group.generate!
1690 @empty_group = Group.generate!
1669 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1691 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1670
1692
1671 assert_equal [], find_issues_with_query(@query)
1693 assert_equal [], find_issues_with_query(@query)
1672 end
1694 end
1673
1695
1674 test "member_of_group filter should return issues with ! empty group" do
1696 test "member_of_group filter should return issues with ! empty group" do
1675 setup_member_of_group
1697 setup_member_of_group
1676 @empty_group = Group.generate!
1698 @empty_group = Group.generate!
1677 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1699 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1678
1700
1679 assert_find_issues_with_query_is_successful @query
1701 assert_find_issues_with_query_is_successful @query
1680 end
1702 end
1681
1703
1682 def setup_assigned_to_role
1704 def setup_assigned_to_role
1683 @manager_role = Role.find_by_name('Manager')
1705 @manager_role = Role.find_by_name('Manager')
1684 @developer_role = Role.find_by_name('Developer')
1706 @developer_role = Role.find_by_name('Developer')
1685
1707
1686 @project = Project.generate!
1708 @project = Project.generate!
1687 @manager = User.generate!
1709 @manager = User.generate!
1688 @developer = User.generate!
1710 @developer = User.generate!
1689 @boss = User.generate!
1711 @boss = User.generate!
1690 @guest = User.generate!
1712 @guest = User.generate!
1691 User.add_to_project(@manager, @project, @manager_role)
1713 User.add_to_project(@manager, @project, @manager_role)
1692 User.add_to_project(@developer, @project, @developer_role)
1714 User.add_to_project(@developer, @project, @developer_role)
1693 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1715 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1694
1716
1695 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1717 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1696 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1718 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1697 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1719 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1698 @issue4 = Issue.generate!(:project => @project, :author_id => @guest.id, :assigned_to_id => @guest.id)
1720 @issue4 = Issue.generate!(:project => @project, :author_id => @guest.id, :assigned_to_id => @guest.id)
1699 @issue5 = Issue.generate!(:project => @project)
1721 @issue5 = Issue.generate!(:project => @project)
1700
1722
1701 @query = IssueQuery.new(:name => '_', :project => @project)
1723 @query = IssueQuery.new(:name => '_', :project => @project)
1702 end
1724 end
1703
1725
1704 test "assigned_to_role filter should search assigned to for users with the Role" do
1726 test "assigned_to_role filter should search assigned to for users with the Role" do
1705 setup_assigned_to_role
1727 setup_assigned_to_role
1706 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1728 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1707
1729
1708 assert_query_result [@issue1, @issue3], @query
1730 assert_query_result [@issue1, @issue3], @query
1709 end
1731 end
1710
1732
1711 test "assigned_to_role filter should search assigned to for users with the Role on the issue project" do
1733 test "assigned_to_role filter should search assigned to for users with the Role on the issue project" do
1712 setup_assigned_to_role
1734 setup_assigned_to_role
1713 other_project = Project.generate!
1735 other_project = Project.generate!
1714 User.add_to_project(@developer, other_project, @manager_role)
1736 User.add_to_project(@developer, other_project, @manager_role)
1715 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1737 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1716
1738
1717 assert_query_result [@issue1, @issue3], @query
1739 assert_query_result [@issue1, @issue3], @query
1718 end
1740 end
1719
1741
1720 test "assigned_to_role filter should return an empty set with empty role" do
1742 test "assigned_to_role filter should return an empty set with empty role" do
1721 setup_assigned_to_role
1743 setup_assigned_to_role
1722 @empty_role = Role.generate!
1744 @empty_role = Role.generate!
1723 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1745 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1724
1746
1725 assert_query_result [], @query
1747 assert_query_result [], @query
1726 end
1748 end
1727
1749
1728 test "assigned_to_role filter should search assigned to for users without the Role" do
1750 test "assigned_to_role filter should search assigned to for users without the Role" do
1729 setup_assigned_to_role
1751 setup_assigned_to_role
1730 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1752 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1731
1753
1732 assert_query_result [@issue2, @issue4, @issue5], @query
1754 assert_query_result [@issue2, @issue4, @issue5], @query
1733 end
1755 end
1734
1756
1735 test "assigned_to_role filter should search assigned to for users not assigned to any Role (none)" do
1757 test "assigned_to_role filter should search assigned to for users not assigned to any Role (none)" do
1736 setup_assigned_to_role
1758 setup_assigned_to_role
1737 @query.add_filter('assigned_to_role', '!*', [''])
1759 @query.add_filter('assigned_to_role', '!*', [''])
1738
1760
1739 assert_query_result [@issue4, @issue5], @query
1761 assert_query_result [@issue4, @issue5], @query
1740 end
1762 end
1741
1763
1742 test "assigned_to_role filter should search assigned to for users assigned to any Role (all)" do
1764 test "assigned_to_role filter should search assigned to for users assigned to any Role (all)" do
1743 setup_assigned_to_role
1765 setup_assigned_to_role
1744 @query.add_filter('assigned_to_role', '*', [''])
1766 @query.add_filter('assigned_to_role', '*', [''])
1745
1767
1746 assert_query_result [@issue1, @issue2, @issue3], @query
1768 assert_query_result [@issue1, @issue2, @issue3], @query
1747 end
1769 end
1748
1770
1749 test "assigned_to_role filter should return issues with ! empty role" do
1771 test "assigned_to_role filter should return issues with ! empty role" do
1750 setup_assigned_to_role
1772 setup_assigned_to_role
1751 @empty_role = Role.generate!
1773 @empty_role = Role.generate!
1752 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1774 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1753
1775
1754 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1776 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1755 end
1777 end
1756
1778
1757 def test_query_column_should_accept_a_symbol_as_caption
1779 def test_query_column_should_accept_a_symbol_as_caption
1758 set_language_if_valid 'en'
1780 set_language_if_valid 'en'
1759 c = QueryColumn.new('foo', :caption => :general_text_Yes)
1781 c = QueryColumn.new('foo', :caption => :general_text_Yes)
1760 assert_equal 'Yes', c.caption
1782 assert_equal 'Yes', c.caption
1761 end
1783 end
1762
1784
1763 def test_query_column_should_accept_a_proc_as_caption
1785 def test_query_column_should_accept_a_proc_as_caption
1764 c = QueryColumn.new('foo', :caption => lambda {'Foo'})
1786 c = QueryColumn.new('foo', :caption => lambda {'Foo'})
1765 assert_equal 'Foo', c.caption
1787 assert_equal 'Foo', c.caption
1766 end
1788 end
1767
1789
1768 def test_date_clause_should_respect_user_time_zone_with_local_default
1790 def test_date_clause_should_respect_user_time_zone_with_local_default
1769 @query = IssueQuery.new(:name => '_')
1791 @query = IssueQuery.new(:name => '_')
1770
1792
1771 # user is in Hawaii (-10)
1793 # user is in Hawaii (-10)
1772 User.current = users(:users_001)
1794 User.current = users(:users_001)
1773 User.current.pref.update_attribute :time_zone, 'Hawaii'
1795 User.current.pref.update_attribute :time_zone, 'Hawaii'
1774
1796
1775 # assume timestamps are stored in server local time
1797 # assume timestamps are stored in server local time
1776 local_zone = Time.zone
1798 local_zone = Time.zone
1777
1799
1778 from = Date.parse '2016-03-20'
1800 from = Date.parse '2016-03-20'
1779 to = Date.parse '2016-03-22'
1801 to = Date.parse '2016-03-22'
1780 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1802 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1781
1803
1782 # the dates should have been interpreted in the user's time zone and
1804 # the dates should have been interpreted in the user's time zone and
1783 # converted to local time
1805 # converted to local time
1784 # what we get exactly in the sql depends on the local time zone, therefore
1806 # what we get exactly in the sql depends on the local time zone, therefore
1785 # it's computed here.
1807 # it's computed here.
1786 f = User.current.time_zone.local(from.year, from.month, from.day).yesterday.end_of_day.in_time_zone(local_zone)
1808 f = User.current.time_zone.local(from.year, from.month, from.day).yesterday.end_of_day.in_time_zone(local_zone)
1787 t = User.current.time_zone.local(to.year, to.month, to.day).end_of_day.in_time_zone(local_zone)
1809 t = User.current.time_zone.local(to.year, to.month, to.day).end_of_day.in_time_zone(local_zone)
1788 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1810 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1789 end
1811 end
1790
1812
1791 def test_date_clause_should_respect_user_time_zone_with_utc_default
1813 def test_date_clause_should_respect_user_time_zone_with_utc_default
1792 @query = IssueQuery.new(:name => '_')
1814 @query = IssueQuery.new(:name => '_')
1793
1815
1794 # user is in Hawaii (-10)
1816 # user is in Hawaii (-10)
1795 User.current = users(:users_001)
1817 User.current = users(:users_001)
1796 User.current.pref.update_attribute :time_zone, 'Hawaii'
1818 User.current.pref.update_attribute :time_zone, 'Hawaii'
1797
1819
1798 # assume timestamps are stored as utc
1820 # assume timestamps are stored as utc
1799 ActiveRecord::Base.default_timezone = :utc
1821 ActiveRecord::Base.default_timezone = :utc
1800
1822
1801 from = Date.parse '2016-03-20'
1823 from = Date.parse '2016-03-20'
1802 to = Date.parse '2016-03-22'
1824 to = Date.parse '2016-03-22'
1803 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1825 assert c = @query.send(:date_clause, 'table', 'field', from, to, false)
1804 # the dates should have been interpreted in the user's time zone and
1826 # the dates should have been interpreted in the user's time zone and
1805 # converted to utc. March 20 in Hawaii begins at 10am UTC.
1827 # converted to utc. March 20 in Hawaii begins at 10am UTC.
1806 f = Time.new(2016, 3, 20, 9, 59, 59, 0).end_of_hour
1828 f = Time.new(2016, 3, 20, 9, 59, 59, 0).end_of_hour
1807 t = Time.new(2016, 3, 23, 9, 59, 59, 0).end_of_hour
1829 t = Time.new(2016, 3, 23, 9, 59, 59, 0).end_of_hour
1808 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1830 assert_equal "table.field > '#{Query.connection.quoted_date f}' AND table.field <= '#{Query.connection.quoted_date t}'", c
1809 ensure
1831 ensure
1810 ActiveRecord::Base.default_timezone = :local # restore Redmine default
1832 ActiveRecord::Base.default_timezone = :local # restore Redmine default
1811 end
1833 end
1812
1834
1813 end
1835 end
@@ -1,81 +1,103
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TimeEntryQueryTest < ActiveSupport::TestCase
20 class TimeEntryQueryTest < ActiveSupport::TestCase
21 fixtures :issues, :projects, :users,
21 fixtures :issues, :projects, :users,
22 :members, :roles, :member_roles,
22 :members, :roles, :member_roles,
23 :trackers, :issue_statuses,
23 :trackers, :issue_statuses,
24 :projects_trackers,
24 :projects_trackers,
25 :journals, :journal_details,
25 :journals, :journal_details,
26 :issue_categories, :enumerations,
26 :issue_categories, :enumerations,
27 :groups_users,
27 :groups_users,
28 :enabled_modules
28 :enabled_modules
29
29
30 def test_filter_values_without_project_should_be_arrays
31 q = TimeEntryQuery.new
32 assert_nil q.project
33
34 q.available_filters.each do |name, filter|
35 values = filter.values
36 assert (values.nil? || values.is_a?(Array)),
37 "#values for #{name} filter returned a #{values.class.name}"
38 end
39 end
40
41 def test_filter_values_with_project_should_be_arrays
42 q = TimeEntryQuery.new(:project => Project.find(1))
43 assert_not_nil q.project
44
45 q.available_filters.each do |name, filter|
46 values = filter.values
47 assert (values.nil? || values.is_a?(Array)),
48 "#values for #{name} filter returned a #{values.class.name}"
49 end
50 end
51
30 def test_cross_project_activity_filter_should_propose_non_active_activities
52 def test_cross_project_activity_filter_should_propose_non_active_activities
31 activity = TimeEntryActivity.create!(:name => 'Disabled', :active => false)
53 activity = TimeEntryActivity.create!(:name => 'Disabled', :active => false)
32 assert !activity.active?
54 assert !activity.active?
33
55
34 query = TimeEntryQuery.new(:name => '_')
56 query = TimeEntryQuery.new(:name => '_')
35 assert options = query.available_filters['activity_id']
57 assert options = query.available_filters['activity_id']
36 assert values = options[:values]
58 assert values = options[:values]
37 assert_include ["Disabled", activity.id.to_s], values
59 assert_include ["Disabled", activity.id.to_s], values
38 end
60 end
39
61
40 def test_activity_filter_should_consider_system_and_project_activities
62 def test_activity_filter_should_consider_system_and_project_activities
41 TimeEntry.delete_all
63 TimeEntry.delete_all
42 system = TimeEntryActivity.create!(:name => 'Foo')
64 system = TimeEntryActivity.create!(:name => 'Foo')
43 TimeEntry.generate!(:activity => system, :hours => 1.0)
65 TimeEntry.generate!(:activity => system, :hours => 1.0)
44 override = TimeEntryActivity.create!(:name => 'Foo', :parent_id => system.id, :project_id => 1)
66 override = TimeEntryActivity.create!(:name => 'Foo', :parent_id => system.id, :project_id => 1)
45 other = TimeEntryActivity.create!(:name => 'Bar')
67 other = TimeEntryActivity.create!(:name => 'Bar')
46 TimeEntry.generate!(:activity => override, :hours => 2.0)
68 TimeEntry.generate!(:activity => override, :hours => 2.0)
47 TimeEntry.generate!(:activity => other, :hours => 4.0)
69 TimeEntry.generate!(:activity => other, :hours => 4.0)
48
70
49 query = TimeEntryQuery.new(:name => '_')
71 query = TimeEntryQuery.new(:name => '_')
50 query.add_filter('activity_id', '=', [system.id.to_s])
72 query.add_filter('activity_id', '=', [system.id.to_s])
51 assert_equal 3.0, query.results_scope.sum(:hours)
73 assert_equal 3.0, query.results_scope.sum(:hours)
52
74
53 query = TimeEntryQuery.new(:name => '_')
75 query = TimeEntryQuery.new(:name => '_')
54 query.add_filter('activity_id', '!', [system.id.to_s])
76 query.add_filter('activity_id', '!', [system.id.to_s])
55 assert_equal 4.0, query.results_scope.sum(:hours)
77 assert_equal 4.0, query.results_scope.sum(:hours)
56 end
78 end
57
79
58 def test_project_query_should_include_project_issue_custom_fields_only_as_filters
80 def test_project_query_should_include_project_issue_custom_fields_only_as_filters
59 global = IssueCustomField.generate!(:is_for_all => true, :is_filter => true)
81 global = IssueCustomField.generate!(:is_for_all => true, :is_filter => true)
60 field_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [3], :is_filter => true)
82 field_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [3], :is_filter => true)
61 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
83 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
62
84
63 query = TimeEntryQuery.new(:project => Project.find(3))
85 query = TimeEntryQuery.new(:project => Project.find(3))
64
86
65 assert_include "issue.cf_#{global.id}", query.available_filters.keys
87 assert_include "issue.cf_#{global.id}", query.available_filters.keys
66 assert_include "issue.cf_#{field_on_project.id}", query.available_filters.keys
88 assert_include "issue.cf_#{field_on_project.id}", query.available_filters.keys
67 assert_not_include "issue.cf_#{field_not_on_project.id}", query.available_filters.keys
89 assert_not_include "issue.cf_#{field_not_on_project.id}", query.available_filters.keys
68 end
90 end
69
91
70 def test_project_query_should_include_project_issue_custom_fields_only_as_columns
92 def test_project_query_should_include_project_issue_custom_fields_only_as_columns
71 global = IssueCustomField.generate!(:is_for_all => true, :is_filter => true)
93 global = IssueCustomField.generate!(:is_for_all => true, :is_filter => true)
72 field_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [3], :is_filter => true)
94 field_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [3], :is_filter => true)
73 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
95 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
74
96
75 query = TimeEntryQuery.new(:project => Project.find(3))
97 query = TimeEntryQuery.new(:project => Project.find(3))
76
98
77 assert_include "issue.cf_#{global.id}", query.available_columns.map(&:name).map(&:to_s)
99 assert_include "issue.cf_#{global.id}", query.available_columns.map(&:name).map(&:to_s)
78 assert_include "issue.cf_#{field_on_project.id}", query.available_columns.map(&:name).map(&:to_s)
100 assert_include "issue.cf_#{field_on_project.id}", query.available_columns.map(&:name).map(&:to_s)
79 assert_not_include "issue.cf_#{field_not_on_project.id}", query.available_columns.map(&:name).map(&:to_s)
101 assert_not_include "issue.cf_#{field_not_on_project.id}", query.available_columns.map(&:name).map(&:to_s)
80 end
102 end
81 end
103 end
General Comments 0
You need to be logged in to leave comments. Login now