##// END OF EJS Templates
Consider custom fields that correspond to the project only (#24014)....
Jean-Philippe Lang -
r15508:960b32238830
parent child
Show More
@@ -1,236 +1,246
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class TimeEntryQuery < Query
19 19
20 20 self.queried_class = TimeEntry
21 21 self.view_permission = :view_time_entries
22 22
23 23 self.available_columns = [
24 24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 25 QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
26 26 QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :caption => l(:label_week)),
27 27 QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
28 28 QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
29 29 QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id"),
30 30 QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
31 31 QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
32 32 QueryColumn.new(:comments),
33 33 QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
34 34 ]
35 35
36 36 def initialize(attributes=nil, *args)
37 37 super attributes
38 38 self.filters ||= {}
39 39 add_filter('spent_on', '*') unless filters.present?
40 40 end
41 41
42 42 def initialize_available_filters
43 43 add_available_filter "spent_on", :type => :date_past
44 44
45 45 principals = []
46 46 versions = []
47 47 if project
48 48 principals += project.principals.visible.sort
49 49 unless project.leaf?
50 50 subprojects = project.descendants.visible.to_a
51 51 if subprojects.any?
52 52 add_available_filter "subproject_id",
53 53 :type => :list_subprojects,
54 54 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
55 55 principals += Principal.member_of(subprojects).visible
56 56 end
57 57 end
58 58 versions = project.shared_versions.to_a
59 59 else
60 60 if all_projects.any?
61 61 # members of visible projects
62 62 principals += Principal.member_of(all_projects).visible
63 63 # project filter
64 64 project_values = []
65 65 if User.current.logged? && User.current.memberships.any?
66 66 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
67 67 end
68 68 project_values += all_projects_values
69 69 add_available_filter("project_id",
70 70 :type => :list, :values => project_values
71 71 ) unless project_values.empty?
72 72 end
73 73 end
74 74
75 75 add_available_filter("issue_id", :type => :tree, :label => :label_issue)
76 76 add_available_filter("issue.tracker_id",
77 77 :type => :list,
78 78 :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
79 79 :values => Tracker.sorted.map {|t| [t.name, t.id.to_s]})
80 80 add_available_filter("issue.status_id",
81 81 :type => :list,
82 82 :name => l("label_attribute_of_issue", :name => l(:field_status)),
83 83 :values => IssueStatus.sorted.map {|s| [s.name, s.id.to_s]})
84 84 add_available_filter("issue.fixed_version_id",
85 85 :type => :list,
86 86 :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
87 87 :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] })
88 88
89 89 principals.uniq!
90 90 principals.sort!
91 91 users = principals.select {|p| p.is_a?(User)}
92 92
93 93 users_values = []
94 94 users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
95 95 users_values += users.collect{|s| [s.name, s.id.to_s] }
96 96 add_available_filter("user_id",
97 97 :type => :list_optional, :values => users_values
98 98 ) unless users_values.empty?
99 99
100 100 activities = (project ? project.activities : TimeEntryActivity.shared)
101 101 add_available_filter("activity_id",
102 102 :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]}
103 103 ) unless activities.empty?
104 104
105 105 add_available_filter "comments", :type => :text
106 106 add_available_filter "hours", :type => :float
107 107
108 108 add_custom_fields_filters(TimeEntryCustomField)
109 add_associations_custom_fields_filters :project, :issue, :user
109 add_associations_custom_fields_filters :project
110 add_custom_fields_filters(issue_custom_fields, :issue)
111 add_associations_custom_fields_filters :user
110 112 end
111 113
112 114 def available_columns
113 115 return @available_columns if @available_columns
114 116 @available_columns = self.class.available_columns.dup
115 117 @available_columns += TimeEntryCustomField.visible.
116 118 map {|cf| QueryCustomFieldColumn.new(cf) }
117 @available_columns += IssueCustomField.visible.
119 @available_columns += issue_custom_fields.visible.
118 120 map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false) }
119 121 @available_columns
120 122 end
121 123
122 124 def default_columns_names
123 125 @default_columns_names ||= [:project, :spent_on, :user, :activity, :issue, :comments, :hours]
124 126 end
125 127
126 128 def default_totalable_names
127 129 [:hours]
128 130 end
129 131
130 132 def base_scope
131 133 TimeEntry.visible.
132 134 joins(:project, :user).
133 135 joins("LEFT OUTER JOIN issues ON issues.id = time_entries.issue_id").
134 136 where(statement)
135 137 end
136 138
137 139 def results_scope(options={})
138 140 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
139 141
140 142 base_scope.
141 143 order(order_option).
142 144 joins(joins_for_order_statement(order_option.join(','))).
143 145 includes(:activity).
144 146 references(:activity)
145 147 end
146 148
147 149 # Returns sum of all the spent hours
148 150 def total_for_hours(scope)
149 151 map_total(scope.sum(:hours)) {|t| t.to_f.round(2)}
150 152 end
151 153
152 154 def sql_for_issue_id_field(field, operator, value)
153 155 case operator
154 156 when "="
155 157 "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
156 158 when "~"
157 159 issue = Issue.where(:id => value.first.to_i).first
158 160 if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
159 161 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
160 162 else
161 163 "1=0"
162 164 end
163 165 when "!*"
164 166 "#{TimeEntry.table_name}.issue_id IS NULL"
165 167 when "*"
166 168 "#{TimeEntry.table_name}.issue_id IS NOT NULL"
167 169 end
168 170 end
169 171
170 172 def sql_for_issue_fixed_version_id_field(field, operator, value)
171 173 issue_ids = Issue.where(:fixed_version_id => value.first.to_i).pluck(:id)
172 174 case operator
173 175 when "="
174 176 if issue_ids.any?
175 177 "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
176 178 else
177 179 "1=0"
178 180 end
179 181 when "!"
180 182 if issue_ids.any?
181 183 "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
182 184 else
183 185 "1=1"
184 186 end
185 187 end
186 188 end
187 189
188 190 def sql_for_activity_id_field(field, operator, value)
189 191 condition_on_id = sql_for_field(field, operator, value, Enumeration.table_name, 'id')
190 192 condition_on_parent_id = sql_for_field(field, operator, value, Enumeration.table_name, 'parent_id')
191 193 ids = value.map(&:to_i).join(',')
192 194 table_name = Enumeration.table_name
193 195 if operator == '='
194 196 "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
195 197 else
196 198 "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
197 199 end
198 200 end
199 201
200 202 def sql_for_issue_tracker_id_field(field, operator, value)
201 203 sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
202 204 end
203 205
204 206 def sql_for_issue_status_id_field(field, operator, value)
205 207 sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
206 208 end
207 209
208 210 # Accepts :from/:to params as shortcut filters
209 211 def build_from_params(params)
210 212 super
211 213 if params[:from].present? && params[:to].present?
212 214 add_filter('spent_on', '><', [params[:from], params[:to]])
213 215 elsif params[:from].present?
214 216 add_filter('spent_on', '>=', [params[:from]])
215 217 elsif params[:to].present?
216 218 add_filter('spent_on', '<=', [params[:to]])
217 219 end
218 220 self
219 221 end
220 222
221 223 def joins_for_order_statement(order_options)
222 224 joins = [super]
223 225
224 226 if order_options
225 227 if order_options.include?('issue_statuses')
226 228 joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
227 229 end
228 230 if order_options.include?('trackers')
229 231 joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
230 232 end
231 233 end
232 234
233 235 joins.compact!
234 236 joins.any? ? joins.join(' ') : nil
235 237 end
238
239 def issue_custom_fields
240 if project
241 project.all_issue_custom_fields
242 else
243 IssueCustomField.where(:is_for_all => true)
244 end
245 end
236 246 end
@@ -1,57 +1,81
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class TimeEntryQueryTest < ActiveSupport::TestCase
21 21 fixtures :issues, :projects, :users,
22 22 :members, :roles, :member_roles,
23 23 :trackers, :issue_statuses,
24 24 :projects_trackers,
25 25 :journals, :journal_details,
26 26 :issue_categories, :enumerations,
27 27 :groups_users,
28 28 :enabled_modules
29 29
30 30 def test_cross_project_activity_filter_should_propose_non_active_activities
31 31 activity = TimeEntryActivity.create!(:name => 'Disabled', :active => false)
32 32 assert !activity.active?
33 33
34 34 query = TimeEntryQuery.new(:name => '_')
35 35 assert options = query.available_filters['activity_id']
36 36 assert values = options[:values]
37 37 assert_include ["Disabled", activity.id.to_s], values
38 38 end
39 39
40 40 def test_activity_filter_should_consider_system_and_project_activities
41 41 TimeEntry.delete_all
42 42 system = TimeEntryActivity.create!(:name => 'Foo')
43 43 TimeEntry.generate!(:activity => system, :hours => 1.0)
44 44 override = TimeEntryActivity.create!(:name => 'Foo', :parent_id => system.id, :project_id => 1)
45 45 other = TimeEntryActivity.create!(:name => 'Bar')
46 46 TimeEntry.generate!(:activity => override, :hours => 2.0)
47 47 TimeEntry.generate!(:activity => other, :hours => 4.0)
48 48
49 49 query = TimeEntryQuery.new(:name => '_')
50 50 query.add_filter('activity_id', '=', [system.id.to_s])
51 51 assert_equal 3.0, query.results_scope.sum(:hours)
52 52
53 53 query = TimeEntryQuery.new(:name => '_')
54 54 query.add_filter('activity_id', '!', [system.id.to_s])
55 55 assert_equal 4.0, query.results_scope.sum(:hours)
56 56 end
57
58 def test_project_query_should_include_project_issue_custom_fields_only_as_filters
59 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)
61 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
62
63 query = TimeEntryQuery.new(:project => Project.find(3))
64
65 assert_include "issue.cf_#{global.id}", query.available_filters.keys
66 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
68 end
69
70 def test_project_query_should_include_project_issue_custom_fields_only_as_columns
71 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)
73 field_not_on_project = IssueCustomField.generate!(:is_for_all => false, :project_ids => [1,2], :is_filter => true)
74
75 query = TimeEntryQuery.new(:project => Project.find(3))
76
77 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)
79 assert_not_include "issue.cf_#{field_not_on_project.id}", query.available_columns.map(&:name).map(&:to_s)
80 end
57 81 end
General Comments 0
You need to be logged in to leave comments. Login now