##// END OF EJS Templates
Adds STI to Query model. Issue queries are now IssueQuery instances....
Jean-Philippe Lang -
r10737:ab066317e62b
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,321
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class IssueQuery < Query
19
20 self.available_columns = [
21 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
22 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
23 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
24 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
25 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
26 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
27 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
28 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
29 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
30 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
31 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
32 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
33 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
34 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
35 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
36 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
37 QueryColumn.new(:relations, :caption => :label_related_issues),
38 QueryColumn.new(:description, :inline => false)
39 ]
40
41 scope :visible, lambda {|*args|
42 user = args.shift || User.current
43 base = Project.allowed_to_condition(user, :view_issues, *args)
44 user_id = user.logged? ? user.id : 0
45
46 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
47 }
48
49 def initialize(attributes=nil, *args)
50 super attributes
51 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
52 end
53
54 # Returns true if the query is visible to +user+ or the current user.
55 def visible?(user=User.current)
56 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
57 end
58
59 def available_filters
60 return @available_filters if @available_filters
61 @available_filters = {
62 "status_id" => {
63 :type => :list_status, :order => 0,
64 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
65 },
66 "tracker_id" => {
67 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
68 },
69 "priority_id" => {
70 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
71 },
72 "subject" => { :type => :text, :order => 8 },
73 "created_on" => { :type => :date_past, :order => 9 },
74 "updated_on" => { :type => :date_past, :order => 10 },
75 "start_date" => { :type => :date, :order => 11 },
76 "due_date" => { :type => :date, :order => 12 },
77 "estimated_hours" => { :type => :float, :order => 13 },
78 "done_ratio" => { :type => :integer, :order => 14 }
79 }
80 IssueRelation::TYPES.each do |relation_type, options|
81 @available_filters[relation_type] = {
82 :type => :relation, :order => @available_filters.size + 100,
83 :label => options[:name]
84 }
85 end
86 principals = []
87 if project
88 principals += project.principals.sort
89 unless project.leaf?
90 subprojects = project.descendants.visible.all
91 if subprojects.any?
92 @available_filters["subproject_id"] = {
93 :type => :list_subprojects, :order => 13,
94 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
95 }
96 principals += Principal.member_of(subprojects)
97 end
98 end
99 else
100 if all_projects.any?
101 # members of visible projects
102 principals += Principal.member_of(all_projects)
103 # project filter
104 project_values = []
105 if User.current.logged? && User.current.memberships.any?
106 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
107 end
108 project_values += all_projects_values
109 @available_filters["project_id"] = {
110 :type => :list, :order => 1, :values => project_values
111 } unless project_values.empty?
112 end
113 end
114 principals.uniq!
115 principals.sort!
116 users = principals.select {|p| p.is_a?(User)}
117
118 assigned_to_values = []
119 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
120 assigned_to_values += (Setting.issue_group_assignment? ?
121 principals : users).collect{|s| [s.name, s.id.to_s] }
122 @available_filters["assigned_to_id"] = {
123 :type => :list_optional, :order => 4, :values => assigned_to_values
124 } unless assigned_to_values.empty?
125
126 author_values = []
127 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
128 author_values += users.collect{|s| [s.name, s.id.to_s] }
129 @available_filters["author_id"] = {
130 :type => :list, :order => 5, :values => author_values
131 } unless author_values.empty?
132
133 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
134 @available_filters["member_of_group"] = {
135 :type => :list_optional, :order => 6, :values => group_values
136 } unless group_values.empty?
137
138 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
139 @available_filters["assigned_to_role"] = {
140 :type => :list_optional, :order => 7, :values => role_values
141 } unless role_values.empty?
142
143 if User.current.logged?
144 @available_filters["watcher_id"] = {
145 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
146 }
147 end
148
149 if project
150 # project specific filters
151 categories = project.issue_categories.all
152 unless categories.empty?
153 @available_filters["category_id"] = {
154 :type => :list_optional, :order => 6,
155 :values => categories.collect{|s| [s.name, s.id.to_s] }
156 }
157 end
158 versions = project.shared_versions.all
159 unless versions.empty?
160 @available_filters["fixed_version_id"] = {
161 :type => :list_optional, :order => 7,
162 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
163 }
164 end
165 add_custom_fields_filters(project.all_issue_custom_fields)
166 else
167 # global filters for cross project issue list
168 system_shared_versions = Version.visible.find_all_by_sharing('system')
169 unless system_shared_versions.empty?
170 @available_filters["fixed_version_id"] = {
171 :type => :list_optional, :order => 7,
172 :values => system_shared_versions.sort.collect{|s|
173 ["#{s.project.name} - #{s.name}", s.id.to_s]
174 }
175 }
176 end
177 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
178 end
179 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
180 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
181 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
182 @available_filters["is_private"] = {
183 :type => :list, :order => 16,
184 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
185 }
186 end
187 Tracker.disabled_core_fields(trackers).each {|field|
188 @available_filters.delete field
189 }
190 @available_filters.each do |field, options|
191 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
192 end
193 @available_filters
194 end
195
196 def available_columns
197 return @available_columns if @available_columns
198 @available_columns = self.class.available_columns.dup
199 @available_columns += (project ?
200 project.all_issue_custom_fields :
201 IssueCustomField.all
202 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
203
204 if User.current.allowed_to?(:view_time_entries, project, :global => true)
205 index = nil
206 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
207 index = (index ? index + 1 : -1)
208 # insert the column after estimated_hours or at the end
209 @available_columns.insert index, QueryColumn.new(:spent_hours,
210 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
211 :default_order => 'desc',
212 :caption => :label_spent_time
213 )
214 end
215
216 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
217 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
218 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
219 end
220
221 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
222 @available_columns.reject! {|column|
223 disabled_fields.include?(column.name.to_s)
224 }
225
226 @available_columns
227 end
228
229 # Returns the issue count
230 def issue_count
231 Issue.visible.count(:include => [:status, :project], :conditions => statement)
232 rescue ::ActiveRecord::StatementInvalid => e
233 raise StatementInvalid.new(e.message)
234 end
235
236 # Returns the issue count by group or nil if query is not grouped
237 def issue_count_by_group
238 r = nil
239 if grouped?
240 begin
241 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
242 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
243 rescue ActiveRecord::RecordNotFound
244 r = {nil => issue_count}
245 end
246 c = group_by_column
247 if c.is_a?(QueryCustomFieldColumn)
248 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
249 end
250 end
251 r
252 rescue ::ActiveRecord::StatementInvalid => e
253 raise StatementInvalid.new(e.message)
254 end
255
256 # Returns the issues
257 # Valid options are :order, :offset, :limit, :include, :conditions
258 def issues(options={})
259 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
260 order_option = nil if order_option.blank?
261
262 issues = Issue.visible.where(options[:conditions]).all(
263 :include => ([:status, :project] + (options[:include] || [])).uniq,
264 :conditions => statement,
265 :order => order_option,
266 :joins => joins_for_order_statement(order_option),
267 :limit => options[:limit],
268 :offset => options[:offset]
269 )
270
271 if has_column?(:spent_hours)
272 Issue.load_visible_spent_hours(issues)
273 end
274 if has_column?(:relations)
275 Issue.load_visible_relations(issues)
276 end
277 issues
278 rescue ::ActiveRecord::StatementInvalid => e
279 raise StatementInvalid.new(e.message)
280 end
281
282 # Returns the issues ids
283 def issue_ids(options={})
284 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
285 order_option = nil if order_option.blank?
286
287 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
288 :conditions => statement,
289 :order => order_option,
290 :joins => joins_for_order_statement(order_option),
291 :limit => options[:limit],
292 :offset => options[:offset]).find_ids
293 rescue ::ActiveRecord::StatementInvalid => e
294 raise StatementInvalid.new(e.message)
295 end
296
297 # Returns the journals
298 # Valid options are :order, :offset, :limit
299 def journals(options={})
300 Journal.visible.all(
301 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
302 :conditions => statement,
303 :order => options[:order],
304 :limit => options[:limit],
305 :offset => options[:offset]
306 )
307 rescue ::ActiveRecord::StatementInvalid => e
308 raise StatementInvalid.new(e.message)
309 end
310
311 # Returns the versions
312 # Valid options are :conditions
313 def versions(options={})
314 Version.visible.where(options[:conditions]).all(
315 :include => :project,
316 :conditions => project_statement
317 )
318 rescue ::ActiveRecord::StatementInvalid => e
319 raise StatementInvalid.new(e.message)
320 end
321 end
@@ -0,0 +1,9
1 class AddQueriesType < ActiveRecord::Migration
2 def up
3 add_column :queries, :type, :string
4 end
5
6 def down
7 remove_column :queries, :type
8 end
9 end
@@ -0,0 +1,9
1 class UpdateQueriesToSti < ActiveRecord::Migration
2 def up
3 ::Query.update_all :type => 'IssueQuery'
4 end
5
6 def down
7 ::Query.update_all :type => nil
8 end
9 end
@@ -1,107 +1,107
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueriesController < ApplicationController
18 class QueriesController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_query, :except => [:new, :create, :index]
20 before_filter :find_query, :except => [:new, :create, :index]
21 before_filter :find_optional_project, :only => [:new, :create]
21 before_filter :find_optional_project, :only => [:new, :create]
22
22
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 include QueriesHelper
25 include QueriesHelper
26
26
27 def index
27 def index
28 case params[:format]
28 case params[:format]
29 when 'xml', 'json'
29 when 'xml', 'json'
30 @offset, @limit = api_offset_and_limit
30 @offset, @limit = api_offset_and_limit
31 else
31 else
32 @limit = per_page_option
32 @limit = per_page_option
33 end
33 end
34
34
35 @query_count = Query.visible.count
35 @query_count = IssueQuery.visible.count
36 @query_pages = Paginator.new self, @query_count, @limit, params['page']
36 @query_pages = Paginator.new self, @query_count, @limit, params['page']
37 @queries = Query.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
37 @queries = IssueQuery.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
38
38
39 respond_to do |format|
39 respond_to do |format|
40 format.html { render :nothing => true }
40 format.html { render :nothing => true }
41 format.api
41 format.api
42 end
42 end
43 end
43 end
44
44
45 def new
45 def new
46 @query = Query.new
46 @query = IssueQuery.new
47 @query.user = User.current
47 @query.user = User.current
48 @query.project = @project
48 @query.project = @project
49 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
49 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
50 build_query_from_params
50 build_query_from_params
51 end
51 end
52
52
53 def create
53 def create
54 @query = Query.new(params[:query])
54 @query = IssueQuery.new(params[:query])
55 @query.user = User.current
55 @query.user = User.current
56 @query.project = params[:query_is_for_all] ? nil : @project
56 @query.project = params[:query_is_for_all] ? nil : @project
57 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
57 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
58 build_query_from_params
58 build_query_from_params
59 @query.column_names = nil if params[:default_columns]
59 @query.column_names = nil if params[:default_columns]
60
60
61 if @query.save
61 if @query.save
62 flash[:notice] = l(:notice_successful_create)
62 flash[:notice] = l(:notice_successful_create)
63 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
63 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
64 else
64 else
65 render :action => 'new', :layout => !request.xhr?
65 render :action => 'new', :layout => !request.xhr?
66 end
66 end
67 end
67 end
68
68
69 def edit
69 def edit
70 end
70 end
71
71
72 def update
72 def update
73 @query.attributes = params[:query]
73 @query.attributes = params[:query]
74 @query.project = nil if params[:query_is_for_all]
74 @query.project = nil if params[:query_is_for_all]
75 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
75 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
76 build_query_from_params
76 build_query_from_params
77 @query.column_names = nil if params[:default_columns]
77 @query.column_names = nil if params[:default_columns]
78
78
79 if @query.save
79 if @query.save
80 flash[:notice] = l(:notice_successful_update)
80 flash[:notice] = l(:notice_successful_update)
81 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
81 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
82 else
82 else
83 render :action => 'edit'
83 render :action => 'edit'
84 end
84 end
85 end
85 end
86
86
87 def destroy
87 def destroy
88 @query.destroy
88 @query.destroy
89 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
89 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
90 end
90 end
91
91
92 private
92 private
93 def find_query
93 def find_query
94 @query = Query.find(params[:id])
94 @query = IssueQuery.find(params[:id])
95 @project = @query.project
95 @project = @query.project
96 render_403 unless @query.editable_by?(User.current)
96 render_403 unless @query.editable_by?(User.current)
97 rescue ActiveRecord::RecordNotFound
97 rescue ActiveRecord::RecordNotFound
98 render_404
98 render_404
99 end
99 end
100
100
101 def find_optional_project
101 def find_optional_project
102 @project = Project.find(params[:project_id]) if params[:project_id]
102 @project = Project.find(params[:project_id]) if params[:project_id]
103 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
103 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
104 rescue ActiveRecord::RecordNotFound
104 rescue ActiveRecord::RecordNotFound
105 render_404
105 render_404
106 end
106 end
107 end
107 end
@@ -1,410 +1,410
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def issue_list(issues, &block)
23 def issue_list(issues, &block)
24 ancestors = []
24 ancestors = []
25 issues.each do |issue|
25 issues.each do |issue|
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 ancestors.pop
27 ancestors.pop
28 end
28 end
29 yield issue, ancestors.size
29 yield issue, ancestors.size
30 ancestors << issue unless issue.leaf?
30 ancestors << issue unless issue.leaf?
31 end
31 end
32 end
32 end
33
33
34 # Renders a HTML/CSS tooltip
34 # Renders a HTML/CSS tooltip
35 #
35 #
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 # that contains this method wrapped in a span with the class of "tip"
37 # that contains this method wrapped in a span with the class of "tip"
38 #
38 #
39 # <div class="tooltip"><%= link_to_issue(issue) %>
39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 # </div>
41 # </div>
42 #
42 #
43 def render_issue_tooltip(issue)
43 def render_issue_tooltip(issue)
44 @cached_label_status ||= l(:field_status)
44 @cached_label_status ||= l(:field_status)
45 @cached_label_start_date ||= l(:field_start_date)
45 @cached_label_start_date ||= l(:field_start_date)
46 @cached_label_due_date ||= l(:field_due_date)
46 @cached_label_due_date ||= l(:field_due_date)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 @cached_label_priority ||= l(:field_priority)
48 @cached_label_priority ||= l(:field_priority)
49 @cached_label_project ||= l(:field_project)
49 @cached_label_project ||= l(:field_project)
50
50
51 link_to_issue(issue) + "<br /><br />".html_safe +
51 link_to_issue(issue) + "<br /><br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 end
58 end
59
59
60 def issue_heading(issue)
60 def issue_heading(issue)
61 h("#{issue.tracker} ##{issue.id}")
61 h("#{issue.tracker} ##{issue.id}")
62 end
62 end
63
63
64 def render_issue_subject_with_tree(issue)
64 def render_issue_subject_with_tree(issue)
65 s = ''
65 s = ''
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 ancestors.each do |ancestor|
67 ancestors.each do |ancestor|
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 end
69 end
70 s << '<div>'
70 s << '<div>'
71 subject = h(issue.subject)
71 subject = h(issue.subject)
72 if issue.is_private?
72 if issue.is_private?
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 end
74 end
75 s << content_tag('h3', subject)
75 s << content_tag('h3', subject)
76 s << '</div>' * (ancestors.size + 1)
76 s << '</div>' * (ancestors.size + 1)
77 s.html_safe
77 s.html_safe
78 end
78 end
79
79
80 def render_descendants_tree(issue)
80 def render_descendants_tree(issue)
81 s = '<form><table class="list issues">'
81 s = '<form><table class="list issues">'
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 css = "issue issue-#{child.id} hascontextmenu"
83 css = "issue issue-#{child.id} hascontextmenu"
84 css << " idnt idnt-#{level}" if level > 0
84 css << " idnt idnt-#{level}" if level > 0
85 s << content_tag('tr',
85 s << content_tag('tr',
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 content_tag('td', h(child.status)) +
88 content_tag('td', h(child.status)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 :class => css)
91 :class => css)
92 end
92 end
93 s << '</table></form>'
93 s << '</table></form>'
94 s.html_safe
94 s.html_safe
95 end
95 end
96
96
97 # Returns a link for adding a new subtask to the given issue
97 # Returns a link for adding a new subtask to the given issue
98 def link_to_new_subtask(issue)
98 def link_to_new_subtask(issue)
99 attrs = {
99 attrs = {
100 :tracker_id => issue.tracker,
100 :tracker_id => issue.tracker,
101 :parent_issue_id => issue
101 :parent_issue_id => issue
102 }
102 }
103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
104 end
104 end
105
105
106 class IssueFieldsRows
106 class IssueFieldsRows
107 include ActionView::Helpers::TagHelper
107 include ActionView::Helpers::TagHelper
108
108
109 def initialize
109 def initialize
110 @left = []
110 @left = []
111 @right = []
111 @right = []
112 end
112 end
113
113
114 def left(*args)
114 def left(*args)
115 args.any? ? @left << cells(*args) : @left
115 args.any? ? @left << cells(*args) : @left
116 end
116 end
117
117
118 def right(*args)
118 def right(*args)
119 args.any? ? @right << cells(*args) : @right
119 args.any? ? @right << cells(*args) : @right
120 end
120 end
121
121
122 def size
122 def size
123 @left.size > @right.size ? @left.size : @right.size
123 @left.size > @right.size ? @left.size : @right.size
124 end
124 end
125
125
126 def to_html
126 def to_html
127 html = ''.html_safe
127 html = ''.html_safe
128 blank = content_tag('th', '') + content_tag('td', '')
128 blank = content_tag('th', '') + content_tag('td', '')
129 size.times do |i|
129 size.times do |i|
130 left = @left[i] || blank
130 left = @left[i] || blank
131 right = @right[i] || blank
131 right = @right[i] || blank
132 html << content_tag('tr', left + right)
132 html << content_tag('tr', left + right)
133 end
133 end
134 html
134 html
135 end
135 end
136
136
137 def cells(label, text, options={})
137 def cells(label, text, options={})
138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
139 end
139 end
140 end
140 end
141
141
142 def issue_fields_rows
142 def issue_fields_rows
143 r = IssueFieldsRows.new
143 r = IssueFieldsRows.new
144 yield r
144 yield r
145 r.to_html
145 r.to_html
146 end
146 end
147
147
148 def render_custom_fields_rows(issue)
148 def render_custom_fields_rows(issue)
149 return if issue.custom_field_values.empty?
149 return if issue.custom_field_values.empty?
150 ordered_values = []
150 ordered_values = []
151 half = (issue.custom_field_values.size / 2.0).ceil
151 half = (issue.custom_field_values.size / 2.0).ceil
152 half.times do |i|
152 half.times do |i|
153 ordered_values << issue.custom_field_values[i]
153 ordered_values << issue.custom_field_values[i]
154 ordered_values << issue.custom_field_values[i + half]
154 ordered_values << issue.custom_field_values[i + half]
155 end
155 end
156 s = "<tr>\n"
156 s = "<tr>\n"
157 n = 0
157 n = 0
158 ordered_values.compact.each do |value|
158 ordered_values.compact.each do |value|
159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
161 n += 1
161 n += 1
162 end
162 end
163 s << "</tr>\n"
163 s << "</tr>\n"
164 s.html_safe
164 s.html_safe
165 end
165 end
166
166
167 def issues_destroy_confirmation_message(issues)
167 def issues_destroy_confirmation_message(issues)
168 issues = [issues] unless issues.is_a?(Array)
168 issues = [issues] unless issues.is_a?(Array)
169 message = l(:text_issues_destroy_confirmation)
169 message = l(:text_issues_destroy_confirmation)
170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
171 if descendant_count > 0
171 if descendant_count > 0
172 issues.each do |issue|
172 issues.each do |issue|
173 next if issue.root?
173 next if issue.root?
174 issues.each do |other_issue|
174 issues.each do |other_issue|
175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
176 end
176 end
177 end
177 end
178 if descendant_count > 0
178 if descendant_count > 0
179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
180 end
180 end
181 end
181 end
182 message
182 message
183 end
183 end
184
184
185 def sidebar_queries
185 def sidebar_queries
186 unless @sidebar_queries
186 unless @sidebar_queries
187 @sidebar_queries = Query.visible.all(
187 @sidebar_queries = IssueQuery.visible.all(
188 :order => "#{Query.table_name}.name ASC",
188 :order => "#{Query.table_name}.name ASC",
189 # Project specific queries and global queries
189 # Project specific queries and global queries
190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
191 )
191 )
192 end
192 end
193 @sidebar_queries
193 @sidebar_queries
194 end
194 end
195
195
196 def query_links(title, queries)
196 def query_links(title, queries)
197 # links to #index on issues/show
197 # links to #index on issues/show
198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
199
199
200 content_tag('h3', h(title)) +
200 content_tag('h3', h(title)) +
201 queries.collect {|query|
201 queries.collect {|query|
202 css = 'query'
202 css = 'query'
203 css << ' selected' if query == @query
203 css << ' selected' if query == @query
204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
205 }.join('<br />').html_safe
205 }.join('<br />').html_safe
206 end
206 end
207
207
208 def render_sidebar_queries
208 def render_sidebar_queries
209 out = ''.html_safe
209 out = ''.html_safe
210 queries = sidebar_queries.select {|q| !q.is_public?}
210 queries = sidebar_queries.select {|q| !q.is_public?}
211 out << query_links(l(:label_my_queries), queries) if queries.any?
211 out << query_links(l(:label_my_queries), queries) if queries.any?
212 queries = sidebar_queries.select {|q| q.is_public?}
212 queries = sidebar_queries.select {|q| q.is_public?}
213 out << query_links(l(:label_query_plural), queries) if queries.any?
213 out << query_links(l(:label_query_plural), queries) if queries.any?
214 out
214 out
215 end
215 end
216
216
217 # Returns the textual representation of a journal details
217 # Returns the textual representation of a journal details
218 # as an array of strings
218 # as an array of strings
219 def details_to_strings(details, no_html=false, options={})
219 def details_to_strings(details, no_html=false, options={})
220 options[:only_path] = (options[:only_path] == false ? false : true)
220 options[:only_path] = (options[:only_path] == false ? false : true)
221 strings = []
221 strings = []
222 values_by_field = {}
222 values_by_field = {}
223 details.each do |detail|
223 details.each do |detail|
224 if detail.property == 'cf'
224 if detail.property == 'cf'
225 field_id = detail.prop_key
225 field_id = detail.prop_key
226 field = CustomField.find_by_id(field_id)
226 field = CustomField.find_by_id(field_id)
227 if field && field.multiple?
227 if field && field.multiple?
228 values_by_field[field_id] ||= {:added => [], :deleted => []}
228 values_by_field[field_id] ||= {:added => [], :deleted => []}
229 if detail.old_value
229 if detail.old_value
230 values_by_field[field_id][:deleted] << detail.old_value
230 values_by_field[field_id][:deleted] << detail.old_value
231 end
231 end
232 if detail.value
232 if detail.value
233 values_by_field[field_id][:added] << detail.value
233 values_by_field[field_id][:added] << detail.value
234 end
234 end
235 next
235 next
236 end
236 end
237 end
237 end
238 strings << show_detail(detail, no_html, options)
238 strings << show_detail(detail, no_html, options)
239 end
239 end
240 values_by_field.each do |field_id, changes|
240 values_by_field.each do |field_id, changes|
241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
242 if changes[:added].any?
242 if changes[:added].any?
243 detail.value = changes[:added]
243 detail.value = changes[:added]
244 strings << show_detail(detail, no_html, options)
244 strings << show_detail(detail, no_html, options)
245 elsif changes[:deleted].any?
245 elsif changes[:deleted].any?
246 detail.old_value = changes[:deleted]
246 detail.old_value = changes[:deleted]
247 strings << show_detail(detail, no_html, options)
247 strings << show_detail(detail, no_html, options)
248 end
248 end
249 end
249 end
250 strings
250 strings
251 end
251 end
252
252
253 # Returns the textual representation of a single journal detail
253 # Returns the textual representation of a single journal detail
254 def show_detail(detail, no_html=false, options={})
254 def show_detail(detail, no_html=false, options={})
255 multiple = false
255 multiple = false
256 case detail.property
256 case detail.property
257 when 'attr'
257 when 'attr'
258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
259 label = l(("field_" + field).to_sym)
259 label = l(("field_" + field).to_sym)
260 case detail.prop_key
260 case detail.prop_key
261 when 'due_date', 'start_date'
261 when 'due_date', 'start_date'
262 value = format_date(detail.value.to_date) if detail.value
262 value = format_date(detail.value.to_date) if detail.value
263 old_value = format_date(detail.old_value.to_date) if detail.old_value
263 old_value = format_date(detail.old_value.to_date) if detail.old_value
264
264
265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
266 'priority_id', 'category_id', 'fixed_version_id'
266 'priority_id', 'category_id', 'fixed_version_id'
267 value = find_name_by_reflection(field, detail.value)
267 value = find_name_by_reflection(field, detail.value)
268 old_value = find_name_by_reflection(field, detail.old_value)
268 old_value = find_name_by_reflection(field, detail.old_value)
269
269
270 when 'estimated_hours'
270 when 'estimated_hours'
271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
273
273
274 when 'parent_id'
274 when 'parent_id'
275 label = l(:field_parent_issue)
275 label = l(:field_parent_issue)
276 value = "##{detail.value}" unless detail.value.blank?
276 value = "##{detail.value}" unless detail.value.blank?
277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
278
278
279 when 'is_private'
279 when 'is_private'
280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
282 end
282 end
283 when 'cf'
283 when 'cf'
284 custom_field = CustomField.find_by_id(detail.prop_key)
284 custom_field = CustomField.find_by_id(detail.prop_key)
285 if custom_field
285 if custom_field
286 multiple = custom_field.multiple?
286 multiple = custom_field.multiple?
287 label = custom_field.name
287 label = custom_field.name
288 value = format_value(detail.value, custom_field.field_format) if detail.value
288 value = format_value(detail.value, custom_field.field_format) if detail.value
289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
290 end
290 end
291 when 'attachment'
291 when 'attachment'
292 label = l(:label_attachment)
292 label = l(:label_attachment)
293 end
293 end
294 call_hook(:helper_issues_show_detail_after_setting,
294 call_hook(:helper_issues_show_detail_after_setting,
295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
296
296
297 label ||= detail.prop_key
297 label ||= detail.prop_key
298 value ||= detail.value
298 value ||= detail.value
299 old_value ||= detail.old_value
299 old_value ||= detail.old_value
300
300
301 unless no_html
301 unless no_html
302 label = content_tag('strong', label)
302 label = content_tag('strong', label)
303 old_value = content_tag("i", h(old_value)) if detail.old_value
303 old_value = content_tag("i", h(old_value)) if detail.old_value
304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
306 # Link to the attachment if it has not been removed
306 # Link to the attachment if it has not been removed
307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
308 if options[:only_path] != false && atta.is_text?
308 if options[:only_path] != false && atta.is_text?
309 value += link_to(
309 value += link_to(
310 image_tag('magnifier.png'),
310 image_tag('magnifier.png'),
311 :controller => 'attachments', :action => 'show',
311 :controller => 'attachments', :action => 'show',
312 :id => atta, :filename => atta.filename
312 :id => atta, :filename => atta.filename
313 )
313 )
314 end
314 end
315 else
315 else
316 value = content_tag("i", h(value)) if value
316 value = content_tag("i", h(value)) if value
317 end
317 end
318 end
318 end
319
319
320 if detail.property == 'attr' && detail.prop_key == 'description'
320 if detail.property == 'attr' && detail.prop_key == 'description'
321 s = l(:text_journal_changed_no_detail, :label => label)
321 s = l(:text_journal_changed_no_detail, :label => label)
322 unless no_html
322 unless no_html
323 diff_link = link_to 'diff',
323 diff_link = link_to 'diff',
324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
325 :detail_id => detail.id, :only_path => options[:only_path]},
325 :detail_id => detail.id, :only_path => options[:only_path]},
326 :title => l(:label_view_diff)
326 :title => l(:label_view_diff)
327 s << " (#{ diff_link })"
327 s << " (#{ diff_link })"
328 end
328 end
329 s.html_safe
329 s.html_safe
330 elsif detail.value.present?
330 elsif detail.value.present?
331 case detail.property
331 case detail.property
332 when 'attr', 'cf'
332 when 'attr', 'cf'
333 if detail.old_value.present?
333 if detail.old_value.present?
334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
335 elsif multiple
335 elsif multiple
336 l(:text_journal_added, :label => label, :value => value).html_safe
336 l(:text_journal_added, :label => label, :value => value).html_safe
337 else
337 else
338 l(:text_journal_set_to, :label => label, :value => value).html_safe
338 l(:text_journal_set_to, :label => label, :value => value).html_safe
339 end
339 end
340 when 'attachment'
340 when 'attachment'
341 l(:text_journal_added, :label => label, :value => value).html_safe
341 l(:text_journal_added, :label => label, :value => value).html_safe
342 end
342 end
343 else
343 else
344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
345 end
345 end
346 end
346 end
347
347
348 # Find the name of an associated record stored in the field attribute
348 # Find the name of an associated record stored in the field attribute
349 def find_name_by_reflection(field, id)
349 def find_name_by_reflection(field, id)
350 association = Issue.reflect_on_association(field.to_sym)
350 association = Issue.reflect_on_association(field.to_sym)
351 if association
351 if association
352 record = association.class_name.constantize.find_by_id(id)
352 record = association.class_name.constantize.find_by_id(id)
353 return record.name if record
353 return record.name if record
354 end
354 end
355 end
355 end
356
356
357 # Renders issue children recursively
357 # Renders issue children recursively
358 def render_api_issue_children(issue, api)
358 def render_api_issue_children(issue, api)
359 return if issue.leaf?
359 return if issue.leaf?
360 api.array :children do
360 api.array :children do
361 issue.children.each do |child|
361 issue.children.each do |child|
362 api.issue(:id => child.id) do
362 api.issue(:id => child.id) do
363 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
363 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
364 api.subject child.subject
364 api.subject child.subject
365 render_api_issue_children(child, api)
365 render_api_issue_children(child, api)
366 end
366 end
367 end
367 end
368 end
368 end
369 end
369 end
370
370
371 def issues_to_csv(issues, project, query, options={})
371 def issues_to_csv(issues, project, query, options={})
372 decimal_separator = l(:general_csv_decimal_separator)
372 decimal_separator = l(:general_csv_decimal_separator)
373 encoding = l(:general_csv_encoding)
373 encoding = l(:general_csv_encoding)
374 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
374 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
375 if options[:description]
375 if options[:description]
376 if description = query.available_columns.detect {|q| q.name == :description}
376 if description = query.available_columns.detect {|q| q.name == :description}
377 columns << description
377 columns << description
378 end
378 end
379 end
379 end
380
380
381 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
381 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
382 # csv header fields
382 # csv header fields
383 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
383 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
384
384
385 # csv lines
385 # csv lines
386 issues.each do |issue|
386 issues.each do |issue|
387 col_values = columns.collect do |column|
387 col_values = columns.collect do |column|
388 s = if column.is_a?(QueryCustomFieldColumn)
388 s = if column.is_a?(QueryCustomFieldColumn)
389 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
389 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
390 show_value(cv)
390 show_value(cv)
391 else
391 else
392 value = column.value(issue)
392 value = column.value(issue)
393 if value.is_a?(Date)
393 if value.is_a?(Date)
394 format_date(value)
394 format_date(value)
395 elsif value.is_a?(Time)
395 elsif value.is_a?(Time)
396 format_time(value)
396 format_time(value)
397 elsif value.is_a?(Float)
397 elsif value.is_a?(Float)
398 ("%.2f" % value).gsub('.', decimal_separator)
398 ("%.2f" % value).gsub('.', decimal_separator)
399 else
399 else
400 value
400 value
401 end
401 end
402 end
402 end
403 s.to_s
403 s.to_s
404 end
404 end
405 csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
405 csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
406 end
406 end
407 end
407 end
408 export
408 export
409 end
409 end
410 end
410 end
@@ -1,173 +1,173
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 def filters_options_for_select(query)
21 def filters_options_for_select(query)
22 options_for_select(filters_options(query))
22 options_for_select(filters_options(query))
23 end
23 end
24
24
25 def filters_options(query)
25 def filters_options(query)
26 options = [[]]
26 options = [[]]
27 sorted_options = query.available_filters.sort do |a, b|
27 sorted_options = query.available_filters.sort do |a, b|
28 ord = 0
28 ord = 0
29 if !(a[1][:order] == 20 && b[1][:order] == 20)
29 if !(a[1][:order] == 20 && b[1][:order] == 20)
30 ord = a[1][:order] <=> b[1][:order]
30 ord = a[1][:order] <=> b[1][:order]
31 else
31 else
32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
34 if cn != 0
34 if cn != 0
35 ord = cn
35 ord = cn
36 else
36 else
37 f = (a[1][:field] <=> b[1][:field])
37 f = (a[1][:field] <=> b[1][:field])
38 if f != 0
38 if f != 0
39 ord = f
39 ord = f
40 else
40 else
41 # assigned_to or author
41 # assigned_to or author
42 ord = (a[0] <=> b[0])
42 ord = (a[0] <=> b[0])
43 end
43 end
44 end
44 end
45 end
45 end
46 ord
46 ord
47 end
47 end
48 options += sorted_options.map do |field, field_options|
48 options += sorted_options.map do |field, field_options|
49 [field_options[:name], field]
49 [field_options[:name], field]
50 end
50 end
51 end
51 end
52
52
53 def available_block_columns_tags(query)
53 def available_block_columns_tags(query)
54 tags = ''.html_safe
54 tags = ''.html_safe
55 query.available_block_columns.each do |column|
55 query.available_block_columns.each do |column|
56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
57 end
57 end
58 tags
58 tags
59 end
59 end
60
60
61 def column_header(column)
61 def column_header(column)
62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
63 :default_order => column.default_order) :
63 :default_order => column.default_order) :
64 content_tag('th', h(column.caption))
64 content_tag('th', h(column.caption))
65 end
65 end
66
66
67 def column_content(column, issue)
67 def column_content(column, issue)
68 value = column.value(issue)
68 value = column.value(issue)
69 if value.is_a?(Array)
69 if value.is_a?(Array)
70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
71 else
71 else
72 column_value(column, issue, value)
72 column_value(column, issue, value)
73 end
73 end
74 end
74 end
75
75
76 def column_value(column, issue, value)
76 def column_value(column, issue, value)
77 case value.class.name
77 case value.class.name
78 when 'String'
78 when 'String'
79 if column.name == :subject
79 if column.name == :subject
80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
81 elsif column.name == :description
81 elsif column.name == :description
82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
83 else
83 else
84 h(value)
84 h(value)
85 end
85 end
86 when 'Time'
86 when 'Time'
87 format_time(value)
87 format_time(value)
88 when 'Date'
88 when 'Date'
89 format_date(value)
89 format_date(value)
90 when 'Fixnum', 'Float'
90 when 'Fixnum', 'Float'
91 if column.name == :done_ratio
91 if column.name == :done_ratio
92 progress_bar(value, :width => '80px')
92 progress_bar(value, :width => '80px')
93 elsif column.name == :spent_hours
93 elsif column.name == :spent_hours
94 sprintf "%.2f", value
94 sprintf "%.2f", value
95 else
95 else
96 h(value.to_s)
96 h(value.to_s)
97 end
97 end
98 when 'User'
98 when 'User'
99 link_to_user value
99 link_to_user value
100 when 'Project'
100 when 'Project'
101 link_to_project value
101 link_to_project value
102 when 'Version'
102 when 'Version'
103 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
103 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
104 when 'TrueClass'
104 when 'TrueClass'
105 l(:general_text_Yes)
105 l(:general_text_Yes)
106 when 'FalseClass'
106 when 'FalseClass'
107 l(:general_text_No)
107 l(:general_text_No)
108 when 'Issue'
108 when 'Issue'
109 link_to_issue(value, :subject => false)
109 link_to_issue(value, :subject => false)
110 when 'IssueRelation'
110 when 'IssueRelation'
111 other = value.other_issue(issue)
111 other = value.other_issue(issue)
112 content_tag('span',
112 content_tag('span',
113 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
113 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
114 :class => value.css_classes_for(issue))
114 :class => value.css_classes_for(issue))
115 else
115 else
116 h(value)
116 h(value)
117 end
117 end
118 end
118 end
119
119
120 # Retrieve query from session or build a new query
120 # Retrieve query from session or build a new query
121 def retrieve_query
121 def retrieve_query
122 if !params[:query_id].blank?
122 if !params[:query_id].blank?
123 cond = "project_id IS NULL"
123 cond = "project_id IS NULL"
124 cond << " OR project_id = #{@project.id}" if @project
124 cond << " OR project_id = #{@project.id}" if @project
125 @query = Query.find(params[:query_id], :conditions => cond)
125 @query = IssueQuery.find(params[:query_id], :conditions => cond)
126 raise ::Unauthorized unless @query.visible?
126 raise ::Unauthorized unless @query.visible?
127 @query.project = @project
127 @query.project = @project
128 session[:query] = {:id => @query.id, :project_id => @query.project_id}
128 session[:query] = {:id => @query.id, :project_id => @query.project_id}
129 sort_clear
129 sort_clear
130 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
130 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
131 # Give it a name, required to be valid
131 # Give it a name, required to be valid
132 @query = Query.new(:name => "_")
132 @query = IssueQuery.new(:name => "_")
133 @query.project = @project
133 @query.project = @project
134 build_query_from_params
134 build_query_from_params
135 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
135 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
136 else
136 else
137 # retrieve from session
137 # retrieve from session
138 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
138 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
139 @query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
139 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
140 @query.project = @project
140 @query.project = @project
141 end
141 end
142 end
142 end
143
143
144 def retrieve_query_from_session
144 def retrieve_query_from_session
145 if session[:query]
145 if session[:query]
146 if session[:query][:id]
146 if session[:query][:id]
147 @query = Query.find_by_id(session[:query][:id])
147 @query = IssueQuery.find_by_id(session[:query][:id])
148 return unless @query
148 return unless @query
149 else
149 else
150 @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
150 @query = IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
151 end
151 end
152 if session[:query].has_key?(:project_id)
152 if session[:query].has_key?(:project_id)
153 @query.project_id = session[:query][:project_id]
153 @query.project_id = session[:query][:project_id]
154 else
154 else
155 @query.project = @project
155 @query.project = @project
156 end
156 end
157 @query
157 @query
158 end
158 end
159 end
159 end
160
160
161 def build_query_from_params
161 def build_query_from_params
162 if params[:fields] || params[:f]
162 if params[:fields] || params[:f]
163 @query.filters = {}
163 @query.filters = {}
164 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
164 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
165 else
165 else
166 @query.available_filters.keys.each do |field|
166 @query.available_filters.keys.each do |field|
167 @query.add_short_filter(field, params[field]) if params[field]
167 @query.add_short_filter(field, params[field]) if params[field]
168 end
168 end
169 end
169 end
170 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
170 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
171 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
171 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
172 end
172 end
173 end
173 end
@@ -1,969 +1,969
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 has_many :users, :through => :members
36 has_many :users, :through => :members
37 has_many :principals, :through => :member_principals, :source => :principal
37 has_many :principals, :through => :member_principals, :source => :principal
38
38
39 has_many :enabled_modules, :dependent => :delete_all
39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 has_many :time_entries, :dependent => :delete_all
44 has_many :time_entries, :dependent => :delete_all
45 has_many :queries, :dependent => :delete_all
45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
46 has_many :documents, :dependent => :destroy
46 has_many :documents, :dependent => :destroy
47 has_many :news, :dependent => :destroy, :include => :author
47 has_many :news, :dependent => :destroy, :include => :author
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 has_one :repository, :conditions => ["is_default = ?", true]
50 has_one :repository, :conditions => ["is_default = ?", true]
51 has_many :repositories, :dependent => :destroy
51 has_many :repositories, :dependent => :destroy
52 has_many :changesets, :through => :repository
52 has_many :changesets, :through => :repository
53 has_one :wiki, :dependent => :destroy
53 has_one :wiki, :dependent => :destroy
54 # Custom field for the project issues
54 # Custom field for the project issues
55 has_and_belongs_to_many :issue_custom_fields,
55 has_and_belongs_to_many :issue_custom_fields,
56 :class_name => 'IssueCustomField',
56 :class_name => 'IssueCustomField',
57 :order => "#{CustomField.table_name}.position",
57 :order => "#{CustomField.table_name}.position",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 :association_foreign_key => 'custom_field_id'
59 :association_foreign_key => 'custom_field_id'
60
60
61 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 acts_as_attachable :view_permission => :view_files,
62 acts_as_attachable :view_permission => :view_files,
63 :delete_permission => :manage_files
63 :delete_permission => :manage_files
64
64
65 acts_as_customizable
65 acts_as_customizable
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 :author => nil
69 :author => nil
70
70
71 attr_protected :status
71 attr_protected :status
72
72
73 validates_presence_of :name, :identifier
73 validates_presence_of :name, :identifier
74 validates_uniqueness_of :identifier
74 validates_uniqueness_of :identifier
75 validates_associated :repository, :wiki
75 validates_associated :repository, :wiki
76 validates_length_of :name, :maximum => 255
76 validates_length_of :name, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 # donwcase letters, digits, dashes but not digits only
79 # donwcase letters, digits, dashes but not digits only
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 # reserved words
81 # reserved words
82 validates_exclusion_of :identifier, :in => %w( new )
82 validates_exclusion_of :identifier, :in => %w( new )
83
83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 before_destroy :delete_all_members
85 before_destroy :delete_all_members
86
86
87 scope :has_module, lambda {|mod|
87 scope :has_module, lambda {|mod|
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
88 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 }
89 }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :all_public, lambda { where(:is_public => true) }
92 scope :all_public, lambda { where(:is_public => true) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :allowed_to, lambda {|*args|
94 scope :allowed_to, lambda {|*args|
95 user = User.current
95 user = User.current
96 permission = nil
96 permission = nil
97 if args.first.is_a?(Symbol)
97 if args.first.is_a?(Symbol)
98 permission = args.shift
98 permission = args.shift
99 else
99 else
100 user = args.shift
100 user = args.shift
101 permission = args.shift
101 permission = args.shift
102 end
102 end
103 where(Project.allowed_to_condition(user, permission, *args))
103 where(Project.allowed_to_condition(user, permission, *args))
104 }
104 }
105 scope :like, lambda {|arg|
105 scope :like, lambda {|arg|
106 if arg.blank?
106 if arg.blank?
107 where(nil)
107 where(nil)
108 else
108 else
109 pattern = "%#{arg.to_s.strip.downcase}%"
109 pattern = "%#{arg.to_s.strip.downcase}%"
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 end
111 end
112 }
112 }
113
113
114 def initialize(attributes=nil, *args)
114 def initialize(attributes=nil, *args)
115 super
115 super
116
116
117 initialized = (attributes || {}).stringify_keys
117 initialized = (attributes || {}).stringify_keys
118 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
118 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
119 self.identifier = Project.next_identifier
119 self.identifier = Project.next_identifier
120 end
120 end
121 if !initialized.key?('is_public')
121 if !initialized.key?('is_public')
122 self.is_public = Setting.default_projects_public?
122 self.is_public = Setting.default_projects_public?
123 end
123 end
124 if !initialized.key?('enabled_module_names')
124 if !initialized.key?('enabled_module_names')
125 self.enabled_module_names = Setting.default_projects_modules
125 self.enabled_module_names = Setting.default_projects_modules
126 end
126 end
127 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
128 self.trackers = Tracker.sorted.all
128 self.trackers = Tracker.sorted.all
129 end
129 end
130 end
130 end
131
131
132 def identifier=(identifier)
132 def identifier=(identifier)
133 super unless identifier_frozen?
133 super unless identifier_frozen?
134 end
134 end
135
135
136 def identifier_frozen?
136 def identifier_frozen?
137 errors[:identifier].blank? && !(new_record? || identifier.blank?)
137 errors[:identifier].blank? && !(new_record? || identifier.blank?)
138 end
138 end
139
139
140 # returns latest created projects
140 # returns latest created projects
141 # non public projects will be returned only if user is a member of those
141 # non public projects will be returned only if user is a member of those
142 def self.latest(user=nil, count=5)
142 def self.latest(user=nil, count=5)
143 visible(user).limit(count).order("created_on DESC").all
143 visible(user).limit(count).order("created_on DESC").all
144 end
144 end
145
145
146 # Returns true if the project is visible to +user+ or to the current user.
146 # Returns true if the project is visible to +user+ or to the current user.
147 def visible?(user=User.current)
147 def visible?(user=User.current)
148 user.allowed_to?(:view_project, self)
148 user.allowed_to?(:view_project, self)
149 end
149 end
150
150
151 # Returns a SQL conditions string used to find all projects visible by the specified user.
151 # Returns a SQL conditions string used to find all projects visible by the specified user.
152 #
152 #
153 # Examples:
153 # Examples:
154 # Project.visible_condition(admin) => "projects.status = 1"
154 # Project.visible_condition(admin) => "projects.status = 1"
155 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
155 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
156 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
156 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
157 def self.visible_condition(user, options={})
157 def self.visible_condition(user, options={})
158 allowed_to_condition(user, :view_project, options)
158 allowed_to_condition(user, :view_project, options)
159 end
159 end
160
160
161 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
161 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
162 #
162 #
163 # Valid options:
163 # Valid options:
164 # * :project => limit the condition to project
164 # * :project => limit the condition to project
165 # * :with_subprojects => limit the condition to project and its subprojects
165 # * :with_subprojects => limit the condition to project and its subprojects
166 # * :member => limit the condition to the user projects
166 # * :member => limit the condition to the user projects
167 def self.allowed_to_condition(user, permission, options={})
167 def self.allowed_to_condition(user, permission, options={})
168 perm = Redmine::AccessControl.permission(permission)
168 perm = Redmine::AccessControl.permission(permission)
169 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
169 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
170 if perm && perm.project_module
170 if perm && perm.project_module
171 # If the permission belongs to a project module, make sure the module is enabled
171 # If the permission belongs to a project module, make sure the module is enabled
172 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
172 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
173 end
173 end
174 if options[:project]
174 if options[:project]
175 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
175 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
176 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
176 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
177 base_statement = "(#{project_statement}) AND (#{base_statement})"
177 base_statement = "(#{project_statement}) AND (#{base_statement})"
178 end
178 end
179
179
180 if user.admin?
180 if user.admin?
181 base_statement
181 base_statement
182 else
182 else
183 statement_by_role = {}
183 statement_by_role = {}
184 unless options[:member]
184 unless options[:member]
185 role = user.logged? ? Role.non_member : Role.anonymous
185 role = user.logged? ? Role.non_member : Role.anonymous
186 if role.allowed_to?(permission)
186 if role.allowed_to?(permission)
187 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
187 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
188 end
188 end
189 end
189 end
190 if user.logged?
190 if user.logged?
191 user.projects_by_role.each do |role, projects|
191 user.projects_by_role.each do |role, projects|
192 if role.allowed_to?(permission) && projects.any?
192 if role.allowed_to?(permission) && projects.any?
193 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
193 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
194 end
194 end
195 end
195 end
196 end
196 end
197 if statement_by_role.empty?
197 if statement_by_role.empty?
198 "1=0"
198 "1=0"
199 else
199 else
200 if block_given?
200 if block_given?
201 statement_by_role.each do |role, statement|
201 statement_by_role.each do |role, statement|
202 if s = yield(role, user)
202 if s = yield(role, user)
203 statement_by_role[role] = "(#{statement} AND (#{s}))"
203 statement_by_role[role] = "(#{statement} AND (#{s}))"
204 end
204 end
205 end
205 end
206 end
206 end
207 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
207 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
208 end
208 end
209 end
209 end
210 end
210 end
211
211
212 # Returns the Systemwide and project specific activities
212 # Returns the Systemwide and project specific activities
213 def activities(include_inactive=false)
213 def activities(include_inactive=false)
214 if include_inactive
214 if include_inactive
215 return all_activities
215 return all_activities
216 else
216 else
217 return active_activities
217 return active_activities
218 end
218 end
219 end
219 end
220
220
221 # Will create a new Project specific Activity or update an existing one
221 # Will create a new Project specific Activity or update an existing one
222 #
222 #
223 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
223 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
224 # does not successfully save.
224 # does not successfully save.
225 def update_or_create_time_entry_activity(id, activity_hash)
225 def update_or_create_time_entry_activity(id, activity_hash)
226 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
226 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
227 self.create_time_entry_activity_if_needed(activity_hash)
227 self.create_time_entry_activity_if_needed(activity_hash)
228 else
228 else
229 activity = project.time_entry_activities.find_by_id(id.to_i)
229 activity = project.time_entry_activities.find_by_id(id.to_i)
230 activity.update_attributes(activity_hash) if activity
230 activity.update_attributes(activity_hash) if activity
231 end
231 end
232 end
232 end
233
233
234 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
234 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
235 #
235 #
236 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
236 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
237 # does not successfully save.
237 # does not successfully save.
238 def create_time_entry_activity_if_needed(activity)
238 def create_time_entry_activity_if_needed(activity)
239 if activity['parent_id']
239 if activity['parent_id']
240
240
241 parent_activity = TimeEntryActivity.find(activity['parent_id'])
241 parent_activity = TimeEntryActivity.find(activity['parent_id'])
242 activity['name'] = parent_activity.name
242 activity['name'] = parent_activity.name
243 activity['position'] = parent_activity.position
243 activity['position'] = parent_activity.position
244
244
245 if Enumeration.overridding_change?(activity, parent_activity)
245 if Enumeration.overridding_change?(activity, parent_activity)
246 project_activity = self.time_entry_activities.create(activity)
246 project_activity = self.time_entry_activities.create(activity)
247
247
248 if project_activity.new_record?
248 if project_activity.new_record?
249 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
249 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
250 else
250 else
251 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
251 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
252 end
252 end
253 end
253 end
254 end
254 end
255 end
255 end
256
256
257 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
257 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
258 #
258 #
259 # Examples:
259 # Examples:
260 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
260 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
261 # project.project_condition(false) => "projects.id = 1"
261 # project.project_condition(false) => "projects.id = 1"
262 def project_condition(with_subprojects)
262 def project_condition(with_subprojects)
263 cond = "#{Project.table_name}.id = #{id}"
263 cond = "#{Project.table_name}.id = #{id}"
264 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
264 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
265 cond
265 cond
266 end
266 end
267
267
268 def self.find(*args)
268 def self.find(*args)
269 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
269 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
270 project = find_by_identifier(*args)
270 project = find_by_identifier(*args)
271 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
271 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
272 project
272 project
273 else
273 else
274 super
274 super
275 end
275 end
276 end
276 end
277
277
278 def self.find_by_param(*args)
278 def self.find_by_param(*args)
279 self.find(*args)
279 self.find(*args)
280 end
280 end
281
281
282 def reload(*args)
282 def reload(*args)
283 @shared_versions = nil
283 @shared_versions = nil
284 @rolled_up_versions = nil
284 @rolled_up_versions = nil
285 @rolled_up_trackers = nil
285 @rolled_up_trackers = nil
286 @all_issue_custom_fields = nil
286 @all_issue_custom_fields = nil
287 @all_time_entry_custom_fields = nil
287 @all_time_entry_custom_fields = nil
288 @to_param = nil
288 @to_param = nil
289 @allowed_parents = nil
289 @allowed_parents = nil
290 @allowed_permissions = nil
290 @allowed_permissions = nil
291 @actions_allowed = nil
291 @actions_allowed = nil
292 super
292 super
293 end
293 end
294
294
295 def to_param
295 def to_param
296 # id is used for projects with a numeric identifier (compatibility)
296 # id is used for projects with a numeric identifier (compatibility)
297 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
297 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
298 end
298 end
299
299
300 def active?
300 def active?
301 self.status == STATUS_ACTIVE
301 self.status == STATUS_ACTIVE
302 end
302 end
303
303
304 def archived?
304 def archived?
305 self.status == STATUS_ARCHIVED
305 self.status == STATUS_ARCHIVED
306 end
306 end
307
307
308 # Archives the project and its descendants
308 # Archives the project and its descendants
309 def archive
309 def archive
310 # Check that there is no issue of a non descendant project that is assigned
310 # Check that there is no issue of a non descendant project that is assigned
311 # to one of the project or descendant versions
311 # to one of the project or descendant versions
312 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
312 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
313 if v_ids.any? &&
313 if v_ids.any? &&
314 Issue.
314 Issue.
315 includes(:project).
315 includes(:project).
316 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
316 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
317 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
317 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
318 exists?
318 exists?
319 return false
319 return false
320 end
320 end
321 Project.transaction do
321 Project.transaction do
322 archive!
322 archive!
323 end
323 end
324 true
324 true
325 end
325 end
326
326
327 # Unarchives the project
327 # Unarchives the project
328 # All its ancestors must be active
328 # All its ancestors must be active
329 def unarchive
329 def unarchive
330 return false if ancestors.detect {|a| !a.active?}
330 return false if ancestors.detect {|a| !a.active?}
331 update_attribute :status, STATUS_ACTIVE
331 update_attribute :status, STATUS_ACTIVE
332 end
332 end
333
333
334 def close
334 def close
335 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
335 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
336 end
336 end
337
337
338 def reopen
338 def reopen
339 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
339 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
340 end
340 end
341
341
342 # Returns an array of projects the project can be moved to
342 # Returns an array of projects the project can be moved to
343 # by the current user
343 # by the current user
344 def allowed_parents
344 def allowed_parents
345 return @allowed_parents if @allowed_parents
345 return @allowed_parents if @allowed_parents
346 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
346 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
347 @allowed_parents = @allowed_parents - self_and_descendants
347 @allowed_parents = @allowed_parents - self_and_descendants
348 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
348 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
349 @allowed_parents << nil
349 @allowed_parents << nil
350 end
350 end
351 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
351 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
352 @allowed_parents << parent
352 @allowed_parents << parent
353 end
353 end
354 @allowed_parents
354 @allowed_parents
355 end
355 end
356
356
357 # Sets the parent of the project with authorization check
357 # Sets the parent of the project with authorization check
358 def set_allowed_parent!(p)
358 def set_allowed_parent!(p)
359 unless p.nil? || p.is_a?(Project)
359 unless p.nil? || p.is_a?(Project)
360 if p.to_s.blank?
360 if p.to_s.blank?
361 p = nil
361 p = nil
362 else
362 else
363 p = Project.find_by_id(p)
363 p = Project.find_by_id(p)
364 return false unless p
364 return false unless p
365 end
365 end
366 end
366 end
367 if p.nil?
367 if p.nil?
368 if !new_record? && allowed_parents.empty?
368 if !new_record? && allowed_parents.empty?
369 return false
369 return false
370 end
370 end
371 elsif !allowed_parents.include?(p)
371 elsif !allowed_parents.include?(p)
372 return false
372 return false
373 end
373 end
374 set_parent!(p)
374 set_parent!(p)
375 end
375 end
376
376
377 # Sets the parent of the project
377 # Sets the parent of the project
378 # Argument can be either a Project, a String, a Fixnum or nil
378 # Argument can be either a Project, a String, a Fixnum or nil
379 def set_parent!(p)
379 def set_parent!(p)
380 unless p.nil? || p.is_a?(Project)
380 unless p.nil? || p.is_a?(Project)
381 if p.to_s.blank?
381 if p.to_s.blank?
382 p = nil
382 p = nil
383 else
383 else
384 p = Project.find_by_id(p)
384 p = Project.find_by_id(p)
385 return false unless p
385 return false unless p
386 end
386 end
387 end
387 end
388 if p == parent && !p.nil?
388 if p == parent && !p.nil?
389 # Nothing to do
389 # Nothing to do
390 true
390 true
391 elsif p.nil? || (p.active? && move_possible?(p))
391 elsif p.nil? || (p.active? && move_possible?(p))
392 set_or_update_position_under(p)
392 set_or_update_position_under(p)
393 Issue.update_versions_from_hierarchy_change(self)
393 Issue.update_versions_from_hierarchy_change(self)
394 true
394 true
395 else
395 else
396 # Can not move to the given target
396 # Can not move to the given target
397 false
397 false
398 end
398 end
399 end
399 end
400
400
401 # Recalculates all lft and rgt values based on project names
401 # Recalculates all lft and rgt values based on project names
402 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
402 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
403 # Used in BuildProjectsTree migration
403 # Used in BuildProjectsTree migration
404 def self.rebuild_tree!
404 def self.rebuild_tree!
405 transaction do
405 transaction do
406 update_all "lft = NULL, rgt = NULL"
406 update_all "lft = NULL, rgt = NULL"
407 rebuild!(false)
407 rebuild!(false)
408 end
408 end
409 end
409 end
410
410
411 # Returns an array of the trackers used by the project and its active sub projects
411 # Returns an array of the trackers used by the project and its active sub projects
412 def rolled_up_trackers
412 def rolled_up_trackers
413 @rolled_up_trackers ||=
413 @rolled_up_trackers ||=
414 Tracker.
414 Tracker.
415 joins(:projects).
415 joins(:projects).
416 select("DISTINCT #{Tracker.table_name}.*").
416 select("DISTINCT #{Tracker.table_name}.*").
417 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
417 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
418 sorted.
418 sorted.
419 all
419 all
420 end
420 end
421
421
422 # Closes open and locked project versions that are completed
422 # Closes open and locked project versions that are completed
423 def close_completed_versions
423 def close_completed_versions
424 Version.transaction do
424 Version.transaction do
425 versions.where(:status => %w(open locked)).all.each do |version|
425 versions.where(:status => %w(open locked)).all.each do |version|
426 if version.completed?
426 if version.completed?
427 version.update_attribute(:status, 'closed')
427 version.update_attribute(:status, 'closed')
428 end
428 end
429 end
429 end
430 end
430 end
431 end
431 end
432
432
433 # Returns a scope of the Versions on subprojects
433 # Returns a scope of the Versions on subprojects
434 def rolled_up_versions
434 def rolled_up_versions
435 @rolled_up_versions ||=
435 @rolled_up_versions ||=
436 Version.scoped(:include => :project,
436 Version.scoped(:include => :project,
437 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
437 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
438 end
438 end
439
439
440 # Returns a scope of the Versions used by the project
440 # Returns a scope of the Versions used by the project
441 def shared_versions
441 def shared_versions
442 if new_record?
442 if new_record?
443 Version.scoped(:include => :project,
443 Version.scoped(:include => :project,
444 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
444 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
445 else
445 else
446 @shared_versions ||= begin
446 @shared_versions ||= begin
447 r = root? ? self : root
447 r = root? ? self : root
448 Version.scoped(:include => :project,
448 Version.scoped(:include => :project,
449 :conditions => "#{Project.table_name}.id = #{id}" +
449 :conditions => "#{Project.table_name}.id = #{id}" +
450 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
450 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
451 " #{Version.table_name}.sharing = 'system'" +
451 " #{Version.table_name}.sharing = 'system'" +
452 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
452 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
453 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
453 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
454 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
454 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
455 "))")
455 "))")
456 end
456 end
457 end
457 end
458 end
458 end
459
459
460 # Returns a hash of project users grouped by role
460 # Returns a hash of project users grouped by role
461 def users_by_role
461 def users_by_role
462 members.includes(:user, :roles).all.inject({}) do |h, m|
462 members.includes(:user, :roles).all.inject({}) do |h, m|
463 m.roles.each do |r|
463 m.roles.each do |r|
464 h[r] ||= []
464 h[r] ||= []
465 h[r] << m.user
465 h[r] << m.user
466 end
466 end
467 h
467 h
468 end
468 end
469 end
469 end
470
470
471 # Deletes all project's members
471 # Deletes all project's members
472 def delete_all_members
472 def delete_all_members
473 me, mr = Member.table_name, MemberRole.table_name
473 me, mr = Member.table_name, MemberRole.table_name
474 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
474 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
475 Member.delete_all(['project_id = ?', id])
475 Member.delete_all(['project_id = ?', id])
476 end
476 end
477
477
478 # Users/groups issues can be assigned to
478 # Users/groups issues can be assigned to
479 def assignable_users
479 def assignable_users
480 assignable = Setting.issue_group_assignment? ? member_principals : members
480 assignable = Setting.issue_group_assignment? ? member_principals : members
481 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
481 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
482 end
482 end
483
483
484 # Returns the mail adresses of users that should be always notified on project events
484 # Returns the mail adresses of users that should be always notified on project events
485 def recipients
485 def recipients
486 notified_users.collect {|user| user.mail}
486 notified_users.collect {|user| user.mail}
487 end
487 end
488
488
489 # Returns the users that should be notified on project events
489 # Returns the users that should be notified on project events
490 def notified_users
490 def notified_users
491 # TODO: User part should be extracted to User#notify_about?
491 # TODO: User part should be extracted to User#notify_about?
492 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
492 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
493 end
493 end
494
494
495 # Returns an array of all custom fields enabled for project issues
495 # Returns an array of all custom fields enabled for project issues
496 # (explictly associated custom fields and custom fields enabled for all projects)
496 # (explictly associated custom fields and custom fields enabled for all projects)
497 def all_issue_custom_fields
497 def all_issue_custom_fields
498 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
498 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
499 end
499 end
500
500
501 # Returns an array of all custom fields enabled for project time entries
501 # Returns an array of all custom fields enabled for project time entries
502 # (explictly associated custom fields and custom fields enabled for all projects)
502 # (explictly associated custom fields and custom fields enabled for all projects)
503 def all_time_entry_custom_fields
503 def all_time_entry_custom_fields
504 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
504 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
505 end
505 end
506
506
507 def project
507 def project
508 self
508 self
509 end
509 end
510
510
511 def <=>(project)
511 def <=>(project)
512 name.downcase <=> project.name.downcase
512 name.downcase <=> project.name.downcase
513 end
513 end
514
514
515 def to_s
515 def to_s
516 name
516 name
517 end
517 end
518
518
519 # Returns a short description of the projects (first lines)
519 # Returns a short description of the projects (first lines)
520 def short_description(length = 255)
520 def short_description(length = 255)
521 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
521 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
522 end
522 end
523
523
524 def css_classes
524 def css_classes
525 s = 'project'
525 s = 'project'
526 s << ' root' if root?
526 s << ' root' if root?
527 s << ' child' if child?
527 s << ' child' if child?
528 s << (leaf? ? ' leaf' : ' parent')
528 s << (leaf? ? ' leaf' : ' parent')
529 unless active?
529 unless active?
530 if archived?
530 if archived?
531 s << ' archived'
531 s << ' archived'
532 else
532 else
533 s << ' closed'
533 s << ' closed'
534 end
534 end
535 end
535 end
536 s
536 s
537 end
537 end
538
538
539 # The earliest start date of a project, based on it's issues and versions
539 # The earliest start date of a project, based on it's issues and versions
540 def start_date
540 def start_date
541 [
541 [
542 issues.minimum('start_date'),
542 issues.minimum('start_date'),
543 shared_versions.collect(&:effective_date),
543 shared_versions.collect(&:effective_date),
544 shared_versions.collect(&:start_date)
544 shared_versions.collect(&:start_date)
545 ].flatten.compact.min
545 ].flatten.compact.min
546 end
546 end
547
547
548 # The latest due date of an issue or version
548 # The latest due date of an issue or version
549 def due_date
549 def due_date
550 [
550 [
551 issues.maximum('due_date'),
551 issues.maximum('due_date'),
552 shared_versions.collect(&:effective_date),
552 shared_versions.collect(&:effective_date),
553 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
553 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
554 ].flatten.compact.max
554 ].flatten.compact.max
555 end
555 end
556
556
557 def overdue?
557 def overdue?
558 active? && !due_date.nil? && (due_date < Date.today)
558 active? && !due_date.nil? && (due_date < Date.today)
559 end
559 end
560
560
561 # Returns the percent completed for this project, based on the
561 # Returns the percent completed for this project, based on the
562 # progress on it's versions.
562 # progress on it's versions.
563 def completed_percent(options={:include_subprojects => false})
563 def completed_percent(options={:include_subprojects => false})
564 if options.delete(:include_subprojects)
564 if options.delete(:include_subprojects)
565 total = self_and_descendants.collect(&:completed_percent).sum
565 total = self_and_descendants.collect(&:completed_percent).sum
566
566
567 total / self_and_descendants.count
567 total / self_and_descendants.count
568 else
568 else
569 if versions.count > 0
569 if versions.count > 0
570 total = versions.collect(&:completed_pourcent).sum
570 total = versions.collect(&:completed_pourcent).sum
571
571
572 total / versions.count
572 total / versions.count
573 else
573 else
574 100
574 100
575 end
575 end
576 end
576 end
577 end
577 end
578
578
579 # Return true if this project allows to do the specified action.
579 # Return true if this project allows to do the specified action.
580 # action can be:
580 # action can be:
581 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
581 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
582 # * a permission Symbol (eg. :edit_project)
582 # * a permission Symbol (eg. :edit_project)
583 def allows_to?(action)
583 def allows_to?(action)
584 if archived?
584 if archived?
585 # No action allowed on archived projects
585 # No action allowed on archived projects
586 return false
586 return false
587 end
587 end
588 unless active? || Redmine::AccessControl.read_action?(action)
588 unless active? || Redmine::AccessControl.read_action?(action)
589 # No write action allowed on closed projects
589 # No write action allowed on closed projects
590 return false
590 return false
591 end
591 end
592 # No action allowed on disabled modules
592 # No action allowed on disabled modules
593 if action.is_a? Hash
593 if action.is_a? Hash
594 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
594 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
595 else
595 else
596 allowed_permissions.include? action
596 allowed_permissions.include? action
597 end
597 end
598 end
598 end
599
599
600 def module_enabled?(module_name)
600 def module_enabled?(module_name)
601 module_name = module_name.to_s
601 module_name = module_name.to_s
602 enabled_modules.detect {|m| m.name == module_name}
602 enabled_modules.detect {|m| m.name == module_name}
603 end
603 end
604
604
605 def enabled_module_names=(module_names)
605 def enabled_module_names=(module_names)
606 if module_names && module_names.is_a?(Array)
606 if module_names && module_names.is_a?(Array)
607 module_names = module_names.collect(&:to_s).reject(&:blank?)
607 module_names = module_names.collect(&:to_s).reject(&:blank?)
608 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
608 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
609 else
609 else
610 enabled_modules.clear
610 enabled_modules.clear
611 end
611 end
612 end
612 end
613
613
614 # Returns an array of the enabled modules names
614 # Returns an array of the enabled modules names
615 def enabled_module_names
615 def enabled_module_names
616 enabled_modules.collect(&:name)
616 enabled_modules.collect(&:name)
617 end
617 end
618
618
619 # Enable a specific module
619 # Enable a specific module
620 #
620 #
621 # Examples:
621 # Examples:
622 # project.enable_module!(:issue_tracking)
622 # project.enable_module!(:issue_tracking)
623 # project.enable_module!("issue_tracking")
623 # project.enable_module!("issue_tracking")
624 def enable_module!(name)
624 def enable_module!(name)
625 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
625 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
626 end
626 end
627
627
628 # Disable a module if it exists
628 # Disable a module if it exists
629 #
629 #
630 # Examples:
630 # Examples:
631 # project.disable_module!(:issue_tracking)
631 # project.disable_module!(:issue_tracking)
632 # project.disable_module!("issue_tracking")
632 # project.disable_module!("issue_tracking")
633 # project.disable_module!(project.enabled_modules.first)
633 # project.disable_module!(project.enabled_modules.first)
634 def disable_module!(target)
634 def disable_module!(target)
635 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
635 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
636 target.destroy unless target.blank?
636 target.destroy unless target.blank?
637 end
637 end
638
638
639 safe_attributes 'name',
639 safe_attributes 'name',
640 'description',
640 'description',
641 'homepage',
641 'homepage',
642 'is_public',
642 'is_public',
643 'identifier',
643 'identifier',
644 'custom_field_values',
644 'custom_field_values',
645 'custom_fields',
645 'custom_fields',
646 'tracker_ids',
646 'tracker_ids',
647 'issue_custom_field_ids'
647 'issue_custom_field_ids'
648
648
649 safe_attributes 'enabled_module_names',
649 safe_attributes 'enabled_module_names',
650 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
650 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
651
651
652 # Returns an array of projects that are in this project's hierarchy
652 # Returns an array of projects that are in this project's hierarchy
653 #
653 #
654 # Example: parents, children, siblings
654 # Example: parents, children, siblings
655 def hierarchy
655 def hierarchy
656 parents = project.self_and_ancestors || []
656 parents = project.self_and_ancestors || []
657 descendants = project.descendants || []
657 descendants = project.descendants || []
658 project_hierarchy = parents | descendants # Set union
658 project_hierarchy = parents | descendants # Set union
659 end
659 end
660
660
661 # Returns an auto-generated project identifier based on the last identifier used
661 # Returns an auto-generated project identifier based on the last identifier used
662 def self.next_identifier
662 def self.next_identifier
663 p = Project.order('created_on DESC').first
663 p = Project.order('created_on DESC').first
664 p.nil? ? nil : p.identifier.to_s.succ
664 p.nil? ? nil : p.identifier.to_s.succ
665 end
665 end
666
666
667 # Copies and saves the Project instance based on the +project+.
667 # Copies and saves the Project instance based on the +project+.
668 # Duplicates the source project's:
668 # Duplicates the source project's:
669 # * Wiki
669 # * Wiki
670 # * Versions
670 # * Versions
671 # * Categories
671 # * Categories
672 # * Issues
672 # * Issues
673 # * Members
673 # * Members
674 # * Queries
674 # * Queries
675 #
675 #
676 # Accepts an +options+ argument to specify what to copy
676 # Accepts an +options+ argument to specify what to copy
677 #
677 #
678 # Examples:
678 # Examples:
679 # project.copy(1) # => copies everything
679 # project.copy(1) # => copies everything
680 # project.copy(1, :only => 'members') # => copies members only
680 # project.copy(1, :only => 'members') # => copies members only
681 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
681 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
682 def copy(project, options={})
682 def copy(project, options={})
683 project = project.is_a?(Project) ? project : Project.find(project)
683 project = project.is_a?(Project) ? project : Project.find(project)
684
684
685 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
685 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
686 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
686 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
687
687
688 Project.transaction do
688 Project.transaction do
689 if save
689 if save
690 reload
690 reload
691 to_be_copied.each do |name|
691 to_be_copied.each do |name|
692 send "copy_#{name}", project
692 send "copy_#{name}", project
693 end
693 end
694 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
694 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
695 save
695 save
696 end
696 end
697 end
697 end
698 end
698 end
699
699
700 # Returns a new unsaved Project instance with attributes copied from +project+
700 # Returns a new unsaved Project instance with attributes copied from +project+
701 def self.copy_from(project)
701 def self.copy_from(project)
702 project = project.is_a?(Project) ? project : Project.find(project)
702 project = project.is_a?(Project) ? project : Project.find(project)
703 # clear unique attributes
703 # clear unique attributes
704 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
704 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
705 copy = Project.new(attributes)
705 copy = Project.new(attributes)
706 copy.enabled_modules = project.enabled_modules
706 copy.enabled_modules = project.enabled_modules
707 copy.trackers = project.trackers
707 copy.trackers = project.trackers
708 copy.custom_values = project.custom_values.collect {|v| v.clone}
708 copy.custom_values = project.custom_values.collect {|v| v.clone}
709 copy.issue_custom_fields = project.issue_custom_fields
709 copy.issue_custom_fields = project.issue_custom_fields
710 copy
710 copy
711 end
711 end
712
712
713 # Yields the given block for each project with its level in the tree
713 # Yields the given block for each project with its level in the tree
714 def self.project_tree(projects, &block)
714 def self.project_tree(projects, &block)
715 ancestors = []
715 ancestors = []
716 projects.sort_by(&:lft).each do |project|
716 projects.sort_by(&:lft).each do |project|
717 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
717 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
718 ancestors.pop
718 ancestors.pop
719 end
719 end
720 yield project, ancestors.size
720 yield project, ancestors.size
721 ancestors << project
721 ancestors << project
722 end
722 end
723 end
723 end
724
724
725 private
725 private
726
726
727 # Copies wiki from +project+
727 # Copies wiki from +project+
728 def copy_wiki(project)
728 def copy_wiki(project)
729 # Check that the source project has a wiki first
729 # Check that the source project has a wiki first
730 unless project.wiki.nil?
730 unless project.wiki.nil?
731 self.wiki ||= Wiki.new
731 self.wiki ||= Wiki.new
732 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
732 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
733 wiki_pages_map = {}
733 wiki_pages_map = {}
734 project.wiki.pages.each do |page|
734 project.wiki.pages.each do |page|
735 # Skip pages without content
735 # Skip pages without content
736 next if page.content.nil?
736 next if page.content.nil?
737 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
737 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
738 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
738 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
739 new_wiki_page.content = new_wiki_content
739 new_wiki_page.content = new_wiki_content
740 wiki.pages << new_wiki_page
740 wiki.pages << new_wiki_page
741 wiki_pages_map[page.id] = new_wiki_page
741 wiki_pages_map[page.id] = new_wiki_page
742 end
742 end
743 wiki.save
743 wiki.save
744 # Reproduce page hierarchy
744 # Reproduce page hierarchy
745 project.wiki.pages.each do |page|
745 project.wiki.pages.each do |page|
746 if page.parent_id && wiki_pages_map[page.id]
746 if page.parent_id && wiki_pages_map[page.id]
747 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
747 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
748 wiki_pages_map[page.id].save
748 wiki_pages_map[page.id].save
749 end
749 end
750 end
750 end
751 end
751 end
752 end
752 end
753
753
754 # Copies versions from +project+
754 # Copies versions from +project+
755 def copy_versions(project)
755 def copy_versions(project)
756 project.versions.each do |version|
756 project.versions.each do |version|
757 new_version = Version.new
757 new_version = Version.new
758 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
758 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
759 self.versions << new_version
759 self.versions << new_version
760 end
760 end
761 end
761 end
762
762
763 # Copies issue categories from +project+
763 # Copies issue categories from +project+
764 def copy_issue_categories(project)
764 def copy_issue_categories(project)
765 project.issue_categories.each do |issue_category|
765 project.issue_categories.each do |issue_category|
766 new_issue_category = IssueCategory.new
766 new_issue_category = IssueCategory.new
767 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
767 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
768 self.issue_categories << new_issue_category
768 self.issue_categories << new_issue_category
769 end
769 end
770 end
770 end
771
771
772 # Copies issues from +project+
772 # Copies issues from +project+
773 def copy_issues(project)
773 def copy_issues(project)
774 # Stores the source issue id as a key and the copied issues as the
774 # Stores the source issue id as a key and the copied issues as the
775 # value. Used to map the two togeather for issue relations.
775 # value. Used to map the two togeather for issue relations.
776 issues_map = {}
776 issues_map = {}
777
777
778 # Store status and reopen locked/closed versions
778 # Store status and reopen locked/closed versions
779 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
779 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
780 version_statuses.each do |version, status|
780 version_statuses.each do |version, status|
781 version.update_attribute :status, 'open'
781 version.update_attribute :status, 'open'
782 end
782 end
783
783
784 # Get issues sorted by root_id, lft so that parent issues
784 # Get issues sorted by root_id, lft so that parent issues
785 # get copied before their children
785 # get copied before their children
786 project.issues.reorder('root_id, lft').all.each do |issue|
786 project.issues.reorder('root_id, lft').all.each do |issue|
787 new_issue = Issue.new
787 new_issue = Issue.new
788 new_issue.copy_from(issue, :subtasks => false, :link => false)
788 new_issue.copy_from(issue, :subtasks => false, :link => false)
789 new_issue.project = self
789 new_issue.project = self
790 # Reassign fixed_versions by name, since names are unique per project
790 # Reassign fixed_versions by name, since names are unique per project
791 if issue.fixed_version && issue.fixed_version.project == project
791 if issue.fixed_version && issue.fixed_version.project == project
792 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
792 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
793 end
793 end
794 # Reassign the category by name, since names are unique per project
794 # Reassign the category by name, since names are unique per project
795 if issue.category
795 if issue.category
796 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
796 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
797 end
797 end
798 # Parent issue
798 # Parent issue
799 if issue.parent_id
799 if issue.parent_id
800 if copied_parent = issues_map[issue.parent_id]
800 if copied_parent = issues_map[issue.parent_id]
801 new_issue.parent_issue_id = copied_parent.id
801 new_issue.parent_issue_id = copied_parent.id
802 end
802 end
803 end
803 end
804
804
805 self.issues << new_issue
805 self.issues << new_issue
806 if new_issue.new_record?
806 if new_issue.new_record?
807 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
807 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
808 else
808 else
809 issues_map[issue.id] = new_issue unless new_issue.new_record?
809 issues_map[issue.id] = new_issue unless new_issue.new_record?
810 end
810 end
811 end
811 end
812
812
813 # Restore locked/closed version statuses
813 # Restore locked/closed version statuses
814 version_statuses.each do |version, status|
814 version_statuses.each do |version, status|
815 version.update_attribute :status, status
815 version.update_attribute :status, status
816 end
816 end
817
817
818 # Relations after in case issues related each other
818 # Relations after in case issues related each other
819 project.issues.each do |issue|
819 project.issues.each do |issue|
820 new_issue = issues_map[issue.id]
820 new_issue = issues_map[issue.id]
821 unless new_issue
821 unless new_issue
822 # Issue was not copied
822 # Issue was not copied
823 next
823 next
824 end
824 end
825
825
826 # Relations
826 # Relations
827 issue.relations_from.each do |source_relation|
827 issue.relations_from.each do |source_relation|
828 new_issue_relation = IssueRelation.new
828 new_issue_relation = IssueRelation.new
829 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
829 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
830 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
830 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
831 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
831 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
832 new_issue_relation.issue_to = source_relation.issue_to
832 new_issue_relation.issue_to = source_relation.issue_to
833 end
833 end
834 new_issue.relations_from << new_issue_relation
834 new_issue.relations_from << new_issue_relation
835 end
835 end
836
836
837 issue.relations_to.each do |source_relation|
837 issue.relations_to.each do |source_relation|
838 new_issue_relation = IssueRelation.new
838 new_issue_relation = IssueRelation.new
839 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
839 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
840 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
840 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
841 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
841 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
842 new_issue_relation.issue_from = source_relation.issue_from
842 new_issue_relation.issue_from = source_relation.issue_from
843 end
843 end
844 new_issue.relations_to << new_issue_relation
844 new_issue.relations_to << new_issue_relation
845 end
845 end
846 end
846 end
847 end
847 end
848
848
849 # Copies members from +project+
849 # Copies members from +project+
850 def copy_members(project)
850 def copy_members(project)
851 # Copy users first, then groups to handle members with inherited and given roles
851 # Copy users first, then groups to handle members with inherited and given roles
852 members_to_copy = []
852 members_to_copy = []
853 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
853 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
854 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
854 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
855
855
856 members_to_copy.each do |member|
856 members_to_copy.each do |member|
857 new_member = Member.new
857 new_member = Member.new
858 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
858 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
859 # only copy non inherited roles
859 # only copy non inherited roles
860 # inherited roles will be added when copying the group membership
860 # inherited roles will be added when copying the group membership
861 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
861 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
862 next if role_ids.empty?
862 next if role_ids.empty?
863 new_member.role_ids = role_ids
863 new_member.role_ids = role_ids
864 new_member.project = self
864 new_member.project = self
865 self.members << new_member
865 self.members << new_member
866 end
866 end
867 end
867 end
868
868
869 # Copies queries from +project+
869 # Copies queries from +project+
870 def copy_queries(project)
870 def copy_queries(project)
871 project.queries.each do |query|
871 project.queries.each do |query|
872 new_query = ::Query.new
872 new_query = IssueQuery.new
873 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
873 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
874 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
874 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
875 new_query.project = self
875 new_query.project = self
876 new_query.user_id = query.user_id
876 new_query.user_id = query.user_id
877 self.queries << new_query
877 self.queries << new_query
878 end
878 end
879 end
879 end
880
880
881 # Copies boards from +project+
881 # Copies boards from +project+
882 def copy_boards(project)
882 def copy_boards(project)
883 project.boards.each do |board|
883 project.boards.each do |board|
884 new_board = Board.new
884 new_board = Board.new
885 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
885 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
886 new_board.project = self
886 new_board.project = self
887 self.boards << new_board
887 self.boards << new_board
888 end
888 end
889 end
889 end
890
890
891 def allowed_permissions
891 def allowed_permissions
892 @allowed_permissions ||= begin
892 @allowed_permissions ||= begin
893 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
893 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
894 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
894 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
895 end
895 end
896 end
896 end
897
897
898 def allowed_actions
898 def allowed_actions
899 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
899 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
900 end
900 end
901
901
902 # Returns all the active Systemwide and project specific activities
902 # Returns all the active Systemwide and project specific activities
903 def active_activities
903 def active_activities
904 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
904 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
905
905
906 if overridden_activity_ids.empty?
906 if overridden_activity_ids.empty?
907 return TimeEntryActivity.shared.active
907 return TimeEntryActivity.shared.active
908 else
908 else
909 return system_activities_and_project_overrides
909 return system_activities_and_project_overrides
910 end
910 end
911 end
911 end
912
912
913 # Returns all the Systemwide and project specific activities
913 # Returns all the Systemwide and project specific activities
914 # (inactive and active)
914 # (inactive and active)
915 def all_activities
915 def all_activities
916 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
916 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
917
917
918 if overridden_activity_ids.empty?
918 if overridden_activity_ids.empty?
919 return TimeEntryActivity.shared
919 return TimeEntryActivity.shared
920 else
920 else
921 return system_activities_and_project_overrides(true)
921 return system_activities_and_project_overrides(true)
922 end
922 end
923 end
923 end
924
924
925 # Returns the systemwide active activities merged with the project specific overrides
925 # Returns the systemwide active activities merged with the project specific overrides
926 def system_activities_and_project_overrides(include_inactive=false)
926 def system_activities_and_project_overrides(include_inactive=false)
927 if include_inactive
927 if include_inactive
928 return TimeEntryActivity.shared.
928 return TimeEntryActivity.shared.
929 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
929 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
930 self.time_entry_activities
930 self.time_entry_activities
931 else
931 else
932 return TimeEntryActivity.shared.active.
932 return TimeEntryActivity.shared.active.
933 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
933 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
934 self.time_entry_activities.active
934 self.time_entry_activities.active
935 end
935 end
936 end
936 end
937
937
938 # Archives subprojects recursively
938 # Archives subprojects recursively
939 def archive!
939 def archive!
940 children.each do |subproject|
940 children.each do |subproject|
941 subproject.send :archive!
941 subproject.send :archive!
942 end
942 end
943 update_attribute :status, STATUS_ARCHIVED
943 update_attribute :status, STATUS_ARCHIVED
944 end
944 end
945
945
946 def update_position_under_parent
946 def update_position_under_parent
947 set_or_update_position_under(parent)
947 set_or_update_position_under(parent)
948 end
948 end
949
949
950 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
950 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
951 def set_or_update_position_under(target_parent)
951 def set_or_update_position_under(target_parent)
952 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
952 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
953 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
953 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
954
954
955 if to_be_inserted_before
955 if to_be_inserted_before
956 move_to_left_of(to_be_inserted_before)
956 move_to_left_of(to_be_inserted_before)
957 elsif target_parent.nil?
957 elsif target_parent.nil?
958 if sibs.empty?
958 if sibs.empty?
959 # move_to_root adds the project in first (ie. left) position
959 # move_to_root adds the project in first (ie. left) position
960 move_to_root
960 move_to_root
961 else
961 else
962 move_to_right_of(sibs.last) unless self == sibs.last
962 move_to_right_of(sibs.last) unless self == sibs.last
963 end
963 end
964 else
964 else
965 # move_to_child_of adds the project in last (ie.right) position
965 # move_to_child_of adds the project in last (ie.right) position
966 move_to_child_of(target_parent)
966 move_to_child_of(target_parent)
967 end
967 end
968 end
968 end
969 end
969 end
@@ -1,1100 +1,804
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @inline = options.key?(:inline) ? options[:inline] : true
30 @inline = options.key?(:inline) ? options[:inline] : true
31 @caption_key = options[:caption] || "field_#{name}"
31 @caption_key = options[:caption] || "field_#{name}"
32 end
32 end
33
33
34 def caption
34 def caption
35 l(@caption_key)
35 l(@caption_key)
36 end
36 end
37
37
38 # Returns true if the column is sortable, otherwise false
38 # Returns true if the column is sortable, otherwise false
39 def sortable?
39 def sortable?
40 !@sortable.nil?
40 !@sortable.nil?
41 end
41 end
42
42
43 def sortable
43 def sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 end
45 end
46
46
47 def inline?
47 def inline?
48 @inline
48 @inline
49 end
49 end
50
50
51 def value(issue)
51 def value(issue)
52 issue.send name
52 issue.send name
53 end
53 end
54
54
55 def css_classes
55 def css_classes
56 name
56 name
57 end
57 end
58 end
58 end
59
59
60 class QueryCustomFieldColumn < QueryColumn
60 class QueryCustomFieldColumn < QueryColumn
61
61
62 def initialize(custom_field)
62 def initialize(custom_field)
63 self.name = "cf_#{custom_field.id}".to_sym
63 self.name = "cf_#{custom_field.id}".to_sym
64 self.sortable = custom_field.order_statement || false
64 self.sortable = custom_field.order_statement || false
65 self.groupable = custom_field.group_statement || false
65 self.groupable = custom_field.group_statement || false
66 @inline = true
66 @inline = true
67 @cf = custom_field
67 @cf = custom_field
68 end
68 end
69
69
70 def caption
70 def caption
71 @cf.name
71 @cf.name
72 end
72 end
73
73
74 def custom_field
74 def custom_field
75 @cf
75 @cf
76 end
76 end
77
77
78 def value(issue)
78 def value(issue)
79 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
79 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
81 end
81 end
82
82
83 def css_classes
83 def css_classes
84 @css_classes ||= "#{name} #{@cf.field_format}"
84 @css_classes ||= "#{name} #{@cf.field_format}"
85 end
85 end
86 end
86 end
87
87
88 class Query < ActiveRecord::Base
88 class Query < ActiveRecord::Base
89 class StatementInvalid < ::ActiveRecord::StatementInvalid
89 class StatementInvalid < ::ActiveRecord::StatementInvalid
90 end
90 end
91
91
92 belongs_to :project
92 belongs_to :project
93 belongs_to :user
93 belongs_to :user
94 serialize :filters
94 serialize :filters
95 serialize :column_names
95 serialize :column_names
96 serialize :sort_criteria, Array
96 serialize :sort_criteria, Array
97
97
98 attr_protected :project_id, :user_id
98 attr_protected :project_id, :user_id
99
99
100 validates_presence_of :name
100 validates_presence_of :name
101 validates_length_of :name, :maximum => 255
101 validates_length_of :name, :maximum => 255
102 validate :validate_query_filters
102 validate :validate_query_filters
103
103
104 class_attribute :operators
104 class_attribute :operators
105 self.operators = {
105 self.operators = {
106 "=" => :label_equals,
106 "=" => :label_equals,
107 "!" => :label_not_equals,
107 "!" => :label_not_equals,
108 "o" => :label_open_issues,
108 "o" => :label_open_issues,
109 "c" => :label_closed_issues,
109 "c" => :label_closed_issues,
110 "!*" => :label_none,
110 "!*" => :label_none,
111 "*" => :label_any,
111 "*" => :label_any,
112 ">=" => :label_greater_or_equal,
112 ">=" => :label_greater_or_equal,
113 "<=" => :label_less_or_equal,
113 "<=" => :label_less_or_equal,
114 "><" => :label_between,
114 "><" => :label_between,
115 "<t+" => :label_in_less_than,
115 "<t+" => :label_in_less_than,
116 ">t+" => :label_in_more_than,
116 ">t+" => :label_in_more_than,
117 "><t+"=> :label_in_the_next_days,
117 "><t+"=> :label_in_the_next_days,
118 "t+" => :label_in,
118 "t+" => :label_in,
119 "t" => :label_today,
119 "t" => :label_today,
120 "w" => :label_this_week,
120 "w" => :label_this_week,
121 ">t-" => :label_less_than_ago,
121 ">t-" => :label_less_than_ago,
122 "<t-" => :label_more_than_ago,
122 "<t-" => :label_more_than_ago,
123 "><t-"=> :label_in_the_past_days,
123 "><t-"=> :label_in_the_past_days,
124 "t-" => :label_ago,
124 "t-" => :label_ago,
125 "~" => :label_contains,
125 "~" => :label_contains,
126 "!~" => :label_not_contains,
126 "!~" => :label_not_contains,
127 "=p" => :label_any_issues_in_project,
127 "=p" => :label_any_issues_in_project,
128 "=!p" => :label_any_issues_not_in_project,
128 "=!p" => :label_any_issues_not_in_project,
129 "!p" => :label_no_issues_in_project
129 "!p" => :label_no_issues_in_project
130 }
130 }
131
131
132 class_attribute :operators_by_filter_type
132 class_attribute :operators_by_filter_type
133 self.operators_by_filter_type = {
133 self.operators_by_filter_type = {
134 :list => [ "=", "!" ],
134 :list => [ "=", "!" ],
135 :list_status => [ "o", "=", "!", "c", "*" ],
135 :list_status => [ "o", "=", "!", "c", "*" ],
136 :list_optional => [ "=", "!", "!*", "*" ],
136 :list_optional => [ "=", "!", "!*", "*" ],
137 :list_subprojects => [ "*", "!*", "=" ],
137 :list_subprojects => [ "*", "!*", "=" ],
138 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
138 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
139 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
139 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
140 :string => [ "=", "~", "!", "!~", "!*", "*" ],
140 :string => [ "=", "~", "!", "!~", "!*", "*" ],
141 :text => [ "~", "!~", "!*", "*" ],
141 :text => [ "~", "!~", "!*", "*" ],
142 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
142 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
143 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
143 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
144 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
144 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
145 }
145 }
146
146
147 class_attribute :available_columns
147 class_attribute :available_columns
148 self.available_columns = [
148 self.available_columns = []
149 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
150 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
151 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
152 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
153 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
154 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
155 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
156 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
157 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
158 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
159 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
160 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
161 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
162 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
163 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
164 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
165 QueryColumn.new(:relations, :caption => :label_related_issues),
166 QueryColumn.new(:description, :inline => false)
167 ]
168
169 scope :visible, lambda {|*args|
170 user = args.shift || User.current
171 base = Project.allowed_to_condition(user, :view_issues, *args)
172 user_id = user.logged? ? user.id : 0
173
174 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
175 }
176
149
177 def initialize(attributes=nil, *args)
150 def initialize(attributes=nil, *args)
178 super attributes
151 super attributes
179 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
180 @is_for_all = project.nil?
152 @is_for_all = project.nil?
181 end
153 end
182
154
183 def validate_query_filters
155 def validate_query_filters
184 filters.each_key do |field|
156 filters.each_key do |field|
185 if values_for(field)
157 if values_for(field)
186 case type_for(field)
158 case type_for(field)
187 when :integer
159 when :integer
188 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
160 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
189 when :float
161 when :float
190 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
162 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
191 when :date, :date_past
163 when :date, :date_past
192 case operator_for(field)
164 case operator_for(field)
193 when "=", ">=", "<=", "><"
165 when "=", ">=", "<=", "><"
194 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
166 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
195 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
167 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
196 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
168 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
197 end
169 end
198 end
170 end
199 end
171 end
200
172
201 add_filter_error(field, :blank) unless
173 add_filter_error(field, :blank) unless
202 # filter requires one or more values
174 # filter requires one or more values
203 (values_for(field) and !values_for(field).first.blank?) or
175 (values_for(field) and !values_for(field).first.blank?) or
204 # filter doesn't require any value
176 # filter doesn't require any value
205 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
177 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
206 end if filters
178 end if filters
207 end
179 end
208
180
209 def add_filter_error(field, message)
181 def add_filter_error(field, message)
210 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
182 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
211 errors.add(:base, m)
183 errors.add(:base, m)
212 end
184 end
213
185
214 # Returns true if the query is visible to +user+ or the current user.
215 def visible?(user=User.current)
216 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
217 end
218
219 def editable_by?(user)
186 def editable_by?(user)
220 return false unless user
187 return false unless user
221 # Admin can edit them all and regular users can edit their private queries
188 # Admin can edit them all and regular users can edit their private queries
222 return true if user.admin? || (!is_public && self.user_id == user.id)
189 return true if user.admin? || (!is_public && self.user_id == user.id)
223 # Members can not edit public queries that are for all project (only admin is allowed to)
190 # Members can not edit public queries that are for all project (only admin is allowed to)
224 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
191 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
225 end
192 end
226
193
227 def trackers
194 def trackers
228 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
195 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
229 end
196 end
230
197
231 # Returns a hash of localized labels for all filter operators
198 # Returns a hash of localized labels for all filter operators
232 def self.operators_labels
199 def self.operators_labels
233 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
200 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
234 end
201 end
235
202
236 def available_filters
237 return @available_filters if @available_filters
238 @available_filters = {
239 "status_id" => {
240 :type => :list_status, :order => 0,
241 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
242 },
243 "tracker_id" => {
244 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
245 },
246 "priority_id" => {
247 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
248 },
249 "subject" => { :type => :text, :order => 8 },
250 "created_on" => { :type => :date_past, :order => 9 },
251 "updated_on" => { :type => :date_past, :order => 10 },
252 "start_date" => { :type => :date, :order => 11 },
253 "due_date" => { :type => :date, :order => 12 },
254 "estimated_hours" => { :type => :float, :order => 13 },
255 "done_ratio" => { :type => :integer, :order => 14 }
256 }
257 IssueRelation::TYPES.each do |relation_type, options|
258 @available_filters[relation_type] = {
259 :type => :relation, :order => @available_filters.size + 100,
260 :label => options[:name]
261 }
262 end
263 principals = []
264 if project
265 principals += project.principals.sort
266 unless project.leaf?
267 subprojects = project.descendants.visible.all
268 if subprojects.any?
269 @available_filters["subproject_id"] = {
270 :type => :list_subprojects, :order => 13,
271 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
272 }
273 principals += Principal.member_of(subprojects)
274 end
275 end
276 else
277 if all_projects.any?
278 # members of visible projects
279 principals += Principal.member_of(all_projects)
280 # project filter
281 project_values = []
282 if User.current.logged? && User.current.memberships.any?
283 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
284 end
285 project_values += all_projects_values
286 @available_filters["project_id"] = {
287 :type => :list, :order => 1, :values => project_values
288 } unless project_values.empty?
289 end
290 end
291 principals.uniq!
292 principals.sort!
293 users = principals.select {|p| p.is_a?(User)}
294
295 assigned_to_values = []
296 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
297 assigned_to_values += (Setting.issue_group_assignment? ?
298 principals : users).collect{|s| [s.name, s.id.to_s] }
299 @available_filters["assigned_to_id"] = {
300 :type => :list_optional, :order => 4, :values => assigned_to_values
301 } unless assigned_to_values.empty?
302
303 author_values = []
304 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
305 author_values += users.collect{|s| [s.name, s.id.to_s] }
306 @available_filters["author_id"] = {
307 :type => :list, :order => 5, :values => author_values
308 } unless author_values.empty?
309
310 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
311 @available_filters["member_of_group"] = {
312 :type => :list_optional, :order => 6, :values => group_values
313 } unless group_values.empty?
314
315 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
316 @available_filters["assigned_to_role"] = {
317 :type => :list_optional, :order => 7, :values => role_values
318 } unless role_values.empty?
319
320 if User.current.logged?
321 @available_filters["watcher_id"] = {
322 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
323 }
324 end
325
326 if project
327 # project specific filters
328 categories = project.issue_categories.all
329 unless categories.empty?
330 @available_filters["category_id"] = {
331 :type => :list_optional, :order => 6,
332 :values => categories.collect{|s| [s.name, s.id.to_s] }
333 }
334 end
335 versions = project.shared_versions.all
336 unless versions.empty?
337 @available_filters["fixed_version_id"] = {
338 :type => :list_optional, :order => 7,
339 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
340 }
341 end
342 add_custom_fields_filters(project.all_issue_custom_fields)
343 else
344 # global filters for cross project issue list
345 system_shared_versions = Version.visible.find_all_by_sharing('system')
346 unless system_shared_versions.empty?
347 @available_filters["fixed_version_id"] = {
348 :type => :list_optional, :order => 7,
349 :values => system_shared_versions.sort.collect{|s|
350 ["#{s.project.name} - #{s.name}", s.id.to_s]
351 }
352 }
353 end
354 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
355 end
356 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
357 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
358 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
359 @available_filters["is_private"] = {
360 :type => :list, :order => 16,
361 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
362 }
363 end
364 Tracker.disabled_core_fields(trackers).each {|field|
365 @available_filters.delete field
366 }
367 @available_filters.each do |field, options|
368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
369 end
370 @available_filters
371 end
372
373 # Returns a representation of the available filters for JSON serialization
203 # Returns a representation of the available filters for JSON serialization
374 def available_filters_as_json
204 def available_filters_as_json
375 json = {}
205 json = {}
376 available_filters.each do |field, options|
206 available_filters.each do |field, options|
377 json[field] = options.slice(:type, :name, :values).stringify_keys
207 json[field] = options.slice(:type, :name, :values).stringify_keys
378 end
208 end
379 json
209 json
380 end
210 end
381
211
382 def all_projects
212 def all_projects
383 @all_projects ||= Project.visible.all
213 @all_projects ||= Project.visible.all
384 end
214 end
385
215
386 def all_projects_values
216 def all_projects_values
387 return @all_projects_values if @all_projects_values
217 return @all_projects_values if @all_projects_values
388
218
389 values = []
219 values = []
390 Project.project_tree(all_projects) do |p, level|
220 Project.project_tree(all_projects) do |p, level|
391 prefix = (level > 0 ? ('--' * level + ' ') : '')
221 prefix = (level > 0 ? ('--' * level + ' ') : '')
392 values << ["#{prefix}#{p.name}", p.id.to_s]
222 values << ["#{prefix}#{p.name}", p.id.to_s]
393 end
223 end
394 @all_projects_values = values
224 @all_projects_values = values
395 end
225 end
396
226
397 def add_filter(field, operator, values)
227 def add_filter(field, operator, values)
398 # values must be an array
228 # values must be an array
399 return unless values.nil? || values.is_a?(Array)
229 return unless values.nil? || values.is_a?(Array)
400 # check if field is defined as an available filter
230 # check if field is defined as an available filter
401 if available_filters.has_key? field
231 if available_filters.has_key? field
402 filter_options = available_filters[field]
232 filter_options = available_filters[field]
403 filters[field] = {:operator => operator, :values => (values || [''])}
233 filters[field] = {:operator => operator, :values => (values || [''])}
404 end
234 end
405 end
235 end
406
236
407 def add_short_filter(field, expression)
237 def add_short_filter(field, expression)
408 return unless expression && available_filters.has_key?(field)
238 return unless expression && available_filters.has_key?(field)
409 field_type = available_filters[field][:type]
239 field_type = available_filters[field][:type]
410 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
240 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
411 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
241 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
412 add_filter field, operator, $1.present? ? $1.split('|') : ['']
242 add_filter field, operator, $1.present? ? $1.split('|') : ['']
413 end || add_filter(field, '=', expression.split('|'))
243 end || add_filter(field, '=', expression.split('|'))
414 end
244 end
415
245
416 # Add multiple filters using +add_filter+
246 # Add multiple filters using +add_filter+
417 def add_filters(fields, operators, values)
247 def add_filters(fields, operators, values)
418 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
248 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
419 fields.each do |field|
249 fields.each do |field|
420 add_filter(field, operators[field], values && values[field])
250 add_filter(field, operators[field], values && values[field])
421 end
251 end
422 end
252 end
423 end
253 end
424
254
425 def has_filter?(field)
255 def has_filter?(field)
426 filters and filters[field]
256 filters and filters[field]
427 end
257 end
428
258
429 def type_for(field)
259 def type_for(field)
430 available_filters[field][:type] if available_filters.has_key?(field)
260 available_filters[field][:type] if available_filters.has_key?(field)
431 end
261 end
432
262
433 def operator_for(field)
263 def operator_for(field)
434 has_filter?(field) ? filters[field][:operator] : nil
264 has_filter?(field) ? filters[field][:operator] : nil
435 end
265 end
436
266
437 def values_for(field)
267 def values_for(field)
438 has_filter?(field) ? filters[field][:values] : nil
268 has_filter?(field) ? filters[field][:values] : nil
439 end
269 end
440
270
441 def value_for(field, index=0)
271 def value_for(field, index=0)
442 (values_for(field) || [])[index]
272 (values_for(field) || [])[index]
443 end
273 end
444
274
445 def label_for(field)
275 def label_for(field)
446 label = available_filters[field][:name] if available_filters.has_key?(field)
276 label = available_filters[field][:name] if available_filters.has_key?(field)
447 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
277 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
448 end
278 end
449
279
450 def available_columns
451 return @available_columns if @available_columns
452 @available_columns = ::Query.available_columns.dup
453 @available_columns += (project ?
454 project.all_issue_custom_fields :
455 IssueCustomField.all
456 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
457
458 if User.current.allowed_to?(:view_time_entries, project, :global => true)
459 index = nil
460 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
461 index = (index ? index + 1 : -1)
462 # insert the column after estimated_hours or at the end
463 @available_columns.insert index, QueryColumn.new(:spent_hours,
464 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
465 :default_order => 'desc',
466 :caption => :label_spent_time
467 )
468 end
469
470 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
471 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
472 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
473 end
474
475 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
476 @available_columns.reject! {|column|
477 disabled_fields.include?(column.name.to_s)
478 }
479
480 @available_columns
481 end
482
483 def self.add_available_column(column)
280 def self.add_available_column(column)
484 self.available_columns << (column) if column.is_a?(QueryColumn)
281 self.available_columns << (column) if column.is_a?(QueryColumn)
485 end
282 end
486
283
487 # Returns an array of columns that can be used to group the results
284 # Returns an array of columns that can be used to group the results
488 def groupable_columns
285 def groupable_columns
489 available_columns.select {|c| c.groupable}
286 available_columns.select {|c| c.groupable}
490 end
287 end
491
288
492 # Returns a Hash of columns and the key for sorting
289 # Returns a Hash of columns and the key for sorting
493 def sortable_columns
290 def sortable_columns
494 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
291 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
495 h[column.name.to_s] = column.sortable
292 h[column.name.to_s] = column.sortable
496 h
293 h
497 })
294 })
498 end
295 end
499
296
500 def columns
297 def columns
501 # preserve the column_names order
298 # preserve the column_names order
502 (has_default_columns? ? default_columns_names : column_names).collect do |name|
299 (has_default_columns? ? default_columns_names : column_names).collect do |name|
503 available_columns.find { |col| col.name == name }
300 available_columns.find { |col| col.name == name }
504 end.compact
301 end.compact
505 end
302 end
506
303
507 def inline_columns
304 def inline_columns
508 columns.select(&:inline?)
305 columns.select(&:inline?)
509 end
306 end
510
307
511 def block_columns
308 def block_columns
512 columns.reject(&:inline?)
309 columns.reject(&:inline?)
513 end
310 end
514
311
515 def available_inline_columns
312 def available_inline_columns
516 available_columns.select(&:inline?)
313 available_columns.select(&:inline?)
517 end
314 end
518
315
519 def available_block_columns
316 def available_block_columns
520 available_columns.reject(&:inline?)
317 available_columns.reject(&:inline?)
521 end
318 end
522
319
523 def default_columns_names
320 def default_columns_names
524 @default_columns_names ||= begin
321 @default_columns_names ||= begin
525 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
322 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
526
323
527 project.present? ? default_columns : [:project] | default_columns
324 project.present? ? default_columns : [:project] | default_columns
528 end
325 end
529 end
326 end
530
327
531 def column_names=(names)
328 def column_names=(names)
532 if names
329 if names
533 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
330 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
534 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
331 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
535 # Set column_names to nil if default columns
332 # Set column_names to nil if default columns
536 if names == default_columns_names
333 if names == default_columns_names
537 names = nil
334 names = nil
538 end
335 end
539 end
336 end
540 write_attribute(:column_names, names)
337 write_attribute(:column_names, names)
541 end
338 end
542
339
543 def has_column?(column)
340 def has_column?(column)
544 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
341 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
545 end
342 end
546
343
547 def has_default_columns?
344 def has_default_columns?
548 column_names.nil? || column_names.empty?
345 column_names.nil? || column_names.empty?
549 end
346 end
550
347
551 def sort_criteria=(arg)
348 def sort_criteria=(arg)
552 c = []
349 c = []
553 if arg.is_a?(Hash)
350 if arg.is_a?(Hash)
554 arg = arg.keys.sort.collect {|k| arg[k]}
351 arg = arg.keys.sort.collect {|k| arg[k]}
555 end
352 end
556 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
353 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
557 write_attribute(:sort_criteria, c)
354 write_attribute(:sort_criteria, c)
558 end
355 end
559
356
560 def sort_criteria
357 def sort_criteria
561 read_attribute(:sort_criteria) || []
358 read_attribute(:sort_criteria) || []
562 end
359 end
563
360
564 def sort_criteria_key(arg)
361 def sort_criteria_key(arg)
565 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
362 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
566 end
363 end
567
364
568 def sort_criteria_order(arg)
365 def sort_criteria_order(arg)
569 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
366 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
570 end
367 end
571
368
572 def sort_criteria_order_for(key)
369 def sort_criteria_order_for(key)
573 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
370 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
574 end
371 end
575
372
576 # Returns the SQL sort order that should be prepended for grouping
373 # Returns the SQL sort order that should be prepended for grouping
577 def group_by_sort_order
374 def group_by_sort_order
578 if grouped? && (column = group_by_column)
375 if grouped? && (column = group_by_column)
579 order = sort_criteria_order_for(column.name) || column.default_order
376 order = sort_criteria_order_for(column.name) || column.default_order
580 column.sortable.is_a?(Array) ?
377 column.sortable.is_a?(Array) ?
581 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
378 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
582 "#{column.sortable} #{order}"
379 "#{column.sortable} #{order}"
583 end
380 end
584 end
381 end
585
382
586 # Returns true if the query is a grouped query
383 # Returns true if the query is a grouped query
587 def grouped?
384 def grouped?
588 !group_by_column.nil?
385 !group_by_column.nil?
589 end
386 end
590
387
591 def group_by_column
388 def group_by_column
592 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
389 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
593 end
390 end
594
391
595 def group_by_statement
392 def group_by_statement
596 group_by_column.try(:groupable)
393 group_by_column.try(:groupable)
597 end
394 end
598
395
599 def project_statement
396 def project_statement
600 project_clauses = []
397 project_clauses = []
601 if project && !project.descendants.active.empty?
398 if project && !project.descendants.active.empty?
602 ids = [project.id]
399 ids = [project.id]
603 if has_filter?("subproject_id")
400 if has_filter?("subproject_id")
604 case operator_for("subproject_id")
401 case operator_for("subproject_id")
605 when '='
402 when '='
606 # include the selected subprojects
403 # include the selected subprojects
607 ids += values_for("subproject_id").each(&:to_i)
404 ids += values_for("subproject_id").each(&:to_i)
608 when '!*'
405 when '!*'
609 # main project only
406 # main project only
610 else
407 else
611 # all subprojects
408 # all subprojects
612 ids += project.descendants.collect(&:id)
409 ids += project.descendants.collect(&:id)
613 end
410 end
614 elsif Setting.display_subprojects_issues?
411 elsif Setting.display_subprojects_issues?
615 ids += project.descendants.collect(&:id)
412 ids += project.descendants.collect(&:id)
616 end
413 end
617 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
414 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
618 elsif project
415 elsif project
619 project_clauses << "#{Project.table_name}.id = %d" % project.id
416 project_clauses << "#{Project.table_name}.id = %d" % project.id
620 end
417 end
621 project_clauses.any? ? project_clauses.join(' AND ') : nil
418 project_clauses.any? ? project_clauses.join(' AND ') : nil
622 end
419 end
623
420
624 def statement
421 def statement
625 # filters clauses
422 # filters clauses
626 filters_clauses = []
423 filters_clauses = []
627 filters.each_key do |field|
424 filters.each_key do |field|
628 next if field == "subproject_id"
425 next if field == "subproject_id"
629 v = values_for(field).clone
426 v = values_for(field).clone
630 next unless v and !v.empty?
427 next unless v and !v.empty?
631 operator = operator_for(field)
428 operator = operator_for(field)
632
429
633 # "me" value subsitution
430 # "me" value subsitution
634 if %w(assigned_to_id author_id watcher_id).include?(field)
431 if %w(assigned_to_id author_id watcher_id).include?(field)
635 if v.delete("me")
432 if v.delete("me")
636 if User.current.logged?
433 if User.current.logged?
637 v.push(User.current.id.to_s)
434 v.push(User.current.id.to_s)
638 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
435 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
639 else
436 else
640 v.push("0")
437 v.push("0")
641 end
438 end
642 end
439 end
643 end
440 end
644
441
645 if field == 'project_id'
442 if field == 'project_id'
646 if v.delete('mine')
443 if v.delete('mine')
647 v += User.current.memberships.map(&:project_id).map(&:to_s)
444 v += User.current.memberships.map(&:project_id).map(&:to_s)
648 end
445 end
649 end
446 end
650
447
651 if field =~ /cf_(\d+)$/
448 if field =~ /cf_(\d+)$/
652 # custom field
449 # custom field
653 filters_clauses << sql_for_custom_field(field, operator, v, $1)
450 filters_clauses << sql_for_custom_field(field, operator, v, $1)
654 elsif respond_to?("sql_for_#{field}_field")
451 elsif respond_to?("sql_for_#{field}_field")
655 # specific statement
452 # specific statement
656 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
453 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
657 else
454 else
658 # regular field
455 # regular field
659 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
456 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
660 end
457 end
661 end if filters and valid?
458 end if filters and valid?
662
459
663 filters_clauses << project_statement
460 filters_clauses << project_statement
664 filters_clauses.reject!(&:blank?)
461 filters_clauses.reject!(&:blank?)
665
462
666 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
463 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
667 end
464 end
668
465
669 # Returns the issue count
670 def issue_count
671 Issue.visible.count(:include => [:status, :project], :conditions => statement)
672 rescue ::ActiveRecord::StatementInvalid => e
673 raise StatementInvalid.new(e.message)
674 end
675
676 # Returns the issue count by group or nil if query is not grouped
677 def issue_count_by_group
678 r = nil
679 if grouped?
680 begin
681 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
682 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
683 rescue ActiveRecord::RecordNotFound
684 r = {nil => issue_count}
685 end
686 c = group_by_column
687 if c.is_a?(QueryCustomFieldColumn)
688 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
689 end
690 end
691 r
692 rescue ::ActiveRecord::StatementInvalid => e
693 raise StatementInvalid.new(e.message)
694 end
695
696 # Returns the issues
697 # Valid options are :order, :offset, :limit, :include, :conditions
698 def issues(options={})
699 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
700 order_option = nil if order_option.blank?
701
702 issues = Issue.visible.where(options[:conditions]).all(
703 :include => ([:status, :project] + (options[:include] || [])).uniq,
704 :conditions => statement,
705 :order => order_option,
706 :joins => joins_for_order_statement(order_option),
707 :limit => options[:limit],
708 :offset => options[:offset]
709 )
710
711 if has_column?(:spent_hours)
712 Issue.load_visible_spent_hours(issues)
713 end
714 if has_column?(:relations)
715 Issue.load_visible_relations(issues)
716 end
717 issues
718 rescue ::ActiveRecord::StatementInvalid => e
719 raise StatementInvalid.new(e.message)
720 end
721
722 # Returns the issues ids
723 def issue_ids(options={})
724 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
725 order_option = nil if order_option.blank?
726
727 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
728 :conditions => statement,
729 :order => order_option,
730 :joins => joins_for_order_statement(order_option),
731 :limit => options[:limit],
732 :offset => options[:offset]).find_ids
733 rescue ::ActiveRecord::StatementInvalid => e
734 raise StatementInvalid.new(e.message)
735 end
736
737 # Returns the journals
738 # Valid options are :order, :offset, :limit
739 def journals(options={})
740 Journal.visible.all(
741 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
742 :conditions => statement,
743 :order => options[:order],
744 :limit => options[:limit],
745 :offset => options[:offset]
746 )
747 rescue ::ActiveRecord::StatementInvalid => e
748 raise StatementInvalid.new(e.message)
749 end
750
751 # Returns the versions
752 # Valid options are :conditions
753 def versions(options={})
754 Version.visible.where(options[:conditions]).all(
755 :include => :project,
756 :conditions => project_statement
757 )
758 rescue ::ActiveRecord::StatementInvalid => e
759 raise StatementInvalid.new(e.message)
760 end
761
762 def sql_for_watcher_id_field(field, operator, value)
466 def sql_for_watcher_id_field(field, operator, value)
763 db_table = Watcher.table_name
467 db_table = Watcher.table_name
764 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
468 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
765 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
469 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
766 end
470 end
767
471
768 def sql_for_member_of_group_field(field, operator, value)
472 def sql_for_member_of_group_field(field, operator, value)
769 if operator == '*' # Any group
473 if operator == '*' # Any group
770 groups = Group.all
474 groups = Group.all
771 operator = '=' # Override the operator since we want to find by assigned_to
475 operator = '=' # Override the operator since we want to find by assigned_to
772 elsif operator == "!*"
476 elsif operator == "!*"
773 groups = Group.all
477 groups = Group.all
774 operator = '!' # Override the operator since we want to find by assigned_to
478 operator = '!' # Override the operator since we want to find by assigned_to
775 else
479 else
776 groups = Group.find_all_by_id(value)
480 groups = Group.find_all_by_id(value)
777 end
481 end
778 groups ||= []
482 groups ||= []
779
483
780 members_of_groups = groups.inject([]) {|user_ids, group|
484 members_of_groups = groups.inject([]) {|user_ids, group|
781 if group && group.user_ids.present?
485 if group && group.user_ids.present?
782 user_ids << group.user_ids
486 user_ids << group.user_ids
783 end
487 end
784 user_ids.flatten.uniq.compact
488 user_ids.flatten.uniq.compact
785 }.sort.collect(&:to_s)
489 }.sort.collect(&:to_s)
786
490
787 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
491 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
788 end
492 end
789
493
790 def sql_for_assigned_to_role_field(field, operator, value)
494 def sql_for_assigned_to_role_field(field, operator, value)
791 case operator
495 case operator
792 when "*", "!*" # Member / Not member
496 when "*", "!*" # Member / Not member
793 sw = operator == "!*" ? 'NOT' : ''
497 sw = operator == "!*" ? 'NOT' : ''
794 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
498 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
795 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
499 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
796 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
500 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
797 when "=", "!"
501 when "=", "!"
798 role_cond = value.any? ?
502 role_cond = value.any? ?
799 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
503 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
800 "1=0"
504 "1=0"
801
505
802 sw = operator == "!" ? 'NOT' : ''
506 sw = operator == "!" ? 'NOT' : ''
803 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
507 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
804 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
508 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
805 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
509 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
806 end
510 end
807 end
511 end
808
512
809 def sql_for_is_private_field(field, operator, value)
513 def sql_for_is_private_field(field, operator, value)
810 op = (operator == "=" ? 'IN' : 'NOT IN')
514 op = (operator == "=" ? 'IN' : 'NOT IN')
811 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
515 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
812
516
813 "#{Issue.table_name}.is_private #{op} (#{va})"
517 "#{Issue.table_name}.is_private #{op} (#{va})"
814 end
518 end
815
519
816 def sql_for_relations(field, operator, value, options={})
520 def sql_for_relations(field, operator, value, options={})
817 relation_options = IssueRelation::TYPES[field]
521 relation_options = IssueRelation::TYPES[field]
818 return relation_options unless relation_options
522 return relation_options unless relation_options
819
523
820 relation_type = field
524 relation_type = field
821 join_column, target_join_column = "issue_from_id", "issue_to_id"
525 join_column, target_join_column = "issue_from_id", "issue_to_id"
822 if relation_options[:reverse] || options[:reverse]
526 if relation_options[:reverse] || options[:reverse]
823 relation_type = relation_options[:reverse] || relation_type
527 relation_type = relation_options[:reverse] || relation_type
824 join_column, target_join_column = target_join_column, join_column
528 join_column, target_join_column = target_join_column, join_column
825 end
529 end
826
530
827 sql = case operator
531 sql = case operator
828 when "*", "!*"
532 when "*", "!*"
829 op = (operator == "*" ? 'IN' : 'NOT IN')
533 op = (operator == "*" ? 'IN' : 'NOT IN')
830 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
534 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
831 when "=", "!"
535 when "=", "!"
832 op = (operator == "=" ? 'IN' : 'NOT IN')
536 op = (operator == "=" ? 'IN' : 'NOT IN')
833 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
537 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
834 when "=p", "=!p", "!p"
538 when "=p", "=!p", "!p"
835 op = (operator == "!p" ? 'NOT IN' : 'IN')
539 op = (operator == "!p" ? 'NOT IN' : 'IN')
836 comp = (operator == "=!p" ? '<>' : '=')
540 comp = (operator == "=!p" ? '<>' : '=')
837 "#{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 = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
541 "#{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 = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
838 end
542 end
839
543
840 if relation_options[:sym] == field && !options[:reverse]
544 if relation_options[:sym] == field && !options[:reverse]
841 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
545 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
842 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
546 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
843 else
547 else
844 sql
548 sql
845 end
549 end
846 end
550 end
847
551
848 IssueRelation::TYPES.keys.each do |relation_type|
552 IssueRelation::TYPES.keys.each do |relation_type|
849 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
553 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
850 end
554 end
851
555
852 private
556 private
853
557
854 def sql_for_custom_field(field, operator, value, custom_field_id)
558 def sql_for_custom_field(field, operator, value, custom_field_id)
855 db_table = CustomValue.table_name
559 db_table = CustomValue.table_name
856 db_field = 'value'
560 db_field = 'value'
857 filter = @available_filters[field]
561 filter = @available_filters[field]
858 return nil unless filter
562 return nil unless filter
859 if filter[:format] == 'user'
563 if filter[:format] == 'user'
860 if value.delete('me')
564 if value.delete('me')
861 value.push User.current.id.to_s
565 value.push User.current.id.to_s
862 end
566 end
863 end
567 end
864 not_in = nil
568 not_in = nil
865 if operator == '!'
569 if operator == '!'
866 # Makes ! operator work for custom fields with multiple values
570 # Makes ! operator work for custom fields with multiple values
867 operator = '='
571 operator = '='
868 not_in = 'NOT'
572 not_in = 'NOT'
869 end
573 end
870 customized_key = "id"
574 customized_key = "id"
871 customized_class = Issue
575 customized_class = Issue
872 if field =~ /^(.+)\.cf_/
576 if field =~ /^(.+)\.cf_/
873 assoc = $1
577 assoc = $1
874 customized_key = "#{assoc}_id"
578 customized_key = "#{assoc}_id"
875 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
579 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
876 raise "Unknown Issue association #{assoc}" unless customized_class
580 raise "Unknown Issue association #{assoc}" unless customized_class
877 end
581 end
878 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
582 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
879 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
583 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
880 end
584 end
881
585
882 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
586 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
883 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
587 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
884 sql = ''
588 sql = ''
885 case operator
589 case operator
886 when "="
590 when "="
887 if value.any?
591 if value.any?
888 case type_for(field)
592 case type_for(field)
889 when :date, :date_past
593 when :date, :date_past
890 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
594 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
891 when :integer
595 when :integer
892 if is_custom_filter
596 if is_custom_filter
893 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
597 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
894 else
598 else
895 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
599 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
896 end
600 end
897 when :float
601 when :float
898 if is_custom_filter
602 if is_custom_filter
899 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
603 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
900 else
604 else
901 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
605 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
902 end
606 end
903 else
607 else
904 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
608 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
905 end
609 end
906 else
610 else
907 # IN an empty set
611 # IN an empty set
908 sql = "1=0"
612 sql = "1=0"
909 end
613 end
910 when "!"
614 when "!"
911 if value.any?
615 if value.any?
912 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
616 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
913 else
617 else
914 # NOT IN an empty set
618 # NOT IN an empty set
915 sql = "1=1"
619 sql = "1=1"
916 end
620 end
917 when "!*"
621 when "!*"
918 sql = "#{db_table}.#{db_field} IS NULL"
622 sql = "#{db_table}.#{db_field} IS NULL"
919 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
623 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
920 when "*"
624 when "*"
921 sql = "#{db_table}.#{db_field} IS NOT NULL"
625 sql = "#{db_table}.#{db_field} IS NOT NULL"
922 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
626 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
923 when ">="
627 when ">="
924 if [:date, :date_past].include?(type_for(field))
628 if [:date, :date_past].include?(type_for(field))
925 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
629 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
926 else
630 else
927 if is_custom_filter
631 if is_custom_filter
928 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
632 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
929 else
633 else
930 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
634 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
931 end
635 end
932 end
636 end
933 when "<="
637 when "<="
934 if [:date, :date_past].include?(type_for(field))
638 if [:date, :date_past].include?(type_for(field))
935 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
639 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
936 else
640 else
937 if is_custom_filter
641 if is_custom_filter
938 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
642 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
939 else
643 else
940 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
644 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
941 end
645 end
942 end
646 end
943 when "><"
647 when "><"
944 if [:date, :date_past].include?(type_for(field))
648 if [:date, :date_past].include?(type_for(field))
945 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
649 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
946 else
650 else
947 if is_custom_filter
651 if is_custom_filter
948 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
652 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
949 else
653 else
950 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
654 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
951 end
655 end
952 end
656 end
953 when "o"
657 when "o"
954 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
658 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
955 when "c"
659 when "c"
956 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
660 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
957 when "><t-"
661 when "><t-"
958 # between today - n days and today
662 # between today - n days and today
959 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
663 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
960 when ">t-"
664 when ">t-"
961 # >= today - n days
665 # >= today - n days
962 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
666 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
963 when "<t-"
667 when "<t-"
964 # <= today - n days
668 # <= today - n days
965 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
669 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
966 when "t-"
670 when "t-"
967 # = n days in past
671 # = n days in past
968 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
672 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
969 when "><t+"
673 when "><t+"
970 # between today and today + n days
674 # between today and today + n days
971 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
675 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
972 when ">t+"
676 when ">t+"
973 # >= today + n days
677 # >= today + n days
974 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
678 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
975 when "<t+"
679 when "<t+"
976 # <= today + n days
680 # <= today + n days
977 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
681 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
978 when "t+"
682 when "t+"
979 # = today + n days
683 # = today + n days
980 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
684 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
981 when "t"
685 when "t"
982 # = today
686 # = today
983 sql = relative_date_clause(db_table, db_field, 0, 0)
687 sql = relative_date_clause(db_table, db_field, 0, 0)
984 when "w"
688 when "w"
985 # = this week
689 # = this week
986 first_day_of_week = l(:general_first_day_of_week).to_i
690 first_day_of_week = l(:general_first_day_of_week).to_i
987 day_of_week = Date.today.cwday
691 day_of_week = Date.today.cwday
988 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
692 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
989 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
693 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
990 when "~"
694 when "~"
991 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
695 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
992 when "!~"
696 when "!~"
993 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
697 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
994 else
698 else
995 raise "Unknown query operator #{operator}"
699 raise "Unknown query operator #{operator}"
996 end
700 end
997
701
998 return sql
702 return sql
999 end
703 end
1000
704
1001 def add_custom_fields_filters(custom_fields, assoc=nil)
705 def add_custom_fields_filters(custom_fields, assoc=nil)
1002 return unless custom_fields.present?
706 return unless custom_fields.present?
1003 @available_filters ||= {}
707 @available_filters ||= {}
1004
708
1005 custom_fields.select(&:is_filter?).each do |field|
709 custom_fields.select(&:is_filter?).each do |field|
1006 case field.field_format
710 case field.field_format
1007 when "text"
711 when "text"
1008 options = { :type => :text, :order => 20 }
712 options = { :type => :text, :order => 20 }
1009 when "list"
713 when "list"
1010 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
714 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
1011 when "date"
715 when "date"
1012 options = { :type => :date, :order => 20 }
716 options = { :type => :date, :order => 20 }
1013 when "bool"
717 when "bool"
1014 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
718 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
1015 when "int"
719 when "int"
1016 options = { :type => :integer, :order => 20 }
720 options = { :type => :integer, :order => 20 }
1017 when "float"
721 when "float"
1018 options = { :type => :float, :order => 20 }
722 options = { :type => :float, :order => 20 }
1019 when "user", "version"
723 when "user", "version"
1020 next unless project
724 next unless project
1021 values = field.possible_values_options(project)
725 values = field.possible_values_options(project)
1022 if User.current.logged? && field.field_format == 'user'
726 if User.current.logged? && field.field_format == 'user'
1023 values.unshift ["<< #{l(:label_me)} >>", "me"]
727 values.unshift ["<< #{l(:label_me)} >>", "me"]
1024 end
728 end
1025 options = { :type => :list_optional, :values => values, :order => 20}
729 options = { :type => :list_optional, :values => values, :order => 20}
1026 else
730 else
1027 options = { :type => :string, :order => 20 }
731 options = { :type => :string, :order => 20 }
1028 end
732 end
1029 filter_id = "cf_#{field.id}"
733 filter_id = "cf_#{field.id}"
1030 filter_name = field.name
734 filter_name = field.name
1031 if assoc.present?
735 if assoc.present?
1032 filter_id = "#{assoc}.#{filter_id}"
736 filter_id = "#{assoc}.#{filter_id}"
1033 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
737 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1034 end
738 end
1035 @available_filters[filter_id] = options.merge({
739 @available_filters[filter_id] = options.merge({
1036 :name => filter_name,
740 :name => filter_name,
1037 :format => field.field_format,
741 :format => field.field_format,
1038 :field => field
742 :field => field
1039 })
743 })
1040 end
744 end
1041 end
745 end
1042
746
1043 def add_associations_custom_fields_filters(*associations)
747 def add_associations_custom_fields_filters(*associations)
1044 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
748 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1045 associations.each do |assoc|
749 associations.each do |assoc|
1046 association_klass = Issue.reflect_on_association(assoc).klass
750 association_klass = Issue.reflect_on_association(assoc).klass
1047 fields_by_class.each do |field_class, fields|
751 fields_by_class.each do |field_class, fields|
1048 if field_class.customized_class <= association_klass
752 if field_class.customized_class <= association_klass
1049 add_custom_fields_filters(fields, assoc)
753 add_custom_fields_filters(fields, assoc)
1050 end
754 end
1051 end
755 end
1052 end
756 end
1053 end
757 end
1054
758
1055 # Returns a SQL clause for a date or datetime field.
759 # Returns a SQL clause for a date or datetime field.
1056 def date_clause(table, field, from, to)
760 def date_clause(table, field, from, to)
1057 s = []
761 s = []
1058 if from
762 if from
1059 from_yesterday = from - 1
763 from_yesterday = from - 1
1060 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
764 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1061 if self.class.default_timezone == :utc
765 if self.class.default_timezone == :utc
1062 from_yesterday_time = from_yesterday_time.utc
766 from_yesterday_time = from_yesterday_time.utc
1063 end
767 end
1064 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
768 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1065 end
769 end
1066 if to
770 if to
1067 to_time = Time.local(to.year, to.month, to.day)
771 to_time = Time.local(to.year, to.month, to.day)
1068 if self.class.default_timezone == :utc
772 if self.class.default_timezone == :utc
1069 to_time = to_time.utc
773 to_time = to_time.utc
1070 end
774 end
1071 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
775 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1072 end
776 end
1073 s.join(' AND ')
777 s.join(' AND ')
1074 end
778 end
1075
779
1076 # Returns a SQL clause for a date or datetime field using relative dates.
780 # Returns a SQL clause for a date or datetime field using relative dates.
1077 def relative_date_clause(table, field, days_from, days_to)
781 def relative_date_clause(table, field, days_from, days_to)
1078 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
782 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1079 end
783 end
1080
784
1081 # Additional joins required for the given sort options
785 # Additional joins required for the given sort options
1082 def joins_for_order_statement(order_options)
786 def joins_for_order_statement(order_options)
1083 joins = []
787 joins = []
1084
788
1085 if order_options
789 if order_options
1086 if order_options.include?('authors')
790 if order_options.include?('authors')
1087 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
791 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1088 end
792 end
1089 order_options.scan(/cf_\d+/).uniq.each do |name|
793 order_options.scan(/cf_\d+/).uniq.each do |name|
1090 column = available_columns.detect {|c| c.name.to_s == name}
794 column = available_columns.detect {|c| c.name.to_s == name}
1091 join = column && column.custom_field.join_for_order_statement
795 join = column && column.custom_field.join_for_order_statement
1092 if join
796 if join
1093 joins << join
797 joins << join
1094 end
798 end
1095 end
799 end
1096 end
800 end
1097
801
1098 joins.any? ? joins.join(' ') : nil
802 joins.any? ? joins.join(' ') : nil
1099 end
803 end
1100 end
804 end
@@ -1,33 +1,33
1 <%= form_tag({:action => 'edit', :tab => 'issues'}, :onsubmit => 'selectAllOptions("selected_columns");') do %>
1 <%= form_tag({:action => 'edit', :tab => 'issues'}, :onsubmit => 'selectAllOptions("selected_columns");') do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :cross_project_issue_relations %></p>
4 <p><%= setting_check_box :cross_project_issue_relations %></p>
5
5
6 <p><%= setting_select :cross_project_subtasks, cross_project_subtasks_options %></p>
6 <p><%= setting_select :cross_project_subtasks, cross_project_subtasks_options %></p>
7
7
8 <p><%= setting_check_box :issue_group_assignment %></p>
8 <p><%= setting_check_box :issue_group_assignment %></p>
9
9
10 <p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
10 <p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
11
11
12 <p><%= setting_check_box :display_subprojects_issues %></p>
12 <p><%= setting_check_box :display_subprojects_issues %></p>
13
13
14 <p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
14 <p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
15
15
16 <p><%= setting_multiselect :non_working_week_days, (1..7).map {|d| [day_name(d), d.to_s]}, :inline => true %></p>
16 <p><%= setting_multiselect :non_working_week_days, (1..7).map {|d| [day_name(d), d.to_s]}, :inline => true %></p>
17
17
18 <p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
18 <p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
19
19
20 <p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
20 <p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
21 </div>
21 </div>
22
22
23 <fieldset class="box">
23 <fieldset class="box">
24 <legend><%= l(:setting_issue_list_default_columns) %></legend>
24 <legend><%= l(:setting_issue_list_default_columns) %></legend>
25 <%= render :partial => 'queries/columns',
25 <%= render :partial => 'queries/columns',
26 :locals => {
26 :locals => {
27 :query => Query.new(:column_names => Setting.issue_list_default_columns),
27 :query => IssueQuery.new(:column_names => Setting.issue_list_default_columns),
28 :tag_name => 'settings[issue_list_default_columns][]'
28 :tag_name => 'settings[issue_list_default_columns][]'
29 } %>
29 } %>
30 </fieldset>
30 </fieldset>
31
31
32 <%= submit_tag l(:button_save) %>
32 <%= submit_tag l(:button_save) %>
33 <% end %>
33 <% end %>
@@ -1,156 +1,165
1 ---
1 ---
2 queries_001:
2 queries_001:
3 id: 1
3 id: 1
4 type: IssueQuery
4 project_id: 1
5 project_id: 1
5 is_public: true
6 is_public: true
6 name: Multiple custom fields query
7 name: Multiple custom fields query
7 filters: |
8 filters: |
8 ---
9 ---
9 cf_1:
10 cf_1:
10 :values:
11 :values:
11 - MySQL
12 - MySQL
12 :operator: "="
13 :operator: "="
13 status_id:
14 status_id:
14 :values:
15 :values:
15 - "1"
16 - "1"
16 :operator: o
17 :operator: o
17 cf_2:
18 cf_2:
18 :values:
19 :values:
19 - "125"
20 - "125"
20 :operator: "="
21 :operator: "="
21
22
22 user_id: 1
23 user_id: 1
23 column_names:
24 column_names:
24 queries_002:
25 queries_002:
25 id: 2
26 id: 2
27 type: IssueQuery
26 project_id: 1
28 project_id: 1
27 is_public: false
29 is_public: false
28 name: Private query for cookbook
30 name: Private query for cookbook
29 filters: |
31 filters: |
30 ---
32 ---
31 tracker_id:
33 tracker_id:
32 :values:
34 :values:
33 - "3"
35 - "3"
34 :operator: "="
36 :operator: "="
35 status_id:
37 status_id:
36 :values:
38 :values:
37 - "1"
39 - "1"
38 :operator: o
40 :operator: o
39
41
40 user_id: 3
42 user_id: 3
41 column_names:
43 column_names:
42 queries_003:
44 queries_003:
43 id: 3
45 id: 3
46 type: IssueQuery
44 project_id:
47 project_id:
45 is_public: false
48 is_public: false
46 name: Private query for all projects
49 name: Private query for all projects
47 filters: |
50 filters: |
48 ---
51 ---
49 tracker_id:
52 tracker_id:
50 :values:
53 :values:
51 - "3"
54 - "3"
52 :operator: "="
55 :operator: "="
53
56
54 user_id: 3
57 user_id: 3
55 column_names:
58 column_names:
56 queries_004:
59 queries_004:
57 id: 4
60 id: 4
61 type: IssueQuery
58 project_id:
62 project_id:
59 is_public: true
63 is_public: true
60 name: Public query for all projects
64 name: Public query for all projects
61 filters: |
65 filters: |
62 ---
66 ---
63 tracker_id:
67 tracker_id:
64 :values:
68 :values:
65 - "3"
69 - "3"
66 :operator: "="
70 :operator: "="
67
71
68 user_id: 2
72 user_id: 2
69 column_names:
73 column_names:
70 queries_005:
74 queries_005:
71 id: 5
75 id: 5
76 type: IssueQuery
72 project_id:
77 project_id:
73 is_public: true
78 is_public: true
74 name: Open issues by priority and tracker
79 name: Open issues by priority and tracker
75 filters: |
80 filters: |
76 ---
81 ---
77 status_id:
82 status_id:
78 :values:
83 :values:
79 - "1"
84 - "1"
80 :operator: o
85 :operator: o
81
86
82 user_id: 1
87 user_id: 1
83 column_names:
88 column_names:
84 sort_criteria: |
89 sort_criteria: |
85 ---
90 ---
86 - - priority
91 - - priority
87 - desc
92 - desc
88 - - tracker
93 - - tracker
89 - asc
94 - asc
90 queries_006:
95 queries_006:
91 id: 6
96 id: 6
97 type: IssueQuery
92 project_id:
98 project_id:
93 is_public: true
99 is_public: true
94 name: Open issues grouped by tracker
100 name: Open issues grouped by tracker
95 filters: |
101 filters: |
96 ---
102 ---
97 status_id:
103 status_id:
98 :values:
104 :values:
99 - "1"
105 - "1"
100 :operator: o
106 :operator: o
101
107
102 user_id: 1
108 user_id: 1
103 column_names:
109 column_names:
104 group_by: tracker
110 group_by: tracker
105 sort_criteria: |
111 sort_criteria: |
106 ---
112 ---
107 - - priority
113 - - priority
108 - desc
114 - desc
109 queries_007:
115 queries_007:
110 id: 7
116 id: 7
117 type: IssueQuery
111 project_id: 2
118 project_id: 2
112 is_public: true
119 is_public: true
113 name: Public query for project 2
120 name: Public query for project 2
114 filters: |
121 filters: |
115 ---
122 ---
116 tracker_id:
123 tracker_id:
117 :values:
124 :values:
118 - "3"
125 - "3"
119 :operator: "="
126 :operator: "="
120
127
121 user_id: 2
128 user_id: 2
122 column_names:
129 column_names:
123 queries_008:
130 queries_008:
124 id: 8
131 id: 8
132 type: IssueQuery
125 project_id: 2
133 project_id: 2
126 is_public: false
134 is_public: false
127 name: Private query for project 2
135 name: Private query for project 2
128 filters: |
136 filters: |
129 ---
137 ---
130 tracker_id:
138 tracker_id:
131 :values:
139 :values:
132 - "3"
140 - "3"
133 :operator: "="
141 :operator: "="
134
142
135 user_id: 2
143 user_id: 2
136 column_names:
144 column_names:
137 queries_009:
145 queries_009:
138 id: 9
146 id: 9
147 type: IssueQuery
139 project_id:
148 project_id:
140 is_public: true
149 is_public: true
141 name: Open issues grouped by list custom field
150 name: Open issues grouped by list custom field
142 filters: |
151 filters: |
143 ---
152 ---
144 status_id:
153 status_id:
145 :values:
154 :values:
146 - "1"
155 - "1"
147 :operator: o
156 :operator: o
148
157
149 user_id: 1
158 user_id: 1
150 column_names:
159 column_names:
151 group_by: cf_1
160 group_by: cf_1
152 sort_criteria: |
161 sort_criteria: |
153 ---
162 ---
154 - - priority
163 - - priority
155 - desc
164 - desc
156
165
@@ -1,97 +1,97
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class CalendarsControllerTest < ActionController::TestCase
20 class CalendarsControllerTest < ActionController::TestCase
21 fixtures :projects,
21 fixtures :projects,
22 :trackers,
22 :trackers,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules
27 :enabled_modules
28
28
29 def test_calendar
29 def test_calendar
30 get :show, :project_id => 1
30 get :show, :project_id => 1
31 assert_response :success
31 assert_response :success
32 assert_template 'calendar'
32 assert_template 'calendar'
33 assert_not_nil assigns(:calendar)
33 assert_not_nil assigns(:calendar)
34 end
34 end
35
35
36 def test_cross_project_calendar
36 def test_cross_project_calendar
37 get :show
37 get :show
38 assert_response :success
38 assert_response :success
39 assert_template 'calendar'
39 assert_template 'calendar'
40 assert_not_nil assigns(:calendar)
40 assert_not_nil assigns(:calendar)
41 end
41 end
42
42
43 context "GET :show" do
43 context "GET :show" do
44 should "run custom queries" do
44 should "run custom queries" do
45 @query = Query.create!(:name => 'Calendar', :is_public => true)
45 @query = IssueQuery.create!(:name => 'Calendar', :is_public => true)
46
46
47 get :show, :query_id => @query.id
47 get :show, :query_id => @query.id
48 assert_response :success
48 assert_response :success
49 end
49 end
50
50
51 end
51 end
52
52
53 def test_week_number_calculation
53 def test_week_number_calculation
54 Setting.start_of_week = 7
54 Setting.start_of_week = 7
55
55
56 get :show, :month => '1', :year => '2010'
56 get :show, :month => '1', :year => '2010'
57 assert_response :success
57 assert_response :success
58
58
59 assert_tag :tag => 'tr',
59 assert_tag :tag => 'tr',
60 :descendant => {:tag => 'td',
60 :descendant => {:tag => 'td',
61 :attributes => {:class => 'week-number'}, :content => '53'},
61 :attributes => {:class => 'week-number'}, :content => '53'},
62 :descendant => {:tag => 'td',
62 :descendant => {:tag => 'td',
63 :attributes => {:class => 'odd'}, :content => '27'},
63 :attributes => {:class => 'odd'}, :content => '27'},
64 :descendant => {:tag => 'td',
64 :descendant => {:tag => 'td',
65 :attributes => {:class => 'even'}, :content => '2'}
65 :attributes => {:class => 'even'}, :content => '2'}
66
66
67 assert_tag :tag => 'tr',
67 assert_tag :tag => 'tr',
68 :descendant => {:tag => 'td',
68 :descendant => {:tag => 'td',
69 :attributes => {:class => 'week-number'}, :content => '1'},
69 :attributes => {:class => 'week-number'}, :content => '1'},
70 :descendant => {:tag => 'td',
70 :descendant => {:tag => 'td',
71 :attributes => {:class => 'odd'}, :content => '3'},
71 :attributes => {:class => 'odd'}, :content => '3'},
72 :descendant => {:tag => 'td',
72 :descendant => {:tag => 'td',
73 :attributes => {:class => 'even'}, :content => '9'}
73 :attributes => {:class => 'even'}, :content => '9'}
74
74
75
75
76 Setting.start_of_week = 1
76 Setting.start_of_week = 1
77 get :show, :month => '1', :year => '2010'
77 get :show, :month => '1', :year => '2010'
78 assert_response :success
78 assert_response :success
79
79
80 assert_tag :tag => 'tr',
80 assert_tag :tag => 'tr',
81 :descendant => {:tag => 'td',
81 :descendant => {:tag => 'td',
82 :attributes => {:class => 'week-number'}, :content => '53'},
82 :attributes => {:class => 'week-number'}, :content => '53'},
83 :descendant => {:tag => 'td',
83 :descendant => {:tag => 'td',
84 :attributes => {:class => 'even'}, :content => '28'},
84 :attributes => {:class => 'even'}, :content => '28'},
85 :descendant => {:tag => 'td',
85 :descendant => {:tag => 'td',
86 :attributes => {:class => 'even'}, :content => '3'}
86 :attributes => {:class => 'even'}, :content => '3'}
87
87
88 assert_tag :tag => 'tr',
88 assert_tag :tag => 'tr',
89 :descendant => {:tag => 'td',
89 :descendant => {:tag => 'td',
90 :attributes => {:class => 'week-number'}, :content => '1'},
90 :attributes => {:class => 'week-number'}, :content => '1'},
91 :descendant => {:tag => 'td',
91 :descendant => {:tag => 'td',
92 :attributes => {:class => 'even'}, :content => '4'},
92 :attributes => {:class => 'even'}, :content => '4'},
93 :descendant => {:tag => 'td',
93 :descendant => {:tag => 'td',
94 :attributes => {:class => 'even'}, :content => '10'}
94 :attributes => {:class => 'even'}, :content => '10'}
95
95
96 end
96 end
97 end
97 end
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,263 +1,263
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 class IssuesControllerTransactionTest < ActionController::TestCase
21 class IssuesControllerTransactionTest < ActionController::TestCase
22 tests IssuesController
22 tests IssuesController
23 fixtures :projects,
23 fixtures :projects,
24 :users,
24 :users,
25 :roles,
25 :roles,
26 :members,
26 :members,
27 :member_roles,
27 :member_roles,
28 :issues,
28 :issues,
29 :issue_statuses,
29 :issue_statuses,
30 :versions,
30 :versions,
31 :trackers,
31 :trackers,
32 :projects_trackers,
32 :projects_trackers,
33 :issue_categories,
33 :issue_categories,
34 :enabled_modules,
34 :enabled_modules,
35 :enumerations,
35 :enumerations,
36 :attachments,
36 :attachments,
37 :workflows,
37 :workflows,
38 :custom_fields,
38 :custom_fields,
39 :custom_values,
39 :custom_values,
40 :custom_fields_projects,
40 :custom_fields_projects,
41 :custom_fields_trackers,
41 :custom_fields_trackers,
42 :time_entries,
42 :time_entries,
43 :journals,
43 :journals,
44 :journal_details,
44 :journal_details,
45 :queries
45 :queries
46
46
47 self.use_transactional_fixtures = false
47 self.use_transactional_fixtures = false
48
48
49 def setup
49 def setup
50 User.current = nil
50 User.current = nil
51 end
51 end
52
52
53 def test_update_stale_issue_should_not_update_the_issue
53 def test_update_stale_issue_should_not_update_the_issue
54 issue = Issue.find(2)
54 issue = Issue.find(2)
55 @request.session[:user_id] = 2
55 @request.session[:user_id] = 2
56
56
57 assert_no_difference 'Journal.count' do
57 assert_no_difference 'Journal.count' do
58 assert_no_difference 'TimeEntry.count' do
58 assert_no_difference 'TimeEntry.count' do
59 put :update,
59 put :update,
60 :id => issue.id,
60 :id => issue.id,
61 :issue => {
61 :issue => {
62 :fixed_version_id => 4,
62 :fixed_version_id => 4,
63 :notes => 'My notes',
63 :notes => 'My notes',
64 :lock_version => (issue.lock_version - 1)
64 :lock_version => (issue.lock_version - 1)
65 },
65 },
66 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
66 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
67 end
67 end
68 end
68 end
69
69
70 assert_response :success
70 assert_response :success
71 assert_template 'edit'
71 assert_template 'edit'
72
72
73 assert_select 'div.conflict'
73 assert_select 'div.conflict'
74 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite'
74 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite'
75 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes'
75 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes'
76 assert_select 'label' do
76 assert_select 'label' do
77 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel'
77 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel'
78 assert_select 'a[href=/issues/2]'
78 assert_select 'a[href=/issues/2]'
79 end
79 end
80 end
80 end
81
81
82 def test_update_stale_issue_should_save_attachments
82 def test_update_stale_issue_should_save_attachments
83 set_tmp_attachments_directory
83 set_tmp_attachments_directory
84 issue = Issue.find(2)
84 issue = Issue.find(2)
85 @request.session[:user_id] = 2
85 @request.session[:user_id] = 2
86
86
87 assert_no_difference 'Journal.count' do
87 assert_no_difference 'Journal.count' do
88 assert_no_difference 'TimeEntry.count' do
88 assert_no_difference 'TimeEntry.count' do
89 assert_difference 'Attachment.count' do
89 assert_difference 'Attachment.count' do
90 put :update,
90 put :update,
91 :id => issue.id,
91 :id => issue.id,
92 :issue => {
92 :issue => {
93 :fixed_version_id => 4,
93 :fixed_version_id => 4,
94 :notes => 'My notes',
94 :notes => 'My notes',
95 :lock_version => (issue.lock_version - 1)
95 :lock_version => (issue.lock_version - 1)
96 },
96 },
97 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
97 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
98 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
98 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
99 end
99 end
100 end
100 end
101 end
101 end
102
102
103 assert_response :success
103 assert_response :success
104 assert_template 'edit'
104 assert_template 'edit'
105 attachment = Attachment.first(:order => 'id DESC')
105 attachment = Attachment.first(:order => 'id DESC')
106 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
106 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
107 assert_tag 'span', :content => /testfile.txt/
107 assert_tag 'span', :content => /testfile.txt/
108 end
108 end
109
109
110 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
110 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
111 issue = Issue.find(2)
111 issue = Issue.find(2)
112 @request.session[:user_id] = 2
112 @request.session[:user_id] = 2
113
113
114 put :update, :id => issue.id,
114 put :update, :id => issue.id,
115 :issue => {
115 :issue => {
116 :fixed_version_id => 4,
116 :fixed_version_id => 4,
117 :notes => '',
117 :notes => '',
118 :lock_version => (issue.lock_version - 1)
118 :lock_version => (issue.lock_version - 1)
119 }
119 }
120
120
121 assert_tag 'div', :attributes => {:class => 'conflict'}
121 assert_tag 'div', :attributes => {:class => 'conflict'}
122 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
122 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
123 assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
123 assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
124 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
124 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
125 end
125 end
126
126
127 def test_update_stale_issue_should_show_conflicting_journals
127 def test_update_stale_issue_should_show_conflicting_journals
128 @request.session[:user_id] = 2
128 @request.session[:user_id] = 2
129
129
130 put :update, :id => 1,
130 put :update, :id => 1,
131 :issue => {
131 :issue => {
132 :fixed_version_id => 4,
132 :fixed_version_id => 4,
133 :notes => '',
133 :notes => '',
134 :lock_version => 2
134 :lock_version => 2
135 },
135 },
136 :last_journal_id => 1
136 :last_journal_id => 1
137
137
138 assert_not_nil assigns(:conflict_journals)
138 assert_not_nil assigns(:conflict_journals)
139 assert_equal 1, assigns(:conflict_journals).size
139 assert_equal 1, assigns(:conflict_journals).size
140 assert_equal 2, assigns(:conflict_journals).first.id
140 assert_equal 2, assigns(:conflict_journals).first.id
141 assert_tag 'div', :attributes => {:class => 'conflict'},
141 assert_tag 'div', :attributes => {:class => 'conflict'},
142 :descendant => {:content => /Some notes with Redmine links/}
142 :descendant => {:content => /Some notes with Redmine links/}
143 end
143 end
144
144
145 def test_update_stale_issue_without_previous_journal_should_show_all_journals
145 def test_update_stale_issue_without_previous_journal_should_show_all_journals
146 @request.session[:user_id] = 2
146 @request.session[:user_id] = 2
147
147
148 put :update, :id => 1,
148 put :update, :id => 1,
149 :issue => {
149 :issue => {
150 :fixed_version_id => 4,
150 :fixed_version_id => 4,
151 :notes => '',
151 :notes => '',
152 :lock_version => 2
152 :lock_version => 2
153 },
153 },
154 :last_journal_id => ''
154 :last_journal_id => ''
155
155
156 assert_not_nil assigns(:conflict_journals)
156 assert_not_nil assigns(:conflict_journals)
157 assert_equal 2, assigns(:conflict_journals).size
157 assert_equal 2, assigns(:conflict_journals).size
158 assert_tag 'div', :attributes => {:class => 'conflict'},
158 assert_tag 'div', :attributes => {:class => 'conflict'},
159 :descendant => {:content => /Some notes with Redmine links/}
159 :descendant => {:content => /Some notes with Redmine links/}
160 assert_tag 'div', :attributes => {:class => 'conflict'},
160 assert_tag 'div', :attributes => {:class => 'conflict'},
161 :descendant => {:content => /Journal notes/}
161 :descendant => {:content => /Journal notes/}
162 end
162 end
163
163
164 def test_update_stale_issue_should_show_private_journals_with_permission_only
164 def test_update_stale_issue_should_show_private_journals_with_permission_only
165 journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
165 journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
166
166
167 @request.session[:user_id] = 2
167 @request.session[:user_id] = 2
168 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
168 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
169 assert_include journal, assigns(:conflict_journals)
169 assert_include journal, assigns(:conflict_journals)
170
170
171 Role.find(1).remove_permission! :view_private_notes
171 Role.find(1).remove_permission! :view_private_notes
172 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
172 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
173 assert_not_include journal, assigns(:conflict_journals)
173 assert_not_include journal, assigns(:conflict_journals)
174 end
174 end
175
175
176 def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
176 def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
177 @request.session[:user_id] = 2
177 @request.session[:user_id] = 2
178
178
179 assert_difference 'Journal.count' do
179 assert_difference 'Journal.count' do
180 put :update, :id => 1,
180 put :update, :id => 1,
181 :issue => {
181 :issue => {
182 :fixed_version_id => 4,
182 :fixed_version_id => 4,
183 :notes => 'overwrite_conflict_resolution',
183 :notes => 'overwrite_conflict_resolution',
184 :lock_version => 2
184 :lock_version => 2
185 },
185 },
186 :conflict_resolution => 'overwrite'
186 :conflict_resolution => 'overwrite'
187 end
187 end
188
188
189 assert_response 302
189 assert_response 302
190 issue = Issue.find(1)
190 issue = Issue.find(1)
191 assert_equal 4, issue.fixed_version_id
191 assert_equal 4, issue.fixed_version_id
192 journal = Journal.first(:order => 'id DESC')
192 journal = Journal.first(:order => 'id DESC')
193 assert_equal 'overwrite_conflict_resolution', journal.notes
193 assert_equal 'overwrite_conflict_resolution', journal.notes
194 assert journal.details.any?
194 assert journal.details.any?
195 end
195 end
196
196
197 def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
197 def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
198 @request.session[:user_id] = 2
198 @request.session[:user_id] = 2
199
199
200 assert_difference 'Journal.count' do
200 assert_difference 'Journal.count' do
201 put :update, :id => 1,
201 put :update, :id => 1,
202 :issue => {
202 :issue => {
203 :fixed_version_id => 4,
203 :fixed_version_id => 4,
204 :notes => 'add_notes_conflict_resolution',
204 :notes => 'add_notes_conflict_resolution',
205 :lock_version => 2
205 :lock_version => 2
206 },
206 },
207 :conflict_resolution => 'add_notes'
207 :conflict_resolution => 'add_notes'
208 end
208 end
209
209
210 assert_response 302
210 assert_response 302
211 issue = Issue.find(1)
211 issue = Issue.find(1)
212 assert_nil issue.fixed_version_id
212 assert_nil issue.fixed_version_id
213 journal = Journal.first(:order => 'id DESC')
213 journal = Journal.first(:order => 'id DESC')
214 assert_equal 'add_notes_conflict_resolution', journal.notes
214 assert_equal 'add_notes_conflict_resolution', journal.notes
215 assert journal.details.empty?
215 assert journal.details.empty?
216 end
216 end
217
217
218 def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
218 def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
219 @request.session[:user_id] = 2
219 @request.session[:user_id] = 2
220
220
221 assert_no_difference 'Journal.count' do
221 assert_no_difference 'Journal.count' do
222 put :update, :id => 1,
222 put :update, :id => 1,
223 :issue => {
223 :issue => {
224 :fixed_version_id => 4,
224 :fixed_version_id => 4,
225 :notes => 'add_notes_conflict_resolution',
225 :notes => 'add_notes_conflict_resolution',
226 :lock_version => 2
226 :lock_version => 2
227 },
227 },
228 :conflict_resolution => 'cancel'
228 :conflict_resolution => 'cancel'
229 end
229 end
230
230
231 assert_redirected_to '/issues/1'
231 assert_redirected_to '/issues/1'
232 issue = Issue.find(1)
232 issue = Issue.find(1)
233 assert_nil issue.fixed_version_id
233 assert_nil issue.fixed_version_id
234 end
234 end
235
235
236 def test_put_update_with_spent_time_and_failure_should_not_add_spent_time
236 def test_put_update_with_spent_time_and_failure_should_not_add_spent_time
237 @request.session[:user_id] = 2
237 @request.session[:user_id] = 2
238
238
239 assert_no_difference('TimeEntry.count') do
239 assert_no_difference('TimeEntry.count') do
240 put :update,
240 put :update,
241 :id => 1,
241 :id => 1,
242 :issue => { :subject => '' },
242 :issue => { :subject => '' },
243 :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id }
243 :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id }
244 assert_response :success
244 assert_response :success
245 end
245 end
246
246
247 assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5'
247 assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5'
248 assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added'
248 assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added'
249 assert_select 'select[name=?]', 'time_entry[activity_id]' do
249 assert_select 'select[name=?]', 'time_entry[activity_id]' do
250 assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id
250 assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id
251 end
251 end
252 end
252 end
253
253
254 def test_index_should_rescue_invalid_sql_query
254 def test_index_should_rescue_invalid_sql_query
255 Query.any_instance.stubs(:statement).returns("INVALID STATEMENT")
255 IssueQuery.any_instance.stubs(:statement).returns("INVALID STATEMENT")
256
256
257 get :index
257 get :index
258 assert_response 500
258 assert_response 500
259 assert_tag 'p', :content => /An error occurred/
259 assert_tag 'p', :content => /An error occurred/
260 assert_nil session[:query]
260 assert_nil session[:query]
261 assert_nil session[:issues_index_sort]
261 assert_nil session[:issues_index_sort]
262 end
262 end
263 end
263 end
@@ -1,65 +1,65
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class QueriesHelperTest < ActionView::TestCase
20 class QueriesHelperTest < ActionView::TestCase
21 include QueriesHelper
21 include QueriesHelper
22 include Redmine::I18n
22 include Redmine::I18n
23
23
24 fixtures :projects, :enabled_modules, :users, :members,
24 fixtures :projects, :enabled_modules, :users, :members,
25 :member_roles, :roles, :trackers, :issue_statuses,
25 :member_roles, :roles, :trackers, :issue_statuses,
26 :issue_categories, :enumerations, :issues,
26 :issue_categories, :enumerations, :issues,
27 :watchers, :custom_fields, :custom_values, :versions,
27 :watchers, :custom_fields, :custom_values, :versions,
28 :queries,
28 :queries,
29 :projects_trackers,
29 :projects_trackers,
30 :custom_fields_trackers
30 :custom_fields_trackers
31
31
32 def test_order
32 def test_order
33 User.current = User.find_by_login('admin')
33 User.current = User.find_by_login('admin')
34 query = Query.new(:project => nil, :name => '_')
34 query = IssueQuery.new(:project => nil, :name => '_')
35 assert_equal 30, query.available_filters.size
35 assert_equal 30, query.available_filters.size
36 fo = filters_options(query)
36 fo = filters_options(query)
37 assert_equal 31, fo.size
37 assert_equal 31, fo.size
38 assert_equal [], fo[0]
38 assert_equal [], fo[0]
39 assert_equal "status_id", fo[1][1]
39 assert_equal "status_id", fo[1][1]
40 assert_equal "project_id", fo[2][1]
40 assert_equal "project_id", fo[2][1]
41 assert_equal "tracker_id", fo[3][1]
41 assert_equal "tracker_id", fo[3][1]
42 assert_equal "priority_id", fo[4][1]
42 assert_equal "priority_id", fo[4][1]
43 assert_equal "watcher_id", fo[17][1]
43 assert_equal "watcher_id", fo[17][1]
44 assert_equal "is_private", fo[18][1]
44 assert_equal "is_private", fo[18][1]
45 end
45 end
46
46
47 def test_order_custom_fields
47 def test_order_custom_fields
48 set_language_if_valid 'en'
48 set_language_if_valid 'en'
49 field = UserCustomField.new(
49 field = UserCustomField.new(
50 :name => 'order test', :field_format => 'string',
50 :name => 'order test', :field_format => 'string',
51 :is_for_all => true, :is_filter => true
51 :is_for_all => true, :is_filter => true
52 )
52 )
53 assert field.save
53 assert field.save
54 User.current = User.find_by_login('admin')
54 User.current = User.find_by_login('admin')
55 query = Query.new(:project => nil, :name => '_')
55 query = IssueQuery.new(:project => nil, :name => '_')
56 assert_equal 32, query.available_filters.size
56 assert_equal 32, query.available_filters.size
57 fo = filters_options(query)
57 fo = filters_options(query)
58 assert_equal 33, fo.size
58 assert_equal 33, fo.size
59 assert_equal "Searchable field", fo[19][0]
59 assert_equal "Searchable field", fo[19][0]
60 assert_equal "Database", fo[20][0]
60 assert_equal "Database", fo[20][0]
61 assert_equal "Project's Development status", fo[21][0]
61 assert_equal "Project's Development status", fo[21][0]
62 assert_equal "Assignee's order test", fo[22][0]
62 assert_equal "Assignee's order test", fo[22][0]
63 assert_equal "Author's order test", fo[23][0]
63 assert_equal "Author's order test", fo[23][0]
64 end
64 end
65 end
65 end
@@ -1,750 +1,750
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../../../test_helper', __FILE__)
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
19
20 class Redmine::Helpers::GanttHelperTest < ActionView::TestCase
20 class Redmine::Helpers::GanttHelperTest < ActionView::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :journals, :journal_details,
22 :journals, :journal_details,
23 :enumerations, :users, :issue_categories,
23 :enumerations, :users, :issue_categories,
24 :projects_trackers,
24 :projects_trackers,
25 :roles,
25 :roles,
26 :member_roles,
26 :member_roles,
27 :members,
27 :members,
28 :enabled_modules,
28 :enabled_modules,
29 :workflows,
29 :workflows,
30 :versions,
30 :versions,
31 :groups_users
31 :groups_users
32
32
33 include ApplicationHelper
33 include ApplicationHelper
34 include ProjectsHelper
34 include ProjectsHelper
35 include IssuesHelper
35 include IssuesHelper
36 include ERB::Util
36 include ERB::Util
37 include Rails.application.routes.url_helpers
37 include Rails.application.routes.url_helpers
38
38
39 def setup
39 def setup
40 setup_with_controller
40 setup_with_controller
41 User.current = User.find(1)
41 User.current = User.find(1)
42 end
42 end
43
43
44 def today
44 def today
45 @today ||= Date.today
45 @today ||= Date.today
46 end
46 end
47
47
48 # Creates a Gantt chart for a 4 week span
48 # Creates a Gantt chart for a 4 week span
49 def create_gantt(project=Project.generate!, options={})
49 def create_gantt(project=Project.generate!, options={})
50 @project = project
50 @project = project
51 @gantt = Redmine::Helpers::Gantt.new(options)
51 @gantt = Redmine::Helpers::Gantt.new(options)
52 @gantt.project = @project
52 @gantt.project = @project
53 @gantt.query = Query.create!(:project => @project, :name => 'Gantt')
53 @gantt.query = IssueQuery.create!(:project => @project, :name => 'Gantt')
54 @gantt.view = self
54 @gantt.view = self
55 @gantt.instance_variable_set('@date_from', options[:date_from] || (today - 14))
55 @gantt.instance_variable_set('@date_from', options[:date_from] || (today - 14))
56 @gantt.instance_variable_set('@date_to', options[:date_to] || (today + 14))
56 @gantt.instance_variable_set('@date_to', options[:date_to] || (today + 14))
57 end
57 end
58
58
59 context "#number_of_rows" do
59 context "#number_of_rows" do
60 context "with one project" do
60 context "with one project" do
61 should "return the number of rows just for that project"
61 should "return the number of rows just for that project"
62 end
62 end
63
63
64 context "with no project" do
64 context "with no project" do
65 should "return the total number of rows for all the projects, resursively"
65 should "return the total number of rows for all the projects, resursively"
66 end
66 end
67
67
68 should "not exceed max_rows option" do
68 should "not exceed max_rows option" do
69 p = Project.generate!
69 p = Project.generate!
70 5.times do
70 5.times do
71 Issue.generate!(:project => p)
71 Issue.generate!(:project => p)
72 end
72 end
73 create_gantt(p)
73 create_gantt(p)
74 @gantt.render
74 @gantt.render
75 assert_equal 6, @gantt.number_of_rows
75 assert_equal 6, @gantt.number_of_rows
76 assert !@gantt.truncated
76 assert !@gantt.truncated
77 create_gantt(p, :max_rows => 3)
77 create_gantt(p, :max_rows => 3)
78 @gantt.render
78 @gantt.render
79 assert_equal 3, @gantt.number_of_rows
79 assert_equal 3, @gantt.number_of_rows
80 assert @gantt.truncated
80 assert @gantt.truncated
81 end
81 end
82 end
82 end
83
83
84 context "#number_of_rows_on_project" do
84 context "#number_of_rows_on_project" do
85 setup do
85 setup do
86 create_gantt
86 create_gantt
87 end
87 end
88
88
89 should "count 0 for an empty the project" do
89 should "count 0 for an empty the project" do
90 assert_equal 0, @gantt.number_of_rows_on_project(@project)
90 assert_equal 0, @gantt.number_of_rows_on_project(@project)
91 end
91 end
92
92
93 should "count the number of issues without a version" do
93 should "count the number of issues without a version" do
94 @project.issues << Issue.generate!(:project => @project, :fixed_version => nil)
94 @project.issues << Issue.generate!(:project => @project, :fixed_version => nil)
95 assert_equal 2, @gantt.number_of_rows_on_project(@project)
95 assert_equal 2, @gantt.number_of_rows_on_project(@project)
96 end
96 end
97
97
98 should "count the number of issues on versions, including cross-project" do
98 should "count the number of issues on versions, including cross-project" do
99 version = Version.generate!
99 version = Version.generate!
100 @project.versions << version
100 @project.versions << version
101 @project.issues << Issue.generate!(:project => @project, :fixed_version => version)
101 @project.issues << Issue.generate!(:project => @project, :fixed_version => version)
102 assert_equal 3, @gantt.number_of_rows_on_project(@project)
102 assert_equal 3, @gantt.number_of_rows_on_project(@project)
103 end
103 end
104 end
104 end
105
105
106 # TODO: more of an integration test
106 # TODO: more of an integration test
107 context "#subjects" do
107 context "#subjects" do
108 setup do
108 setup do
109 create_gantt
109 create_gantt
110 @project.enabled_module_names = [:issue_tracking]
110 @project.enabled_module_names = [:issue_tracking]
111 @tracker = Tracker.generate!
111 @tracker = Tracker.generate!
112 @project.trackers << @tracker
112 @project.trackers << @tracker
113 @version = Version.generate!(:effective_date => (today + 7), :sharing => 'none')
113 @version = Version.generate!(:effective_date => (today + 7), :sharing => 'none')
114 @project.versions << @version
114 @project.versions << @version
115 @issue = Issue.generate!(:fixed_version => @version,
115 @issue = Issue.generate!(:fixed_version => @version,
116 :subject => "gantt#line_for_project",
116 :subject => "gantt#line_for_project",
117 :tracker => @tracker,
117 :tracker => @tracker,
118 :project => @project,
118 :project => @project,
119 :done_ratio => 30,
119 :done_ratio => 30,
120 :start_date => (today - 1),
120 :start_date => (today - 1),
121 :due_date => (today + 7))
121 :due_date => (today + 7))
122 @project.issues << @issue
122 @project.issues << @issue
123 end
123 end
124
124
125 context "project" do
125 context "project" do
126 should "be rendered" do
126 should "be rendered" do
127 @output_buffer = @gantt.subjects
127 @output_buffer = @gantt.subjects
128 assert_select "div.project-name a", /#{@project.name}/
128 assert_select "div.project-name a", /#{@project.name}/
129 end
129 end
130
130
131 should "have an indent of 4" do
131 should "have an indent of 4" do
132 @output_buffer = @gantt.subjects
132 @output_buffer = @gantt.subjects
133 assert_select "div.project-name[style*=left:4px]"
133 assert_select "div.project-name[style*=left:4px]"
134 end
134 end
135 end
135 end
136
136
137 context "version" do
137 context "version" do
138 should "be rendered" do
138 should "be rendered" do
139 @output_buffer = @gantt.subjects
139 @output_buffer = @gantt.subjects
140 assert_select "div.version-name a", /#{@version.name}/
140 assert_select "div.version-name a", /#{@version.name}/
141 end
141 end
142
142
143 should "be indented 24 (one level)" do
143 should "be indented 24 (one level)" do
144 @output_buffer = @gantt.subjects
144 @output_buffer = @gantt.subjects
145 assert_select "div.version-name[style*=left:24px]"
145 assert_select "div.version-name[style*=left:24px]"
146 end
146 end
147
147
148 context "without assigned issues" do
148 context "without assigned issues" do
149 setup do
149 setup do
150 @version = Version.generate!(:effective_date => (today + 14),
150 @version = Version.generate!(:effective_date => (today + 14),
151 :sharing => 'none',
151 :sharing => 'none',
152 :name => 'empty_version')
152 :name => 'empty_version')
153 @project.versions << @version
153 @project.versions << @version
154 end
154 end
155
155
156 should "not be rendered" do
156 should "not be rendered" do
157 @output_buffer = @gantt.subjects
157 @output_buffer = @gantt.subjects
158 assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0
158 assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0
159 end
159 end
160 end
160 end
161 end
161 end
162
162
163 context "issue" do
163 context "issue" do
164 should "be rendered" do
164 should "be rendered" do
165 @output_buffer = @gantt.subjects
165 @output_buffer = @gantt.subjects
166 assert_select "div.issue-subject", /#{@issue.subject}/
166 assert_select "div.issue-subject", /#{@issue.subject}/
167 end
167 end
168
168
169 should "be indented 44 (two levels)" do
169 should "be indented 44 (two levels)" do
170 @output_buffer = @gantt.subjects
170 @output_buffer = @gantt.subjects
171 assert_select "div.issue-subject[style*=left:44px]"
171 assert_select "div.issue-subject[style*=left:44px]"
172 end
172 end
173
173
174 context "assigned to a shared version of another project" do
174 context "assigned to a shared version of another project" do
175 setup do
175 setup do
176 p = Project.generate!
176 p = Project.generate!
177 p.enabled_module_names = [:issue_tracking]
177 p.enabled_module_names = [:issue_tracking]
178 @shared_version = Version.generate!(:sharing => 'system')
178 @shared_version = Version.generate!(:sharing => 'system')
179 p.versions << @shared_version
179 p.versions << @shared_version
180 # Reassign the issue to a shared version of another project
180 # Reassign the issue to a shared version of another project
181 @issue = Issue.generate!(:fixed_version => @shared_version,
181 @issue = Issue.generate!(:fixed_version => @shared_version,
182 :subject => "gantt#assigned_to_shared_version",
182 :subject => "gantt#assigned_to_shared_version",
183 :tracker => @tracker,
183 :tracker => @tracker,
184 :project => @project,
184 :project => @project,
185 :done_ratio => 30,
185 :done_ratio => 30,
186 :start_date => (today - 1),
186 :start_date => (today - 1),
187 :due_date => (today + 7))
187 :due_date => (today + 7))
188 @project.issues << @issue
188 @project.issues << @issue
189 end
189 end
190
190
191 should "be rendered" do
191 should "be rendered" do
192 @output_buffer = @gantt.subjects
192 @output_buffer = @gantt.subjects
193 assert_select "div.issue-subject", /#{@issue.subject}/
193 assert_select "div.issue-subject", /#{@issue.subject}/
194 end
194 end
195 end
195 end
196
196
197 context "with subtasks" do
197 context "with subtasks" do
198 setup do
198 setup do
199 attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version}
199 attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version}
200 @child1 = Issue.generate!(
200 @child1 = Issue.generate!(
201 attrs.merge(:subject => 'child1',
201 attrs.merge(:subject => 'child1',
202 :parent_issue_id => @issue.id,
202 :parent_issue_id => @issue.id,
203 :start_date => (today - 1),
203 :start_date => (today - 1),
204 :due_date => (today + 2))
204 :due_date => (today + 2))
205 )
205 )
206 @child2 = Issue.generate!(
206 @child2 = Issue.generate!(
207 attrs.merge(:subject => 'child2',
207 attrs.merge(:subject => 'child2',
208 :parent_issue_id => @issue.id,
208 :parent_issue_id => @issue.id,
209 :start_date => today,
209 :start_date => today,
210 :due_date => (today + 7))
210 :due_date => (today + 7))
211 )
211 )
212 @grandchild = Issue.generate!(
212 @grandchild = Issue.generate!(
213 attrs.merge(:subject => 'grandchild',
213 attrs.merge(:subject => 'grandchild',
214 :parent_issue_id => @child1.id,
214 :parent_issue_id => @child1.id,
215 :start_date => (today - 1),
215 :start_date => (today - 1),
216 :due_date => (today + 2))
216 :due_date => (today + 2))
217 )
217 )
218 end
218 end
219
219
220 should "indent subtasks" do
220 should "indent subtasks" do
221 @output_buffer = @gantt.subjects
221 @output_buffer = @gantt.subjects
222 # parent task 44px
222 # parent task 44px
223 assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/
223 assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/
224 # children 64px
224 # children 64px
225 assert_select "div.issue-subject[style*=left:64px]", /child1/
225 assert_select "div.issue-subject[style*=left:64px]", /child1/
226 assert_select "div.issue-subject[style*=left:64px]", /child2/
226 assert_select "div.issue-subject[style*=left:64px]", /child2/
227 # grandchild 84px
227 # grandchild 84px
228 assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @output_buffer
228 assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @output_buffer
229 end
229 end
230 end
230 end
231 end
231 end
232 end
232 end
233
233
234 context "#lines" do
234 context "#lines" do
235 setup do
235 setup do
236 create_gantt
236 create_gantt
237 @project.enabled_module_names = [:issue_tracking]
237 @project.enabled_module_names = [:issue_tracking]
238 @tracker = Tracker.generate!
238 @tracker = Tracker.generate!
239 @project.trackers << @tracker
239 @project.trackers << @tracker
240 @version = Version.generate!(:effective_date => (today + 7))
240 @version = Version.generate!(:effective_date => (today + 7))
241 @project.versions << @version
241 @project.versions << @version
242 @issue = Issue.generate!(:fixed_version => @version,
242 @issue = Issue.generate!(:fixed_version => @version,
243 :subject => "gantt#line_for_project",
243 :subject => "gantt#line_for_project",
244 :tracker => @tracker,
244 :tracker => @tracker,
245 :project => @project,
245 :project => @project,
246 :done_ratio => 30,
246 :done_ratio => 30,
247 :start_date => (today - 1),
247 :start_date => (today - 1),
248 :due_date => (today + 7))
248 :due_date => (today + 7))
249 @project.issues << @issue
249 @project.issues << @issue
250 @output_buffer = @gantt.lines
250 @output_buffer = @gantt.lines
251 end
251 end
252
252
253 context "project" do
253 context "project" do
254 should "be rendered" do
254 should "be rendered" do
255 assert_select "div.project.task_todo"
255 assert_select "div.project.task_todo"
256 assert_select "div.project.starting"
256 assert_select "div.project.starting"
257 assert_select "div.project.ending"
257 assert_select "div.project.ending"
258 assert_select "div.label.project", /#{@project.name}/
258 assert_select "div.label.project", /#{@project.name}/
259 end
259 end
260 end
260 end
261
261
262 context "version" do
262 context "version" do
263 should "be rendered" do
263 should "be rendered" do
264 assert_select "div.version.task_todo"
264 assert_select "div.version.task_todo"
265 assert_select "div.version.starting"
265 assert_select "div.version.starting"
266 assert_select "div.version.ending"
266 assert_select "div.version.ending"
267 assert_select "div.label.version", /#{@version.name}/
267 assert_select "div.label.version", /#{@version.name}/
268 end
268 end
269 end
269 end
270
270
271 context "issue" do
271 context "issue" do
272 should "be rendered" do
272 should "be rendered" do
273 assert_select "div.task_todo"
273 assert_select "div.task_todo"
274 assert_select "div.task.label", /#{@issue.done_ratio}/
274 assert_select "div.task.label", /#{@issue.done_ratio}/
275 assert_select "div.tooltip", /#{@issue.subject}/
275 assert_select "div.tooltip", /#{@issue.subject}/
276 end
276 end
277 end
277 end
278 end
278 end
279
279
280 context "#render_project" do
280 context "#render_project" do
281 should "be tested"
281 should "be tested"
282 end
282 end
283
283
284 context "#render_issues" do
284 context "#render_issues" do
285 should "be tested"
285 should "be tested"
286 end
286 end
287
287
288 context "#render_version" do
288 context "#render_version" do
289 should "be tested"
289 should "be tested"
290 end
290 end
291
291
292 context "#subject_for_project" do
292 context "#subject_for_project" do
293 setup do
293 setup do
294 create_gantt
294 create_gantt
295 end
295 end
296
296
297 context ":html format" do
297 context ":html format" do
298 should "add an absolute positioned div" do
298 should "add an absolute positioned div" do
299 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
299 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
300 assert_select "div[style*=absolute]"
300 assert_select "div[style*=absolute]"
301 end
301 end
302
302
303 should "use the indent option to move the div to the right" do
303 should "use the indent option to move the div to the right" do
304 @output_buffer = @gantt.subject_for_project(@project, {:format => :html, :indent => 40})
304 @output_buffer = @gantt.subject_for_project(@project, {:format => :html, :indent => 40})
305 assert_select "div[style*=left:40]"
305 assert_select "div[style*=left:40]"
306 end
306 end
307
307
308 should "include the project name" do
308 should "include the project name" do
309 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
309 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
310 assert_select 'div', :text => /#{@project.name}/
310 assert_select 'div', :text => /#{@project.name}/
311 end
311 end
312
312
313 should "include a link to the project" do
313 should "include a link to the project" do
314 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
314 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
315 assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/
315 assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/
316 end
316 end
317
317
318 should "style overdue projects" do
318 should "style overdue projects" do
319 @project.enabled_module_names = [:issue_tracking]
319 @project.enabled_module_names = [:issue_tracking]
320 @project.versions << Version.generate!(:effective_date => (today - 1))
320 @project.versions << Version.generate!(:effective_date => (today - 1))
321 assert @project.reload.overdue?, "Need an overdue project for this test"
321 assert @project.reload.overdue?, "Need an overdue project for this test"
322 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
322 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
323 assert_select 'div span.project-overdue'
323 assert_select 'div span.project-overdue'
324 end
324 end
325 end
325 end
326 should "test the PNG format"
326 should "test the PNG format"
327 should "test the PDF format"
327 should "test the PDF format"
328 end
328 end
329
329
330 context "#line_for_project" do
330 context "#line_for_project" do
331 setup do
331 setup do
332 create_gantt
332 create_gantt
333 @project.enabled_module_names = [:issue_tracking]
333 @project.enabled_module_names = [:issue_tracking]
334 @tracker = Tracker.generate!
334 @tracker = Tracker.generate!
335 @project.trackers << @tracker
335 @project.trackers << @tracker
336 @version = Version.generate!(:effective_date => (today - 1))
336 @version = Version.generate!(:effective_date => (today - 1))
337 @project.versions << @version
337 @project.versions << @version
338 @project.issues << Issue.generate!(:fixed_version => @version,
338 @project.issues << Issue.generate!(:fixed_version => @version,
339 :subject => "gantt#line_for_project",
339 :subject => "gantt#line_for_project",
340 :tracker => @tracker,
340 :tracker => @tracker,
341 :project => @project,
341 :project => @project,
342 :done_ratio => 30,
342 :done_ratio => 30,
343 :start_date => (today - 7),
343 :start_date => (today - 7),
344 :due_date => (today + 7))
344 :due_date => (today + 7))
345 end
345 end
346
346
347 context ":html format" do
347 context ":html format" do
348 context "todo line" do
348 context "todo line" do
349 should "start from the starting point on the left" do
349 should "start from the starting point on the left" do
350 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
350 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
351 assert_select "div.project.task_todo[style*=left:28px]", true, @output_buffer
351 assert_select "div.project.task_todo[style*=left:28px]", true, @output_buffer
352 end
352 end
353
353
354 should "be the total width of the project" do
354 should "be the total width of the project" do
355 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
355 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
356 assert_select "div.project.task_todo[style*=width:58px]", true, @output_buffer
356 assert_select "div.project.task_todo[style*=width:58px]", true, @output_buffer
357 end
357 end
358 end
358 end
359
359
360 context "late line" do
360 context "late line" do
361 should_eventually "start from the starting point on the left" do
361 should_eventually "start from the starting point on the left" do
362 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
362 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
363 assert_select "div.project.task_late[style*=left:28px]", true, @output_buffer
363 assert_select "div.project.task_late[style*=left:28px]", true, @output_buffer
364 end
364 end
365
365
366 should_eventually "be the total delayed width of the project" do
366 should_eventually "be the total delayed width of the project" do
367 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
367 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
368 assert_select "div.project.task_late[style*=width:30px]", true, @output_buffer
368 assert_select "div.project.task_late[style*=width:30px]", true, @output_buffer
369 end
369 end
370 end
370 end
371
371
372 context "done line" do
372 context "done line" do
373 should_eventually "start from the starting point on the left" do
373 should_eventually "start from the starting point on the left" do
374 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
374 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
375 assert_select "div.project.task_done[style*=left:28px]", true, @output_buffer
375 assert_select "div.project.task_done[style*=left:28px]", true, @output_buffer
376 end
376 end
377
377
378 should_eventually "Be the total done width of the project" do
378 should_eventually "Be the total done width of the project" do
379 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
379 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
380 assert_select "div.project.task_done[style*=width:18px]", true, @output_buffer
380 assert_select "div.project.task_done[style*=width:18px]", true, @output_buffer
381 end
381 end
382 end
382 end
383
383
384 context "starting marker" do
384 context "starting marker" do
385 should "not appear if the starting point is off the gantt chart" do
385 should "not appear if the starting point is off the gantt chart" do
386 # Shift the date range of the chart
386 # Shift the date range of the chart
387 @gantt.instance_variable_set('@date_from', today)
387 @gantt.instance_variable_set('@date_from', today)
388 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
388 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
389 assert_select "div.project.starting", false, @output_buffer
389 assert_select "div.project.starting", false, @output_buffer
390 end
390 end
391
391
392 should "appear at the starting point" do
392 should "appear at the starting point" do
393 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
393 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
394 assert_select "div.project.starting[style*=left:28px]", true, @output_buffer
394 assert_select "div.project.starting[style*=left:28px]", true, @output_buffer
395 end
395 end
396 end
396 end
397
397
398 context "ending marker" do
398 context "ending marker" do
399 should "not appear if the starting point is off the gantt chart" do
399 should "not appear if the starting point is off the gantt chart" do
400 # Shift the date range of the chart
400 # Shift the date range of the chart
401 @gantt.instance_variable_set('@date_to', (today - 14))
401 @gantt.instance_variable_set('@date_to', (today - 14))
402 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
402 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
403 assert_select "div.project.ending", false, @output_buffer
403 assert_select "div.project.ending", false, @output_buffer
404 end
404 end
405
405
406 should "appear at the end of the date range" do
406 should "appear at the end of the date range" do
407 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
407 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
408 assert_select "div.project.ending[style*=left:88px]", true, @output_buffer
408 assert_select "div.project.ending[style*=left:88px]", true, @output_buffer
409 end
409 end
410 end
410 end
411
411
412 context "status content" do
412 context "status content" do
413 should "appear at the far left, even if it's far in the past" do
413 should "appear at the far left, even if it's far in the past" do
414 @gantt.instance_variable_set('@date_to', (today - 14))
414 @gantt.instance_variable_set('@date_to', (today - 14))
415 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
415 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
416 assert_select "div.project.label", /#{@project.name}/
416 assert_select "div.project.label", /#{@project.name}/
417 end
417 end
418
418
419 should "show the project name" do
419 should "show the project name" do
420 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
420 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
421 assert_select "div.project.label", /#{@project.name}/
421 assert_select "div.project.label", /#{@project.name}/
422 end
422 end
423
423
424 should_eventually "show the percent complete" do
424 should_eventually "show the percent complete" do
425 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
425 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
426 assert_select "div.project.label", /0%/
426 assert_select "div.project.label", /0%/
427 end
427 end
428 end
428 end
429 end
429 end
430 should "test the PNG format"
430 should "test the PNG format"
431 should "test the PDF format"
431 should "test the PDF format"
432 end
432 end
433
433
434 context "#subject_for_version" do
434 context "#subject_for_version" do
435 setup do
435 setup do
436 create_gantt
436 create_gantt
437 @project.enabled_module_names = [:issue_tracking]
437 @project.enabled_module_names = [:issue_tracking]
438 @tracker = Tracker.generate!
438 @tracker = Tracker.generate!
439 @project.trackers << @tracker
439 @project.trackers << @tracker
440 @version = Version.generate!(:effective_date => (today - 1))
440 @version = Version.generate!(:effective_date => (today - 1))
441 @project.versions << @version
441 @project.versions << @version
442 @project.issues << Issue.generate!(:fixed_version => @version,
442 @project.issues << Issue.generate!(:fixed_version => @version,
443 :subject => "gantt#subject_for_version",
443 :subject => "gantt#subject_for_version",
444 :tracker => @tracker,
444 :tracker => @tracker,
445 :project => @project,
445 :project => @project,
446 :start_date => today)
446 :start_date => today)
447
447
448 end
448 end
449
449
450 context ":html format" do
450 context ":html format" do
451 should "add an absolute positioned div" do
451 should "add an absolute positioned div" do
452 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
452 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
453 assert_select "div[style*=absolute]"
453 assert_select "div[style*=absolute]"
454 end
454 end
455
455
456 should "use the indent option to move the div to the right" do
456 should "use the indent option to move the div to the right" do
457 @output_buffer = @gantt.subject_for_version(@version, {:format => :html, :indent => 40})
457 @output_buffer = @gantt.subject_for_version(@version, {:format => :html, :indent => 40})
458 assert_select "div[style*=left:40]"
458 assert_select "div[style*=left:40]"
459 end
459 end
460
460
461 should "include the version name" do
461 should "include the version name" do
462 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
462 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
463 assert_select 'div', :text => /#{@version.name}/
463 assert_select 'div', :text => /#{@version.name}/
464 end
464 end
465
465
466 should "include a link to the version" do
466 should "include a link to the version" do
467 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
467 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
468 assert_select 'a[href=?]', Regexp.escape("/versions/#{@version.to_param}"), :text => /#{@version.name}/
468 assert_select 'a[href=?]', Regexp.escape("/versions/#{@version.to_param}"), :text => /#{@version.name}/
469 end
469 end
470
470
471 should "style late versions" do
471 should "style late versions" do
472 assert @version.overdue?, "Need an overdue version for this test"
472 assert @version.overdue?, "Need an overdue version for this test"
473 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
473 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
474 assert_select 'div span.version-behind-schedule'
474 assert_select 'div span.version-behind-schedule'
475 end
475 end
476
476
477 should "style behind schedule versions" do
477 should "style behind schedule versions" do
478 assert @version.behind_schedule?, "Need a behind schedule version for this test"
478 assert @version.behind_schedule?, "Need a behind schedule version for this test"
479 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
479 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
480 assert_select 'div span.version-behind-schedule'
480 assert_select 'div span.version-behind-schedule'
481 end
481 end
482 end
482 end
483 should "test the PNG format"
483 should "test the PNG format"
484 should "test the PDF format"
484 should "test the PDF format"
485 end
485 end
486
486
487 context "#line_for_version" do
487 context "#line_for_version" do
488 setup do
488 setup do
489 create_gantt
489 create_gantt
490 @project.enabled_module_names = [:issue_tracking]
490 @project.enabled_module_names = [:issue_tracking]
491 @tracker = Tracker.generate!
491 @tracker = Tracker.generate!
492 @project.trackers << @tracker
492 @project.trackers << @tracker
493 @version = Version.generate!(:effective_date => (today + 7))
493 @version = Version.generate!(:effective_date => (today + 7))
494 @project.versions << @version
494 @project.versions << @version
495 @project.issues << Issue.generate!(:fixed_version => @version,
495 @project.issues << Issue.generate!(:fixed_version => @version,
496 :subject => "gantt#line_for_project",
496 :subject => "gantt#line_for_project",
497 :tracker => @tracker,
497 :tracker => @tracker,
498 :project => @project,
498 :project => @project,
499 :done_ratio => 30,
499 :done_ratio => 30,
500 :start_date => (today - 7),
500 :start_date => (today - 7),
501 :due_date => (today + 7))
501 :due_date => (today + 7))
502 end
502 end
503
503
504 context ":html format" do
504 context ":html format" do
505 context "todo line" do
505 context "todo line" do
506 should "start from the starting point on the left" do
506 should "start from the starting point on the left" do
507 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
507 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
508 assert_select "div.version.task_todo[style*=left:28px]", true, @output_buffer
508 assert_select "div.version.task_todo[style*=left:28px]", true, @output_buffer
509 end
509 end
510
510
511 should "be the total width of the version" do
511 should "be the total width of the version" do
512 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
512 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
513 assert_select "div.version.task_todo[style*=width:58px]", true, @output_buffer
513 assert_select "div.version.task_todo[style*=width:58px]", true, @output_buffer
514 end
514 end
515 end
515 end
516
516
517 context "late line" do
517 context "late line" do
518 should "start from the starting point on the left" do
518 should "start from the starting point on the left" do
519 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
519 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
520 assert_select "div.version.task_late[style*=left:28px]", true, @output_buffer
520 assert_select "div.version.task_late[style*=left:28px]", true, @output_buffer
521 end
521 end
522
522
523 should "be the total delayed width of the version" do
523 should "be the total delayed width of the version" do
524 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
524 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
525 assert_select "div.version.task_late[style*=width:30px]", true, @output_buffer
525 assert_select "div.version.task_late[style*=width:30px]", true, @output_buffer
526 end
526 end
527 end
527 end
528
528
529 context "done line" do
529 context "done line" do
530 should "start from the starting point on the left" do
530 should "start from the starting point on the left" do
531 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
531 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
532 assert_select "div.version.task_done[style*=left:28px]", true, @output_buffer
532 assert_select "div.version.task_done[style*=left:28px]", true, @output_buffer
533 end
533 end
534
534
535 should "be the total done width of the version" do
535 should "be the total done width of the version" do
536 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
536 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
537 assert_select "div.version.task_done[style*=width:16px]", true, @output_buffer
537 assert_select "div.version.task_done[style*=width:16px]", true, @output_buffer
538 end
538 end
539 end
539 end
540
540
541 context "starting marker" do
541 context "starting marker" do
542 should "not appear if the starting point is off the gantt chart" do
542 should "not appear if the starting point is off the gantt chart" do
543 # Shift the date range of the chart
543 # Shift the date range of the chart
544 @gantt.instance_variable_set('@date_from', today)
544 @gantt.instance_variable_set('@date_from', today)
545 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
545 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
546 assert_select "div.version.starting", false
546 assert_select "div.version.starting", false
547 end
547 end
548
548
549 should "appear at the starting point" do
549 should "appear at the starting point" do
550 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
550 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
551 assert_select "div.version.starting[style*=left:28px]", true, @output_buffer
551 assert_select "div.version.starting[style*=left:28px]", true, @output_buffer
552 end
552 end
553 end
553 end
554
554
555 context "ending marker" do
555 context "ending marker" do
556 should "not appear if the starting point is off the gantt chart" do
556 should "not appear if the starting point is off the gantt chart" do
557 # Shift the date range of the chart
557 # Shift the date range of the chart
558 @gantt.instance_variable_set('@date_to', (today - 14))
558 @gantt.instance_variable_set('@date_to', (today - 14))
559 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
559 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
560 assert_select "div.version.ending", false
560 assert_select "div.version.ending", false
561 end
561 end
562
562
563 should "appear at the end of the date range" do
563 should "appear at the end of the date range" do
564 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
564 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
565 assert_select "div.version.ending[style*=left:88px]", true, @output_buffer
565 assert_select "div.version.ending[style*=left:88px]", true, @output_buffer
566 end
566 end
567 end
567 end
568
568
569 context "status content" do
569 context "status content" do
570 should "appear at the far left, even if it's far in the past" do
570 should "appear at the far left, even if it's far in the past" do
571 @gantt.instance_variable_set('@date_to', (today - 14))
571 @gantt.instance_variable_set('@date_to', (today - 14))
572 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
572 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
573 assert_select "div.version.label", /#{@version.name}/
573 assert_select "div.version.label", /#{@version.name}/
574 end
574 end
575
575
576 should "show the version name" do
576 should "show the version name" do
577 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
577 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
578 assert_select "div.version.label", /#{@version.name}/
578 assert_select "div.version.label", /#{@version.name}/
579 end
579 end
580
580
581 should "show the percent complete" do
581 should "show the percent complete" do
582 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
582 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
583 assert_select "div.version.label", /30%/
583 assert_select "div.version.label", /30%/
584 end
584 end
585 end
585 end
586 end
586 end
587 should "test the PNG format"
587 should "test the PNG format"
588 should "test the PDF format"
588 should "test the PDF format"
589 end
589 end
590
590
591 context "#subject_for_issue" do
591 context "#subject_for_issue" do
592 setup do
592 setup do
593 create_gantt
593 create_gantt
594 @project.enabled_module_names = [:issue_tracking]
594 @project.enabled_module_names = [:issue_tracking]
595 @tracker = Tracker.generate!
595 @tracker = Tracker.generate!
596 @project.trackers << @tracker
596 @project.trackers << @tracker
597 @issue = Issue.generate!(:subject => "gantt#subject_for_issue",
597 @issue = Issue.generate!(:subject => "gantt#subject_for_issue",
598 :tracker => @tracker,
598 :tracker => @tracker,
599 :project => @project,
599 :project => @project,
600 :start_date => (today - 3),
600 :start_date => (today - 3),
601 :due_date => (today - 1))
601 :due_date => (today - 1))
602 @project.issues << @issue
602 @project.issues << @issue
603 end
603 end
604
604
605 context ":html format" do
605 context ":html format" do
606 should "add an absolute positioned div" do
606 should "add an absolute positioned div" do
607 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
607 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
608 assert_select "div[style*=absolute]"
608 assert_select "div[style*=absolute]"
609 end
609 end
610
610
611 should "use the indent option to move the div to the right" do
611 should "use the indent option to move the div to the right" do
612 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40})
612 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40})
613 assert_select "div[style*=left:40]"
613 assert_select "div[style*=left:40]"
614 end
614 end
615
615
616 should "include the issue subject" do
616 should "include the issue subject" do
617 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
617 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
618 assert_select 'div', :text => /#{@issue.subject}/
618 assert_select 'div', :text => /#{@issue.subject}/
619 end
619 end
620
620
621 should "include a link to the issue" do
621 should "include a link to the issue" do
622 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
622 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
623 assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/
623 assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/
624 end
624 end
625
625
626 should "style overdue issues" do
626 should "style overdue issues" do
627 assert @issue.overdue?, "Need an overdue issue for this test"
627 assert @issue.overdue?, "Need an overdue issue for this test"
628 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
628 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
629 assert_select 'div span.issue-overdue'
629 assert_select 'div span.issue-overdue'
630 end
630 end
631 end
631 end
632 should "test the PNG format"
632 should "test the PNG format"
633 should "test the PDF format"
633 should "test the PDF format"
634 end
634 end
635
635
636 context "#line_for_issue" do
636 context "#line_for_issue" do
637 setup do
637 setup do
638 create_gantt
638 create_gantt
639 @project.enabled_module_names = [:issue_tracking]
639 @project.enabled_module_names = [:issue_tracking]
640 @tracker = Tracker.generate!
640 @tracker = Tracker.generate!
641 @project.trackers << @tracker
641 @project.trackers << @tracker
642 @version = Version.generate!(:effective_date => (today + 7))
642 @version = Version.generate!(:effective_date => (today + 7))
643 @project.versions << @version
643 @project.versions << @version
644 @issue = Issue.generate!(:fixed_version => @version,
644 @issue = Issue.generate!(:fixed_version => @version,
645 :subject => "gantt#line_for_project",
645 :subject => "gantt#line_for_project",
646 :tracker => @tracker,
646 :tracker => @tracker,
647 :project => @project,
647 :project => @project,
648 :done_ratio => 30,
648 :done_ratio => 30,
649 :start_date => (today - 7),
649 :start_date => (today - 7),
650 :due_date => (today + 7))
650 :due_date => (today + 7))
651 @project.issues << @issue
651 @project.issues << @issue
652 end
652 end
653
653
654 context ":html format" do
654 context ":html format" do
655 context "todo line" do
655 context "todo line" do
656 should "start from the starting point on the left" do
656 should "start from the starting point on the left" do
657 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
657 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
658 assert_select "div.task_todo[style*=left:28px]", true, @output_buffer
658 assert_select "div.task_todo[style*=left:28px]", true, @output_buffer
659 end
659 end
660
660
661 should "be the total width of the issue" do
661 should "be the total width of the issue" do
662 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
662 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
663 assert_select "div.task_todo[style*=width:58px]", true, @output_buffer
663 assert_select "div.task_todo[style*=width:58px]", true, @output_buffer
664 end
664 end
665 end
665 end
666
666
667 context "late line" do
667 context "late line" do
668 should "start from the starting point on the left" do
668 should "start from the starting point on the left" do
669 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
669 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
670 assert_select "div.task_late[style*=left:28px]", true, @output_buffer
670 assert_select "div.task_late[style*=left:28px]", true, @output_buffer
671 end
671 end
672
672
673 should "be the total delayed width of the issue" do
673 should "be the total delayed width of the issue" do
674 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
674 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
675 assert_select "div.task_late[style*=width:30px]", true, @output_buffer
675 assert_select "div.task_late[style*=width:30px]", true, @output_buffer
676 end
676 end
677 end
677 end
678
678
679 context "done line" do
679 context "done line" do
680 should "start from the starting point on the left" do
680 should "start from the starting point on the left" do
681 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
681 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
682 assert_select "div.task_done[style*=left:28px]", true, @output_buffer
682 assert_select "div.task_done[style*=left:28px]", true, @output_buffer
683 end
683 end
684
684
685 should "be the total done width of the issue" do
685 should "be the total done width of the issue" do
686 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
686 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
687 # 15 days * 4 px * 30% - 2 px for borders = 16 px
687 # 15 days * 4 px * 30% - 2 px for borders = 16 px
688 assert_select "div.task_done[style*=width:16px]", true, @output_buffer
688 assert_select "div.task_done[style*=width:16px]", true, @output_buffer
689 end
689 end
690
690
691 should "not be the total done width if the chart starts after issue start date" do
691 should "not be the total done width if the chart starts after issue start date" do
692 create_gantt(@project, :date_from => (today - 5))
692 create_gantt(@project, :date_from => (today - 5))
693 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
693 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
694 assert_select "div.task_done[style*=left:0px]", true, @output_buffer
694 assert_select "div.task_done[style*=left:0px]", true, @output_buffer
695 assert_select "div.task_done[style*=width:8px]", true, @output_buffer
695 assert_select "div.task_done[style*=width:8px]", true, @output_buffer
696 end
696 end
697
697
698 context "for completed issue" do
698 context "for completed issue" do
699 setup do
699 setup do
700 @issue.done_ratio = 100
700 @issue.done_ratio = 100
701 end
701 end
702
702
703 should "be the total width of the issue" do
703 should "be the total width of the issue" do
704 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
704 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
705 assert_select "div.task_done[style*=width:58px]", true, @output_buffer
705 assert_select "div.task_done[style*=width:58px]", true, @output_buffer
706 end
706 end
707
707
708 should "be the total width of the issue with due_date=start_date" do
708 should "be the total width of the issue with due_date=start_date" do
709 @issue.due_date = @issue.start_date
709 @issue.due_date = @issue.start_date
710 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
710 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
711 assert_select "div.task_done[style*=width:2px]", true, @output_buffer
711 assert_select "div.task_done[style*=width:2px]", true, @output_buffer
712 end
712 end
713 end
713 end
714 end
714 end
715
715
716 context "status content" do
716 context "status content" do
717 should "appear at the far left, even if it's far in the past" do
717 should "appear at the far left, even if it's far in the past" do
718 @gantt.instance_variable_set('@date_to', (today - 14))
718 @gantt.instance_variable_set('@date_to', (today - 14))
719 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
719 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
720 assert_select "div.task.label", true, @output_buffer
720 assert_select "div.task.label", true, @output_buffer
721 end
721 end
722
722
723 should "show the issue status" do
723 should "show the issue status" do
724 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
724 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
725 assert_select "div.task.label", /#{@issue.status.name}/
725 assert_select "div.task.label", /#{@issue.status.name}/
726 end
726 end
727
727
728 should "show the percent complete" do
728 should "show the percent complete" do
729 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
729 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
730 assert_select "div.task.label", /30%/
730 assert_select "div.task.label", /30%/
731 end
731 end
732 end
732 end
733 end
733 end
734
734
735 should "have an issue tooltip" do
735 should "have an issue tooltip" do
736 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
736 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
737 assert_select "div.tooltip", /#{@issue.subject}/
737 assert_select "div.tooltip", /#{@issue.subject}/
738 end
738 end
739 should "test the PNG format"
739 should "test the PNG format"
740 should "test the PDF format"
740 should "test the PDF format"
741 end
741 end
742
742
743 context "#to_image" do
743 context "#to_image" do
744 should "be tested"
744 should "be tested"
745 end
745 end
746
746
747 context "#to_pdf" do
747 context "#to_pdf" do
748 should "be tested"
748 should "be tested"
749 end
749 end
750 end
750 end
@@ -1,1249 +1,1248
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :projects, :enabled_modules, :users, :members,
23 fixtures :projects, :enabled_modules, :users, :members,
24 :member_roles, :roles, :trackers, :issue_statuses,
24 :member_roles, :roles, :trackers, :issue_statuses,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :watchers, :custom_fields, :custom_values, :versions,
26 :watchers, :custom_fields, :custom_values, :versions,
27 :queries,
27 :queries,
28 :projects_trackers,
28 :projects_trackers,
29 :custom_fields_trackers
29 :custom_fields_trackers
30
30
31 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
31 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
32 query = Query.new(:project => nil, :name => '_')
32 query = IssueQuery.new(:project => nil, :name => '_')
33 assert query.available_filters.has_key?('cf_1')
33 assert query.available_filters.has_key?('cf_1')
34 assert !query.available_filters.has_key?('cf_3')
34 assert !query.available_filters.has_key?('cf_3')
35 end
35 end
36
36
37 def test_system_shared_versions_should_be_available_in_global_queries
37 def test_system_shared_versions_should_be_available_in_global_queries
38 Version.find(2).update_attribute :sharing, 'system'
38 Version.find(2).update_attribute :sharing, 'system'
39 query = Query.new(:project => nil, :name => '_')
39 query = IssueQuery.new(:project => nil, :name => '_')
40 assert query.available_filters.has_key?('fixed_version_id')
40 assert query.available_filters.has_key?('fixed_version_id')
41 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
41 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
42 end
42 end
43
43
44 def test_project_filter_in_global_queries
44 def test_project_filter_in_global_queries
45 query = Query.new(:project => nil, :name => '_')
45 query = IssueQuery.new(:project => nil, :name => '_')
46 project_filter = query.available_filters["project_id"]
46 project_filter = query.available_filters["project_id"]
47 assert_not_nil project_filter
47 assert_not_nil project_filter
48 project_ids = project_filter[:values].map{|p| p[1]}
48 project_ids = project_filter[:values].map{|p| p[1]}
49 assert project_ids.include?("1") #public project
49 assert project_ids.include?("1") #public project
50 assert !project_ids.include?("2") #private project user cannot see
50 assert !project_ids.include?("2") #private project user cannot see
51 end
51 end
52
52
53 def find_issues_with_query(query)
53 def find_issues_with_query(query)
54 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
54 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
55 query.statement
55 query.statement
56 ).all
56 ).all
57 end
57 end
58
58
59 def assert_find_issues_with_query_is_successful(query)
59 def assert_find_issues_with_query_is_successful(query)
60 assert_nothing_raised do
60 assert_nothing_raised do
61 find_issues_with_query(query)
61 find_issues_with_query(query)
62 end
62 end
63 end
63 end
64
64
65 def assert_query_statement_includes(query, condition)
65 def assert_query_statement_includes(query, condition)
66 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
66 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
67 end
67 end
68
68
69 def assert_query_result(expected, query)
69 def assert_query_result(expected, query)
70 assert_nothing_raised do
70 assert_nothing_raised do
71 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
71 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
72 assert_equal expected.size, query.issue_count
72 assert_equal expected.size, query.issue_count
73 end
73 end
74 end
74 end
75
75
76 def test_query_should_allow_shared_versions_for_a_project_query
76 def test_query_should_allow_shared_versions_for_a_project_query
77 subproject_version = Version.find(4)
77 subproject_version = Version.find(4)
78 query = Query.new(:project => Project.find(1), :name => '_')
78 query = IssueQuery.new(:project => Project.find(1), :name => '_')
79 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
79 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
80
80
81 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
81 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
82 end
82 end
83
83
84 def test_query_with_multiple_custom_fields
84 def test_query_with_multiple_custom_fields
85 query = Query.find(1)
85 query = IssueQuery.find(1)
86 assert query.valid?
86 assert query.valid?
87 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
87 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
88 issues = find_issues_with_query(query)
88 issues = find_issues_with_query(query)
89 assert_equal 1, issues.length
89 assert_equal 1, issues.length
90 assert_equal Issue.find(3), issues.first
90 assert_equal Issue.find(3), issues.first
91 end
91 end
92
92
93 def test_operator_none
93 def test_operator_none
94 query = Query.new(:project => Project.find(1), :name => '_')
94 query = IssueQuery.new(:project => Project.find(1), :name => '_')
95 query.add_filter('fixed_version_id', '!*', [''])
95 query.add_filter('fixed_version_id', '!*', [''])
96 query.add_filter('cf_1', '!*', [''])
96 query.add_filter('cf_1', '!*', [''])
97 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
97 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
98 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
98 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
99 find_issues_with_query(query)
99 find_issues_with_query(query)
100 end
100 end
101
101
102 def test_operator_none_for_integer
102 def test_operator_none_for_integer
103 query = Query.new(:project => Project.find(1), :name => '_')
103 query = IssueQuery.new(:project => Project.find(1), :name => '_')
104 query.add_filter('estimated_hours', '!*', [''])
104 query.add_filter('estimated_hours', '!*', [''])
105 issues = find_issues_with_query(query)
105 issues = find_issues_with_query(query)
106 assert !issues.empty?
106 assert !issues.empty?
107 assert issues.all? {|i| !i.estimated_hours}
107 assert issues.all? {|i| !i.estimated_hours}
108 end
108 end
109
109
110 def test_operator_none_for_date
110 def test_operator_none_for_date
111 query = Query.new(:project => Project.find(1), :name => '_')
111 query = IssueQuery.new(:project => Project.find(1), :name => '_')
112 query.add_filter('start_date', '!*', [''])
112 query.add_filter('start_date', '!*', [''])
113 issues = find_issues_with_query(query)
113 issues = find_issues_with_query(query)
114 assert !issues.empty?
114 assert !issues.empty?
115 assert issues.all? {|i| i.start_date.nil?}
115 assert issues.all? {|i| i.start_date.nil?}
116 end
116 end
117
117
118 def test_operator_none_for_string_custom_field
118 def test_operator_none_for_string_custom_field
119 query = Query.new(:project => Project.find(1), :name => '_')
119 query = IssueQuery.new(:project => Project.find(1), :name => '_')
120 query.add_filter('cf_2', '!*', [''])
120 query.add_filter('cf_2', '!*', [''])
121 assert query.has_filter?('cf_2')
121 assert query.has_filter?('cf_2')
122 issues = find_issues_with_query(query)
122 issues = find_issues_with_query(query)
123 assert !issues.empty?
123 assert !issues.empty?
124 assert issues.all? {|i| i.custom_field_value(2).blank?}
124 assert issues.all? {|i| i.custom_field_value(2).blank?}
125 end
125 end
126
126
127 def test_operator_all
127 def test_operator_all
128 query = Query.new(:project => Project.find(1), :name => '_')
128 query = IssueQuery.new(:project => Project.find(1), :name => '_')
129 query.add_filter('fixed_version_id', '*', [''])
129 query.add_filter('fixed_version_id', '*', [''])
130 query.add_filter('cf_1', '*', [''])
130 query.add_filter('cf_1', '*', [''])
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
132 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
132 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
133 find_issues_with_query(query)
133 find_issues_with_query(query)
134 end
134 end
135
135
136 def test_operator_all_for_date
136 def test_operator_all_for_date
137 query = Query.new(:project => Project.find(1), :name => '_')
137 query = IssueQuery.new(:project => Project.find(1), :name => '_')
138 query.add_filter('start_date', '*', [''])
138 query.add_filter('start_date', '*', [''])
139 issues = find_issues_with_query(query)
139 issues = find_issues_with_query(query)
140 assert !issues.empty?
140 assert !issues.empty?
141 assert issues.all? {|i| i.start_date.present?}
141 assert issues.all? {|i| i.start_date.present?}
142 end
142 end
143
143
144 def test_operator_all_for_string_custom_field
144 def test_operator_all_for_string_custom_field
145 query = Query.new(:project => Project.find(1), :name => '_')
145 query = IssueQuery.new(:project => Project.find(1), :name => '_')
146 query.add_filter('cf_2', '*', [''])
146 query.add_filter('cf_2', '*', [''])
147 assert query.has_filter?('cf_2')
147 assert query.has_filter?('cf_2')
148 issues = find_issues_with_query(query)
148 issues = find_issues_with_query(query)
149 assert !issues.empty?
149 assert !issues.empty?
150 assert issues.all? {|i| i.custom_field_value(2).present?}
150 assert issues.all? {|i| i.custom_field_value(2).present?}
151 end
151 end
152
152
153 def test_numeric_filter_should_not_accept_non_numeric_values
153 def test_numeric_filter_should_not_accept_non_numeric_values
154 query = Query.new(:name => '_')
154 query = IssueQuery.new(:name => '_')
155 query.add_filter('estimated_hours', '=', ['a'])
155 query.add_filter('estimated_hours', '=', ['a'])
156
156
157 assert query.has_filter?('estimated_hours')
157 assert query.has_filter?('estimated_hours')
158 assert !query.valid?
158 assert !query.valid?
159 end
159 end
160
160
161 def test_operator_is_on_float
161 def test_operator_is_on_float
162 Issue.update_all("estimated_hours = 171.2", "id=2")
162 Issue.update_all("estimated_hours = 171.2", "id=2")
163
163
164 query = Query.new(:name => '_')
164 query = IssueQuery.new(:name => '_')
165 query.add_filter('estimated_hours', '=', ['171.20'])
165 query.add_filter('estimated_hours', '=', ['171.20'])
166 issues = find_issues_with_query(query)
166 issues = find_issues_with_query(query)
167 assert_equal 1, issues.size
167 assert_equal 1, issues.size
168 assert_equal 2, issues.first.id
168 assert_equal 2, issues.first.id
169 end
169 end
170
170
171 def test_operator_is_on_integer_custom_field
171 def test_operator_is_on_integer_custom_field
172 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
172 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
173 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
173 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
174 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
174 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
175 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
175 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
176
176
177 query = Query.new(:name => '_')
177 query = IssueQuery.new(:name => '_')
178 query.add_filter("cf_#{f.id}", '=', ['12'])
178 query.add_filter("cf_#{f.id}", '=', ['12'])
179 issues = find_issues_with_query(query)
179 issues = find_issues_with_query(query)
180 assert_equal 1, issues.size
180 assert_equal 1, issues.size
181 assert_equal 2, issues.first.id
181 assert_equal 2, issues.first.id
182 end
182 end
183
183
184 def test_operator_is_on_integer_custom_field_should_accept_negative_value
184 def test_operator_is_on_integer_custom_field_should_accept_negative_value
185 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
185 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
186 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
186 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
187 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
187 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
188 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
188 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
189
189
190 query = Query.new(:name => '_')
190 query = IssueQuery.new(:name => '_')
191 query.add_filter("cf_#{f.id}", '=', ['-12'])
191 query.add_filter("cf_#{f.id}", '=', ['-12'])
192 assert query.valid?
192 assert query.valid?
193 issues = find_issues_with_query(query)
193 issues = find_issues_with_query(query)
194 assert_equal 1, issues.size
194 assert_equal 1, issues.size
195 assert_equal 2, issues.first.id
195 assert_equal 2, issues.first.id
196 end
196 end
197
197
198 def test_operator_is_on_float_custom_field
198 def test_operator_is_on_float_custom_field
199 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
199 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
200 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
200 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
201 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
201 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
202 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
202 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
203
203
204 query = Query.new(:name => '_')
204 query = IssueQuery.new(:name => '_')
205 query.add_filter("cf_#{f.id}", '=', ['12.7'])
205 query.add_filter("cf_#{f.id}", '=', ['12.7'])
206 issues = find_issues_with_query(query)
206 issues = find_issues_with_query(query)
207 assert_equal 1, issues.size
207 assert_equal 1, issues.size
208 assert_equal 2, issues.first.id
208 assert_equal 2, issues.first.id
209 end
209 end
210
210
211 def test_operator_is_on_float_custom_field_should_accept_negative_value
211 def test_operator_is_on_float_custom_field_should_accept_negative_value
212 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
212 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
213 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
213 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
214 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
214 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
215 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
215 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
216
216
217 query = Query.new(:name => '_')
217 query = IssueQuery.new(:name => '_')
218 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
218 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
219 assert query.valid?
219 assert query.valid?
220 issues = find_issues_with_query(query)
220 issues = find_issues_with_query(query)
221 assert_equal 1, issues.size
221 assert_equal 1, issues.size
222 assert_equal 2, issues.first.id
222 assert_equal 2, issues.first.id
223 end
223 end
224
224
225 def test_operator_is_on_multi_list_custom_field
225 def test_operator_is_on_multi_list_custom_field
226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
227 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
227 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
228 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
228 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
229 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
229 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
230 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
230 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
231
231
232 query = Query.new(:name => '_')
232 query = IssueQuery.new(:name => '_')
233 query.add_filter("cf_#{f.id}", '=', ['value1'])
233 query.add_filter("cf_#{f.id}", '=', ['value1'])
234 issues = find_issues_with_query(query)
234 issues = find_issues_with_query(query)
235 assert_equal [1, 3], issues.map(&:id).sort
235 assert_equal [1, 3], issues.map(&:id).sort
236
236
237 query = Query.new(:name => '_')
237 query = IssueQuery.new(:name => '_')
238 query.add_filter("cf_#{f.id}", '=', ['value2'])
238 query.add_filter("cf_#{f.id}", '=', ['value2'])
239 issues = find_issues_with_query(query)
239 issues = find_issues_with_query(query)
240 assert_equal [1], issues.map(&:id).sort
240 assert_equal [1], issues.map(&:id).sort
241 end
241 end
242
242
243 def test_operator_is_not_on_multi_list_custom_field
243 def test_operator_is_not_on_multi_list_custom_field
244 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
244 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
245 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
245 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
246 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
246 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
247 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
247 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
248 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
248 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
249
249
250 query = Query.new(:name => '_')
250 query = IssueQuery.new(:name => '_')
251 query.add_filter("cf_#{f.id}", '!', ['value1'])
251 query.add_filter("cf_#{f.id}", '!', ['value1'])
252 issues = find_issues_with_query(query)
252 issues = find_issues_with_query(query)
253 assert !issues.map(&:id).include?(1)
253 assert !issues.map(&:id).include?(1)
254 assert !issues.map(&:id).include?(3)
254 assert !issues.map(&:id).include?(3)
255
255
256 query = Query.new(:name => '_')
256 query = IssueQuery.new(:name => '_')
257 query.add_filter("cf_#{f.id}", '!', ['value2'])
257 query.add_filter("cf_#{f.id}", '!', ['value2'])
258 issues = find_issues_with_query(query)
258 issues = find_issues_with_query(query)
259 assert !issues.map(&:id).include?(1)
259 assert !issues.map(&:id).include?(1)
260 assert issues.map(&:id).include?(3)
260 assert issues.map(&:id).include?(3)
261 end
261 end
262
262
263 def test_operator_is_on_is_private_field
263 def test_operator_is_on_is_private_field
264 # is_private filter only available for those who can set issues private
264 # is_private filter only available for those who can set issues private
265 User.current = User.find(2)
265 User.current = User.find(2)
266
266
267 query = Query.new(:name => '_')
267 query = IssueQuery.new(:name => '_')
268 assert query.available_filters.key?('is_private')
268 assert query.available_filters.key?('is_private')
269
269
270 query.add_filter("is_private", '=', ['1'])
270 query.add_filter("is_private", '=', ['1'])
271 issues = find_issues_with_query(query)
271 issues = find_issues_with_query(query)
272 assert issues.any?
272 assert issues.any?
273 assert_nil issues.detect {|issue| !issue.is_private?}
273 assert_nil issues.detect {|issue| !issue.is_private?}
274 ensure
274 ensure
275 User.current = nil
275 User.current = nil
276 end
276 end
277
277
278 def test_operator_is_not_on_is_private_field
278 def test_operator_is_not_on_is_private_field
279 # is_private filter only available for those who can set issues private
279 # is_private filter only available for those who can set issues private
280 User.current = User.find(2)
280 User.current = User.find(2)
281
281
282 query = Query.new(:name => '_')
282 query = IssueQuery.new(:name => '_')
283 assert query.available_filters.key?('is_private')
283 assert query.available_filters.key?('is_private')
284
284
285 query.add_filter("is_private", '!', ['1'])
285 query.add_filter("is_private", '!', ['1'])
286 issues = find_issues_with_query(query)
286 issues = find_issues_with_query(query)
287 assert issues.any?
287 assert issues.any?
288 assert_nil issues.detect {|issue| issue.is_private?}
288 assert_nil issues.detect {|issue| issue.is_private?}
289 ensure
289 ensure
290 User.current = nil
290 User.current = nil
291 end
291 end
292
292
293 def test_operator_greater_than
293 def test_operator_greater_than
294 query = Query.new(:project => Project.find(1), :name => '_')
294 query = IssueQuery.new(:project => Project.find(1), :name => '_')
295 query.add_filter('done_ratio', '>=', ['40'])
295 query.add_filter('done_ratio', '>=', ['40'])
296 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
296 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
297 find_issues_with_query(query)
297 find_issues_with_query(query)
298 end
298 end
299
299
300 def test_operator_greater_than_a_float
300 def test_operator_greater_than_a_float
301 query = Query.new(:project => Project.find(1), :name => '_')
301 query = IssueQuery.new(:project => Project.find(1), :name => '_')
302 query.add_filter('estimated_hours', '>=', ['40.5'])
302 query.add_filter('estimated_hours', '>=', ['40.5'])
303 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
303 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
304 find_issues_with_query(query)
304 find_issues_with_query(query)
305 end
305 end
306
306
307 def test_operator_greater_than_on_int_custom_field
307 def test_operator_greater_than_on_int_custom_field
308 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
308 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
309 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
309 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
310 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
310 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
312
312
313 query = Query.new(:project => Project.find(1), :name => '_')
313 query = IssueQuery.new(:project => Project.find(1), :name => '_')
314 query.add_filter("cf_#{f.id}", '>=', ['8'])
314 query.add_filter("cf_#{f.id}", '>=', ['8'])
315 issues = find_issues_with_query(query)
315 issues = find_issues_with_query(query)
316 assert_equal 1, issues.size
316 assert_equal 1, issues.size
317 assert_equal 2, issues.first.id
317 assert_equal 2, issues.first.id
318 end
318 end
319
319
320 def test_operator_lesser_than
320 def test_operator_lesser_than
321 query = Query.new(:project => Project.find(1), :name => '_')
321 query = IssueQuery.new(:project => Project.find(1), :name => '_')
322 query.add_filter('done_ratio', '<=', ['30'])
322 query.add_filter('done_ratio', '<=', ['30'])
323 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
323 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
324 find_issues_with_query(query)
324 find_issues_with_query(query)
325 end
325 end
326
326
327 def test_operator_lesser_than_on_custom_field
327 def test_operator_lesser_than_on_custom_field
328 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
328 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
329 query = Query.new(:project => Project.find(1), :name => '_')
329 query = IssueQuery.new(:project => Project.find(1), :name => '_')
330 query.add_filter("cf_#{f.id}", '<=', ['30'])
330 query.add_filter("cf_#{f.id}", '<=', ['30'])
331 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
331 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
332 find_issues_with_query(query)
332 find_issues_with_query(query)
333 end
333 end
334
334
335 def test_operator_between
335 def test_operator_between
336 query = Query.new(:project => Project.find(1), :name => '_')
336 query = IssueQuery.new(:project => Project.find(1), :name => '_')
337 query.add_filter('done_ratio', '><', ['30', '40'])
337 query.add_filter('done_ratio', '><', ['30', '40'])
338 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
338 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
339 find_issues_with_query(query)
339 find_issues_with_query(query)
340 end
340 end
341
341
342 def test_operator_between_on_custom_field
342 def test_operator_between_on_custom_field
343 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
343 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
344 query = Query.new(:project => Project.find(1), :name => '_')
344 query = IssueQuery.new(:project => Project.find(1), :name => '_')
345 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
345 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
346 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
346 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
347 find_issues_with_query(query)
347 find_issues_with_query(query)
348 end
348 end
349
349
350 def test_date_filter_should_not_accept_non_date_values
350 def test_date_filter_should_not_accept_non_date_values
351 query = Query.new(:name => '_')
351 query = IssueQuery.new(:name => '_')
352 query.add_filter('created_on', '=', ['a'])
352 query.add_filter('created_on', '=', ['a'])
353
353
354 assert query.has_filter?('created_on')
354 assert query.has_filter?('created_on')
355 assert !query.valid?
355 assert !query.valid?
356 end
356 end
357
357
358 def test_date_filter_should_not_accept_invalid_date_values
358 def test_date_filter_should_not_accept_invalid_date_values
359 query = Query.new(:name => '_')
359 query = IssueQuery.new(:name => '_')
360 query.add_filter('created_on', '=', ['2011-01-34'])
360 query.add_filter('created_on', '=', ['2011-01-34'])
361
361
362 assert query.has_filter?('created_on')
362 assert query.has_filter?('created_on')
363 assert !query.valid?
363 assert !query.valid?
364 end
364 end
365
365
366 def test_relative_date_filter_should_not_accept_non_integer_values
366 def test_relative_date_filter_should_not_accept_non_integer_values
367 query = Query.new(:name => '_')
367 query = IssueQuery.new(:name => '_')
368 query.add_filter('created_on', '>t-', ['a'])
368 query.add_filter('created_on', '>t-', ['a'])
369
369
370 assert query.has_filter?('created_on')
370 assert query.has_filter?('created_on')
371 assert !query.valid?
371 assert !query.valid?
372 end
372 end
373
373
374 def test_operator_date_equals
374 def test_operator_date_equals
375 query = Query.new(:name => '_')
375 query = IssueQuery.new(:name => '_')
376 query.add_filter('due_date', '=', ['2011-07-10'])
376 query.add_filter('due_date', '=', ['2011-07-10'])
377 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
377 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
378 find_issues_with_query(query)
378 find_issues_with_query(query)
379 end
379 end
380
380
381 def test_operator_date_lesser_than
381 def test_operator_date_lesser_than
382 query = Query.new(:name => '_')
382 query = IssueQuery.new(:name => '_')
383 query.add_filter('due_date', '<=', ['2011-07-10'])
383 query.add_filter('due_date', '<=', ['2011-07-10'])
384 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
384 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
385 find_issues_with_query(query)
385 find_issues_with_query(query)
386 end
386 end
387
387
388 def test_operator_date_greater_than
388 def test_operator_date_greater_than
389 query = Query.new(:name => '_')
389 query = IssueQuery.new(:name => '_')
390 query.add_filter('due_date', '>=', ['2011-07-10'])
390 query.add_filter('due_date', '>=', ['2011-07-10'])
391 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
391 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
392 find_issues_with_query(query)
392 find_issues_with_query(query)
393 end
393 end
394
394
395 def test_operator_date_between
395 def test_operator_date_between
396 query = Query.new(:name => '_')
396 query = IssueQuery.new(:name => '_')
397 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
397 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
398 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
398 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
399 find_issues_with_query(query)
399 find_issues_with_query(query)
400 end
400 end
401
401
402 def test_operator_in_more_than
402 def test_operator_in_more_than
403 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
403 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
404 query = Query.new(:project => Project.find(1), :name => '_')
404 query = IssueQuery.new(:project => Project.find(1), :name => '_')
405 query.add_filter('due_date', '>t+', ['15'])
405 query.add_filter('due_date', '>t+', ['15'])
406 issues = find_issues_with_query(query)
406 issues = find_issues_with_query(query)
407 assert !issues.empty?
407 assert !issues.empty?
408 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
408 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
409 end
409 end
410
410
411 def test_operator_in_less_than
411 def test_operator_in_less_than
412 query = Query.new(:project => Project.find(1), :name => '_')
412 query = IssueQuery.new(:project => Project.find(1), :name => '_')
413 query.add_filter('due_date', '<t+', ['15'])
413 query.add_filter('due_date', '<t+', ['15'])
414 issues = find_issues_with_query(query)
414 issues = find_issues_with_query(query)
415 assert !issues.empty?
415 assert !issues.empty?
416 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
416 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
417 end
417 end
418
418
419 def test_operator_in_the_next_days
419 def test_operator_in_the_next_days
420 query = Query.new(:project => Project.find(1), :name => '_')
420 query = IssueQuery.new(:project => Project.find(1), :name => '_')
421 query.add_filter('due_date', '><t+', ['15'])
421 query.add_filter('due_date', '><t+', ['15'])
422 issues = find_issues_with_query(query)
422 issues = find_issues_with_query(query)
423 assert !issues.empty?
423 assert !issues.empty?
424 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
424 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
425 end
425 end
426
426
427 def test_operator_less_than_ago
427 def test_operator_less_than_ago
428 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
428 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
429 query = Query.new(:project => Project.find(1), :name => '_')
429 query = IssueQuery.new(:project => Project.find(1), :name => '_')
430 query.add_filter('due_date', '>t-', ['3'])
430 query.add_filter('due_date', '>t-', ['3'])
431 issues = find_issues_with_query(query)
431 issues = find_issues_with_query(query)
432 assert !issues.empty?
432 assert !issues.empty?
433 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
433 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
434 end
434 end
435
435
436 def test_operator_in_the_past_days
436 def test_operator_in_the_past_days
437 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
437 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
438 query = Query.new(:project => Project.find(1), :name => '_')
438 query = IssueQuery.new(:project => Project.find(1), :name => '_')
439 query.add_filter('due_date', '><t-', ['3'])
439 query.add_filter('due_date', '><t-', ['3'])
440 issues = find_issues_with_query(query)
440 issues = find_issues_with_query(query)
441 assert !issues.empty?
441 assert !issues.empty?
442 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
442 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
443 end
443 end
444
444
445 def test_operator_more_than_ago
445 def test_operator_more_than_ago
446 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
446 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
447 query = Query.new(:project => Project.find(1), :name => '_')
447 query = IssueQuery.new(:project => Project.find(1), :name => '_')
448 query.add_filter('due_date', '<t-', ['10'])
448 query.add_filter('due_date', '<t-', ['10'])
449 assert query.statement.include?("#{Issue.table_name}.due_date <=")
449 assert query.statement.include?("#{Issue.table_name}.due_date <=")
450 issues = find_issues_with_query(query)
450 issues = find_issues_with_query(query)
451 assert !issues.empty?
451 assert !issues.empty?
452 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
452 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
453 end
453 end
454
454
455 def test_operator_in
455 def test_operator_in
456 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
456 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
457 query = Query.new(:project => Project.find(1), :name => '_')
457 query = IssueQuery.new(:project => Project.find(1), :name => '_')
458 query.add_filter('due_date', 't+', ['2'])
458 query.add_filter('due_date', 't+', ['2'])
459 issues = find_issues_with_query(query)
459 issues = find_issues_with_query(query)
460 assert !issues.empty?
460 assert !issues.empty?
461 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
461 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
462 end
462 end
463
463
464 def test_operator_ago
464 def test_operator_ago
465 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
465 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
466 query = Query.new(:project => Project.find(1), :name => '_')
466 query = IssueQuery.new(:project => Project.find(1), :name => '_')
467 query.add_filter('due_date', 't-', ['3'])
467 query.add_filter('due_date', 't-', ['3'])
468 issues = find_issues_with_query(query)
468 issues = find_issues_with_query(query)
469 assert !issues.empty?
469 assert !issues.empty?
470 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
470 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
471 end
471 end
472
472
473 def test_operator_today
473 def test_operator_today
474 query = Query.new(:project => Project.find(1), :name => '_')
474 query = IssueQuery.new(:project => Project.find(1), :name => '_')
475 query.add_filter('due_date', 't', [''])
475 query.add_filter('due_date', 't', [''])
476 issues = find_issues_with_query(query)
476 issues = find_issues_with_query(query)
477 assert !issues.empty?
477 assert !issues.empty?
478 issues.each {|issue| assert_equal Date.today, issue.due_date}
478 issues.each {|issue| assert_equal Date.today, issue.due_date}
479 end
479 end
480
480
481 def test_operator_this_week_on_date
481 def test_operator_this_week_on_date
482 query = Query.new(:project => Project.find(1), :name => '_')
482 query = IssueQuery.new(:project => Project.find(1), :name => '_')
483 query.add_filter('due_date', 'w', [''])
483 query.add_filter('due_date', 'w', [''])
484 find_issues_with_query(query)
484 find_issues_with_query(query)
485 end
485 end
486
486
487 def test_operator_this_week_on_datetime
487 def test_operator_this_week_on_datetime
488 query = Query.new(:project => Project.find(1), :name => '_')
488 query = IssueQuery.new(:project => Project.find(1), :name => '_')
489 query.add_filter('created_on', 'w', [''])
489 query.add_filter('created_on', 'w', [''])
490 find_issues_with_query(query)
490 find_issues_with_query(query)
491 end
491 end
492
492
493 def test_operator_contains
493 def test_operator_contains
494 query = Query.new(:project => Project.find(1), :name => '_')
494 query = IssueQuery.new(:project => Project.find(1), :name => '_')
495 query.add_filter('subject', '~', ['uNable'])
495 query.add_filter('subject', '~', ['uNable'])
496 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
496 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
497 result = find_issues_with_query(query)
497 result = find_issues_with_query(query)
498 assert result.empty?
498 assert result.empty?
499 result.each {|issue| assert issue.subject.downcase.include?('unable') }
499 result.each {|issue| assert issue.subject.downcase.include?('unable') }
500 end
500 end
501
501
502 def test_range_for_this_week_with_week_starting_on_monday
502 def test_range_for_this_week_with_week_starting_on_monday
503 I18n.locale = :fr
503 I18n.locale = :fr
504 assert_equal '1', I18n.t(:general_first_day_of_week)
504 assert_equal '1', I18n.t(:general_first_day_of_week)
505
505
506 Date.stubs(:today).returns(Date.parse('2011-04-29'))
506 Date.stubs(:today).returns(Date.parse('2011-04-29'))
507
507
508 query = Query.new(:project => Project.find(1), :name => '_')
508 query = IssueQuery.new(:project => Project.find(1), :name => '_')
509 query.add_filter('due_date', 'w', [''])
509 query.add_filter('due_date', 'w', [''])
510 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
510 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
511 I18n.locale = :en
511 I18n.locale = :en
512 end
512 end
513
513
514 def test_range_for_this_week_with_week_starting_on_sunday
514 def test_range_for_this_week_with_week_starting_on_sunday
515 I18n.locale = :en
515 I18n.locale = :en
516 assert_equal '7', I18n.t(:general_first_day_of_week)
516 assert_equal '7', I18n.t(:general_first_day_of_week)
517
517
518 Date.stubs(:today).returns(Date.parse('2011-04-29'))
518 Date.stubs(:today).returns(Date.parse('2011-04-29'))
519
519
520 query = Query.new(:project => Project.find(1), :name => '_')
520 query = IssueQuery.new(:project => Project.find(1), :name => '_')
521 query.add_filter('due_date', 'w', [''])
521 query.add_filter('due_date', 'w', [''])
522 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
522 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
523 end
523 end
524
524
525 def test_operator_does_not_contains
525 def test_operator_does_not_contains
526 query = Query.new(:project => Project.find(1), :name => '_')
526 query = IssueQuery.new(:project => Project.find(1), :name => '_')
527 query.add_filter('subject', '!~', ['uNable'])
527 query.add_filter('subject', '!~', ['uNable'])
528 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
528 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
529 find_issues_with_query(query)
529 find_issues_with_query(query)
530 end
530 end
531
531
532 def test_filter_assigned_to_me
532 def test_filter_assigned_to_me
533 user = User.find(2)
533 user = User.find(2)
534 group = Group.find(10)
534 group = Group.find(10)
535 User.current = user
535 User.current = user
536 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
536 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
537 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
537 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
538 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
538 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
539 group.users << user
539 group.users << user
540
540
541 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
541 query = IssueQuery.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
542 result = query.issues
542 result = query.issues
543 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
543 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
544
544
545 assert result.include?(i1)
545 assert result.include?(i1)
546 assert result.include?(i2)
546 assert result.include?(i2)
547 assert !result.include?(i3)
547 assert !result.include?(i3)
548 end
548 end
549
549
550 def test_user_custom_field_filtered_on_me
550 def test_user_custom_field_filtered_on_me
551 User.current = User.find(2)
551 User.current = User.find(2)
552 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
552 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
553 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
553 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
554 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
554 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
555
555
556 query = Query.new(:name => '_', :project => Project.find(1))
556 query = IssueQuery.new(:name => '_', :project => Project.find(1))
557 filter = query.available_filters["cf_#{cf.id}"]
557 filter = query.available_filters["cf_#{cf.id}"]
558 assert_not_nil filter
558 assert_not_nil filter
559 assert_include 'me', filter[:values].map{|v| v[1]}
559 assert_include 'me', filter[:values].map{|v| v[1]}
560
560
561 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
561 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
562 result = query.issues
562 result = query.issues
563 assert_equal 1, result.size
563 assert_equal 1, result.size
564 assert_equal issue1, result.first
564 assert_equal issue1, result.first
565 end
565 end
566
566
567 def test_filter_my_projects
567 def test_filter_my_projects
568 User.current = User.find(2)
568 User.current = User.find(2)
569 query = Query.new(:name => '_')
569 query = IssueQuery.new(:name => '_')
570 filter = query.available_filters['project_id']
570 filter = query.available_filters['project_id']
571 assert_not_nil filter
571 assert_not_nil filter
572 assert_include 'mine', filter[:values].map{|v| v[1]}
572 assert_include 'mine', filter[:values].map{|v| v[1]}
573
573
574 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
574 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
575 result = query.issues
575 result = query.issues
576 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
576 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
577 end
577 end
578
578
579 def test_filter_watched_issues
579 def test_filter_watched_issues
580 User.current = User.find(1)
580 User.current = User.find(1)
581 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
581 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
582 result = find_issues_with_query(query)
582 result = find_issues_with_query(query)
583 assert_not_nil result
583 assert_not_nil result
584 assert !result.empty?
584 assert !result.empty?
585 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
585 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
586 User.current = nil
586 User.current = nil
587 end
587 end
588
588
589 def test_filter_unwatched_issues
589 def test_filter_unwatched_issues
590 User.current = User.find(1)
590 User.current = User.find(1)
591 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
591 query = IssueQuery.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
592 result = find_issues_with_query(query)
592 result = find_issues_with_query(query)
593 assert_not_nil result
593 assert_not_nil result
594 assert !result.empty?
594 assert !result.empty?
595 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
595 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
596 User.current = nil
596 User.current = nil
597 end
597 end
598
598
599 def test_filter_on_project_custom_field
599 def test_filter_on_project_custom_field
600 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
600 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
601 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
601 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
602 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
602 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
603
603
604 query = Query.new(:name => '_')
604 query = IssueQuery.new(:name => '_')
605 filter_name = "project.cf_#{field.id}"
605 filter_name = "project.cf_#{field.id}"
606 assert_include filter_name, query.available_filters.keys
606 assert_include filter_name, query.available_filters.keys
607 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
607 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
608 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
608 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
609 end
609 end
610
610
611 def test_filter_on_author_custom_field
611 def test_filter_on_author_custom_field
612 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
612 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
613 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
613 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
614
614
615 query = Query.new(:name => '_')
615 query = IssueQuery.new(:name => '_')
616 filter_name = "author.cf_#{field.id}"
616 filter_name = "author.cf_#{field.id}"
617 assert_include filter_name, query.available_filters.keys
617 assert_include filter_name, query.available_filters.keys
618 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
618 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
619 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
619 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
620 end
620 end
621
621
622 def test_filter_on_assigned_to_custom_field
622 def test_filter_on_assigned_to_custom_field
623 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
623 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
624 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
624 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
625
625
626 query = Query.new(:name => '_')
626 query = IssueQuery.new(:name => '_')
627 filter_name = "assigned_to.cf_#{field.id}"
627 filter_name = "assigned_to.cf_#{field.id}"
628 assert_include filter_name, query.available_filters.keys
628 assert_include filter_name, query.available_filters.keys
629 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
629 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
630 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
630 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
631 end
631 end
632
632
633 def test_filter_on_fixed_version_custom_field
633 def test_filter_on_fixed_version_custom_field
634 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
634 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
635 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
635 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
636
636
637 query = Query.new(:name => '_')
637 query = IssueQuery.new(:name => '_')
638 filter_name = "fixed_version.cf_#{field.id}"
638 filter_name = "fixed_version.cf_#{field.id}"
639 assert_include filter_name, query.available_filters.keys
639 assert_include filter_name, query.available_filters.keys
640 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
640 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
641 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
641 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
642 end
642 end
643
643
644 def test_filter_on_relations_with_a_specific_issue
644 def test_filter_on_relations_with_a_specific_issue
645 IssueRelation.delete_all
645 IssueRelation.delete_all
646 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
646 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
647 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
647 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
648
648
649 query = Query.new(:name => '_')
649 query = IssueQuery.new(:name => '_')
650 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
650 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
651 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
651 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
652
652
653 query = Query.new(:name => '_')
653 query = IssueQuery.new(:name => '_')
654 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
654 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
655 assert_equal [1], find_issues_with_query(query).map(&:id).sort
655 assert_equal [1], find_issues_with_query(query).map(&:id).sort
656 end
656 end
657
657
658 def test_filter_on_relations_with_any_issues_in_a_project
658 def test_filter_on_relations_with_any_issues_in_a_project
659 IssueRelation.delete_all
659 IssueRelation.delete_all
660 with_settings :cross_project_issue_relations => '1' do
660 with_settings :cross_project_issue_relations => '1' do
661 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
661 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
662 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
662 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
663 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
663 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
664 end
664 end
665
665
666 query = Query.new(:name => '_')
666 query = IssueQuery.new(:name => '_')
667 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
667 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
668 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
668 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
669
669
670 query = Query.new(:name => '_')
670 query = IssueQuery.new(:name => '_')
671 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
671 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
672 assert_equal [1], find_issues_with_query(query).map(&:id).sort
672 assert_equal [1], find_issues_with_query(query).map(&:id).sort
673
673
674 query = Query.new(:name => '_')
674 query = IssueQuery.new(:name => '_')
675 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
675 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
676 assert_equal [], find_issues_with_query(query).map(&:id).sort
676 assert_equal [], find_issues_with_query(query).map(&:id).sort
677 end
677 end
678
678
679 def test_filter_on_relations_with_any_issues_not_in_a_project
679 def test_filter_on_relations_with_any_issues_not_in_a_project
680 IssueRelation.delete_all
680 IssueRelation.delete_all
681 with_settings :cross_project_issue_relations => '1' do
681 with_settings :cross_project_issue_relations => '1' do
682 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
682 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
683 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
683 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
684 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
684 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
685 end
685 end
686
686
687 query = Query.new(:name => '_')
687 query = IssueQuery.new(:name => '_')
688 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
688 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
689 assert_equal [1], find_issues_with_query(query).map(&:id).sort
689 assert_equal [1], find_issues_with_query(query).map(&:id).sort
690 end
690 end
691
691
692 def test_filter_on_relations_with_no_issues_in_a_project
692 def test_filter_on_relations_with_no_issues_in_a_project
693 IssueRelation.delete_all
693 IssueRelation.delete_all
694 with_settings :cross_project_issue_relations => '1' do
694 with_settings :cross_project_issue_relations => '1' do
695 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
695 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
696 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
696 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
697 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
697 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
698 end
698 end
699
699
700 query = Query.new(:name => '_')
700 query = IssueQuery.new(:name => '_')
701 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
701 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
702 ids = find_issues_with_query(query).map(&:id).sort
702 ids = find_issues_with_query(query).map(&:id).sort
703 assert_include 2, ids
703 assert_include 2, ids
704 assert_not_include 1, ids
704 assert_not_include 1, ids
705 assert_not_include 3, ids
705 assert_not_include 3, ids
706 end
706 end
707
707
708 def test_filter_on_relations_with_no_issues
708 def test_filter_on_relations_with_no_issues
709 IssueRelation.delete_all
709 IssueRelation.delete_all
710 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
710 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
711 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
711 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
712
712
713 query = Query.new(:name => '_')
713 query = IssueQuery.new(:name => '_')
714 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
714 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
715 ids = find_issues_with_query(query).map(&:id)
715 ids = find_issues_with_query(query).map(&:id)
716 assert_equal [], ids & [1, 2, 3]
716 assert_equal [], ids & [1, 2, 3]
717 assert_include 4, ids
717 assert_include 4, ids
718 end
718 end
719
719
720 def test_filter_on_relations_with_any_issues
720 def test_filter_on_relations_with_any_issues
721 IssueRelation.delete_all
721 IssueRelation.delete_all
722 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
722 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
723 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
723 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
724
724
725 query = Query.new(:name => '_')
725 query = IssueQuery.new(:name => '_')
726 query.filters = {"relates" => {:operator => '*', :values => ['']}}
726 query.filters = {"relates" => {:operator => '*', :values => ['']}}
727 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
727 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
728 end
728 end
729
729
730 def test_statement_should_be_nil_with_no_filters
730 def test_statement_should_be_nil_with_no_filters
731 q = Query.new(:name => '_')
731 q = IssueQuery.new(:name => '_')
732 q.filters = {}
732 q.filters = {}
733
733
734 assert q.valid?
734 assert q.valid?
735 assert_nil q.statement
735 assert_nil q.statement
736 end
736 end
737
737
738 def test_default_columns
738 def test_default_columns
739 q = Query.new
739 q = IssueQuery.new
740 assert q.columns.any?
740 assert q.columns.any?
741 assert q.inline_columns.any?
741 assert q.inline_columns.any?
742 assert q.block_columns.empty?
742 assert q.block_columns.empty?
743 end
743 end
744
744
745 def test_set_column_names
745 def test_set_column_names
746 q = Query.new
746 q = IssueQuery.new
747 q.column_names = ['tracker', :subject, '', 'unknonw_column']
747 q.column_names = ['tracker', :subject, '', 'unknonw_column']
748 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
748 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
749 c = q.columns.first
749 c = q.columns.first
750 assert q.has_column?(c)
750 assert q.has_column?(c)
751 end
751 end
752
752
753 def test_inline_and_block_columns
753 def test_inline_and_block_columns
754 q = Query.new
754 q = IssueQuery.new
755 q.column_names = ['subject', 'description', 'tracker']
755 q.column_names = ['subject', 'description', 'tracker']
756
756
757 assert_equal [:subject, :tracker], q.inline_columns.map(&:name)
757 assert_equal [:subject, :tracker], q.inline_columns.map(&:name)
758 assert_equal [:description], q.block_columns.map(&:name)
758 assert_equal [:description], q.block_columns.map(&:name)
759 end
759 end
760
760
761 def test_custom_field_columns_should_be_inline
761 def test_custom_field_columns_should_be_inline
762 q = Query.new
762 q = IssueQuery.new
763 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
763 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
764 assert columns.any?
764 assert columns.any?
765 assert_nil columns.detect {|column| !column.inline?}
765 assert_nil columns.detect {|column| !column.inline?}
766 end
766 end
767
767
768 def test_query_should_preload_spent_hours
768 def test_query_should_preload_spent_hours
769 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
769 q = IssueQuery.new(:name => '_', :column_names => [:subject, :spent_hours])
770 assert q.has_column?(:spent_hours)
770 assert q.has_column?(:spent_hours)
771 issues = q.issues
771 issues = q.issues
772 assert_not_nil issues.first.instance_variable_get("@spent_hours")
772 assert_not_nil issues.first.instance_variable_get("@spent_hours")
773 end
773 end
774
774
775 def test_groupable_columns_should_include_custom_fields
775 def test_groupable_columns_should_include_custom_fields
776 q = Query.new
776 q = IssueQuery.new
777 column = q.groupable_columns.detect {|c| c.name == :cf_1}
777 column = q.groupable_columns.detect {|c| c.name == :cf_1}
778 assert_not_nil column
778 assert_not_nil column
779 assert_kind_of QueryCustomFieldColumn, column
779 assert_kind_of QueryCustomFieldColumn, column
780 end
780 end
781
781
782 def test_groupable_columns_should_not_include_multi_custom_fields
782 def test_groupable_columns_should_not_include_multi_custom_fields
783 field = CustomField.find(1)
783 field = CustomField.find(1)
784 field.update_attribute :multiple, true
784 field.update_attribute :multiple, true
785
785
786 q = Query.new
786 q = IssueQuery.new
787 column = q.groupable_columns.detect {|c| c.name == :cf_1}
787 column = q.groupable_columns.detect {|c| c.name == :cf_1}
788 assert_nil column
788 assert_nil column
789 end
789 end
790
790
791 def test_groupable_columns_should_include_user_custom_fields
791 def test_groupable_columns_should_include_user_custom_fields
792 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
792 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
793
793
794 q = Query.new
794 q = IssueQuery.new
795 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
795 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
796 end
796 end
797
797
798 def test_groupable_columns_should_include_version_custom_fields
798 def test_groupable_columns_should_include_version_custom_fields
799 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
799 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
800
800
801 q = Query.new
801 q = IssueQuery.new
802 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
802 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
803 end
803 end
804
804
805 def test_grouped_with_valid_column
805 def test_grouped_with_valid_column
806 q = Query.new(:group_by => 'status')
806 q = IssueQuery.new(:group_by => 'status')
807 assert q.grouped?
807 assert q.grouped?
808 assert_not_nil q.group_by_column
808 assert_not_nil q.group_by_column
809 assert_equal :status, q.group_by_column.name
809 assert_equal :status, q.group_by_column.name
810 assert_not_nil q.group_by_statement
810 assert_not_nil q.group_by_statement
811 assert_equal 'status', q.group_by_statement
811 assert_equal 'status', q.group_by_statement
812 end
812 end
813
813
814 def test_grouped_with_invalid_column
814 def test_grouped_with_invalid_column
815 q = Query.new(:group_by => 'foo')
815 q = IssueQuery.new(:group_by => 'foo')
816 assert !q.grouped?
816 assert !q.grouped?
817 assert_nil q.group_by_column
817 assert_nil q.group_by_column
818 assert_nil q.group_by_statement
818 assert_nil q.group_by_statement
819 end
819 end
820
820
821 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
821 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
822 with_settings :user_format => 'lastname_coma_firstname' do
822 with_settings :user_format => 'lastname_coma_firstname' do
823 q = Query.new
823 q = IssueQuery.new
824 assert q.sortable_columns.has_key?('assigned_to')
824 assert q.sortable_columns.has_key?('assigned_to')
825 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
825 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
826 end
826 end
827 end
827 end
828
828
829 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
829 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
830 with_settings :user_format => 'lastname_coma_firstname' do
830 with_settings :user_format => 'lastname_coma_firstname' do
831 q = Query.new
831 q = IssueQuery.new
832 assert q.sortable_columns.has_key?('author')
832 assert q.sortable_columns.has_key?('author')
833 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
833 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
834 end
834 end
835 end
835 end
836
836
837 def test_sortable_columns_should_include_custom_field
837 def test_sortable_columns_should_include_custom_field
838 q = Query.new
838 q = IssueQuery.new
839 assert q.sortable_columns['cf_1']
839 assert q.sortable_columns['cf_1']
840 end
840 end
841
841
842 def test_sortable_columns_should_not_include_multi_custom_field
842 def test_sortable_columns_should_not_include_multi_custom_field
843 field = CustomField.find(1)
843 field = CustomField.find(1)
844 field.update_attribute :multiple, true
844 field.update_attribute :multiple, true
845
845
846 q = Query.new
846 q = IssueQuery.new
847 assert !q.sortable_columns['cf_1']
847 assert !q.sortable_columns['cf_1']
848 end
848 end
849
849
850 def test_default_sort
850 def test_default_sort
851 q = Query.new
851 q = IssueQuery.new
852 assert_equal [], q.sort_criteria
852 assert_equal [], q.sort_criteria
853 end
853 end
854
854
855 def test_set_sort_criteria_with_hash
855 def test_set_sort_criteria_with_hash
856 q = Query.new
856 q = IssueQuery.new
857 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
857 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
858 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
858 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
859 end
859 end
860
860
861 def test_set_sort_criteria_with_array
861 def test_set_sort_criteria_with_array
862 q = Query.new
862 q = IssueQuery.new
863 q.sort_criteria = [['priority', 'desc'], 'tracker']
863 q.sort_criteria = [['priority', 'desc'], 'tracker']
864 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
864 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
865 end
865 end
866
866
867 def test_create_query_with_sort
867 def test_create_query_with_sort
868 q = Query.new(:name => 'Sorted')
868 q = IssueQuery.new(:name => 'Sorted')
869 q.sort_criteria = [['priority', 'desc'], 'tracker']
869 q.sort_criteria = [['priority', 'desc'], 'tracker']
870 assert q.save
870 assert q.save
871 q.reload
871 q.reload
872 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
872 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
873 end
873 end
874
874
875 def test_sort_by_string_custom_field_asc
875 def test_sort_by_string_custom_field_asc
876 q = Query.new
876 q = IssueQuery.new
877 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
877 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
878 assert c
878 assert c
879 assert c.sortable
879 assert c.sortable
880 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
880 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
881 q.statement
881 q.statement
882 ).order("#{c.sortable} ASC").all
882 ).order("#{c.sortable} ASC").all
883 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
883 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
884 assert !values.empty?
884 assert !values.empty?
885 assert_equal values.sort, values
885 assert_equal values.sort, values
886 end
886 end
887
887
888 def test_sort_by_string_custom_field_desc
888 def test_sort_by_string_custom_field_desc
889 q = Query.new
889 q = IssueQuery.new
890 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
890 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
891 assert c
891 assert c
892 assert c.sortable
892 assert c.sortable
893 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
893 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
894 q.statement
894 q.statement
895 ).order("#{c.sortable} DESC").all
895 ).order("#{c.sortable} DESC").all
896 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
896 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
897 assert !values.empty?
897 assert !values.empty?
898 assert_equal values.sort.reverse, values
898 assert_equal values.sort.reverse, values
899 end
899 end
900
900
901 def test_sort_by_float_custom_field_asc
901 def test_sort_by_float_custom_field_asc
902 q = Query.new
902 q = IssueQuery.new
903 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
903 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
904 assert c
904 assert c
905 assert c.sortable
905 assert c.sortable
906 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
906 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
907 q.statement
907 q.statement
908 ).order("#{c.sortable} ASC").all
908 ).order("#{c.sortable} ASC").all
909 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
909 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
910 assert !values.empty?
910 assert !values.empty?
911 assert_equal values.sort, values
911 assert_equal values.sort, values
912 end
912 end
913
913
914 def test_invalid_query_should_raise_query_statement_invalid_error
914 def test_invalid_query_should_raise_query_statement_invalid_error
915 q = Query.new
915 q = IssueQuery.new
916 assert_raise Query::StatementInvalid do
916 assert_raise Query::StatementInvalid do
917 q.issues(:conditions => "foo = 1")
917 q.issues(:conditions => "foo = 1")
918 end
918 end
919 end
919 end
920
920
921 def test_issue_count
921 def test_issue_count
922 q = Query.new(:name => '_')
922 q = IssueQuery.new(:name => '_')
923 issue_count = q.issue_count
923 issue_count = q.issue_count
924 assert_equal q.issues.size, issue_count
924 assert_equal q.issues.size, issue_count
925 end
925 end
926
926
927 def test_issue_count_with_archived_issues
927 def test_issue_count_with_archived_issues
928 p = Project.generate! do |project|
928 p = Project.generate! do |project|
929 project.status = Project::STATUS_ARCHIVED
929 project.status = Project::STATUS_ARCHIVED
930 end
930 end
931 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
931 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
932 assert !i.visible?
932 assert !i.visible?
933
933
934 test_issue_count
934 test_issue_count
935 end
935 end
936
936
937 def test_issue_count_by_association_group
937 def test_issue_count_by_association_group
938 q = Query.new(:name => '_', :group_by => 'assigned_to')
938 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
939 count_by_group = q.issue_count_by_group
939 count_by_group = q.issue_count_by_group
940 assert_kind_of Hash, count_by_group
940 assert_kind_of Hash, count_by_group
941 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
941 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
942 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
942 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
943 assert count_by_group.has_key?(User.find(3))
943 assert count_by_group.has_key?(User.find(3))
944 end
944 end
945
945
946 def test_issue_count_by_list_custom_field_group
946 def test_issue_count_by_list_custom_field_group
947 q = Query.new(:name => '_', :group_by => 'cf_1')
947 q = IssueQuery.new(:name => '_', :group_by => 'cf_1')
948 count_by_group = q.issue_count_by_group
948 count_by_group = q.issue_count_by_group
949 assert_kind_of Hash, count_by_group
949 assert_kind_of Hash, count_by_group
950 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
950 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
951 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
951 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
952 assert count_by_group.has_key?('MySQL')
952 assert count_by_group.has_key?('MySQL')
953 end
953 end
954
954
955 def test_issue_count_by_date_custom_field_group
955 def test_issue_count_by_date_custom_field_group
956 q = Query.new(:name => '_', :group_by => 'cf_8')
956 q = IssueQuery.new(:name => '_', :group_by => 'cf_8')
957 count_by_group = q.issue_count_by_group
957 count_by_group = q.issue_count_by_group
958 assert_kind_of Hash, count_by_group
958 assert_kind_of Hash, count_by_group
959 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
959 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
960 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
960 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
961 end
961 end
962
962
963 def test_issue_count_with_nil_group_only
963 def test_issue_count_with_nil_group_only
964 Issue.update_all("assigned_to_id = NULL")
964 Issue.update_all("assigned_to_id = NULL")
965
965
966 q = Query.new(:name => '_', :group_by => 'assigned_to')
966 q = IssueQuery.new(:name => '_', :group_by => 'assigned_to')
967 count_by_group = q.issue_count_by_group
967 count_by_group = q.issue_count_by_group
968 assert_kind_of Hash, count_by_group
968 assert_kind_of Hash, count_by_group
969 assert_equal 1, count_by_group.keys.size
969 assert_equal 1, count_by_group.keys.size
970 assert_nil count_by_group.keys.first
970 assert_nil count_by_group.keys.first
971 end
971 end
972
972
973 def test_issue_ids
973 def test_issue_ids
974 q = Query.new(:name => '_')
974 q = IssueQuery.new(:name => '_')
975 order = "issues.subject, issues.id"
975 order = "issues.subject, issues.id"
976 issues = q.issues(:order => order)
976 issues = q.issues(:order => order)
977 assert_equal issues.map(&:id), q.issue_ids(:order => order)
977 assert_equal issues.map(&:id), q.issue_ids(:order => order)
978 end
978 end
979
979
980 def test_label_for
980 def test_label_for
981 set_language_if_valid 'en'
981 set_language_if_valid 'en'
982 q = Query.new
982 q = IssueQuery.new
983 assert_equal 'Assignee', q.label_for('assigned_to_id')
983 assert_equal 'Assignee', q.label_for('assigned_to_id')
984 end
984 end
985
985
986 def test_label_for_fr
986 def test_label_for_fr
987 set_language_if_valid 'fr'
987 set_language_if_valid 'fr'
988 q = Query.new
988 q = IssueQuery.new
989 s = "Assign\xc3\xa9 \xc3\xa0"
989 s = "Assign\xc3\xa9 \xc3\xa0"
990 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
990 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
991 assert_equal s, q.label_for('assigned_to_id')
991 assert_equal s, q.label_for('assigned_to_id')
992 end
992 end
993
993
994 def test_editable_by
994 def test_editable_by
995 admin = User.find(1)
995 admin = User.find(1)
996 manager = User.find(2)
996 manager = User.find(2)
997 developer = User.find(3)
997 developer = User.find(3)
998
998
999 # Public query on project 1
999 # Public query on project 1
1000 q = Query.find(1)
1000 q = IssueQuery.find(1)
1001 assert q.editable_by?(admin)
1001 assert q.editable_by?(admin)
1002 assert q.editable_by?(manager)
1002 assert q.editable_by?(manager)
1003 assert !q.editable_by?(developer)
1003 assert !q.editable_by?(developer)
1004
1004
1005 # Private query on project 1
1005 # Private query on project 1
1006 q = Query.find(2)
1006 q = IssueQuery.find(2)
1007 assert q.editable_by?(admin)
1007 assert q.editable_by?(admin)
1008 assert !q.editable_by?(manager)
1008 assert !q.editable_by?(manager)
1009 assert q.editable_by?(developer)
1009 assert q.editable_by?(developer)
1010
1010
1011 # Private query for all projects
1011 # Private query for all projects
1012 q = Query.find(3)
1012 q = IssueQuery.find(3)
1013 assert q.editable_by?(admin)
1013 assert q.editable_by?(admin)
1014 assert !q.editable_by?(manager)
1014 assert !q.editable_by?(manager)
1015 assert q.editable_by?(developer)
1015 assert q.editable_by?(developer)
1016
1016
1017 # Public query for all projects
1017 # Public query for all projects
1018 q = Query.find(4)
1018 q = IssueQuery.find(4)
1019 assert q.editable_by?(admin)
1019 assert q.editable_by?(admin)
1020 assert !q.editable_by?(manager)
1020 assert !q.editable_by?(manager)
1021 assert !q.editable_by?(developer)
1021 assert !q.editable_by?(developer)
1022 end
1022 end
1023
1023
1024 def test_visible_scope
1024 def test_visible_scope
1025 query_ids = Query.visible(User.anonymous).map(&:id)
1025 query_ids = IssueQuery.visible(User.anonymous).map(&:id)
1026
1026
1027 assert query_ids.include?(1), 'public query on public project was not visible'
1027 assert query_ids.include?(1), 'public query on public project was not visible'
1028 assert query_ids.include?(4), 'public query for all projects was not visible'
1028 assert query_ids.include?(4), 'public query for all projects was not visible'
1029 assert !query_ids.include?(2), 'private query on public project was visible'
1029 assert !query_ids.include?(2), 'private query on public project was visible'
1030 assert !query_ids.include?(3), 'private query for all projects was visible'
1030 assert !query_ids.include?(3), 'private query for all projects was visible'
1031 assert !query_ids.include?(7), 'public query on private project was visible'
1031 assert !query_ids.include?(7), 'public query on private project was visible'
1032 end
1032 end
1033
1033
1034 context "#available_filters" do
1034 context "#available_filters" do
1035 setup do
1035 setup do
1036 @query = Query.new(:name => "_")
1036 @query = IssueQuery.new(:name => "_")
1037 end
1037 end
1038
1038
1039 should "include users of visible projects in cross-project view" do
1039 should "include users of visible projects in cross-project view" do
1040 users = @query.available_filters["assigned_to_id"]
1040 users = @query.available_filters["assigned_to_id"]
1041 assert_not_nil users
1041 assert_not_nil users
1042 assert users[:values].map{|u|u[1]}.include?("3")
1042 assert users[:values].map{|u|u[1]}.include?("3")
1043 end
1043 end
1044
1044
1045 should "include users of subprojects" do
1045 should "include users of subprojects" do
1046 user1 = User.generate!
1046 user1 = User.generate!
1047 user2 = User.generate!
1047 user2 = User.generate!
1048 project = Project.find(1)
1048 project = Project.find(1)
1049 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1049 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1050 @query.project = project
1050 @query.project = project
1051
1051
1052 users = @query.available_filters["assigned_to_id"]
1052 users = @query.available_filters["assigned_to_id"]
1053 assert_not_nil users
1053 assert_not_nil users
1054 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1054 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1055 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1055 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1056 end
1056 end
1057
1057
1058 should "include visible projects in cross-project view" do
1058 should "include visible projects in cross-project view" do
1059 projects = @query.available_filters["project_id"]
1059 projects = @query.available_filters["project_id"]
1060 assert_not_nil projects
1060 assert_not_nil projects
1061 assert projects[:values].map{|u|u[1]}.include?("1")
1061 assert projects[:values].map{|u|u[1]}.include?("1")
1062 end
1062 end
1063
1063
1064 context "'member_of_group' filter" do
1064 context "'member_of_group' filter" do
1065 should "be present" do
1065 should "be present" do
1066 assert @query.available_filters.keys.include?("member_of_group")
1066 assert @query.available_filters.keys.include?("member_of_group")
1067 end
1067 end
1068
1068
1069 should "be an optional list" do
1069 should "be an optional list" do
1070 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
1070 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
1071 end
1071 end
1072
1072
1073 should "have a list of the groups as values" do
1073 should "have a list of the groups as values" do
1074 Group.destroy_all # No fixtures
1074 Group.destroy_all # No fixtures
1075 group1 = Group.generate!.reload
1075 group1 = Group.generate!.reload
1076 group2 = Group.generate!.reload
1076 group2 = Group.generate!.reload
1077
1077
1078 expected_group_list = [
1078 expected_group_list = [
1079 [group1.name, group1.id.to_s],
1079 [group1.name, group1.id.to_s],
1080 [group2.name, group2.id.to_s]
1080 [group2.name, group2.id.to_s]
1081 ]
1081 ]
1082 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
1082 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
1083 end
1083 end
1084
1084
1085 end
1085 end
1086
1086
1087 context "'assigned_to_role' filter" do
1087 context "'assigned_to_role' filter" do
1088 should "be present" do
1088 should "be present" do
1089 assert @query.available_filters.keys.include?("assigned_to_role")
1089 assert @query.available_filters.keys.include?("assigned_to_role")
1090 end
1090 end
1091
1091
1092 should "be an optional list" do
1092 should "be an optional list" do
1093 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
1093 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
1094 end
1094 end
1095
1095
1096 should "have a list of the Roles as values" do
1096 should "have a list of the Roles as values" do
1097 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1097 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1098 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1098 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1099 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1099 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1100 end
1100 end
1101
1101
1102 should "not include the built in Roles as values" do
1102 should "not include the built in Roles as values" do
1103 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1103 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1104 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1104 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1105 end
1105 end
1106
1106
1107 end
1107 end
1108
1108
1109 end
1109 end
1110
1110
1111 context "#statement" do
1111 context "#statement" do
1112 context "with 'member_of_group' filter" do
1112 context "with 'member_of_group' filter" do
1113 setup do
1113 setup do
1114 Group.destroy_all # No fixtures
1114 Group.destroy_all # No fixtures
1115 @user_in_group = User.generate!
1115 @user_in_group = User.generate!
1116 @second_user_in_group = User.generate!
1116 @second_user_in_group = User.generate!
1117 @user_in_group2 = User.generate!
1117 @user_in_group2 = User.generate!
1118 @user_not_in_group = User.generate!
1118 @user_not_in_group = User.generate!
1119
1119
1120 @group = Group.generate!.reload
1120 @group = Group.generate!.reload
1121 @group.users << @user_in_group
1121 @group.users << @user_in_group
1122 @group.users << @second_user_in_group
1122 @group.users << @second_user_in_group
1123
1123
1124 @group2 = Group.generate!.reload
1124 @group2 = Group.generate!.reload
1125 @group2.users << @user_in_group2
1125 @group2.users << @user_in_group2
1126
1126
1127 end
1127 end
1128
1128
1129 should "search assigned to for users in the group" do
1129 should "search assigned to for users in the group" do
1130 @query = Query.new(:name => '_')
1130 @query = IssueQuery.new(:name => '_')
1131 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1131 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1132
1132
1133 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
1133 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
1134 assert_find_issues_with_query_is_successful @query
1134 assert_find_issues_with_query_is_successful @query
1135 end
1135 end
1136
1136
1137 should "search not assigned to any group member (none)" do
1137 should "search not assigned to any group member (none)" do
1138 @query = Query.new(:name => '_')
1138 @query = IssueQuery.new(:name => '_')
1139 @query.add_filter('member_of_group', '!*', [''])
1139 @query.add_filter('member_of_group', '!*', [''])
1140
1140
1141 # Users not in a group
1141 # Users not in a group
1142 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1142 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1143 assert_find_issues_with_query_is_successful @query
1143 assert_find_issues_with_query_is_successful @query
1144 end
1144 end
1145
1145
1146 should "search assigned to any group member (all)" do
1146 should "search assigned to any group member (all)" do
1147 @query = Query.new(:name => '_')
1147 @query = IssueQuery.new(:name => '_')
1148 @query.add_filter('member_of_group', '*', [''])
1148 @query.add_filter('member_of_group', '*', [''])
1149
1149
1150 # Only users in a group
1150 # Only users in a group
1151 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1151 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1152 assert_find_issues_with_query_is_successful @query
1152 assert_find_issues_with_query_is_successful @query
1153 end
1153 end
1154
1154
1155 should "return an empty set with = empty group" do
1155 should "return an empty set with = empty group" do
1156 @empty_group = Group.generate!
1156 @empty_group = Group.generate!
1157 @query = Query.new(:name => '_')
1157 @query = IssueQuery.new(:name => '_')
1158 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1158 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1159
1159
1160 assert_equal [], find_issues_with_query(@query)
1160 assert_equal [], find_issues_with_query(@query)
1161 end
1161 end
1162
1162
1163 should "return issues with ! empty group" do
1163 should "return issues with ! empty group" do
1164 @empty_group = Group.generate!
1164 @empty_group = Group.generate!
1165 @query = Query.new(:name => '_')
1165 @query = IssueQuery.new(:name => '_')
1166 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1166 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1167
1167
1168 assert_find_issues_with_query_is_successful @query
1168 assert_find_issues_with_query_is_successful @query
1169 end
1169 end
1170 end
1170 end
1171
1171
1172 context "with 'assigned_to_role' filter" do
1172 context "with 'assigned_to_role' filter" do
1173 setup do
1173 setup do
1174 @manager_role = Role.find_by_name('Manager')
1174 @manager_role = Role.find_by_name('Manager')
1175 @developer_role = Role.find_by_name('Developer')
1175 @developer_role = Role.find_by_name('Developer')
1176
1176
1177 @project = Project.generate!
1177 @project = Project.generate!
1178 @manager = User.generate!
1178 @manager = User.generate!
1179 @developer = User.generate!
1179 @developer = User.generate!
1180 @boss = User.generate!
1180 @boss = User.generate!
1181 @guest = User.generate!
1181 @guest = User.generate!
1182 User.add_to_project(@manager, @project, @manager_role)
1182 User.add_to_project(@manager, @project, @manager_role)
1183 User.add_to_project(@developer, @project, @developer_role)
1183 User.add_to_project(@developer, @project, @developer_role)
1184 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1184 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1185
1185
1186 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1186 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1187 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1187 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1188 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1188 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1189 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1189 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1190 @issue5 = Issue.generate!(:project => @project)
1190 @issue5 = Issue.generate!(:project => @project)
1191 end
1191 end
1192
1192
1193 should "search assigned to for users with the Role" do
1193 should "search assigned to for users with the Role" do
1194 @query = Query.new(:name => '_', :project => @project)
1194 @query = IssueQuery.new(:name => '_', :project => @project)
1195 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1195 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1196
1196
1197 assert_query_result [@issue1, @issue3], @query
1197 assert_query_result [@issue1, @issue3], @query
1198 end
1198 end
1199
1199
1200 should "search assigned to for users with the Role on the issue project" do
1200 should "search assigned to for users with the Role on the issue project" do
1201 other_project = Project.generate!
1201 other_project = Project.generate!
1202 User.add_to_project(@developer, other_project, @manager_role)
1202 User.add_to_project(@developer, other_project, @manager_role)
1203
1203
1204 @query = Query.new(:name => '_', :project => @project)
1204 @query = IssueQuery.new(:name => '_', :project => @project)
1205 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1205 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1206
1206
1207 assert_query_result [@issue1, @issue3], @query
1207 assert_query_result [@issue1, @issue3], @query
1208 end
1208 end
1209
1209
1210 should "return an empty set with empty role" do
1210 should "return an empty set with empty role" do
1211 @empty_role = Role.generate!
1211 @empty_role = Role.generate!
1212 @query = Query.new(:name => '_', :project => @project)
1212 @query = IssueQuery.new(:name => '_', :project => @project)
1213 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1213 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1214
1214
1215 assert_query_result [], @query
1215 assert_query_result [], @query
1216 end
1216 end
1217
1217
1218 should "search assigned to for users without the Role" do
1218 should "search assigned to for users without the Role" do
1219 @query = Query.new(:name => '_', :project => @project)
1219 @query = IssueQuery.new(:name => '_', :project => @project)
1220 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1220 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1221
1221
1222 assert_query_result [@issue2, @issue4, @issue5], @query
1222 assert_query_result [@issue2, @issue4, @issue5], @query
1223 end
1223 end
1224
1224
1225 should "search assigned to for users not assigned to any Role (none)" do
1225 should "search assigned to for users not assigned to any Role (none)" do
1226 @query = Query.new(:name => '_', :project => @project)
1226 @query = IssueQuery.new(:name => '_', :project => @project)
1227 @query.add_filter('assigned_to_role', '!*', [''])
1227 @query.add_filter('assigned_to_role', '!*', [''])
1228
1228
1229 assert_query_result [@issue4, @issue5], @query
1229 assert_query_result [@issue4, @issue5], @query
1230 end
1230 end
1231
1231
1232 should "search assigned to for users assigned to any Role (all)" do
1232 should "search assigned to for users assigned to any Role (all)" do
1233 @query = Query.new(:name => '_', :project => @project)
1233 @query = IssueQuery.new(:name => '_', :project => @project)
1234 @query.add_filter('assigned_to_role', '*', [''])
1234 @query.add_filter('assigned_to_role', '*', [''])
1235
1235
1236 assert_query_result [@issue1, @issue2, @issue3], @query
1236 assert_query_result [@issue1, @issue2, @issue3], @query
1237 end
1237 end
1238
1238
1239 should "return issues with ! empty role" do
1239 should "return issues with ! empty role" do
1240 @empty_role = Role.generate!
1240 @empty_role = Role.generate!
1241 @query = Query.new(:name => '_', :project => @project)
1241 @query = IssueQuery.new(:name => '_', :project => @project)
1242 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1242 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1243
1243
1244 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1244 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1245 end
1245 end
1246 end
1246 end
1247 end
1247 end
1248
1249 end
1248 end
General Comments 0
You need to be logged in to leave comments. Login now