##// 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 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueriesController < ApplicationController
19 19 menu_item :issues
20 20 before_filter :find_query, :except => [:new, :create, :index]
21 21 before_filter :find_optional_project, :only => [:new, :create]
22 22
23 23 accept_api_auth :index
24 24
25 25 include QueriesHelper
26 26
27 27 def index
28 28 case params[:format]
29 29 when 'xml', 'json'
30 30 @offset, @limit = api_offset_and_limit
31 31 else
32 32 @limit = per_page_option
33 33 end
34 34
35 @query_count = Query.visible.count
35 @query_count = IssueQuery.visible.count
36 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 39 respond_to do |format|
40 40 format.html { render :nothing => true }
41 41 format.api
42 42 end
43 43 end
44 44
45 45 def new
46 @query = Query.new
46 @query = IssueQuery.new
47 47 @query.user = User.current
48 48 @query.project = @project
49 49 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
50 50 build_query_from_params
51 51 end
52 52
53 53 def create
54 @query = Query.new(params[:query])
54 @query = IssueQuery.new(params[:query])
55 55 @query.user = User.current
56 56 @query.project = params[:query_is_for_all] ? nil : @project
57 57 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
58 58 build_query_from_params
59 59 @query.column_names = nil if params[:default_columns]
60 60
61 61 if @query.save
62 62 flash[:notice] = l(:notice_successful_create)
63 63 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
64 64 else
65 65 render :action => 'new', :layout => !request.xhr?
66 66 end
67 67 end
68 68
69 69 def edit
70 70 end
71 71
72 72 def update
73 73 @query.attributes = params[:query]
74 74 @query.project = nil if params[:query_is_for_all]
75 75 @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
76 76 build_query_from_params
77 77 @query.column_names = nil if params[:default_columns]
78 78
79 79 if @query.save
80 80 flash[:notice] = l(:notice_successful_update)
81 81 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
82 82 else
83 83 render :action => 'edit'
84 84 end
85 85 end
86 86
87 87 def destroy
88 88 @query.destroy
89 89 redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
90 90 end
91 91
92 92 private
93 93 def find_query
94 @query = Query.find(params[:id])
94 @query = IssueQuery.find(params[:id])
95 95 @project = @query.project
96 96 render_403 unless @query.editable_by?(User.current)
97 97 rescue ActiveRecord::RecordNotFound
98 98 render_404
99 99 end
100 100
101 101 def find_optional_project
102 102 @project = Project.find(params[:project_id]) if params[:project_id]
103 103 render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
104 104 rescue ActiveRecord::RecordNotFound
105 105 render_404
106 106 end
107 107 end
@@ -1,410 +1,410
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module IssuesHelper
21 21 include ApplicationHelper
22 22
23 23 def issue_list(issues, &block)
24 24 ancestors = []
25 25 issues.each do |issue|
26 26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 27 ancestors.pop
28 28 end
29 29 yield issue, ancestors.size
30 30 ancestors << issue unless issue.leaf?
31 31 end
32 32 end
33 33
34 34 # Renders a HTML/CSS tooltip
35 35 #
36 36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 37 # that contains this method wrapped in a span with the class of "tip"
38 38 #
39 39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 41 # </div>
42 42 #
43 43 def render_issue_tooltip(issue)
44 44 @cached_label_status ||= l(:field_status)
45 45 @cached_label_start_date ||= l(:field_start_date)
46 46 @cached_label_due_date ||= l(:field_due_date)
47 47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 48 @cached_label_priority ||= l(:field_priority)
49 49 @cached_label_project ||= l(:field_project)
50 50
51 51 link_to_issue(issue) + "<br /><br />".html_safe +
52 52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 58 end
59 59
60 60 def issue_heading(issue)
61 61 h("#{issue.tracker} ##{issue.id}")
62 62 end
63 63
64 64 def render_issue_subject_with_tree(issue)
65 65 s = ''
66 66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 67 ancestors.each do |ancestor|
68 68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 69 end
70 70 s << '<div>'
71 71 subject = h(issue.subject)
72 72 if issue.is_private?
73 73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 74 end
75 75 s << content_tag('h3', subject)
76 76 s << '</div>' * (ancestors.size + 1)
77 77 s.html_safe
78 78 end
79 79
80 80 def render_descendants_tree(issue)
81 81 s = '<form><table class="list issues">'
82 82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 83 css = "issue issue-#{child.id} hascontextmenu"
84 84 css << " idnt idnt-#{level}" if level > 0
85 85 s << content_tag('tr',
86 86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 88 content_tag('td', h(child.status)) +
89 89 content_tag('td', link_to_user(child.assigned_to)) +
90 90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 91 :class => css)
92 92 end
93 93 s << '</table></form>'
94 94 s.html_safe
95 95 end
96 96
97 97 # Returns a link for adding a new subtask to the given issue
98 98 def link_to_new_subtask(issue)
99 99 attrs = {
100 100 :tracker_id => issue.tracker,
101 101 :parent_issue_id => issue
102 102 }
103 103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
104 104 end
105 105
106 106 class IssueFieldsRows
107 107 include ActionView::Helpers::TagHelper
108 108
109 109 def initialize
110 110 @left = []
111 111 @right = []
112 112 end
113 113
114 114 def left(*args)
115 115 args.any? ? @left << cells(*args) : @left
116 116 end
117 117
118 118 def right(*args)
119 119 args.any? ? @right << cells(*args) : @right
120 120 end
121 121
122 122 def size
123 123 @left.size > @right.size ? @left.size : @right.size
124 124 end
125 125
126 126 def to_html
127 127 html = ''.html_safe
128 128 blank = content_tag('th', '') + content_tag('td', '')
129 129 size.times do |i|
130 130 left = @left[i] || blank
131 131 right = @right[i] || blank
132 132 html << content_tag('tr', left + right)
133 133 end
134 134 html
135 135 end
136 136
137 137 def cells(label, text, options={})
138 138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
139 139 end
140 140 end
141 141
142 142 def issue_fields_rows
143 143 r = IssueFieldsRows.new
144 144 yield r
145 145 r.to_html
146 146 end
147 147
148 148 def render_custom_fields_rows(issue)
149 149 return if issue.custom_field_values.empty?
150 150 ordered_values = []
151 151 half = (issue.custom_field_values.size / 2.0).ceil
152 152 half.times do |i|
153 153 ordered_values << issue.custom_field_values[i]
154 154 ordered_values << issue.custom_field_values[i + half]
155 155 end
156 156 s = "<tr>\n"
157 157 n = 0
158 158 ordered_values.compact.each do |value|
159 159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
160 160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
161 161 n += 1
162 162 end
163 163 s << "</tr>\n"
164 164 s.html_safe
165 165 end
166 166
167 167 def issues_destroy_confirmation_message(issues)
168 168 issues = [issues] unless issues.is_a?(Array)
169 169 message = l(:text_issues_destroy_confirmation)
170 170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
171 171 if descendant_count > 0
172 172 issues.each do |issue|
173 173 next if issue.root?
174 174 issues.each do |other_issue|
175 175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
176 176 end
177 177 end
178 178 if descendant_count > 0
179 179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
180 180 end
181 181 end
182 182 message
183 183 end
184 184
185 185 def sidebar_queries
186 186 unless @sidebar_queries
187 @sidebar_queries = Query.visible.all(
187 @sidebar_queries = IssueQuery.visible.all(
188 188 :order => "#{Query.table_name}.name ASC",
189 189 # Project specific queries and global queries
190 190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
191 191 )
192 192 end
193 193 @sidebar_queries
194 194 end
195 195
196 196 def query_links(title, queries)
197 197 # links to #index on issues/show
198 198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
199 199
200 200 content_tag('h3', h(title)) +
201 201 queries.collect {|query|
202 202 css = 'query'
203 203 css << ' selected' if query == @query
204 204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
205 205 }.join('<br />').html_safe
206 206 end
207 207
208 208 def render_sidebar_queries
209 209 out = ''.html_safe
210 210 queries = sidebar_queries.select {|q| !q.is_public?}
211 211 out << query_links(l(:label_my_queries), queries) if queries.any?
212 212 queries = sidebar_queries.select {|q| q.is_public?}
213 213 out << query_links(l(:label_query_plural), queries) if queries.any?
214 214 out
215 215 end
216 216
217 217 # Returns the textual representation of a journal details
218 218 # as an array of strings
219 219 def details_to_strings(details, no_html=false, options={})
220 220 options[:only_path] = (options[:only_path] == false ? false : true)
221 221 strings = []
222 222 values_by_field = {}
223 223 details.each do |detail|
224 224 if detail.property == 'cf'
225 225 field_id = detail.prop_key
226 226 field = CustomField.find_by_id(field_id)
227 227 if field && field.multiple?
228 228 values_by_field[field_id] ||= {:added => [], :deleted => []}
229 229 if detail.old_value
230 230 values_by_field[field_id][:deleted] << detail.old_value
231 231 end
232 232 if detail.value
233 233 values_by_field[field_id][:added] << detail.value
234 234 end
235 235 next
236 236 end
237 237 end
238 238 strings << show_detail(detail, no_html, options)
239 239 end
240 240 values_by_field.each do |field_id, changes|
241 241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
242 242 if changes[:added].any?
243 243 detail.value = changes[:added]
244 244 strings << show_detail(detail, no_html, options)
245 245 elsif changes[:deleted].any?
246 246 detail.old_value = changes[:deleted]
247 247 strings << show_detail(detail, no_html, options)
248 248 end
249 249 end
250 250 strings
251 251 end
252 252
253 253 # Returns the textual representation of a single journal detail
254 254 def show_detail(detail, no_html=false, options={})
255 255 multiple = false
256 256 case detail.property
257 257 when 'attr'
258 258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
259 259 label = l(("field_" + field).to_sym)
260 260 case detail.prop_key
261 261 when 'due_date', 'start_date'
262 262 value = format_date(detail.value.to_date) if detail.value
263 263 old_value = format_date(detail.old_value.to_date) if detail.old_value
264 264
265 265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
266 266 'priority_id', 'category_id', 'fixed_version_id'
267 267 value = find_name_by_reflection(field, detail.value)
268 268 old_value = find_name_by_reflection(field, detail.old_value)
269 269
270 270 when 'estimated_hours'
271 271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
272 272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
273 273
274 274 when 'parent_id'
275 275 label = l(:field_parent_issue)
276 276 value = "##{detail.value}" unless detail.value.blank?
277 277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
278 278
279 279 when 'is_private'
280 280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
281 281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
282 282 end
283 283 when 'cf'
284 284 custom_field = CustomField.find_by_id(detail.prop_key)
285 285 if custom_field
286 286 multiple = custom_field.multiple?
287 287 label = custom_field.name
288 288 value = format_value(detail.value, custom_field.field_format) if detail.value
289 289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
290 290 end
291 291 when 'attachment'
292 292 label = l(:label_attachment)
293 293 end
294 294 call_hook(:helper_issues_show_detail_after_setting,
295 295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
296 296
297 297 label ||= detail.prop_key
298 298 value ||= detail.value
299 299 old_value ||= detail.old_value
300 300
301 301 unless no_html
302 302 label = content_tag('strong', label)
303 303 old_value = content_tag("i", h(old_value)) if detail.old_value
304 304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
305 305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
306 306 # Link to the attachment if it has not been removed
307 307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
308 308 if options[:only_path] != false && atta.is_text?
309 309 value += link_to(
310 310 image_tag('magnifier.png'),
311 311 :controller => 'attachments', :action => 'show',
312 312 :id => atta, :filename => atta.filename
313 313 )
314 314 end
315 315 else
316 316 value = content_tag("i", h(value)) if value
317 317 end
318 318 end
319 319
320 320 if detail.property == 'attr' && detail.prop_key == 'description'
321 321 s = l(:text_journal_changed_no_detail, :label => label)
322 322 unless no_html
323 323 diff_link = link_to 'diff',
324 324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
325 325 :detail_id => detail.id, :only_path => options[:only_path]},
326 326 :title => l(:label_view_diff)
327 327 s << " (#{ diff_link })"
328 328 end
329 329 s.html_safe
330 330 elsif detail.value.present?
331 331 case detail.property
332 332 when 'attr', 'cf'
333 333 if detail.old_value.present?
334 334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
335 335 elsif multiple
336 336 l(:text_journal_added, :label => label, :value => value).html_safe
337 337 else
338 338 l(:text_journal_set_to, :label => label, :value => value).html_safe
339 339 end
340 340 when 'attachment'
341 341 l(:text_journal_added, :label => label, :value => value).html_safe
342 342 end
343 343 else
344 344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
345 345 end
346 346 end
347 347
348 348 # Find the name of an associated record stored in the field attribute
349 349 def find_name_by_reflection(field, id)
350 350 association = Issue.reflect_on_association(field.to_sym)
351 351 if association
352 352 record = association.class_name.constantize.find_by_id(id)
353 353 return record.name if record
354 354 end
355 355 end
356 356
357 357 # Renders issue children recursively
358 358 def render_api_issue_children(issue, api)
359 359 return if issue.leaf?
360 360 api.array :children do
361 361 issue.children.each do |child|
362 362 api.issue(:id => child.id) do
363 363 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
364 364 api.subject child.subject
365 365 render_api_issue_children(child, api)
366 366 end
367 367 end
368 368 end
369 369 end
370 370
371 371 def issues_to_csv(issues, project, query, options={})
372 372 decimal_separator = l(:general_csv_decimal_separator)
373 373 encoding = l(:general_csv_encoding)
374 374 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
375 375 if options[:description]
376 376 if description = query.available_columns.detect {|q| q.name == :description}
377 377 columns << description
378 378 end
379 379 end
380 380
381 381 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
382 382 # csv header fields
383 383 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
384 384
385 385 # csv lines
386 386 issues.each do |issue|
387 387 col_values = columns.collect do |column|
388 388 s = if column.is_a?(QueryCustomFieldColumn)
389 389 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
390 390 show_value(cv)
391 391 else
392 392 value = column.value(issue)
393 393 if value.is_a?(Date)
394 394 format_date(value)
395 395 elsif value.is_a?(Time)
396 396 format_time(value)
397 397 elsif value.is_a?(Float)
398 398 ("%.2f" % value).gsub('.', decimal_separator)
399 399 else
400 400 value
401 401 end
402 402 end
403 403 s.to_s
404 404 end
405 405 csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
406 406 end
407 407 end
408 408 export
409 409 end
410 410 end
@@ -1,173 +1,173
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module QueriesHelper
21 21 def filters_options_for_select(query)
22 22 options_for_select(filters_options(query))
23 23 end
24 24
25 25 def filters_options(query)
26 26 options = [[]]
27 27 sorted_options = query.available_filters.sort do |a, b|
28 28 ord = 0
29 29 if !(a[1][:order] == 20 && b[1][:order] == 20)
30 30 ord = a[1][:order] <=> b[1][:order]
31 31 else
32 32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
33 33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
34 34 if cn != 0
35 35 ord = cn
36 36 else
37 37 f = (a[1][:field] <=> b[1][:field])
38 38 if f != 0
39 39 ord = f
40 40 else
41 41 # assigned_to or author
42 42 ord = (a[0] <=> b[0])
43 43 end
44 44 end
45 45 end
46 46 ord
47 47 end
48 48 options += sorted_options.map do |field, field_options|
49 49 [field_options[:name], field]
50 50 end
51 51 end
52 52
53 53 def available_block_columns_tags(query)
54 54 tags = ''.html_safe
55 55 query.available_block_columns.each do |column|
56 56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
57 57 end
58 58 tags
59 59 end
60 60
61 61 def column_header(column)
62 62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
63 63 :default_order => column.default_order) :
64 64 content_tag('th', h(column.caption))
65 65 end
66 66
67 67 def column_content(column, issue)
68 68 value = column.value(issue)
69 69 if value.is_a?(Array)
70 70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
71 71 else
72 72 column_value(column, issue, value)
73 73 end
74 74 end
75 75
76 76 def column_value(column, issue, value)
77 77 case value.class.name
78 78 when 'String'
79 79 if column.name == :subject
80 80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
81 81 elsif column.name == :description
82 82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
83 83 else
84 84 h(value)
85 85 end
86 86 when 'Time'
87 87 format_time(value)
88 88 when 'Date'
89 89 format_date(value)
90 90 when 'Fixnum', 'Float'
91 91 if column.name == :done_ratio
92 92 progress_bar(value, :width => '80px')
93 93 elsif column.name == :spent_hours
94 94 sprintf "%.2f", value
95 95 else
96 96 h(value.to_s)
97 97 end
98 98 when 'User'
99 99 link_to_user value
100 100 when 'Project'
101 101 link_to_project value
102 102 when 'Version'
103 103 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
104 104 when 'TrueClass'
105 105 l(:general_text_Yes)
106 106 when 'FalseClass'
107 107 l(:general_text_No)
108 108 when 'Issue'
109 109 link_to_issue(value, :subject => false)
110 110 when 'IssueRelation'
111 111 other = value.other_issue(issue)
112 112 content_tag('span',
113 113 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
114 114 :class => value.css_classes_for(issue))
115 115 else
116 116 h(value)
117 117 end
118 118 end
119 119
120 120 # Retrieve query from session or build a new query
121 121 def retrieve_query
122 122 if !params[:query_id].blank?
123 123 cond = "project_id IS NULL"
124 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 126 raise ::Unauthorized unless @query.visible?
127 127 @query.project = @project
128 128 session[:query] = {:id => @query.id, :project_id => @query.project_id}
129 129 sort_clear
130 130 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
131 131 # Give it a name, required to be valid
132 @query = Query.new(:name => "_")
132 @query = IssueQuery.new(:name => "_")
133 133 @query.project = @project
134 134 build_query_from_params
135 135 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
136 136 else
137 137 # retrieve from session
138 @query = Query.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])
138 @query = IssueQuery.find_by_id(session[:query][:id]) if session[:query][:id]
139 @query ||= IssueQuery.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
140 140 @query.project = @project
141 141 end
142 142 end
143 143
144 144 def retrieve_query_from_session
145 145 if session[:query]
146 146 if session[:query][:id]
147 @query = Query.find_by_id(session[:query][:id])
147 @query = IssueQuery.find_by_id(session[:query][:id])
148 148 return unless @query
149 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 151 end
152 152 if session[:query].has_key?(:project_id)
153 153 @query.project_id = session[:query][:project_id]
154 154 else
155 155 @query.project = @project
156 156 end
157 157 @query
158 158 end
159 159 end
160 160
161 161 def build_query_from_params
162 162 if params[:fields] || params[:f]
163 163 @query.filters = {}
164 164 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
165 165 else
166 166 @query.available_filters.keys.each do |field|
167 167 @query.add_short_filter(field, params[field]) if params[field]
168 168 end
169 169 end
170 170 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
171 171 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
172 172 end
173 173 end
@@ -1,969 +1,969
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_CLOSED = 5
24 24 STATUS_ARCHIVED = 9
25 25
26 26 # Maximum length for project identifiers
27 27 IDENTIFIER_MAX_LENGTH = 100
28 28
29 29 # Specific overidden Activities
30 30 has_many :time_entry_activities
31 31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 32 has_many :memberships, :class_name => 'Member'
33 33 has_many :member_principals, :class_name => 'Member',
34 34 :include => :principal,
35 35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 36 has_many :users, :through => :members
37 37 has_many :principals, :through => :member_principals, :source => :principal
38 38
39 39 has_many :enabled_modules, :dependent => :delete_all
40 40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 42 has_many :issue_changes, :through => :issues, :source => :journals
43 43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 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 46 has_many :documents, :dependent => :destroy
47 47 has_many :news, :dependent => :destroy, :include => :author
48 48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 50 has_one :repository, :conditions => ["is_default = ?", true]
51 51 has_many :repositories, :dependent => :destroy
52 52 has_many :changesets, :through => :repository
53 53 has_one :wiki, :dependent => :destroy
54 54 # Custom field for the project issues
55 55 has_and_belongs_to_many :issue_custom_fields,
56 56 :class_name => 'IssueCustomField',
57 57 :order => "#{CustomField.table_name}.position",
58 58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 59 :association_foreign_key => 'custom_field_id'
60 60
61 61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 62 acts_as_attachable :view_permission => :view_files,
63 63 :delete_permission => :manage_files
64 64
65 65 acts_as_customizable
66 66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 69 :author => nil
70 70
71 71 attr_protected :status
72 72
73 73 validates_presence_of :name, :identifier
74 74 validates_uniqueness_of :identifier
75 75 validates_associated :repository, :wiki
76 76 validates_length_of :name, :maximum => 255
77 77 validates_length_of :homepage, :maximum => 255
78 78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 79 # donwcase letters, digits, dashes but not digits only
80 80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 81 # reserved words
82 82 validates_exclusion_of :identifier, :in => %w( new )
83 83
84 84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 85 before_destroy :delete_all_members
86 86
87 87 scope :has_module, lambda {|mod|
88 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 90 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 91 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 92 scope :all_public, lambda { where(:is_public => true) }
93 93 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 94 scope :allowed_to, lambda {|*args|
95 95 user = User.current
96 96 permission = nil
97 97 if args.first.is_a?(Symbol)
98 98 permission = args.shift
99 99 else
100 100 user = args.shift
101 101 permission = args.shift
102 102 end
103 103 where(Project.allowed_to_condition(user, permission, *args))
104 104 }
105 105 scope :like, lambda {|arg|
106 106 if arg.blank?
107 107 where(nil)
108 108 else
109 109 pattern = "%#{arg.to_s.strip.downcase}%"
110 110 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 111 end
112 112 }
113 113
114 114 def initialize(attributes=nil, *args)
115 115 super
116 116
117 117 initialized = (attributes || {}).stringify_keys
118 118 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
119 119 self.identifier = Project.next_identifier
120 120 end
121 121 if !initialized.key?('is_public')
122 122 self.is_public = Setting.default_projects_public?
123 123 end
124 124 if !initialized.key?('enabled_module_names')
125 125 self.enabled_module_names = Setting.default_projects_modules
126 126 end
127 127 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
128 128 self.trackers = Tracker.sorted.all
129 129 end
130 130 end
131 131
132 132 def identifier=(identifier)
133 133 super unless identifier_frozen?
134 134 end
135 135
136 136 def identifier_frozen?
137 137 errors[:identifier].blank? && !(new_record? || identifier.blank?)
138 138 end
139 139
140 140 # returns latest created projects
141 141 # non public projects will be returned only if user is a member of those
142 142 def self.latest(user=nil, count=5)
143 143 visible(user).limit(count).order("created_on DESC").all
144 144 end
145 145
146 146 # Returns true if the project is visible to +user+ or to the current user.
147 147 def visible?(user=User.current)
148 148 user.allowed_to?(:view_project, self)
149 149 end
150 150
151 151 # Returns a SQL conditions string used to find all projects visible by the specified user.
152 152 #
153 153 # Examples:
154 154 # Project.visible_condition(admin) => "projects.status = 1"
155 155 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
156 156 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
157 157 def self.visible_condition(user, options={})
158 158 allowed_to_condition(user, :view_project, options)
159 159 end
160 160
161 161 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
162 162 #
163 163 # Valid options:
164 164 # * :project => limit the condition to project
165 165 # * :with_subprojects => limit the condition to project and its subprojects
166 166 # * :member => limit the condition to the user projects
167 167 def self.allowed_to_condition(user, permission, options={})
168 168 perm = Redmine::AccessControl.permission(permission)
169 169 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
170 170 if perm && perm.project_module
171 171 # If the permission belongs to a project module, make sure the module is enabled
172 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 173 end
174 174 if options[:project]
175 175 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
176 176 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
177 177 base_statement = "(#{project_statement}) AND (#{base_statement})"
178 178 end
179 179
180 180 if user.admin?
181 181 base_statement
182 182 else
183 183 statement_by_role = {}
184 184 unless options[:member]
185 185 role = user.logged? ? Role.non_member : Role.anonymous
186 186 if role.allowed_to?(permission)
187 187 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
188 188 end
189 189 end
190 190 if user.logged?
191 191 user.projects_by_role.each do |role, projects|
192 192 if role.allowed_to?(permission) && projects.any?
193 193 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
194 194 end
195 195 end
196 196 end
197 197 if statement_by_role.empty?
198 198 "1=0"
199 199 else
200 200 if block_given?
201 201 statement_by_role.each do |role, statement|
202 202 if s = yield(role, user)
203 203 statement_by_role[role] = "(#{statement} AND (#{s}))"
204 204 end
205 205 end
206 206 end
207 207 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
208 208 end
209 209 end
210 210 end
211 211
212 212 # Returns the Systemwide and project specific activities
213 213 def activities(include_inactive=false)
214 214 if include_inactive
215 215 return all_activities
216 216 else
217 217 return active_activities
218 218 end
219 219 end
220 220
221 221 # Will create a new Project specific Activity or update an existing one
222 222 #
223 223 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
224 224 # does not successfully save.
225 225 def update_or_create_time_entry_activity(id, activity_hash)
226 226 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
227 227 self.create_time_entry_activity_if_needed(activity_hash)
228 228 else
229 229 activity = project.time_entry_activities.find_by_id(id.to_i)
230 230 activity.update_attributes(activity_hash) if activity
231 231 end
232 232 end
233 233
234 234 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
235 235 #
236 236 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
237 237 # does not successfully save.
238 238 def create_time_entry_activity_if_needed(activity)
239 239 if activity['parent_id']
240 240
241 241 parent_activity = TimeEntryActivity.find(activity['parent_id'])
242 242 activity['name'] = parent_activity.name
243 243 activity['position'] = parent_activity.position
244 244
245 245 if Enumeration.overridding_change?(activity, parent_activity)
246 246 project_activity = self.time_entry_activities.create(activity)
247 247
248 248 if project_activity.new_record?
249 249 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
250 250 else
251 251 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
252 252 end
253 253 end
254 254 end
255 255 end
256 256
257 257 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
258 258 #
259 259 # Examples:
260 260 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
261 261 # project.project_condition(false) => "projects.id = 1"
262 262 def project_condition(with_subprojects)
263 263 cond = "#{Project.table_name}.id = #{id}"
264 264 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
265 265 cond
266 266 end
267 267
268 268 def self.find(*args)
269 269 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
270 270 project = find_by_identifier(*args)
271 271 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
272 272 project
273 273 else
274 274 super
275 275 end
276 276 end
277 277
278 278 def self.find_by_param(*args)
279 279 self.find(*args)
280 280 end
281 281
282 282 def reload(*args)
283 283 @shared_versions = nil
284 284 @rolled_up_versions = nil
285 285 @rolled_up_trackers = nil
286 286 @all_issue_custom_fields = nil
287 287 @all_time_entry_custom_fields = nil
288 288 @to_param = nil
289 289 @allowed_parents = nil
290 290 @allowed_permissions = nil
291 291 @actions_allowed = nil
292 292 super
293 293 end
294 294
295 295 def to_param
296 296 # id is used for projects with a numeric identifier (compatibility)
297 297 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
298 298 end
299 299
300 300 def active?
301 301 self.status == STATUS_ACTIVE
302 302 end
303 303
304 304 def archived?
305 305 self.status == STATUS_ARCHIVED
306 306 end
307 307
308 308 # Archives the project and its descendants
309 309 def archive
310 310 # Check that there is no issue of a non descendant project that is assigned
311 311 # to one of the project or descendant versions
312 312 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
313 313 if v_ids.any? &&
314 314 Issue.
315 315 includes(:project).
316 316 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
317 317 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
318 318 exists?
319 319 return false
320 320 end
321 321 Project.transaction do
322 322 archive!
323 323 end
324 324 true
325 325 end
326 326
327 327 # Unarchives the project
328 328 # All its ancestors must be active
329 329 def unarchive
330 330 return false if ancestors.detect {|a| !a.active?}
331 331 update_attribute :status, STATUS_ACTIVE
332 332 end
333 333
334 334 def close
335 335 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
336 336 end
337 337
338 338 def reopen
339 339 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
340 340 end
341 341
342 342 # Returns an array of projects the project can be moved to
343 343 # by the current user
344 344 def allowed_parents
345 345 return @allowed_parents if @allowed_parents
346 346 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
347 347 @allowed_parents = @allowed_parents - self_and_descendants
348 348 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
349 349 @allowed_parents << nil
350 350 end
351 351 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
352 352 @allowed_parents << parent
353 353 end
354 354 @allowed_parents
355 355 end
356 356
357 357 # Sets the parent of the project with authorization check
358 358 def set_allowed_parent!(p)
359 359 unless p.nil? || p.is_a?(Project)
360 360 if p.to_s.blank?
361 361 p = nil
362 362 else
363 363 p = Project.find_by_id(p)
364 364 return false unless p
365 365 end
366 366 end
367 367 if p.nil?
368 368 if !new_record? && allowed_parents.empty?
369 369 return false
370 370 end
371 371 elsif !allowed_parents.include?(p)
372 372 return false
373 373 end
374 374 set_parent!(p)
375 375 end
376 376
377 377 # Sets the parent of the project
378 378 # Argument can be either a Project, a String, a Fixnum or nil
379 379 def set_parent!(p)
380 380 unless p.nil? || p.is_a?(Project)
381 381 if p.to_s.blank?
382 382 p = nil
383 383 else
384 384 p = Project.find_by_id(p)
385 385 return false unless p
386 386 end
387 387 end
388 388 if p == parent && !p.nil?
389 389 # Nothing to do
390 390 true
391 391 elsif p.nil? || (p.active? && move_possible?(p))
392 392 set_or_update_position_under(p)
393 393 Issue.update_versions_from_hierarchy_change(self)
394 394 true
395 395 else
396 396 # Can not move to the given target
397 397 false
398 398 end
399 399 end
400 400
401 401 # Recalculates all lft and rgt values based on project names
402 402 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
403 403 # Used in BuildProjectsTree migration
404 404 def self.rebuild_tree!
405 405 transaction do
406 406 update_all "lft = NULL, rgt = NULL"
407 407 rebuild!(false)
408 408 end
409 409 end
410 410
411 411 # Returns an array of the trackers used by the project and its active sub projects
412 412 def rolled_up_trackers
413 413 @rolled_up_trackers ||=
414 414 Tracker.
415 415 joins(:projects).
416 416 select("DISTINCT #{Tracker.table_name}.*").
417 417 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
418 418 sorted.
419 419 all
420 420 end
421 421
422 422 # Closes open and locked project versions that are completed
423 423 def close_completed_versions
424 424 Version.transaction do
425 425 versions.where(:status => %w(open locked)).all.each do |version|
426 426 if version.completed?
427 427 version.update_attribute(:status, 'closed')
428 428 end
429 429 end
430 430 end
431 431 end
432 432
433 433 # Returns a scope of the Versions on subprojects
434 434 def rolled_up_versions
435 435 @rolled_up_versions ||=
436 436 Version.scoped(:include => :project,
437 437 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
438 438 end
439 439
440 440 # Returns a scope of the Versions used by the project
441 441 def shared_versions
442 442 if new_record?
443 443 Version.scoped(:include => :project,
444 444 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
445 445 else
446 446 @shared_versions ||= begin
447 447 r = root? ? self : root
448 448 Version.scoped(:include => :project,
449 449 :conditions => "#{Project.table_name}.id = #{id}" +
450 450 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
451 451 " #{Version.table_name}.sharing = 'system'" +
452 452 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
453 453 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
454 454 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
455 455 "))")
456 456 end
457 457 end
458 458 end
459 459
460 460 # Returns a hash of project users grouped by role
461 461 def users_by_role
462 462 members.includes(:user, :roles).all.inject({}) do |h, m|
463 463 m.roles.each do |r|
464 464 h[r] ||= []
465 465 h[r] << m.user
466 466 end
467 467 h
468 468 end
469 469 end
470 470
471 471 # Deletes all project's members
472 472 def delete_all_members
473 473 me, mr = Member.table_name, MemberRole.table_name
474 474 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
475 475 Member.delete_all(['project_id = ?', id])
476 476 end
477 477
478 478 # Users/groups issues can be assigned to
479 479 def assignable_users
480 480 assignable = Setting.issue_group_assignment? ? member_principals : members
481 481 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
482 482 end
483 483
484 484 # Returns the mail adresses of users that should be always notified on project events
485 485 def recipients
486 486 notified_users.collect {|user| user.mail}
487 487 end
488 488
489 489 # Returns the users that should be notified on project events
490 490 def notified_users
491 491 # TODO: User part should be extracted to User#notify_about?
492 492 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
493 493 end
494 494
495 495 # Returns an array of all custom fields enabled for project issues
496 496 # (explictly associated custom fields and custom fields enabled for all projects)
497 497 def all_issue_custom_fields
498 498 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
499 499 end
500 500
501 501 # Returns an array of all custom fields enabled for project time entries
502 502 # (explictly associated custom fields and custom fields enabled for all projects)
503 503 def all_time_entry_custom_fields
504 504 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
505 505 end
506 506
507 507 def project
508 508 self
509 509 end
510 510
511 511 def <=>(project)
512 512 name.downcase <=> project.name.downcase
513 513 end
514 514
515 515 def to_s
516 516 name
517 517 end
518 518
519 519 # Returns a short description of the projects (first lines)
520 520 def short_description(length = 255)
521 521 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
522 522 end
523 523
524 524 def css_classes
525 525 s = 'project'
526 526 s << ' root' if root?
527 527 s << ' child' if child?
528 528 s << (leaf? ? ' leaf' : ' parent')
529 529 unless active?
530 530 if archived?
531 531 s << ' archived'
532 532 else
533 533 s << ' closed'
534 534 end
535 535 end
536 536 s
537 537 end
538 538
539 539 # The earliest start date of a project, based on it's issues and versions
540 540 def start_date
541 541 [
542 542 issues.minimum('start_date'),
543 543 shared_versions.collect(&:effective_date),
544 544 shared_versions.collect(&:start_date)
545 545 ].flatten.compact.min
546 546 end
547 547
548 548 # The latest due date of an issue or version
549 549 def due_date
550 550 [
551 551 issues.maximum('due_date'),
552 552 shared_versions.collect(&:effective_date),
553 553 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
554 554 ].flatten.compact.max
555 555 end
556 556
557 557 def overdue?
558 558 active? && !due_date.nil? && (due_date < Date.today)
559 559 end
560 560
561 561 # Returns the percent completed for this project, based on the
562 562 # progress on it's versions.
563 563 def completed_percent(options={:include_subprojects => false})
564 564 if options.delete(:include_subprojects)
565 565 total = self_and_descendants.collect(&:completed_percent).sum
566 566
567 567 total / self_and_descendants.count
568 568 else
569 569 if versions.count > 0
570 570 total = versions.collect(&:completed_pourcent).sum
571 571
572 572 total / versions.count
573 573 else
574 574 100
575 575 end
576 576 end
577 577 end
578 578
579 579 # Return true if this project allows to do the specified action.
580 580 # action can be:
581 581 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
582 582 # * a permission Symbol (eg. :edit_project)
583 583 def allows_to?(action)
584 584 if archived?
585 585 # No action allowed on archived projects
586 586 return false
587 587 end
588 588 unless active? || Redmine::AccessControl.read_action?(action)
589 589 # No write action allowed on closed projects
590 590 return false
591 591 end
592 592 # No action allowed on disabled modules
593 593 if action.is_a? Hash
594 594 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
595 595 else
596 596 allowed_permissions.include? action
597 597 end
598 598 end
599 599
600 600 def module_enabled?(module_name)
601 601 module_name = module_name.to_s
602 602 enabled_modules.detect {|m| m.name == module_name}
603 603 end
604 604
605 605 def enabled_module_names=(module_names)
606 606 if module_names && module_names.is_a?(Array)
607 607 module_names = module_names.collect(&:to_s).reject(&:blank?)
608 608 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
609 609 else
610 610 enabled_modules.clear
611 611 end
612 612 end
613 613
614 614 # Returns an array of the enabled modules names
615 615 def enabled_module_names
616 616 enabled_modules.collect(&:name)
617 617 end
618 618
619 619 # Enable a specific module
620 620 #
621 621 # Examples:
622 622 # project.enable_module!(:issue_tracking)
623 623 # project.enable_module!("issue_tracking")
624 624 def enable_module!(name)
625 625 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
626 626 end
627 627
628 628 # Disable a module if it exists
629 629 #
630 630 # Examples:
631 631 # project.disable_module!(:issue_tracking)
632 632 # project.disable_module!("issue_tracking")
633 633 # project.disable_module!(project.enabled_modules.first)
634 634 def disable_module!(target)
635 635 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
636 636 target.destroy unless target.blank?
637 637 end
638 638
639 639 safe_attributes 'name',
640 640 'description',
641 641 'homepage',
642 642 'is_public',
643 643 'identifier',
644 644 'custom_field_values',
645 645 'custom_fields',
646 646 'tracker_ids',
647 647 'issue_custom_field_ids'
648 648
649 649 safe_attributes 'enabled_module_names',
650 650 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
651 651
652 652 # Returns an array of projects that are in this project's hierarchy
653 653 #
654 654 # Example: parents, children, siblings
655 655 def hierarchy
656 656 parents = project.self_and_ancestors || []
657 657 descendants = project.descendants || []
658 658 project_hierarchy = parents | descendants # Set union
659 659 end
660 660
661 661 # Returns an auto-generated project identifier based on the last identifier used
662 662 def self.next_identifier
663 663 p = Project.order('created_on DESC').first
664 664 p.nil? ? nil : p.identifier.to_s.succ
665 665 end
666 666
667 667 # Copies and saves the Project instance based on the +project+.
668 668 # Duplicates the source project's:
669 669 # * Wiki
670 670 # * Versions
671 671 # * Categories
672 672 # * Issues
673 673 # * Members
674 674 # * Queries
675 675 #
676 676 # Accepts an +options+ argument to specify what to copy
677 677 #
678 678 # Examples:
679 679 # project.copy(1) # => copies everything
680 680 # project.copy(1, :only => 'members') # => copies members only
681 681 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
682 682 def copy(project, options={})
683 683 project = project.is_a?(Project) ? project : Project.find(project)
684 684
685 685 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
686 686 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
687 687
688 688 Project.transaction do
689 689 if save
690 690 reload
691 691 to_be_copied.each do |name|
692 692 send "copy_#{name}", project
693 693 end
694 694 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
695 695 save
696 696 end
697 697 end
698 698 end
699 699
700 700 # Returns a new unsaved Project instance with attributes copied from +project+
701 701 def self.copy_from(project)
702 702 project = project.is_a?(Project) ? project : Project.find(project)
703 703 # clear unique attributes
704 704 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
705 705 copy = Project.new(attributes)
706 706 copy.enabled_modules = project.enabled_modules
707 707 copy.trackers = project.trackers
708 708 copy.custom_values = project.custom_values.collect {|v| v.clone}
709 709 copy.issue_custom_fields = project.issue_custom_fields
710 710 copy
711 711 end
712 712
713 713 # Yields the given block for each project with its level in the tree
714 714 def self.project_tree(projects, &block)
715 715 ancestors = []
716 716 projects.sort_by(&:lft).each do |project|
717 717 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
718 718 ancestors.pop
719 719 end
720 720 yield project, ancestors.size
721 721 ancestors << project
722 722 end
723 723 end
724 724
725 725 private
726 726
727 727 # Copies wiki from +project+
728 728 def copy_wiki(project)
729 729 # Check that the source project has a wiki first
730 730 unless project.wiki.nil?
731 731 self.wiki ||= Wiki.new
732 732 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
733 733 wiki_pages_map = {}
734 734 project.wiki.pages.each do |page|
735 735 # Skip pages without content
736 736 next if page.content.nil?
737 737 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
738 738 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
739 739 new_wiki_page.content = new_wiki_content
740 740 wiki.pages << new_wiki_page
741 741 wiki_pages_map[page.id] = new_wiki_page
742 742 end
743 743 wiki.save
744 744 # Reproduce page hierarchy
745 745 project.wiki.pages.each do |page|
746 746 if page.parent_id && wiki_pages_map[page.id]
747 747 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
748 748 wiki_pages_map[page.id].save
749 749 end
750 750 end
751 751 end
752 752 end
753 753
754 754 # Copies versions from +project+
755 755 def copy_versions(project)
756 756 project.versions.each do |version|
757 757 new_version = Version.new
758 758 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
759 759 self.versions << new_version
760 760 end
761 761 end
762 762
763 763 # Copies issue categories from +project+
764 764 def copy_issue_categories(project)
765 765 project.issue_categories.each do |issue_category|
766 766 new_issue_category = IssueCategory.new
767 767 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
768 768 self.issue_categories << new_issue_category
769 769 end
770 770 end
771 771
772 772 # Copies issues from +project+
773 773 def copy_issues(project)
774 774 # Stores the source issue id as a key and the copied issues as the
775 775 # value. Used to map the two togeather for issue relations.
776 776 issues_map = {}
777 777
778 778 # Store status and reopen locked/closed versions
779 779 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
780 780 version_statuses.each do |version, status|
781 781 version.update_attribute :status, 'open'
782 782 end
783 783
784 784 # Get issues sorted by root_id, lft so that parent issues
785 785 # get copied before their children
786 786 project.issues.reorder('root_id, lft').all.each do |issue|
787 787 new_issue = Issue.new
788 788 new_issue.copy_from(issue, :subtasks => false, :link => false)
789 789 new_issue.project = self
790 790 # Reassign fixed_versions by name, since names are unique per project
791 791 if issue.fixed_version && issue.fixed_version.project == project
792 792 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
793 793 end
794 794 # Reassign the category by name, since names are unique per project
795 795 if issue.category
796 796 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
797 797 end
798 798 # Parent issue
799 799 if issue.parent_id
800 800 if copied_parent = issues_map[issue.parent_id]
801 801 new_issue.parent_issue_id = copied_parent.id
802 802 end
803 803 end
804 804
805 805 self.issues << new_issue
806 806 if new_issue.new_record?
807 807 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
808 808 else
809 809 issues_map[issue.id] = new_issue unless new_issue.new_record?
810 810 end
811 811 end
812 812
813 813 # Restore locked/closed version statuses
814 814 version_statuses.each do |version, status|
815 815 version.update_attribute :status, status
816 816 end
817 817
818 818 # Relations after in case issues related each other
819 819 project.issues.each do |issue|
820 820 new_issue = issues_map[issue.id]
821 821 unless new_issue
822 822 # Issue was not copied
823 823 next
824 824 end
825 825
826 826 # Relations
827 827 issue.relations_from.each do |source_relation|
828 828 new_issue_relation = IssueRelation.new
829 829 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
830 830 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
831 831 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
832 832 new_issue_relation.issue_to = source_relation.issue_to
833 833 end
834 834 new_issue.relations_from << new_issue_relation
835 835 end
836 836
837 837 issue.relations_to.each do |source_relation|
838 838 new_issue_relation = IssueRelation.new
839 839 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
840 840 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
841 841 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
842 842 new_issue_relation.issue_from = source_relation.issue_from
843 843 end
844 844 new_issue.relations_to << new_issue_relation
845 845 end
846 846 end
847 847 end
848 848
849 849 # Copies members from +project+
850 850 def copy_members(project)
851 851 # Copy users first, then groups to handle members with inherited and given roles
852 852 members_to_copy = []
853 853 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
854 854 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
855 855
856 856 members_to_copy.each do |member|
857 857 new_member = Member.new
858 858 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
859 859 # only copy non inherited roles
860 860 # inherited roles will be added when copying the group membership
861 861 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
862 862 next if role_ids.empty?
863 863 new_member.role_ids = role_ids
864 864 new_member.project = self
865 865 self.members << new_member
866 866 end
867 867 end
868 868
869 869 # Copies queries from +project+
870 870 def copy_queries(project)
871 871 project.queries.each do |query|
872 new_query = ::Query.new
872 new_query = IssueQuery.new
873 873 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
874 874 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
875 875 new_query.project = self
876 876 new_query.user_id = query.user_id
877 877 self.queries << new_query
878 878 end
879 879 end
880 880
881 881 # Copies boards from +project+
882 882 def copy_boards(project)
883 883 project.boards.each do |board|
884 884 new_board = Board.new
885 885 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
886 886 new_board.project = self
887 887 self.boards << new_board
888 888 end
889 889 end
890 890
891 891 def allowed_permissions
892 892 @allowed_permissions ||= begin
893 893 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
894 894 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
895 895 end
896 896 end
897 897
898 898 def allowed_actions
899 899 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
900 900 end
901 901
902 902 # Returns all the active Systemwide and project specific activities
903 903 def active_activities
904 904 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
905 905
906 906 if overridden_activity_ids.empty?
907 907 return TimeEntryActivity.shared.active
908 908 else
909 909 return system_activities_and_project_overrides
910 910 end
911 911 end
912 912
913 913 # Returns all the Systemwide and project specific activities
914 914 # (inactive and active)
915 915 def all_activities
916 916 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
917 917
918 918 if overridden_activity_ids.empty?
919 919 return TimeEntryActivity.shared
920 920 else
921 921 return system_activities_and_project_overrides(true)
922 922 end
923 923 end
924 924
925 925 # Returns the systemwide active activities merged with the project specific overrides
926 926 def system_activities_and_project_overrides(include_inactive=false)
927 927 if include_inactive
928 928 return TimeEntryActivity.shared.
929 929 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
930 930 self.time_entry_activities
931 931 else
932 932 return TimeEntryActivity.shared.active.
933 933 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
934 934 self.time_entry_activities.active
935 935 end
936 936 end
937 937
938 938 # Archives subprojects recursively
939 939 def archive!
940 940 children.each do |subproject|
941 941 subproject.send :archive!
942 942 end
943 943 update_attribute :status, STATUS_ARCHIVED
944 944 end
945 945
946 946 def update_position_under_parent
947 947 set_or_update_position_under(parent)
948 948 end
949 949
950 950 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
951 951 def set_or_update_position_under(target_parent)
952 952 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
953 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 955 if to_be_inserted_before
956 956 move_to_left_of(to_be_inserted_before)
957 957 elsif target_parent.nil?
958 958 if sibs.empty?
959 959 # move_to_root adds the project in first (ie. left) position
960 960 move_to_root
961 961 else
962 962 move_to_right_of(sibs.last) unless self == sibs.last
963 963 end
964 964 else
965 965 # move_to_child_of adds the project in last (ie.right) position
966 966 move_to_child_of(target_parent)
967 967 end
968 968 end
969 969 end
@@ -1,1100 +1,804
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @inline = options.key?(:inline) ? options[:inline] : true
31 31 @caption_key = options[:caption] || "field_#{name}"
32 32 end
33 33
34 34 def caption
35 35 l(@caption_key)
36 36 end
37 37
38 38 # Returns true if the column is sortable, otherwise false
39 39 def sortable?
40 40 !@sortable.nil?
41 41 end
42 42
43 43 def sortable
44 44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 45 end
46 46
47 47 def inline?
48 48 @inline
49 49 end
50 50
51 51 def value(issue)
52 52 issue.send name
53 53 end
54 54
55 55 def css_classes
56 56 name
57 57 end
58 58 end
59 59
60 60 class QueryCustomFieldColumn < QueryColumn
61 61
62 62 def initialize(custom_field)
63 63 self.name = "cf_#{custom_field.id}".to_sym
64 64 self.sortable = custom_field.order_statement || false
65 65 self.groupable = custom_field.group_statement || false
66 66 @inline = true
67 67 @cf = custom_field
68 68 end
69 69
70 70 def caption
71 71 @cf.name
72 72 end
73 73
74 74 def custom_field
75 75 @cf
76 76 end
77 77
78 78 def value(issue)
79 79 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
80 80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
81 81 end
82 82
83 83 def css_classes
84 84 @css_classes ||= "#{name} #{@cf.field_format}"
85 85 end
86 86 end
87 87
88 88 class Query < ActiveRecord::Base
89 89 class StatementInvalid < ::ActiveRecord::StatementInvalid
90 90 end
91 91
92 92 belongs_to :project
93 93 belongs_to :user
94 94 serialize :filters
95 95 serialize :column_names
96 96 serialize :sort_criteria, Array
97 97
98 98 attr_protected :project_id, :user_id
99 99
100 100 validates_presence_of :name
101 101 validates_length_of :name, :maximum => 255
102 102 validate :validate_query_filters
103 103
104 104 class_attribute :operators
105 105 self.operators = {
106 106 "=" => :label_equals,
107 107 "!" => :label_not_equals,
108 108 "o" => :label_open_issues,
109 109 "c" => :label_closed_issues,
110 110 "!*" => :label_none,
111 111 "*" => :label_any,
112 112 ">=" => :label_greater_or_equal,
113 113 "<=" => :label_less_or_equal,
114 114 "><" => :label_between,
115 115 "<t+" => :label_in_less_than,
116 116 ">t+" => :label_in_more_than,
117 117 "><t+"=> :label_in_the_next_days,
118 118 "t+" => :label_in,
119 119 "t" => :label_today,
120 120 "w" => :label_this_week,
121 121 ">t-" => :label_less_than_ago,
122 122 "<t-" => :label_more_than_ago,
123 123 "><t-"=> :label_in_the_past_days,
124 124 "t-" => :label_ago,
125 125 "~" => :label_contains,
126 126 "!~" => :label_not_contains,
127 127 "=p" => :label_any_issues_in_project,
128 128 "=!p" => :label_any_issues_not_in_project,
129 129 "!p" => :label_no_issues_in_project
130 130 }
131 131
132 132 class_attribute :operators_by_filter_type
133 133 self.operators_by_filter_type = {
134 134 :list => [ "=", "!" ],
135 135 :list_status => [ "o", "=", "!", "c", "*" ],
136 136 :list_optional => [ "=", "!", "!*", "*" ],
137 137 :list_subprojects => [ "*", "!*", "=" ],
138 138 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
139 139 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
140 140 :string => [ "=", "~", "!", "!~", "!*", "*" ],
141 141 :text => [ "~", "!~", "!*", "*" ],
142 142 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
143 143 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
144 144 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
145 145 }
146 146
147 147 class_attribute :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 }
148 self.available_columns = []
176 149
177 150 def initialize(attributes=nil, *args)
178 151 super attributes
179 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
180 152 @is_for_all = project.nil?
181 153 end
182 154
183 155 def validate_query_filters
184 156 filters.each_key do |field|
185 157 if values_for(field)
186 158 case type_for(field)
187 159 when :integer
188 160 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
189 161 when :float
190 162 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
191 163 when :date, :date_past
192 164 case operator_for(field)
193 165 when "=", ">=", "<=", "><"
194 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 167 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
196 168 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
197 169 end
198 170 end
199 171 end
200 172
201 173 add_filter_error(field, :blank) unless
202 174 # filter requires one or more values
203 175 (values_for(field) and !values_for(field).first.blank?) or
204 176 # filter doesn't require any value
205 177 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
206 178 end if filters
207 179 end
208 180
209 181 def add_filter_error(field, message)
210 182 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
211 183 errors.add(:base, m)
212 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 186 def editable_by?(user)
220 187 return false unless user
221 188 # Admin can edit them all and regular users can edit their private queries
222 189 return true if user.admin? || (!is_public && self.user_id == user.id)
223 190 # Members can not edit public queries that are for all project (only admin is allowed to)
224 191 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
225 192 end
226 193
227 194 def trackers
228 195 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
229 196 end
230 197
231 198 # Returns a hash of localized labels for all filter operators
232 199 def self.operators_labels
233 200 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
234 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 203 # Returns a representation of the available filters for JSON serialization
374 204 def available_filters_as_json
375 205 json = {}
376 206 available_filters.each do |field, options|
377 207 json[field] = options.slice(:type, :name, :values).stringify_keys
378 208 end
379 209 json
380 210 end
381 211
382 212 def all_projects
383 213 @all_projects ||= Project.visible.all
384 214 end
385 215
386 216 def all_projects_values
387 217 return @all_projects_values if @all_projects_values
388 218
389 219 values = []
390 220 Project.project_tree(all_projects) do |p, level|
391 221 prefix = (level > 0 ? ('--' * level + ' ') : '')
392 222 values << ["#{prefix}#{p.name}", p.id.to_s]
393 223 end
394 224 @all_projects_values = values
395 225 end
396 226
397 227 def add_filter(field, operator, values)
398 228 # values must be an array
399 229 return unless values.nil? || values.is_a?(Array)
400 230 # check if field is defined as an available filter
401 231 if available_filters.has_key? field
402 232 filter_options = available_filters[field]
403 233 filters[field] = {:operator => operator, :values => (values || [''])}
404 234 end
405 235 end
406 236
407 237 def add_short_filter(field, expression)
408 238 return unless expression && available_filters.has_key?(field)
409 239 field_type = available_filters[field][:type]
410 240 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
411 241 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
412 242 add_filter field, operator, $1.present? ? $1.split('|') : ['']
413 243 end || add_filter(field, '=', expression.split('|'))
414 244 end
415 245
416 246 # Add multiple filters using +add_filter+
417 247 def add_filters(fields, operators, values)
418 248 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
419 249 fields.each do |field|
420 250 add_filter(field, operators[field], values && values[field])
421 251 end
422 252 end
423 253 end
424 254
425 255 def has_filter?(field)
426 256 filters and filters[field]
427 257 end
428 258
429 259 def type_for(field)
430 260 available_filters[field][:type] if available_filters.has_key?(field)
431 261 end
432 262
433 263 def operator_for(field)
434 264 has_filter?(field) ? filters[field][:operator] : nil
435 265 end
436 266
437 267 def values_for(field)
438 268 has_filter?(field) ? filters[field][:values] : nil
439 269 end
440 270
441 271 def value_for(field, index=0)
442 272 (values_for(field) || [])[index]
443 273 end
444 274
445 275 def label_for(field)
446 276 label = available_filters[field][:name] if available_filters.has_key?(field)
447 277 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
448 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 280 def self.add_available_column(column)
484 281 self.available_columns << (column) if column.is_a?(QueryColumn)
485 282 end
486 283
487 284 # Returns an array of columns that can be used to group the results
488 285 def groupable_columns
489 286 available_columns.select {|c| c.groupable}
490 287 end
491 288
492 289 # Returns a Hash of columns and the key for sorting
493 290 def sortable_columns
494 291 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
495 292 h[column.name.to_s] = column.sortable
496 293 h
497 294 })
498 295 end
499 296
500 297 def columns
501 298 # preserve the column_names order
502 299 (has_default_columns? ? default_columns_names : column_names).collect do |name|
503 300 available_columns.find { |col| col.name == name }
504 301 end.compact
505 302 end
506 303
507 304 def inline_columns
508 305 columns.select(&:inline?)
509 306 end
510 307
511 308 def block_columns
512 309 columns.reject(&:inline?)
513 310 end
514 311
515 312 def available_inline_columns
516 313 available_columns.select(&:inline?)
517 314 end
518 315
519 316 def available_block_columns
520 317 available_columns.reject(&:inline?)
521 318 end
522 319
523 320 def default_columns_names
524 321 @default_columns_names ||= begin
525 322 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
526 323
527 324 project.present? ? default_columns : [:project] | default_columns
528 325 end
529 326 end
530 327
531 328 def column_names=(names)
532 329 if names
533 330 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
534 331 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
535 332 # Set column_names to nil if default columns
536 333 if names == default_columns_names
537 334 names = nil
538 335 end
539 336 end
540 337 write_attribute(:column_names, names)
541 338 end
542 339
543 340 def has_column?(column)
544 341 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
545 342 end
546 343
547 344 def has_default_columns?
548 345 column_names.nil? || column_names.empty?
549 346 end
550 347
551 348 def sort_criteria=(arg)
552 349 c = []
553 350 if arg.is_a?(Hash)
554 351 arg = arg.keys.sort.collect {|k| arg[k]}
555 352 end
556 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 354 write_attribute(:sort_criteria, c)
558 355 end
559 356
560 357 def sort_criteria
561 358 read_attribute(:sort_criteria) || []
562 359 end
563 360
564 361 def sort_criteria_key(arg)
565 362 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
566 363 end
567 364
568 365 def sort_criteria_order(arg)
569 366 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
570 367 end
571 368
572 369 def sort_criteria_order_for(key)
573 370 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
574 371 end
575 372
576 373 # Returns the SQL sort order that should be prepended for grouping
577 374 def group_by_sort_order
578 375 if grouped? && (column = group_by_column)
579 376 order = sort_criteria_order_for(column.name) || column.default_order
580 377 column.sortable.is_a?(Array) ?
581 378 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
582 379 "#{column.sortable} #{order}"
583 380 end
584 381 end
585 382
586 383 # Returns true if the query is a grouped query
587 384 def grouped?
588 385 !group_by_column.nil?
589 386 end
590 387
591 388 def group_by_column
592 389 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
593 390 end
594 391
595 392 def group_by_statement
596 393 group_by_column.try(:groupable)
597 394 end
598 395
599 396 def project_statement
600 397 project_clauses = []
601 398 if project && !project.descendants.active.empty?
602 399 ids = [project.id]
603 400 if has_filter?("subproject_id")
604 401 case operator_for("subproject_id")
605 402 when '='
606 403 # include the selected subprojects
607 404 ids += values_for("subproject_id").each(&:to_i)
608 405 when '!*'
609 406 # main project only
610 407 else
611 408 # all subprojects
612 409 ids += project.descendants.collect(&:id)
613 410 end
614 411 elsif Setting.display_subprojects_issues?
615 412 ids += project.descendants.collect(&:id)
616 413 end
617 414 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
618 415 elsif project
619 416 project_clauses << "#{Project.table_name}.id = %d" % project.id
620 417 end
621 418 project_clauses.any? ? project_clauses.join(' AND ') : nil
622 419 end
623 420
624 421 def statement
625 422 # filters clauses
626 423 filters_clauses = []
627 424 filters.each_key do |field|
628 425 next if field == "subproject_id"
629 426 v = values_for(field).clone
630 427 next unless v and !v.empty?
631 428 operator = operator_for(field)
632 429
633 430 # "me" value subsitution
634 431 if %w(assigned_to_id author_id watcher_id).include?(field)
635 432 if v.delete("me")
636 433 if User.current.logged?
637 434 v.push(User.current.id.to_s)
638 435 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
639 436 else
640 437 v.push("0")
641 438 end
642 439 end
643 440 end
644 441
645 442 if field == 'project_id'
646 443 if v.delete('mine')
647 444 v += User.current.memberships.map(&:project_id).map(&:to_s)
648 445 end
649 446 end
650 447
651 448 if field =~ /cf_(\d+)$/
652 449 # custom field
653 450 filters_clauses << sql_for_custom_field(field, operator, v, $1)
654 451 elsif respond_to?("sql_for_#{field}_field")
655 452 # specific statement
656 453 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
657 454 else
658 455 # regular field
659 456 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
660 457 end
661 458 end if filters and valid?
662 459
663 460 filters_clauses << project_statement
664 461 filters_clauses.reject!(&:blank?)
665 462
666 463 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
667 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 466 def sql_for_watcher_id_field(field, operator, value)
763 467 db_table = Watcher.table_name
764 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 469 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
766 470 end
767 471
768 472 def sql_for_member_of_group_field(field, operator, value)
769 473 if operator == '*' # Any group
770 474 groups = Group.all
771 475 operator = '=' # Override the operator since we want to find by assigned_to
772 476 elsif operator == "!*"
773 477 groups = Group.all
774 478 operator = '!' # Override the operator since we want to find by assigned_to
775 479 else
776 480 groups = Group.find_all_by_id(value)
777 481 end
778 482 groups ||= []
779 483
780 484 members_of_groups = groups.inject([]) {|user_ids, group|
781 485 if group && group.user_ids.present?
782 486 user_ids << group.user_ids
783 487 end
784 488 user_ids.flatten.uniq.compact
785 489 }.sort.collect(&:to_s)
786 490
787 491 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
788 492 end
789 493
790 494 def sql_for_assigned_to_role_field(field, operator, value)
791 495 case operator
792 496 when "*", "!*" # Member / Not member
793 497 sw = operator == "!*" ? 'NOT' : ''
794 498 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
795 499 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
796 500 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
797 501 when "=", "!"
798 502 role_cond = value.any? ?
799 503 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
800 504 "1=0"
801 505
802 506 sw = operator == "!" ? 'NOT' : ''
803 507 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
804 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 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 510 end
807 511 end
808 512
809 513 def sql_for_is_private_field(field, operator, value)
810 514 op = (operator == "=" ? 'IN' : 'NOT IN')
811 515 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
812 516
813 517 "#{Issue.table_name}.is_private #{op} (#{va})"
814 518 end
815 519
816 520 def sql_for_relations(field, operator, value, options={})
817 521 relation_options = IssueRelation::TYPES[field]
818 522 return relation_options unless relation_options
819 523
820 524 relation_type = field
821 525 join_column, target_join_column = "issue_from_id", "issue_to_id"
822 526 if relation_options[:reverse] || options[:reverse]
823 527 relation_type = relation_options[:reverse] || relation_type
824 528 join_column, target_join_column = target_join_column, join_column
825 529 end
826 530
827 531 sql = case operator
828 532 when "*", "!*"
829 533 op = (operator == "*" ? 'IN' : 'NOT IN')
830 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 535 when "=", "!"
832 536 op = (operator == "=" ? 'IN' : 'NOT IN')
833 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 538 when "=p", "=!p", "!p"
835 539 op = (operator == "!p" ? 'NOT IN' : 'IN')
836 540 comp = (operator == "=!p" ? '<>' : '=')
837 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 542 end
839 543
840 544 if relation_options[:sym] == field && !options[:reverse]
841 545 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
842 546 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
843 547 else
844 548 sql
845 549 end
846 550 end
847 551
848 552 IssueRelation::TYPES.keys.each do |relation_type|
849 553 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
850 554 end
851 555
852 556 private
853 557
854 558 def sql_for_custom_field(field, operator, value, custom_field_id)
855 559 db_table = CustomValue.table_name
856 560 db_field = 'value'
857 561 filter = @available_filters[field]
858 562 return nil unless filter
859 563 if filter[:format] == 'user'
860 564 if value.delete('me')
861 565 value.push User.current.id.to_s
862 566 end
863 567 end
864 568 not_in = nil
865 569 if operator == '!'
866 570 # Makes ! operator work for custom fields with multiple values
867 571 operator = '='
868 572 not_in = 'NOT'
869 573 end
870 574 customized_key = "id"
871 575 customized_class = Issue
872 576 if field =~ /^(.+)\.cf_/
873 577 assoc = $1
874 578 customized_key = "#{assoc}_id"
875 579 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
876 580 raise "Unknown Issue association #{assoc}" unless customized_class
877 581 end
878 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 583 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
880 584 end
881 585
882 586 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
883 587 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
884 588 sql = ''
885 589 case operator
886 590 when "="
887 591 if value.any?
888 592 case type_for(field)
889 593 when :date, :date_past
890 594 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
891 595 when :integer
892 596 if is_custom_filter
893 597 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
894 598 else
895 599 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
896 600 end
897 601 when :float
898 602 if is_custom_filter
899 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 604 else
901 605 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
902 606 end
903 607 else
904 608 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
905 609 end
906 610 else
907 611 # IN an empty set
908 612 sql = "1=0"
909 613 end
910 614 when "!"
911 615 if value.any?
912 616 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
913 617 else
914 618 # NOT IN an empty set
915 619 sql = "1=1"
916 620 end
917 621 when "!*"
918 622 sql = "#{db_table}.#{db_field} IS NULL"
919 623 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
920 624 when "*"
921 625 sql = "#{db_table}.#{db_field} IS NOT NULL"
922 626 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
923 627 when ">="
924 628 if [:date, :date_past].include?(type_for(field))
925 629 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
926 630 else
927 631 if is_custom_filter
928 632 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
929 633 else
930 634 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
931 635 end
932 636 end
933 637 when "<="
934 638 if [:date, :date_past].include?(type_for(field))
935 639 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
936 640 else
937 641 if is_custom_filter
938 642 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
939 643 else
940 644 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
941 645 end
942 646 end
943 647 when "><"
944 648 if [:date, :date_past].include?(type_for(field))
945 649 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
946 650 else
947 651 if is_custom_filter
948 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 653 else
950 654 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
951 655 end
952 656 end
953 657 when "o"
954 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 659 when "c"
956 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 661 when "><t-"
958 662 # between today - n days and today
959 663 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
960 664 when ">t-"
961 665 # >= today - n days
962 666 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
963 667 when "<t-"
964 668 # <= today - n days
965 669 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
966 670 when "t-"
967 671 # = n days in past
968 672 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
969 673 when "><t+"
970 674 # between today and today + n days
971 675 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
972 676 when ">t+"
973 677 # >= today + n days
974 678 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
975 679 when "<t+"
976 680 # <= today + n days
977 681 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
978 682 when "t+"
979 683 # = today + n days
980 684 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
981 685 when "t"
982 686 # = today
983 687 sql = relative_date_clause(db_table, db_field, 0, 0)
984 688 when "w"
985 689 # = this week
986 690 first_day_of_week = l(:general_first_day_of_week).to_i
987 691 day_of_week = Date.today.cwday
988 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 693 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
990 694 when "~"
991 695 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
992 696 when "!~"
993 697 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
994 698 else
995 699 raise "Unknown query operator #{operator}"
996 700 end
997 701
998 702 return sql
999 703 end
1000 704
1001 705 def add_custom_fields_filters(custom_fields, assoc=nil)
1002 706 return unless custom_fields.present?
1003 707 @available_filters ||= {}
1004 708
1005 709 custom_fields.select(&:is_filter?).each do |field|
1006 710 case field.field_format
1007 711 when "text"
1008 712 options = { :type => :text, :order => 20 }
1009 713 when "list"
1010 714 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
1011 715 when "date"
1012 716 options = { :type => :date, :order => 20 }
1013 717 when "bool"
1014 718 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
1015 719 when "int"
1016 720 options = { :type => :integer, :order => 20 }
1017 721 when "float"
1018 722 options = { :type => :float, :order => 20 }
1019 723 when "user", "version"
1020 724 next unless project
1021 725 values = field.possible_values_options(project)
1022 726 if User.current.logged? && field.field_format == 'user'
1023 727 values.unshift ["<< #{l(:label_me)} >>", "me"]
1024 728 end
1025 729 options = { :type => :list_optional, :values => values, :order => 20}
1026 730 else
1027 731 options = { :type => :string, :order => 20 }
1028 732 end
1029 733 filter_id = "cf_#{field.id}"
1030 734 filter_name = field.name
1031 735 if assoc.present?
1032 736 filter_id = "#{assoc}.#{filter_id}"
1033 737 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1034 738 end
1035 739 @available_filters[filter_id] = options.merge({
1036 740 :name => filter_name,
1037 741 :format => field.field_format,
1038 742 :field => field
1039 743 })
1040 744 end
1041 745 end
1042 746
1043 747 def add_associations_custom_fields_filters(*associations)
1044 748 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1045 749 associations.each do |assoc|
1046 750 association_klass = Issue.reflect_on_association(assoc).klass
1047 751 fields_by_class.each do |field_class, fields|
1048 752 if field_class.customized_class <= association_klass
1049 753 add_custom_fields_filters(fields, assoc)
1050 754 end
1051 755 end
1052 756 end
1053 757 end
1054 758
1055 759 # Returns a SQL clause for a date or datetime field.
1056 760 def date_clause(table, field, from, to)
1057 761 s = []
1058 762 if from
1059 763 from_yesterday = from - 1
1060 764 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1061 765 if self.class.default_timezone == :utc
1062 766 from_yesterday_time = from_yesterday_time.utc
1063 767 end
1064 768 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1065 769 end
1066 770 if to
1067 771 to_time = Time.local(to.year, to.month, to.day)
1068 772 if self.class.default_timezone == :utc
1069 773 to_time = to_time.utc
1070 774 end
1071 775 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1072 776 end
1073 777 s.join(' AND ')
1074 778 end
1075 779
1076 780 # Returns a SQL clause for a date or datetime field using relative dates.
1077 781 def relative_date_clause(table, field, days_from, days_to)
1078 782 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1079 783 end
1080 784
1081 785 # Additional joins required for the given sort options
1082 786 def joins_for_order_statement(order_options)
1083 787 joins = []
1084 788
1085 789 if order_options
1086 790 if order_options.include?('authors')
1087 791 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1088 792 end
1089 793 order_options.scan(/cf_\d+/).uniq.each do |name|
1090 794 column = available_columns.detect {|c| c.name.to_s == name}
1091 795 join = column && column.custom_field.join_for_order_statement
1092 796 if join
1093 797 joins << join
1094 798 end
1095 799 end
1096 800 end
1097 801
1098 802 joins.any? ? joins.join(' ') : nil
1099 803 end
1100 804 end
@@ -1,33 +1,33
1 1 <%= form_tag({:action => 'edit', :tab => 'issues'}, :onsubmit => 'selectAllOptions("selected_columns");') do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_check_box :cross_project_issue_relations %></p>
5 5
6 6 <p><%= setting_select :cross_project_subtasks, cross_project_subtasks_options %></p>
7 7
8 8 <p><%= setting_check_box :issue_group_assignment %></p>
9 9
10 10 <p><%= setting_check_box :default_issue_start_date_to_creation_date %></p>
11 11
12 12 <p><%= setting_check_box :display_subprojects_issues %></p>
13 13
14 14 <p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
15 15
16 16 <p><%= setting_multiselect :non_working_week_days, (1..7).map {|d| [day_name(d), d.to_s]}, :inline => true %></p>
17 17
18 18 <p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
19 19
20 20 <p><%= setting_text_field :gantt_items_limit, :size => 6 %></p>
21 21 </div>
22 22
23 23 <fieldset class="box">
24 24 <legend><%= l(:setting_issue_list_default_columns) %></legend>
25 25 <%= render :partial => 'queries/columns',
26 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 28 :tag_name => 'settings[issue_list_default_columns][]'
29 29 } %>
30 30 </fieldset>
31 31
32 32 <%= submit_tag l(:button_save) %>
33 33 <% end %>
@@ -1,156 +1,165
1 1 ---
2 2 queries_001:
3 3 id: 1
4 type: IssueQuery
4 5 project_id: 1
5 6 is_public: true
6 7 name: Multiple custom fields query
7 8 filters: |
8 9 ---
9 10 cf_1:
10 11 :values:
11 12 - MySQL
12 13 :operator: "="
13 14 status_id:
14 15 :values:
15 16 - "1"
16 17 :operator: o
17 18 cf_2:
18 19 :values:
19 20 - "125"
20 21 :operator: "="
21 22
22 23 user_id: 1
23 24 column_names:
24 25 queries_002:
25 26 id: 2
27 type: IssueQuery
26 28 project_id: 1
27 29 is_public: false
28 30 name: Private query for cookbook
29 31 filters: |
30 32 ---
31 33 tracker_id:
32 34 :values:
33 35 - "3"
34 36 :operator: "="
35 37 status_id:
36 38 :values:
37 39 - "1"
38 40 :operator: o
39 41
40 42 user_id: 3
41 43 column_names:
42 44 queries_003:
43 45 id: 3
46 type: IssueQuery
44 47 project_id:
45 48 is_public: false
46 49 name: Private query for all projects
47 50 filters: |
48 51 ---
49 52 tracker_id:
50 53 :values:
51 54 - "3"
52 55 :operator: "="
53 56
54 57 user_id: 3
55 58 column_names:
56 59 queries_004:
57 60 id: 4
61 type: IssueQuery
58 62 project_id:
59 63 is_public: true
60 64 name: Public query for all projects
61 65 filters: |
62 66 ---
63 67 tracker_id:
64 68 :values:
65 69 - "3"
66 70 :operator: "="
67 71
68 72 user_id: 2
69 73 column_names:
70 74 queries_005:
71 75 id: 5
76 type: IssueQuery
72 77 project_id:
73 78 is_public: true
74 79 name: Open issues by priority and tracker
75 80 filters: |
76 81 ---
77 82 status_id:
78 83 :values:
79 84 - "1"
80 85 :operator: o
81 86
82 87 user_id: 1
83 88 column_names:
84 89 sort_criteria: |
85 90 ---
86 91 - - priority
87 92 - desc
88 93 - - tracker
89 94 - asc
90 95 queries_006:
91 96 id: 6
97 type: IssueQuery
92 98 project_id:
93 99 is_public: true
94 100 name: Open issues grouped by tracker
95 101 filters: |
96 102 ---
97 103 status_id:
98 104 :values:
99 105 - "1"
100 106 :operator: o
101 107
102 108 user_id: 1
103 109 column_names:
104 110 group_by: tracker
105 111 sort_criteria: |
106 112 ---
107 113 - - priority
108 114 - desc
109 115 queries_007:
110 116 id: 7
117 type: IssueQuery
111 118 project_id: 2
112 119 is_public: true
113 120 name: Public query for project 2
114 121 filters: |
115 122 ---
116 123 tracker_id:
117 124 :values:
118 125 - "3"
119 126 :operator: "="
120 127
121 128 user_id: 2
122 129 column_names:
123 130 queries_008:
124 131 id: 8
132 type: IssueQuery
125 133 project_id: 2
126 134 is_public: false
127 135 name: Private query for project 2
128 136 filters: |
129 137 ---
130 138 tracker_id:
131 139 :values:
132 140 - "3"
133 141 :operator: "="
134 142
135 143 user_id: 2
136 144 column_names:
137 145 queries_009:
138 146 id: 9
147 type: IssueQuery
139 148 project_id:
140 149 is_public: true
141 150 name: Open issues grouped by list custom field
142 151 filters: |
143 152 ---
144 153 status_id:
145 154 :values:
146 155 - "1"
147 156 :operator: o
148 157
149 158 user_id: 1
150 159 column_names:
151 160 group_by: cf_1
152 161 sort_criteria: |
153 162 ---
154 163 - - priority
155 164 - desc
156 165
@@ -1,97 +1,97
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class CalendarsControllerTest < ActionController::TestCase
21 21 fixtures :projects,
22 22 :trackers,
23 23 :projects_trackers,
24 24 :roles,
25 25 :member_roles,
26 26 :members,
27 27 :enabled_modules
28 28
29 29 def test_calendar
30 30 get :show, :project_id => 1
31 31 assert_response :success
32 32 assert_template 'calendar'
33 33 assert_not_nil assigns(:calendar)
34 34 end
35 35
36 36 def test_cross_project_calendar
37 37 get :show
38 38 assert_response :success
39 39 assert_template 'calendar'
40 40 assert_not_nil assigns(:calendar)
41 41 end
42 42
43 43 context "GET :show" do
44 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 47 get :show, :query_id => @query.id
48 48 assert_response :success
49 49 end
50 50
51 51 end
52 52
53 53 def test_week_number_calculation
54 54 Setting.start_of_week = 7
55 55
56 56 get :show, :month => '1', :year => '2010'
57 57 assert_response :success
58 58
59 59 assert_tag :tag => 'tr',
60 60 :descendant => {:tag => 'td',
61 61 :attributes => {:class => 'week-number'}, :content => '53'},
62 62 :descendant => {:tag => 'td',
63 63 :attributes => {:class => 'odd'}, :content => '27'},
64 64 :descendant => {:tag => 'td',
65 65 :attributes => {:class => 'even'}, :content => '2'}
66 66
67 67 assert_tag :tag => 'tr',
68 68 :descendant => {:tag => 'td',
69 69 :attributes => {:class => 'week-number'}, :content => '1'},
70 70 :descendant => {:tag => 'td',
71 71 :attributes => {:class => 'odd'}, :content => '3'},
72 72 :descendant => {:tag => 'td',
73 73 :attributes => {:class => 'even'}, :content => '9'}
74 74
75 75
76 76 Setting.start_of_week = 1
77 77 get :show, :month => '1', :year => '2010'
78 78 assert_response :success
79 79
80 80 assert_tag :tag => 'tr',
81 81 :descendant => {:tag => 'td',
82 82 :attributes => {:class => 'week-number'}, :content => '53'},
83 83 :descendant => {:tag => 'td',
84 84 :attributes => {:class => 'even'}, :content => '28'},
85 85 :descendant => {:tag => 'td',
86 86 :attributes => {:class => 'even'}, :content => '3'}
87 87
88 88 assert_tag :tag => 'tr',
89 89 :descendant => {:tag => 'td',
90 90 :attributes => {:class => 'week-number'}, :content => '1'},
91 91 :descendant => {:tag => 'td',
92 92 :attributes => {:class => 'even'}, :content => '4'},
93 93 :descendant => {:tag => 'td',
94 94 :attributes => {:class => 'even'}, :content => '10'}
95 95
96 96 end
97 97 end
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,263 +1,263
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'issues_controller'
20 20
21 21 class IssuesControllerTransactionTest < ActionController::TestCase
22 22 tests IssuesController
23 23 fixtures :projects,
24 24 :users,
25 25 :roles,
26 26 :members,
27 27 :member_roles,
28 28 :issues,
29 29 :issue_statuses,
30 30 :versions,
31 31 :trackers,
32 32 :projects_trackers,
33 33 :issue_categories,
34 34 :enabled_modules,
35 35 :enumerations,
36 36 :attachments,
37 37 :workflows,
38 38 :custom_fields,
39 39 :custom_values,
40 40 :custom_fields_projects,
41 41 :custom_fields_trackers,
42 42 :time_entries,
43 43 :journals,
44 44 :journal_details,
45 45 :queries
46 46
47 47 self.use_transactional_fixtures = false
48 48
49 49 def setup
50 50 User.current = nil
51 51 end
52 52
53 53 def test_update_stale_issue_should_not_update_the_issue
54 54 issue = Issue.find(2)
55 55 @request.session[:user_id] = 2
56 56
57 57 assert_no_difference 'Journal.count' do
58 58 assert_no_difference 'TimeEntry.count' do
59 59 put :update,
60 60 :id => issue.id,
61 61 :issue => {
62 62 :fixed_version_id => 4,
63 63 :notes => 'My notes',
64 64 :lock_version => (issue.lock_version - 1)
65 65 },
66 66 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
67 67 end
68 68 end
69 69
70 70 assert_response :success
71 71 assert_template 'edit'
72 72
73 73 assert_select 'div.conflict'
74 74 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite'
75 75 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes'
76 76 assert_select 'label' do
77 77 assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel'
78 78 assert_select 'a[href=/issues/2]'
79 79 end
80 80 end
81 81
82 82 def test_update_stale_issue_should_save_attachments
83 83 set_tmp_attachments_directory
84 84 issue = Issue.find(2)
85 85 @request.session[:user_id] = 2
86 86
87 87 assert_no_difference 'Journal.count' do
88 88 assert_no_difference 'TimeEntry.count' do
89 89 assert_difference 'Attachment.count' do
90 90 put :update,
91 91 :id => issue.id,
92 92 :issue => {
93 93 :fixed_version_id => 4,
94 94 :notes => 'My notes',
95 95 :lock_version => (issue.lock_version - 1)
96 96 },
97 97 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}},
98 98 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id }
99 99 end
100 100 end
101 101 end
102 102
103 103 assert_response :success
104 104 assert_template 'edit'
105 105 attachment = Attachment.first(:order => 'id DESC')
106 106 assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token}
107 107 assert_tag 'span', :content => /testfile.txt/
108 108 end
109 109
110 110 def test_update_stale_issue_without_notes_should_not_show_add_notes_option
111 111 issue = Issue.find(2)
112 112 @request.session[:user_id] = 2
113 113
114 114 put :update, :id => issue.id,
115 115 :issue => {
116 116 :fixed_version_id => 4,
117 117 :notes => '',
118 118 :lock_version => (issue.lock_version - 1)
119 119 }
120 120
121 121 assert_tag 'div', :attributes => {:class => 'conflict'}
122 122 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'}
123 123 assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'}
124 124 assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'}
125 125 end
126 126
127 127 def test_update_stale_issue_should_show_conflicting_journals
128 128 @request.session[:user_id] = 2
129 129
130 130 put :update, :id => 1,
131 131 :issue => {
132 132 :fixed_version_id => 4,
133 133 :notes => '',
134 134 :lock_version => 2
135 135 },
136 136 :last_journal_id => 1
137 137
138 138 assert_not_nil assigns(:conflict_journals)
139 139 assert_equal 1, assigns(:conflict_journals).size
140 140 assert_equal 2, assigns(:conflict_journals).first.id
141 141 assert_tag 'div', :attributes => {:class => 'conflict'},
142 142 :descendant => {:content => /Some notes with Redmine links/}
143 143 end
144 144
145 145 def test_update_stale_issue_without_previous_journal_should_show_all_journals
146 146 @request.session[:user_id] = 2
147 147
148 148 put :update, :id => 1,
149 149 :issue => {
150 150 :fixed_version_id => 4,
151 151 :notes => '',
152 152 :lock_version => 2
153 153 },
154 154 :last_journal_id => ''
155 155
156 156 assert_not_nil assigns(:conflict_journals)
157 157 assert_equal 2, assigns(:conflict_journals).size
158 158 assert_tag 'div', :attributes => {:class => 'conflict'},
159 159 :descendant => {:content => /Some notes with Redmine links/}
160 160 assert_tag 'div', :attributes => {:class => 'conflict'},
161 161 :descendant => {:content => /Journal notes/}
162 162 end
163 163
164 164 def test_update_stale_issue_should_show_private_journals_with_permission_only
165 165 journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1)
166 166
167 167 @request.session[:user_id] = 2
168 168 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
169 169 assert_include journal, assigns(:conflict_journals)
170 170
171 171 Role.find(1).remove_permission! :view_private_notes
172 172 put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => ''
173 173 assert_not_include journal, assigns(:conflict_journals)
174 174 end
175 175
176 176 def test_update_stale_issue_with_overwrite_conflict_resolution_should_update
177 177 @request.session[:user_id] = 2
178 178
179 179 assert_difference 'Journal.count' do
180 180 put :update, :id => 1,
181 181 :issue => {
182 182 :fixed_version_id => 4,
183 183 :notes => 'overwrite_conflict_resolution',
184 184 :lock_version => 2
185 185 },
186 186 :conflict_resolution => 'overwrite'
187 187 end
188 188
189 189 assert_response 302
190 190 issue = Issue.find(1)
191 191 assert_equal 4, issue.fixed_version_id
192 192 journal = Journal.first(:order => 'id DESC')
193 193 assert_equal 'overwrite_conflict_resolution', journal.notes
194 194 assert journal.details.any?
195 195 end
196 196
197 197 def test_update_stale_issue_with_add_notes_conflict_resolution_should_update
198 198 @request.session[:user_id] = 2
199 199
200 200 assert_difference 'Journal.count' do
201 201 put :update, :id => 1,
202 202 :issue => {
203 203 :fixed_version_id => 4,
204 204 :notes => 'add_notes_conflict_resolution',
205 205 :lock_version => 2
206 206 },
207 207 :conflict_resolution => 'add_notes'
208 208 end
209 209
210 210 assert_response 302
211 211 issue = Issue.find(1)
212 212 assert_nil issue.fixed_version_id
213 213 journal = Journal.first(:order => 'id DESC')
214 214 assert_equal 'add_notes_conflict_resolution', journal.notes
215 215 assert journal.details.empty?
216 216 end
217 217
218 218 def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating
219 219 @request.session[:user_id] = 2
220 220
221 221 assert_no_difference 'Journal.count' do
222 222 put :update, :id => 1,
223 223 :issue => {
224 224 :fixed_version_id => 4,
225 225 :notes => 'add_notes_conflict_resolution',
226 226 :lock_version => 2
227 227 },
228 228 :conflict_resolution => 'cancel'
229 229 end
230 230
231 231 assert_redirected_to '/issues/1'
232 232 issue = Issue.find(1)
233 233 assert_nil issue.fixed_version_id
234 234 end
235 235
236 236 def test_put_update_with_spent_time_and_failure_should_not_add_spent_time
237 237 @request.session[:user_id] = 2
238 238
239 239 assert_no_difference('TimeEntry.count') do
240 240 put :update,
241 241 :id => 1,
242 242 :issue => { :subject => '' },
243 243 :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id }
244 244 assert_response :success
245 245 end
246 246
247 247 assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5'
248 248 assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added'
249 249 assert_select 'select[name=?]', 'time_entry[activity_id]' do
250 250 assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id
251 251 end
252 252 end
253 253
254 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 257 get :index
258 258 assert_response 500
259 259 assert_tag 'p', :content => /An error occurred/
260 260 assert_nil session[:query]
261 261 assert_nil session[:issues_index_sort]
262 262 end
263 263 end
@@ -1,65 +1,65
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class QueriesHelperTest < ActionView::TestCase
21 21 include QueriesHelper
22 22 include Redmine::I18n
23 23
24 24 fixtures :projects, :enabled_modules, :users, :members,
25 25 :member_roles, :roles, :trackers, :issue_statuses,
26 26 :issue_categories, :enumerations, :issues,
27 27 :watchers, :custom_fields, :custom_values, :versions,
28 28 :queries,
29 29 :projects_trackers,
30 30 :custom_fields_trackers
31 31
32 32 def test_order
33 33 User.current = User.find_by_login('admin')
34 query = Query.new(:project => nil, :name => '_')
34 query = IssueQuery.new(:project => nil, :name => '_')
35 35 assert_equal 30, query.available_filters.size
36 36 fo = filters_options(query)
37 37 assert_equal 31, fo.size
38 38 assert_equal [], fo[0]
39 39 assert_equal "status_id", fo[1][1]
40 40 assert_equal "project_id", fo[2][1]
41 41 assert_equal "tracker_id", fo[3][1]
42 42 assert_equal "priority_id", fo[4][1]
43 43 assert_equal "watcher_id", fo[17][1]
44 44 assert_equal "is_private", fo[18][1]
45 45 end
46 46
47 47 def test_order_custom_fields
48 48 set_language_if_valid 'en'
49 49 field = UserCustomField.new(
50 50 :name => 'order test', :field_format => 'string',
51 51 :is_for_all => true, :is_filter => true
52 52 )
53 53 assert field.save
54 54 User.current = User.find_by_login('admin')
55 query = Query.new(:project => nil, :name => '_')
55 query = IssueQuery.new(:project => nil, :name => '_')
56 56 assert_equal 32, query.available_filters.size
57 57 fo = filters_options(query)
58 58 assert_equal 33, fo.size
59 59 assert_equal "Searchable field", fo[19][0]
60 60 assert_equal "Database", fo[20][0]
61 61 assert_equal "Project's Development status", fo[21][0]
62 62 assert_equal "Assignee's order test", fo[22][0]
63 63 assert_equal "Author's order test", fo[23][0]
64 64 end
65 65 end
@@ -1,750 +1,750
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../../../test_helper', __FILE__)
19 19
20 20 class Redmine::Helpers::GanttHelperTest < ActionView::TestCase
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :journals, :journal_details,
23 23 :enumerations, :users, :issue_categories,
24 24 :projects_trackers,
25 25 :roles,
26 26 :member_roles,
27 27 :members,
28 28 :enabled_modules,
29 29 :workflows,
30 30 :versions,
31 31 :groups_users
32 32
33 33 include ApplicationHelper
34 34 include ProjectsHelper
35 35 include IssuesHelper
36 36 include ERB::Util
37 37 include Rails.application.routes.url_helpers
38 38
39 39 def setup
40 40 setup_with_controller
41 41 User.current = User.find(1)
42 42 end
43 43
44 44 def today
45 45 @today ||= Date.today
46 46 end
47 47
48 48 # Creates a Gantt chart for a 4 week span
49 49 def create_gantt(project=Project.generate!, options={})
50 50 @project = project
51 51 @gantt = Redmine::Helpers::Gantt.new(options)
52 52 @gantt.project = @project
53 @gantt.query = Query.create!(:project => @project, :name => 'Gantt')
53 @gantt.query = IssueQuery.create!(:project => @project, :name => 'Gantt')
54 54 @gantt.view = self
55 55 @gantt.instance_variable_set('@date_from', options[:date_from] || (today - 14))
56 56 @gantt.instance_variable_set('@date_to', options[:date_to] || (today + 14))
57 57 end
58 58
59 59 context "#number_of_rows" do
60 60 context "with one project" do
61 61 should "return the number of rows just for that project"
62 62 end
63 63
64 64 context "with no project" do
65 65 should "return the total number of rows for all the projects, resursively"
66 66 end
67 67
68 68 should "not exceed max_rows option" do
69 69 p = Project.generate!
70 70 5.times do
71 71 Issue.generate!(:project => p)
72 72 end
73 73 create_gantt(p)
74 74 @gantt.render
75 75 assert_equal 6, @gantt.number_of_rows
76 76 assert !@gantt.truncated
77 77 create_gantt(p, :max_rows => 3)
78 78 @gantt.render
79 79 assert_equal 3, @gantt.number_of_rows
80 80 assert @gantt.truncated
81 81 end
82 82 end
83 83
84 84 context "#number_of_rows_on_project" do
85 85 setup do
86 86 create_gantt
87 87 end
88 88
89 89 should "count 0 for an empty the project" do
90 90 assert_equal 0, @gantt.number_of_rows_on_project(@project)
91 91 end
92 92
93 93 should "count the number of issues without a version" do
94 94 @project.issues << Issue.generate!(:project => @project, :fixed_version => nil)
95 95 assert_equal 2, @gantt.number_of_rows_on_project(@project)
96 96 end
97 97
98 98 should "count the number of issues on versions, including cross-project" do
99 99 version = Version.generate!
100 100 @project.versions << version
101 101 @project.issues << Issue.generate!(:project => @project, :fixed_version => version)
102 102 assert_equal 3, @gantt.number_of_rows_on_project(@project)
103 103 end
104 104 end
105 105
106 106 # TODO: more of an integration test
107 107 context "#subjects" do
108 108 setup do
109 109 create_gantt
110 110 @project.enabled_module_names = [:issue_tracking]
111 111 @tracker = Tracker.generate!
112 112 @project.trackers << @tracker
113 113 @version = Version.generate!(:effective_date => (today + 7), :sharing => 'none')
114 114 @project.versions << @version
115 115 @issue = Issue.generate!(:fixed_version => @version,
116 116 :subject => "gantt#line_for_project",
117 117 :tracker => @tracker,
118 118 :project => @project,
119 119 :done_ratio => 30,
120 120 :start_date => (today - 1),
121 121 :due_date => (today + 7))
122 122 @project.issues << @issue
123 123 end
124 124
125 125 context "project" do
126 126 should "be rendered" do
127 127 @output_buffer = @gantt.subjects
128 128 assert_select "div.project-name a", /#{@project.name}/
129 129 end
130 130
131 131 should "have an indent of 4" do
132 132 @output_buffer = @gantt.subjects
133 133 assert_select "div.project-name[style*=left:4px]"
134 134 end
135 135 end
136 136
137 137 context "version" do
138 138 should "be rendered" do
139 139 @output_buffer = @gantt.subjects
140 140 assert_select "div.version-name a", /#{@version.name}/
141 141 end
142 142
143 143 should "be indented 24 (one level)" do
144 144 @output_buffer = @gantt.subjects
145 145 assert_select "div.version-name[style*=left:24px]"
146 146 end
147 147
148 148 context "without assigned issues" do
149 149 setup do
150 150 @version = Version.generate!(:effective_date => (today + 14),
151 151 :sharing => 'none',
152 152 :name => 'empty_version')
153 153 @project.versions << @version
154 154 end
155 155
156 156 should "not be rendered" do
157 157 @output_buffer = @gantt.subjects
158 158 assert_select "div.version-name a", :text => /#{@version.name}/, :count => 0
159 159 end
160 160 end
161 161 end
162 162
163 163 context "issue" do
164 164 should "be rendered" do
165 165 @output_buffer = @gantt.subjects
166 166 assert_select "div.issue-subject", /#{@issue.subject}/
167 167 end
168 168
169 169 should "be indented 44 (two levels)" do
170 170 @output_buffer = @gantt.subjects
171 171 assert_select "div.issue-subject[style*=left:44px]"
172 172 end
173 173
174 174 context "assigned to a shared version of another project" do
175 175 setup do
176 176 p = Project.generate!
177 177 p.enabled_module_names = [:issue_tracking]
178 178 @shared_version = Version.generate!(:sharing => 'system')
179 179 p.versions << @shared_version
180 180 # Reassign the issue to a shared version of another project
181 181 @issue = Issue.generate!(:fixed_version => @shared_version,
182 182 :subject => "gantt#assigned_to_shared_version",
183 183 :tracker => @tracker,
184 184 :project => @project,
185 185 :done_ratio => 30,
186 186 :start_date => (today - 1),
187 187 :due_date => (today + 7))
188 188 @project.issues << @issue
189 189 end
190 190
191 191 should "be rendered" do
192 192 @output_buffer = @gantt.subjects
193 193 assert_select "div.issue-subject", /#{@issue.subject}/
194 194 end
195 195 end
196 196
197 197 context "with subtasks" do
198 198 setup do
199 199 attrs = {:project => @project, :tracker => @tracker, :fixed_version => @version}
200 200 @child1 = Issue.generate!(
201 201 attrs.merge(:subject => 'child1',
202 202 :parent_issue_id => @issue.id,
203 203 :start_date => (today - 1),
204 204 :due_date => (today + 2))
205 205 )
206 206 @child2 = Issue.generate!(
207 207 attrs.merge(:subject => 'child2',
208 208 :parent_issue_id => @issue.id,
209 209 :start_date => today,
210 210 :due_date => (today + 7))
211 211 )
212 212 @grandchild = Issue.generate!(
213 213 attrs.merge(:subject => 'grandchild',
214 214 :parent_issue_id => @child1.id,
215 215 :start_date => (today - 1),
216 216 :due_date => (today + 2))
217 217 )
218 218 end
219 219
220 220 should "indent subtasks" do
221 221 @output_buffer = @gantt.subjects
222 222 # parent task 44px
223 223 assert_select "div.issue-subject[style*=left:44px]", /#{@issue.subject}/
224 224 # children 64px
225 225 assert_select "div.issue-subject[style*=left:64px]", /child1/
226 226 assert_select "div.issue-subject[style*=left:64px]", /child2/
227 227 # grandchild 84px
228 228 assert_select "div.issue-subject[style*=left:84px]", /grandchild/, @output_buffer
229 229 end
230 230 end
231 231 end
232 232 end
233 233
234 234 context "#lines" do
235 235 setup do
236 236 create_gantt
237 237 @project.enabled_module_names = [:issue_tracking]
238 238 @tracker = Tracker.generate!
239 239 @project.trackers << @tracker
240 240 @version = Version.generate!(:effective_date => (today + 7))
241 241 @project.versions << @version
242 242 @issue = Issue.generate!(:fixed_version => @version,
243 243 :subject => "gantt#line_for_project",
244 244 :tracker => @tracker,
245 245 :project => @project,
246 246 :done_ratio => 30,
247 247 :start_date => (today - 1),
248 248 :due_date => (today + 7))
249 249 @project.issues << @issue
250 250 @output_buffer = @gantt.lines
251 251 end
252 252
253 253 context "project" do
254 254 should "be rendered" do
255 255 assert_select "div.project.task_todo"
256 256 assert_select "div.project.starting"
257 257 assert_select "div.project.ending"
258 258 assert_select "div.label.project", /#{@project.name}/
259 259 end
260 260 end
261 261
262 262 context "version" do
263 263 should "be rendered" do
264 264 assert_select "div.version.task_todo"
265 265 assert_select "div.version.starting"
266 266 assert_select "div.version.ending"
267 267 assert_select "div.label.version", /#{@version.name}/
268 268 end
269 269 end
270 270
271 271 context "issue" do
272 272 should "be rendered" do
273 273 assert_select "div.task_todo"
274 274 assert_select "div.task.label", /#{@issue.done_ratio}/
275 275 assert_select "div.tooltip", /#{@issue.subject}/
276 276 end
277 277 end
278 278 end
279 279
280 280 context "#render_project" do
281 281 should "be tested"
282 282 end
283 283
284 284 context "#render_issues" do
285 285 should "be tested"
286 286 end
287 287
288 288 context "#render_version" do
289 289 should "be tested"
290 290 end
291 291
292 292 context "#subject_for_project" do
293 293 setup do
294 294 create_gantt
295 295 end
296 296
297 297 context ":html format" do
298 298 should "add an absolute positioned div" do
299 299 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
300 300 assert_select "div[style*=absolute]"
301 301 end
302 302
303 303 should "use the indent option to move the div to the right" do
304 304 @output_buffer = @gantt.subject_for_project(@project, {:format => :html, :indent => 40})
305 305 assert_select "div[style*=left:40]"
306 306 end
307 307
308 308 should "include the project name" do
309 309 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
310 310 assert_select 'div', :text => /#{@project.name}/
311 311 end
312 312
313 313 should "include a link to the project" do
314 314 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
315 315 assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/
316 316 end
317 317
318 318 should "style overdue projects" do
319 319 @project.enabled_module_names = [:issue_tracking]
320 320 @project.versions << Version.generate!(:effective_date => (today - 1))
321 321 assert @project.reload.overdue?, "Need an overdue project for this test"
322 322 @output_buffer = @gantt.subject_for_project(@project, {:format => :html})
323 323 assert_select 'div span.project-overdue'
324 324 end
325 325 end
326 326 should "test the PNG format"
327 327 should "test the PDF format"
328 328 end
329 329
330 330 context "#line_for_project" do
331 331 setup do
332 332 create_gantt
333 333 @project.enabled_module_names = [:issue_tracking]
334 334 @tracker = Tracker.generate!
335 335 @project.trackers << @tracker
336 336 @version = Version.generate!(:effective_date => (today - 1))
337 337 @project.versions << @version
338 338 @project.issues << Issue.generate!(:fixed_version => @version,
339 339 :subject => "gantt#line_for_project",
340 340 :tracker => @tracker,
341 341 :project => @project,
342 342 :done_ratio => 30,
343 343 :start_date => (today - 7),
344 344 :due_date => (today + 7))
345 345 end
346 346
347 347 context ":html format" do
348 348 context "todo line" do
349 349 should "start from the starting point on the left" do
350 350 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
351 351 assert_select "div.project.task_todo[style*=left:28px]", true, @output_buffer
352 352 end
353 353
354 354 should "be the total width of the project" do
355 355 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
356 356 assert_select "div.project.task_todo[style*=width:58px]", true, @output_buffer
357 357 end
358 358 end
359 359
360 360 context "late line" do
361 361 should_eventually "start from the starting point on the left" do
362 362 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
363 363 assert_select "div.project.task_late[style*=left:28px]", true, @output_buffer
364 364 end
365 365
366 366 should_eventually "be the total delayed width of the project" do
367 367 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
368 368 assert_select "div.project.task_late[style*=width:30px]", true, @output_buffer
369 369 end
370 370 end
371 371
372 372 context "done line" do
373 373 should_eventually "start from the starting point on the left" do
374 374 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
375 375 assert_select "div.project.task_done[style*=left:28px]", true, @output_buffer
376 376 end
377 377
378 378 should_eventually "Be the total done width of the project" do
379 379 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
380 380 assert_select "div.project.task_done[style*=width:18px]", true, @output_buffer
381 381 end
382 382 end
383 383
384 384 context "starting marker" do
385 385 should "not appear if the starting point is off the gantt chart" do
386 386 # Shift the date range of the chart
387 387 @gantt.instance_variable_set('@date_from', today)
388 388 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
389 389 assert_select "div.project.starting", false, @output_buffer
390 390 end
391 391
392 392 should "appear at the starting point" do
393 393 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
394 394 assert_select "div.project.starting[style*=left:28px]", true, @output_buffer
395 395 end
396 396 end
397 397
398 398 context "ending marker" do
399 399 should "not appear if the starting point is off the gantt chart" do
400 400 # Shift the date range of the chart
401 401 @gantt.instance_variable_set('@date_to', (today - 14))
402 402 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
403 403 assert_select "div.project.ending", false, @output_buffer
404 404 end
405 405
406 406 should "appear at the end of the date range" do
407 407 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
408 408 assert_select "div.project.ending[style*=left:88px]", true, @output_buffer
409 409 end
410 410 end
411 411
412 412 context "status content" do
413 413 should "appear at the far left, even if it's far in the past" do
414 414 @gantt.instance_variable_set('@date_to', (today - 14))
415 415 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
416 416 assert_select "div.project.label", /#{@project.name}/
417 417 end
418 418
419 419 should "show the project name" do
420 420 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
421 421 assert_select "div.project.label", /#{@project.name}/
422 422 end
423 423
424 424 should_eventually "show the percent complete" do
425 425 @output_buffer = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
426 426 assert_select "div.project.label", /0%/
427 427 end
428 428 end
429 429 end
430 430 should "test the PNG format"
431 431 should "test the PDF format"
432 432 end
433 433
434 434 context "#subject_for_version" do
435 435 setup do
436 436 create_gantt
437 437 @project.enabled_module_names = [:issue_tracking]
438 438 @tracker = Tracker.generate!
439 439 @project.trackers << @tracker
440 440 @version = Version.generate!(:effective_date => (today - 1))
441 441 @project.versions << @version
442 442 @project.issues << Issue.generate!(:fixed_version => @version,
443 443 :subject => "gantt#subject_for_version",
444 444 :tracker => @tracker,
445 445 :project => @project,
446 446 :start_date => today)
447 447
448 448 end
449 449
450 450 context ":html format" do
451 451 should "add an absolute positioned div" do
452 452 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
453 453 assert_select "div[style*=absolute]"
454 454 end
455 455
456 456 should "use the indent option to move the div to the right" do
457 457 @output_buffer = @gantt.subject_for_version(@version, {:format => :html, :indent => 40})
458 458 assert_select "div[style*=left:40]"
459 459 end
460 460
461 461 should "include the version name" do
462 462 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
463 463 assert_select 'div', :text => /#{@version.name}/
464 464 end
465 465
466 466 should "include a link to the version" do
467 467 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
468 468 assert_select 'a[href=?]', Regexp.escape("/versions/#{@version.to_param}"), :text => /#{@version.name}/
469 469 end
470 470
471 471 should "style late versions" do
472 472 assert @version.overdue?, "Need an overdue version for this test"
473 473 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
474 474 assert_select 'div span.version-behind-schedule'
475 475 end
476 476
477 477 should "style behind schedule versions" do
478 478 assert @version.behind_schedule?, "Need a behind schedule version for this test"
479 479 @output_buffer = @gantt.subject_for_version(@version, {:format => :html})
480 480 assert_select 'div span.version-behind-schedule'
481 481 end
482 482 end
483 483 should "test the PNG format"
484 484 should "test the PDF format"
485 485 end
486 486
487 487 context "#line_for_version" do
488 488 setup do
489 489 create_gantt
490 490 @project.enabled_module_names = [:issue_tracking]
491 491 @tracker = Tracker.generate!
492 492 @project.trackers << @tracker
493 493 @version = Version.generate!(:effective_date => (today + 7))
494 494 @project.versions << @version
495 495 @project.issues << Issue.generate!(:fixed_version => @version,
496 496 :subject => "gantt#line_for_project",
497 497 :tracker => @tracker,
498 498 :project => @project,
499 499 :done_ratio => 30,
500 500 :start_date => (today - 7),
501 501 :due_date => (today + 7))
502 502 end
503 503
504 504 context ":html format" do
505 505 context "todo line" do
506 506 should "start from the starting point on the left" do
507 507 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
508 508 assert_select "div.version.task_todo[style*=left:28px]", true, @output_buffer
509 509 end
510 510
511 511 should "be the total width of the version" do
512 512 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
513 513 assert_select "div.version.task_todo[style*=width:58px]", true, @output_buffer
514 514 end
515 515 end
516 516
517 517 context "late line" do
518 518 should "start from the starting point on the left" do
519 519 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
520 520 assert_select "div.version.task_late[style*=left:28px]", true, @output_buffer
521 521 end
522 522
523 523 should "be the total delayed width of the version" do
524 524 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
525 525 assert_select "div.version.task_late[style*=width:30px]", true, @output_buffer
526 526 end
527 527 end
528 528
529 529 context "done line" do
530 530 should "start from the starting point on the left" do
531 531 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
532 532 assert_select "div.version.task_done[style*=left:28px]", true, @output_buffer
533 533 end
534 534
535 535 should "be the total done width of the version" do
536 536 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
537 537 assert_select "div.version.task_done[style*=width:16px]", true, @output_buffer
538 538 end
539 539 end
540 540
541 541 context "starting marker" do
542 542 should "not appear if the starting point is off the gantt chart" do
543 543 # Shift the date range of the chart
544 544 @gantt.instance_variable_set('@date_from', today)
545 545 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
546 546 assert_select "div.version.starting", false
547 547 end
548 548
549 549 should "appear at the starting point" do
550 550 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
551 551 assert_select "div.version.starting[style*=left:28px]", true, @output_buffer
552 552 end
553 553 end
554 554
555 555 context "ending marker" do
556 556 should "not appear if the starting point is off the gantt chart" do
557 557 # Shift the date range of the chart
558 558 @gantt.instance_variable_set('@date_to', (today - 14))
559 559 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
560 560 assert_select "div.version.ending", false
561 561 end
562 562
563 563 should "appear at the end of the date range" do
564 564 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
565 565 assert_select "div.version.ending[style*=left:88px]", true, @output_buffer
566 566 end
567 567 end
568 568
569 569 context "status content" do
570 570 should "appear at the far left, even if it's far in the past" do
571 571 @gantt.instance_variable_set('@date_to', (today - 14))
572 572 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
573 573 assert_select "div.version.label", /#{@version.name}/
574 574 end
575 575
576 576 should "show the version name" do
577 577 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
578 578 assert_select "div.version.label", /#{@version.name}/
579 579 end
580 580
581 581 should "show the percent complete" do
582 582 @output_buffer = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
583 583 assert_select "div.version.label", /30%/
584 584 end
585 585 end
586 586 end
587 587 should "test the PNG format"
588 588 should "test the PDF format"
589 589 end
590 590
591 591 context "#subject_for_issue" do
592 592 setup do
593 593 create_gantt
594 594 @project.enabled_module_names = [:issue_tracking]
595 595 @tracker = Tracker.generate!
596 596 @project.trackers << @tracker
597 597 @issue = Issue.generate!(:subject => "gantt#subject_for_issue",
598 598 :tracker => @tracker,
599 599 :project => @project,
600 600 :start_date => (today - 3),
601 601 :due_date => (today - 1))
602 602 @project.issues << @issue
603 603 end
604 604
605 605 context ":html format" do
606 606 should "add an absolute positioned div" do
607 607 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
608 608 assert_select "div[style*=absolute]"
609 609 end
610 610
611 611 should "use the indent option to move the div to the right" do
612 612 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40})
613 613 assert_select "div[style*=left:40]"
614 614 end
615 615
616 616 should "include the issue subject" do
617 617 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
618 618 assert_select 'div', :text => /#{@issue.subject}/
619 619 end
620 620
621 621 should "include a link to the issue" do
622 622 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
623 623 assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/
624 624 end
625 625
626 626 should "style overdue issues" do
627 627 assert @issue.overdue?, "Need an overdue issue for this test"
628 628 @output_buffer = @gantt.subject_for_issue(@issue, {:format => :html})
629 629 assert_select 'div span.issue-overdue'
630 630 end
631 631 end
632 632 should "test the PNG format"
633 633 should "test the PDF format"
634 634 end
635 635
636 636 context "#line_for_issue" do
637 637 setup do
638 638 create_gantt
639 639 @project.enabled_module_names = [:issue_tracking]
640 640 @tracker = Tracker.generate!
641 641 @project.trackers << @tracker
642 642 @version = Version.generate!(:effective_date => (today + 7))
643 643 @project.versions << @version
644 644 @issue = Issue.generate!(:fixed_version => @version,
645 645 :subject => "gantt#line_for_project",
646 646 :tracker => @tracker,
647 647 :project => @project,
648 648 :done_ratio => 30,
649 649 :start_date => (today - 7),
650 650 :due_date => (today + 7))
651 651 @project.issues << @issue
652 652 end
653 653
654 654 context ":html format" do
655 655 context "todo line" do
656 656 should "start from the starting point on the left" do
657 657 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
658 658 assert_select "div.task_todo[style*=left:28px]", true, @output_buffer
659 659 end
660 660
661 661 should "be the total width of the issue" do
662 662 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
663 663 assert_select "div.task_todo[style*=width:58px]", true, @output_buffer
664 664 end
665 665 end
666 666
667 667 context "late line" do
668 668 should "start from the starting point on the left" do
669 669 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
670 670 assert_select "div.task_late[style*=left:28px]", true, @output_buffer
671 671 end
672 672
673 673 should "be the total delayed width of the issue" do
674 674 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
675 675 assert_select "div.task_late[style*=width:30px]", true, @output_buffer
676 676 end
677 677 end
678 678
679 679 context "done line" do
680 680 should "start from the starting point on the left" do
681 681 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
682 682 assert_select "div.task_done[style*=left:28px]", true, @output_buffer
683 683 end
684 684
685 685 should "be the total done width of the issue" do
686 686 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
687 687 # 15 days * 4 px * 30% - 2 px for borders = 16 px
688 688 assert_select "div.task_done[style*=width:16px]", true, @output_buffer
689 689 end
690 690
691 691 should "not be the total done width if the chart starts after issue start date" do
692 692 create_gantt(@project, :date_from => (today - 5))
693 693 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
694 694 assert_select "div.task_done[style*=left:0px]", true, @output_buffer
695 695 assert_select "div.task_done[style*=width:8px]", true, @output_buffer
696 696 end
697 697
698 698 context "for completed issue" do
699 699 setup do
700 700 @issue.done_ratio = 100
701 701 end
702 702
703 703 should "be the total width of the issue" do
704 704 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
705 705 assert_select "div.task_done[style*=width:58px]", true, @output_buffer
706 706 end
707 707
708 708 should "be the total width of the issue with due_date=start_date" do
709 709 @issue.due_date = @issue.start_date
710 710 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
711 711 assert_select "div.task_done[style*=width:2px]", true, @output_buffer
712 712 end
713 713 end
714 714 end
715 715
716 716 context "status content" do
717 717 should "appear at the far left, even if it's far in the past" do
718 718 @gantt.instance_variable_set('@date_to', (today - 14))
719 719 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
720 720 assert_select "div.task.label", true, @output_buffer
721 721 end
722 722
723 723 should "show the issue status" do
724 724 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
725 725 assert_select "div.task.label", /#{@issue.status.name}/
726 726 end
727 727
728 728 should "show the percent complete" do
729 729 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
730 730 assert_select "div.task.label", /30%/
731 731 end
732 732 end
733 733 end
734 734
735 735 should "have an issue tooltip" do
736 736 @output_buffer = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
737 737 assert_select "div.tooltip", /#{@issue.subject}/
738 738 end
739 739 should "test the PNG format"
740 740 should "test the PDF format"
741 741 end
742 742
743 743 context "#to_image" do
744 744 should "be tested"
745 745 end
746 746
747 747 context "#to_pdf" do
748 748 should "be tested"
749 749 end
750 750 end
@@ -1,1249 +1,1248
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class QueryTest < ActiveSupport::TestCase
21 21 include Redmine::I18n
22 22
23 23 fixtures :projects, :enabled_modules, :users, :members,
24 24 :member_roles, :roles, :trackers, :issue_statuses,
25 25 :issue_categories, :enumerations, :issues,
26 26 :watchers, :custom_fields, :custom_values, :versions,
27 27 :queries,
28 28 :projects_trackers,
29 29 :custom_fields_trackers
30 30
31 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 33 assert query.available_filters.has_key?('cf_1')
34 34 assert !query.available_filters.has_key?('cf_3')
35 35 end
36 36
37 37 def test_system_shared_versions_should_be_available_in_global_queries
38 38 Version.find(2).update_attribute :sharing, 'system'
39 query = Query.new(:project => nil, :name => '_')
39 query = IssueQuery.new(:project => nil, :name => '_')
40 40 assert query.available_filters.has_key?('fixed_version_id')
41 41 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
42 42 end
43 43
44 44 def test_project_filter_in_global_queries
45 query = Query.new(:project => nil, :name => '_')
45 query = IssueQuery.new(:project => nil, :name => '_')
46 46 project_filter = query.available_filters["project_id"]
47 47 assert_not_nil project_filter
48 48 project_ids = project_filter[:values].map{|p| p[1]}
49 49 assert project_ids.include?("1") #public project
50 50 assert !project_ids.include?("2") #private project user cannot see
51 51 end
52 52
53 53 def find_issues_with_query(query)
54 54 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
55 55 query.statement
56 56 ).all
57 57 end
58 58
59 59 def assert_find_issues_with_query_is_successful(query)
60 60 assert_nothing_raised do
61 61 find_issues_with_query(query)
62 62 end
63 63 end
64 64
65 65 def assert_query_statement_includes(query, condition)
66 66 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
67 67 end
68 68
69 69 def assert_query_result(expected, query)
70 70 assert_nothing_raised do
71 71 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
72 72 assert_equal expected.size, query.issue_count
73 73 end
74 74 end
75 75
76 76 def test_query_should_allow_shared_versions_for_a_project_query
77 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 79 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
80 80
81 81 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
82 82 end
83 83
84 84 def test_query_with_multiple_custom_fields
85 query = Query.find(1)
85 query = IssueQuery.find(1)
86 86 assert query.valid?
87 87 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
88 88 issues = find_issues_with_query(query)
89 89 assert_equal 1, issues.length
90 90 assert_equal Issue.find(3), issues.first
91 91 end
92 92
93 93 def test_operator_none
94 query = Query.new(:project => Project.find(1), :name => '_')
94 query = IssueQuery.new(:project => Project.find(1), :name => '_')
95 95 query.add_filter('fixed_version_id', '!*', [''])
96 96 query.add_filter('cf_1', '!*', [''])
97 97 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
98 98 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
99 99 find_issues_with_query(query)
100 100 end
101 101
102 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 104 query.add_filter('estimated_hours', '!*', [''])
105 105 issues = find_issues_with_query(query)
106 106 assert !issues.empty?
107 107 assert issues.all? {|i| !i.estimated_hours}
108 108 end
109 109
110 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 112 query.add_filter('start_date', '!*', [''])
113 113 issues = find_issues_with_query(query)
114 114 assert !issues.empty?
115 115 assert issues.all? {|i| i.start_date.nil?}
116 116 end
117 117
118 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 120 query.add_filter('cf_2', '!*', [''])
121 121 assert query.has_filter?('cf_2')
122 122 issues = find_issues_with_query(query)
123 123 assert !issues.empty?
124 124 assert issues.all? {|i| i.custom_field_value(2).blank?}
125 125 end
126 126
127 127 def test_operator_all
128 query = Query.new(:project => Project.find(1), :name => '_')
128 query = IssueQuery.new(:project => Project.find(1), :name => '_')
129 129 query.add_filter('fixed_version_id', '*', [''])
130 130 query.add_filter('cf_1', '*', [''])
131 131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
132 132 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
133 133 find_issues_with_query(query)
134 134 end
135 135
136 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 138 query.add_filter('start_date', '*', [''])
139 139 issues = find_issues_with_query(query)
140 140 assert !issues.empty?
141 141 assert issues.all? {|i| i.start_date.present?}
142 142 end
143 143
144 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 146 query.add_filter('cf_2', '*', [''])
147 147 assert query.has_filter?('cf_2')
148 148 issues = find_issues_with_query(query)
149 149 assert !issues.empty?
150 150 assert issues.all? {|i| i.custom_field_value(2).present?}
151 151 end
152 152
153 153 def test_numeric_filter_should_not_accept_non_numeric_values
154 query = Query.new(:name => '_')
154 query = IssueQuery.new(:name => '_')
155 155 query.add_filter('estimated_hours', '=', ['a'])
156 156
157 157 assert query.has_filter?('estimated_hours')
158 158 assert !query.valid?
159 159 end
160 160
161 161 def test_operator_is_on_float
162 162 Issue.update_all("estimated_hours = 171.2", "id=2")
163 163
164 query = Query.new(:name => '_')
164 query = IssueQuery.new(:name => '_')
165 165 query.add_filter('estimated_hours', '=', ['171.20'])
166 166 issues = find_issues_with_query(query)
167 167 assert_equal 1, issues.size
168 168 assert_equal 2, issues.first.id
169 169 end
170 170
171 171 def test_operator_is_on_integer_custom_field
172 172 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
173 173 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
174 174 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
175 175 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
176 176
177 query = Query.new(:name => '_')
177 query = IssueQuery.new(:name => '_')
178 178 query.add_filter("cf_#{f.id}", '=', ['12'])
179 179 issues = find_issues_with_query(query)
180 180 assert_equal 1, issues.size
181 181 assert_equal 2, issues.first.id
182 182 end
183 183
184 184 def test_operator_is_on_integer_custom_field_should_accept_negative_value
185 185 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
186 186 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
187 187 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
188 188 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
189 189
190 query = Query.new(:name => '_')
190 query = IssueQuery.new(:name => '_')
191 191 query.add_filter("cf_#{f.id}", '=', ['-12'])
192 192 assert query.valid?
193 193 issues = find_issues_with_query(query)
194 194 assert_equal 1, issues.size
195 195 assert_equal 2, issues.first.id
196 196 end
197 197
198 198 def test_operator_is_on_float_custom_field
199 199 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
200 200 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
201 201 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
202 202 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
203 203
204 query = Query.new(:name => '_')
204 query = IssueQuery.new(:name => '_')
205 205 query.add_filter("cf_#{f.id}", '=', ['12.7'])
206 206 issues = find_issues_with_query(query)
207 207 assert_equal 1, issues.size
208 208 assert_equal 2, issues.first.id
209 209 end
210 210
211 211 def test_operator_is_on_float_custom_field_should_accept_negative_value
212 212 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
213 213 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
214 214 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
215 215 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
216 216
217 query = Query.new(:name => '_')
217 query = IssueQuery.new(:name => '_')
218 218 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
219 219 assert query.valid?
220 220 issues = find_issues_with_query(query)
221 221 assert_equal 1, issues.size
222 222 assert_equal 2, issues.first.id
223 223 end
224 224
225 225 def test_operator_is_on_multi_list_custom_field
226 226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
227 227 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
228 228 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
229 229 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
230 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 233 query.add_filter("cf_#{f.id}", '=', ['value1'])
234 234 issues = find_issues_with_query(query)
235 235 assert_equal [1, 3], issues.map(&:id).sort
236 236
237 query = Query.new(:name => '_')
237 query = IssueQuery.new(:name => '_')
238 238 query.add_filter("cf_#{f.id}", '=', ['value2'])
239 239 issues = find_issues_with_query(query)
240 240 assert_equal [1], issues.map(&:id).sort
241 241 end
242 242
243 243 def test_operator_is_not_on_multi_list_custom_field
244 244 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
245 245 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
246 246 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
247 247 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
248 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 251 query.add_filter("cf_#{f.id}", '!', ['value1'])
252 252 issues = find_issues_with_query(query)
253 253 assert !issues.map(&:id).include?(1)
254 254 assert !issues.map(&:id).include?(3)
255 255
256 query = Query.new(:name => '_')
256 query = IssueQuery.new(:name => '_')
257 257 query.add_filter("cf_#{f.id}", '!', ['value2'])
258 258 issues = find_issues_with_query(query)
259 259 assert !issues.map(&:id).include?(1)
260 260 assert issues.map(&:id).include?(3)
261 261 end
262 262
263 263 def test_operator_is_on_is_private_field
264 264 # is_private filter only available for those who can set issues private
265 265 User.current = User.find(2)
266 266
267 query = Query.new(:name => '_')
267 query = IssueQuery.new(:name => '_')
268 268 assert query.available_filters.key?('is_private')
269 269
270 270 query.add_filter("is_private", '=', ['1'])
271 271 issues = find_issues_with_query(query)
272 272 assert issues.any?
273 273 assert_nil issues.detect {|issue| !issue.is_private?}
274 274 ensure
275 275 User.current = nil
276 276 end
277 277
278 278 def test_operator_is_not_on_is_private_field
279 279 # is_private filter only available for those who can set issues private
280 280 User.current = User.find(2)
281 281
282 query = Query.new(:name => '_')
282 query = IssueQuery.new(:name => '_')
283 283 assert query.available_filters.key?('is_private')
284 284
285 285 query.add_filter("is_private", '!', ['1'])
286 286 issues = find_issues_with_query(query)
287 287 assert issues.any?
288 288 assert_nil issues.detect {|issue| issue.is_private?}
289 289 ensure
290 290 User.current = nil
291 291 end
292 292
293 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 295 query.add_filter('done_ratio', '>=', ['40'])
296 296 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
297 297 find_issues_with_query(query)
298 298 end
299 299
300 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 302 query.add_filter('estimated_hours', '>=', ['40.5'])
303 303 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
304 304 find_issues_with_query(query)
305 305 end
306 306
307 307 def test_operator_greater_than_on_int_custom_field
308 308 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
309 309 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
310 310 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
311 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 314 query.add_filter("cf_#{f.id}", '>=', ['8'])
315 315 issues = find_issues_with_query(query)
316 316 assert_equal 1, issues.size
317 317 assert_equal 2, issues.first.id
318 318 end
319 319
320 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 322 query.add_filter('done_ratio', '<=', ['30'])
323 323 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
324 324 find_issues_with_query(query)
325 325 end
326 326
327 327 def test_operator_lesser_than_on_custom_field
328 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 330 query.add_filter("cf_#{f.id}", '<=', ['30'])
331 331 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
332 332 find_issues_with_query(query)
333 333 end
334 334
335 335 def test_operator_between
336 query = Query.new(:project => Project.find(1), :name => '_')
336 query = IssueQuery.new(:project => Project.find(1), :name => '_')
337 337 query.add_filter('done_ratio', '><', ['30', '40'])
338 338 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
339 339 find_issues_with_query(query)
340 340 end
341 341
342 342 def test_operator_between_on_custom_field
343 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 345 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
346 346 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
347 347 find_issues_with_query(query)
348 348 end
349 349
350 350 def test_date_filter_should_not_accept_non_date_values
351 query = Query.new(:name => '_')
351 query = IssueQuery.new(:name => '_')
352 352 query.add_filter('created_on', '=', ['a'])
353 353
354 354 assert query.has_filter?('created_on')
355 355 assert !query.valid?
356 356 end
357 357
358 358 def test_date_filter_should_not_accept_invalid_date_values
359 query = Query.new(:name => '_')
359 query = IssueQuery.new(:name => '_')
360 360 query.add_filter('created_on', '=', ['2011-01-34'])
361 361
362 362 assert query.has_filter?('created_on')
363 363 assert !query.valid?
364 364 end
365 365
366 366 def test_relative_date_filter_should_not_accept_non_integer_values
367 query = Query.new(:name => '_')
367 query = IssueQuery.new(:name => '_')
368 368 query.add_filter('created_on', '>t-', ['a'])
369 369
370 370 assert query.has_filter?('created_on')
371 371 assert !query.valid?
372 372 end
373 373
374 374 def test_operator_date_equals
375 query = Query.new(:name => '_')
375 query = IssueQuery.new(:name => '_')
376 376 query.add_filter('due_date', '=', ['2011-07-10'])
377 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 378 find_issues_with_query(query)
379 379 end
380 380
381 381 def test_operator_date_lesser_than
382 query = Query.new(:name => '_')
382 query = IssueQuery.new(:name => '_')
383 383 query.add_filter('due_date', '<=', ['2011-07-10'])
384 384 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
385 385 find_issues_with_query(query)
386 386 end
387 387
388 388 def test_operator_date_greater_than
389 query = Query.new(:name => '_')
389 query = IssueQuery.new(:name => '_')
390 390 query.add_filter('due_date', '>=', ['2011-07-10'])
391 391 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
392 392 find_issues_with_query(query)
393 393 end
394 394
395 395 def test_operator_date_between
396 query = Query.new(:name => '_')
396 query = IssueQuery.new(:name => '_')
397 397 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
398 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 399 find_issues_with_query(query)
400 400 end
401 401
402 402 def test_operator_in_more_than
403 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 405 query.add_filter('due_date', '>t+', ['15'])
406 406 issues = find_issues_with_query(query)
407 407 assert !issues.empty?
408 408 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
409 409 end
410 410
411 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 413 query.add_filter('due_date', '<t+', ['15'])
414 414 issues = find_issues_with_query(query)
415 415 assert !issues.empty?
416 416 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
417 417 end
418 418
419 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 421 query.add_filter('due_date', '><t+', ['15'])
422 422 issues = find_issues_with_query(query)
423 423 assert !issues.empty?
424 424 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
425 425 end
426 426
427 427 def test_operator_less_than_ago
428 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 430 query.add_filter('due_date', '>t-', ['3'])
431 431 issues = find_issues_with_query(query)
432 432 assert !issues.empty?
433 433 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
434 434 end
435 435
436 436 def test_operator_in_the_past_days
437 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 439 query.add_filter('due_date', '><t-', ['3'])
440 440 issues = find_issues_with_query(query)
441 441 assert !issues.empty?
442 442 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
443 443 end
444 444
445 445 def test_operator_more_than_ago
446 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 448 query.add_filter('due_date', '<t-', ['10'])
449 449 assert query.statement.include?("#{Issue.table_name}.due_date <=")
450 450 issues = find_issues_with_query(query)
451 451 assert !issues.empty?
452 452 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
453 453 end
454 454
455 455 def test_operator_in
456 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 458 query.add_filter('due_date', 't+', ['2'])
459 459 issues = find_issues_with_query(query)
460 460 assert !issues.empty?
461 461 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
462 462 end
463 463
464 464 def test_operator_ago
465 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 467 query.add_filter('due_date', 't-', ['3'])
468 468 issues = find_issues_with_query(query)
469 469 assert !issues.empty?
470 470 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
471 471 end
472 472
473 473 def test_operator_today
474 query = Query.new(:project => Project.find(1), :name => '_')
474 query = IssueQuery.new(:project => Project.find(1), :name => '_')
475 475 query.add_filter('due_date', 't', [''])
476 476 issues = find_issues_with_query(query)
477 477 assert !issues.empty?
478 478 issues.each {|issue| assert_equal Date.today, issue.due_date}
479 479 end
480 480
481 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 483 query.add_filter('due_date', 'w', [''])
484 484 find_issues_with_query(query)
485 485 end
486 486
487 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 489 query.add_filter('created_on', 'w', [''])
490 490 find_issues_with_query(query)
491 491 end
492 492
493 493 def test_operator_contains
494 query = Query.new(:project => Project.find(1), :name => '_')
494 query = IssueQuery.new(:project => Project.find(1), :name => '_')
495 495 query.add_filter('subject', '~', ['uNable'])
496 496 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
497 497 result = find_issues_with_query(query)
498 498 assert result.empty?
499 499 result.each {|issue| assert issue.subject.downcase.include?('unable') }
500 500 end
501 501
502 502 def test_range_for_this_week_with_week_starting_on_monday
503 503 I18n.locale = :fr
504 504 assert_equal '1', I18n.t(:general_first_day_of_week)
505 505
506 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 509 query.add_filter('due_date', 'w', [''])
510 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 511 I18n.locale = :en
512 512 end
513 513
514 514 def test_range_for_this_week_with_week_starting_on_sunday
515 515 I18n.locale = :en
516 516 assert_equal '7', I18n.t(:general_first_day_of_week)
517 517
518 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 521 query.add_filter('due_date', 'w', [''])
522 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 523 end
524 524
525 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 527 query.add_filter('subject', '!~', ['uNable'])
528 528 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
529 529 find_issues_with_query(query)
530 530 end
531 531
532 532 def test_filter_assigned_to_me
533 533 user = User.find(2)
534 534 group = Group.find(10)
535 535 User.current = user
536 536 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
537 537 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
538 538 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
539 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 542 result = query.issues
543 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 545 assert result.include?(i1)
546 546 assert result.include?(i2)
547 547 assert !result.include?(i3)
548 548 end
549 549
550 550 def test_user_custom_field_filtered_on_me
551 551 User.current = User.find(2)
552 552 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
553 553 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
554 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 557 filter = query.available_filters["cf_#{cf.id}"]
558 558 assert_not_nil filter
559 559 assert_include 'me', filter[:values].map{|v| v[1]}
560 560
561 561 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
562 562 result = query.issues
563 563 assert_equal 1, result.size
564 564 assert_equal issue1, result.first
565 565 end
566 566
567 567 def test_filter_my_projects
568 568 User.current = User.find(2)
569 query = Query.new(:name => '_')
569 query = IssueQuery.new(:name => '_')
570 570 filter = query.available_filters['project_id']
571 571 assert_not_nil filter
572 572 assert_include 'mine', filter[:values].map{|v| v[1]}
573 573
574 574 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
575 575 result = query.issues
576 576 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
577 577 end
578 578
579 579 def test_filter_watched_issues
580 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 582 result = find_issues_with_query(query)
583 583 assert_not_nil result
584 584 assert !result.empty?
585 585 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
586 586 User.current = nil
587 587 end
588 588
589 589 def test_filter_unwatched_issues
590 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 592 result = find_issues_with_query(query)
593 593 assert_not_nil result
594 594 assert !result.empty?
595 595 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
596 596 User.current = nil
597 597 end
598 598
599 599 def test_filter_on_project_custom_field
600 600 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
601 601 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
602 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 605 filter_name = "project.cf_#{field.id}"
606 606 assert_include filter_name, query.available_filters.keys
607 607 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
608 608 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
609 609 end
610 610
611 611 def test_filter_on_author_custom_field
612 612 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
613 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 616 filter_name = "author.cf_#{field.id}"
617 617 assert_include filter_name, query.available_filters.keys
618 618 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
619 619 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
620 620 end
621 621
622 622 def test_filter_on_assigned_to_custom_field
623 623 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
624 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 627 filter_name = "assigned_to.cf_#{field.id}"
628 628 assert_include filter_name, query.available_filters.keys
629 629 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
630 630 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
631 631 end
632 632
633 633 def test_filter_on_fixed_version_custom_field
634 634 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
635 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 638 filter_name = "fixed_version.cf_#{field.id}"
639 639 assert_include filter_name, query.available_filters.keys
640 640 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
641 641 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
642 642 end
643 643
644 644 def test_filter_on_relations_with_a_specific_issue
645 645 IssueRelation.delete_all
646 646 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
647 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 650 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
651 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 654 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
655 655 assert_equal [1], find_issues_with_query(query).map(&:id).sort
656 656 end
657 657
658 658 def test_filter_on_relations_with_any_issues_in_a_project
659 659 IssueRelation.delete_all
660 660 with_settings :cross_project_issue_relations => '1' do
661 661 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
662 662 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
663 663 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
664 664 end
665 665
666 query = Query.new(:name => '_')
666 query = IssueQuery.new(:name => '_')
667 667 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
668 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 671 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
672 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 675 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
676 676 assert_equal [], find_issues_with_query(query).map(&:id).sort
677 677 end
678 678
679 679 def test_filter_on_relations_with_any_issues_not_in_a_project
680 680 IssueRelation.delete_all
681 681 with_settings :cross_project_issue_relations => '1' do
682 682 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
683 683 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
684 684 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
685 685 end
686 686
687 query = Query.new(:name => '_')
687 query = IssueQuery.new(:name => '_')
688 688 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
689 689 assert_equal [1], find_issues_with_query(query).map(&:id).sort
690 690 end
691 691
692 692 def test_filter_on_relations_with_no_issues_in_a_project
693 693 IssueRelation.delete_all
694 694 with_settings :cross_project_issue_relations => '1' do
695 695 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
696 696 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
697 697 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
698 698 end
699 699
700 query = Query.new(:name => '_')
700 query = IssueQuery.new(:name => '_')
701 701 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
702 702 ids = find_issues_with_query(query).map(&:id).sort
703 703 assert_include 2, ids
704 704 assert_not_include 1, ids
705 705 assert_not_include 3, ids
706 706 end
707 707
708 708 def test_filter_on_relations_with_no_issues
709 709 IssueRelation.delete_all
710 710 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
711 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 714 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
715 715 ids = find_issues_with_query(query).map(&:id)
716 716 assert_equal [], ids & [1, 2, 3]
717 717 assert_include 4, ids
718 718 end
719 719
720 720 def test_filter_on_relations_with_any_issues
721 721 IssueRelation.delete_all
722 722 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
723 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 726 query.filters = {"relates" => {:operator => '*', :values => ['']}}
727 727 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
728 728 end
729 729
730 730 def test_statement_should_be_nil_with_no_filters
731 q = Query.new(:name => '_')
731 q = IssueQuery.new(:name => '_')
732 732 q.filters = {}
733 733
734 734 assert q.valid?
735 735 assert_nil q.statement
736 736 end
737 737
738 738 def test_default_columns
739 q = Query.new
739 q = IssueQuery.new
740 740 assert q.columns.any?
741 741 assert q.inline_columns.any?
742 742 assert q.block_columns.empty?
743 743 end
744 744
745 745 def test_set_column_names
746 q = Query.new
746 q = IssueQuery.new
747 747 q.column_names = ['tracker', :subject, '', 'unknonw_column']
748 748 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
749 749 c = q.columns.first
750 750 assert q.has_column?(c)
751 751 end
752 752
753 753 def test_inline_and_block_columns
754 q = Query.new
754 q = IssueQuery.new
755 755 q.column_names = ['subject', 'description', 'tracker']
756 756
757 757 assert_equal [:subject, :tracker], q.inline_columns.map(&:name)
758 758 assert_equal [:description], q.block_columns.map(&:name)
759 759 end
760 760
761 761 def test_custom_field_columns_should_be_inline
762 q = Query.new
762 q = IssueQuery.new
763 763 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
764 764 assert columns.any?
765 765 assert_nil columns.detect {|column| !column.inline?}
766 766 end
767 767
768 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 770 assert q.has_column?(:spent_hours)
771 771 issues = q.issues
772 772 assert_not_nil issues.first.instance_variable_get("@spent_hours")
773 773 end
774 774
775 775 def test_groupable_columns_should_include_custom_fields
776 q = Query.new
776 q = IssueQuery.new
777 777 column = q.groupable_columns.detect {|c| c.name == :cf_1}
778 778 assert_not_nil column
779 779 assert_kind_of QueryCustomFieldColumn, column
780 780 end
781 781
782 782 def test_groupable_columns_should_not_include_multi_custom_fields
783 783 field = CustomField.find(1)
784 784 field.update_attribute :multiple, true
785 785
786 q = Query.new
786 q = IssueQuery.new
787 787 column = q.groupable_columns.detect {|c| c.name == :cf_1}
788 788 assert_nil column
789 789 end
790 790
791 791 def test_groupable_columns_should_include_user_custom_fields
792 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 795 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
796 796 end
797 797
798 798 def test_groupable_columns_should_include_version_custom_fields
799 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 802 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
803 803 end
804 804
805 805 def test_grouped_with_valid_column
806 q = Query.new(:group_by => 'status')
806 q = IssueQuery.new(:group_by => 'status')
807 807 assert q.grouped?
808 808 assert_not_nil q.group_by_column
809 809 assert_equal :status, q.group_by_column.name
810 810 assert_not_nil q.group_by_statement
811 811 assert_equal 'status', q.group_by_statement
812 812 end
813 813
814 814 def test_grouped_with_invalid_column
815 q = Query.new(:group_by => 'foo')
815 q = IssueQuery.new(:group_by => 'foo')
816 816 assert !q.grouped?
817 817 assert_nil q.group_by_column
818 818 assert_nil q.group_by_statement
819 819 end
820 820
821 821 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
822 822 with_settings :user_format => 'lastname_coma_firstname' do
823 q = Query.new
823 q = IssueQuery.new
824 824 assert q.sortable_columns.has_key?('assigned_to')
825 825 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
826 826 end
827 827 end
828 828
829 829 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
830 830 with_settings :user_format => 'lastname_coma_firstname' do
831 q = Query.new
831 q = IssueQuery.new
832 832 assert q.sortable_columns.has_key?('author')
833 833 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
834 834 end
835 835 end
836 836
837 837 def test_sortable_columns_should_include_custom_field
838 q = Query.new
838 q = IssueQuery.new
839 839 assert q.sortable_columns['cf_1']
840 840 end
841 841
842 842 def test_sortable_columns_should_not_include_multi_custom_field
843 843 field = CustomField.find(1)
844 844 field.update_attribute :multiple, true
845 845
846 q = Query.new
846 q = IssueQuery.new
847 847 assert !q.sortable_columns['cf_1']
848 848 end
849 849
850 850 def test_default_sort
851 q = Query.new
851 q = IssueQuery.new
852 852 assert_equal [], q.sort_criteria
853 853 end
854 854
855 855 def test_set_sort_criteria_with_hash
856 q = Query.new
856 q = IssueQuery.new
857 857 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
858 858 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
859 859 end
860 860
861 861 def test_set_sort_criteria_with_array
862 q = Query.new
862 q = IssueQuery.new
863 863 q.sort_criteria = [['priority', 'desc'], 'tracker']
864 864 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
865 865 end
866 866
867 867 def test_create_query_with_sort
868 q = Query.new(:name => 'Sorted')
868 q = IssueQuery.new(:name => 'Sorted')
869 869 q.sort_criteria = [['priority', 'desc'], 'tracker']
870 870 assert q.save
871 871 q.reload
872 872 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
873 873 end
874 874
875 875 def test_sort_by_string_custom_field_asc
876 q = Query.new
876 q = IssueQuery.new
877 877 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
878 878 assert c
879 879 assert c.sortable
880 880 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
881 881 q.statement
882 882 ).order("#{c.sortable} ASC").all
883 883 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
884 884 assert !values.empty?
885 885 assert_equal values.sort, values
886 886 end
887 887
888 888 def test_sort_by_string_custom_field_desc
889 q = Query.new
889 q = IssueQuery.new
890 890 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
891 891 assert c
892 892 assert c.sortable
893 893 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
894 894 q.statement
895 895 ).order("#{c.sortable} DESC").all
896 896 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
897 897 assert !values.empty?
898 898 assert_equal values.sort.reverse, values
899 899 end
900 900
901 901 def test_sort_by_float_custom_field_asc
902 q = Query.new
902 q = IssueQuery.new
903 903 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
904 904 assert c
905 905 assert c.sortable
906 906 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
907 907 q.statement
908 908 ).order("#{c.sortable} ASC").all
909 909 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
910 910 assert !values.empty?
911 911 assert_equal values.sort, values
912 912 end
913 913
914 914 def test_invalid_query_should_raise_query_statement_invalid_error
915 q = Query.new
915 q = IssueQuery.new
916 916 assert_raise Query::StatementInvalid do
917 917 q.issues(:conditions => "foo = 1")
918 918 end
919 919 end
920 920
921 921 def test_issue_count
922 q = Query.new(:name => '_')
922 q = IssueQuery.new(:name => '_')
923 923 issue_count = q.issue_count
924 924 assert_equal q.issues.size, issue_count
925 925 end
926 926
927 927 def test_issue_count_with_archived_issues
928 928 p = Project.generate! do |project|
929 929 project.status = Project::STATUS_ARCHIVED
930 930 end
931 931 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
932 932 assert !i.visible?
933 933
934 934 test_issue_count
935 935 end
936 936
937 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 939 count_by_group = q.issue_count_by_group
940 940 assert_kind_of Hash, count_by_group
941 941 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
942 942 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
943 943 assert count_by_group.has_key?(User.find(3))
944 944 end
945 945
946 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 948 count_by_group = q.issue_count_by_group
949 949 assert_kind_of Hash, count_by_group
950 950 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
951 951 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
952 952 assert count_by_group.has_key?('MySQL')
953 953 end
954 954
955 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 957 count_by_group = q.issue_count_by_group
958 958 assert_kind_of Hash, count_by_group
959 959 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
960 960 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
961 961 end
962 962
963 963 def test_issue_count_with_nil_group_only
964 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 967 count_by_group = q.issue_count_by_group
968 968 assert_kind_of Hash, count_by_group
969 969 assert_equal 1, count_by_group.keys.size
970 970 assert_nil count_by_group.keys.first
971 971 end
972 972
973 973 def test_issue_ids
974 q = Query.new(:name => '_')
974 q = IssueQuery.new(:name => '_')
975 975 order = "issues.subject, issues.id"
976 976 issues = q.issues(:order => order)
977 977 assert_equal issues.map(&:id), q.issue_ids(:order => order)
978 978 end
979 979
980 980 def test_label_for
981 981 set_language_if_valid 'en'
982 q = Query.new
982 q = IssueQuery.new
983 983 assert_equal 'Assignee', q.label_for('assigned_to_id')
984 984 end
985 985
986 986 def test_label_for_fr
987 987 set_language_if_valid 'fr'
988 q = Query.new
988 q = IssueQuery.new
989 989 s = "Assign\xc3\xa9 \xc3\xa0"
990 990 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
991 991 assert_equal s, q.label_for('assigned_to_id')
992 992 end
993 993
994 994 def test_editable_by
995 995 admin = User.find(1)
996 996 manager = User.find(2)
997 997 developer = User.find(3)
998 998
999 999 # Public query on project 1
1000 q = Query.find(1)
1000 q = IssueQuery.find(1)
1001 1001 assert q.editable_by?(admin)
1002 1002 assert q.editable_by?(manager)
1003 1003 assert !q.editable_by?(developer)
1004 1004
1005 1005 # Private query on project 1
1006 q = Query.find(2)
1006 q = IssueQuery.find(2)
1007 1007 assert q.editable_by?(admin)
1008 1008 assert !q.editable_by?(manager)
1009 1009 assert q.editable_by?(developer)
1010 1010
1011 1011 # Private query for all projects
1012 q = Query.find(3)
1012 q = IssueQuery.find(3)
1013 1013 assert q.editable_by?(admin)
1014 1014 assert !q.editable_by?(manager)
1015 1015 assert q.editable_by?(developer)
1016 1016
1017 1017 # Public query for all projects
1018 q = Query.find(4)
1018 q = IssueQuery.find(4)
1019 1019 assert q.editable_by?(admin)
1020 1020 assert !q.editable_by?(manager)
1021 1021 assert !q.editable_by?(developer)
1022 1022 end
1023 1023
1024 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 1027 assert query_ids.include?(1), 'public query on public project was not visible'
1028 1028 assert query_ids.include?(4), 'public query for all projects was not visible'
1029 1029 assert !query_ids.include?(2), 'private query on public project was visible'
1030 1030 assert !query_ids.include?(3), 'private query for all projects was visible'
1031 1031 assert !query_ids.include?(7), 'public query on private project was visible'
1032 1032 end
1033 1033
1034 1034 context "#available_filters" do
1035 1035 setup do
1036 @query = Query.new(:name => "_")
1036 @query = IssueQuery.new(:name => "_")
1037 1037 end
1038 1038
1039 1039 should "include users of visible projects in cross-project view" do
1040 1040 users = @query.available_filters["assigned_to_id"]
1041 1041 assert_not_nil users
1042 1042 assert users[:values].map{|u|u[1]}.include?("3")
1043 1043 end
1044 1044
1045 1045 should "include users of subprojects" do
1046 1046 user1 = User.generate!
1047 1047 user2 = User.generate!
1048 1048 project = Project.find(1)
1049 1049 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1050 1050 @query.project = project
1051 1051
1052 1052 users = @query.available_filters["assigned_to_id"]
1053 1053 assert_not_nil users
1054 1054 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1055 1055 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1056 1056 end
1057 1057
1058 1058 should "include visible projects in cross-project view" do
1059 1059 projects = @query.available_filters["project_id"]
1060 1060 assert_not_nil projects
1061 1061 assert projects[:values].map{|u|u[1]}.include?("1")
1062 1062 end
1063 1063
1064 1064 context "'member_of_group' filter" do
1065 1065 should "be present" do
1066 1066 assert @query.available_filters.keys.include?("member_of_group")
1067 1067 end
1068 1068
1069 1069 should "be an optional list" do
1070 1070 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
1071 1071 end
1072 1072
1073 1073 should "have a list of the groups as values" do
1074 1074 Group.destroy_all # No fixtures
1075 1075 group1 = Group.generate!.reload
1076 1076 group2 = Group.generate!.reload
1077 1077
1078 1078 expected_group_list = [
1079 1079 [group1.name, group1.id.to_s],
1080 1080 [group2.name, group2.id.to_s]
1081 1081 ]
1082 1082 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
1083 1083 end
1084 1084
1085 1085 end
1086 1086
1087 1087 context "'assigned_to_role' filter" do
1088 1088 should "be present" do
1089 1089 assert @query.available_filters.keys.include?("assigned_to_role")
1090 1090 end
1091 1091
1092 1092 should "be an optional list" do
1093 1093 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
1094 1094 end
1095 1095
1096 1096 should "have a list of the Roles as values" do
1097 1097 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1098 1098 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1099 1099 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1100 1100 end
1101 1101
1102 1102 should "not include the built in Roles as values" do
1103 1103 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1104 1104 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1105 1105 end
1106 1106
1107 1107 end
1108 1108
1109 1109 end
1110 1110
1111 1111 context "#statement" do
1112 1112 context "with 'member_of_group' filter" do
1113 1113 setup do
1114 1114 Group.destroy_all # No fixtures
1115 1115 @user_in_group = User.generate!
1116 1116 @second_user_in_group = User.generate!
1117 1117 @user_in_group2 = User.generate!
1118 1118 @user_not_in_group = User.generate!
1119 1119
1120 1120 @group = Group.generate!.reload
1121 1121 @group.users << @user_in_group
1122 1122 @group.users << @second_user_in_group
1123 1123
1124 1124 @group2 = Group.generate!.reload
1125 1125 @group2.users << @user_in_group2
1126 1126
1127 1127 end
1128 1128
1129 1129 should "search assigned to for users in the group" do
1130 @query = Query.new(:name => '_')
1130 @query = IssueQuery.new(:name => '_')
1131 1131 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1132 1132
1133 1133 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
1134 1134 assert_find_issues_with_query_is_successful @query
1135 1135 end
1136 1136
1137 1137 should "search not assigned to any group member (none)" do
1138 @query = Query.new(:name => '_')
1138 @query = IssueQuery.new(:name => '_')
1139 1139 @query.add_filter('member_of_group', '!*', [''])
1140 1140
1141 1141 # Users not in a group
1142 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 1143 assert_find_issues_with_query_is_successful @query
1144 1144 end
1145 1145
1146 1146 should "search assigned to any group member (all)" do
1147 @query = Query.new(:name => '_')
1147 @query = IssueQuery.new(:name => '_')
1148 1148 @query.add_filter('member_of_group', '*', [''])
1149 1149
1150 1150 # Only users in a group
1151 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 1152 assert_find_issues_with_query_is_successful @query
1153 1153 end
1154 1154
1155 1155 should "return an empty set with = empty group" do
1156 1156 @empty_group = Group.generate!
1157 @query = Query.new(:name => '_')
1157 @query = IssueQuery.new(:name => '_')
1158 1158 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1159 1159
1160 1160 assert_equal [], find_issues_with_query(@query)
1161 1161 end
1162 1162
1163 1163 should "return issues with ! empty group" do
1164 1164 @empty_group = Group.generate!
1165 @query = Query.new(:name => '_')
1165 @query = IssueQuery.new(:name => '_')
1166 1166 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1167 1167
1168 1168 assert_find_issues_with_query_is_successful @query
1169 1169 end
1170 1170 end
1171 1171
1172 1172 context "with 'assigned_to_role' filter" do
1173 1173 setup do
1174 1174 @manager_role = Role.find_by_name('Manager')
1175 1175 @developer_role = Role.find_by_name('Developer')
1176 1176
1177 1177 @project = Project.generate!
1178 1178 @manager = User.generate!
1179 1179 @developer = User.generate!
1180 1180 @boss = User.generate!
1181 1181 @guest = User.generate!
1182 1182 User.add_to_project(@manager, @project, @manager_role)
1183 1183 User.add_to_project(@developer, @project, @developer_role)
1184 1184 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1185 1185
1186 1186 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1187 1187 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1188 1188 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1189 1189 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1190 1190 @issue5 = Issue.generate!(:project => @project)
1191 1191 end
1192 1192
1193 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 1195 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1196 1196
1197 1197 assert_query_result [@issue1, @issue3], @query
1198 1198 end
1199 1199
1200 1200 should "search assigned to for users with the Role on the issue project" do
1201 1201 other_project = Project.generate!
1202 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 1205 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1206 1206
1207 1207 assert_query_result [@issue1, @issue3], @query
1208 1208 end
1209 1209
1210 1210 should "return an empty set with empty role" do
1211 1211 @empty_role = Role.generate!
1212 @query = Query.new(:name => '_', :project => @project)
1212 @query = IssueQuery.new(:name => '_', :project => @project)
1213 1213 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1214 1214
1215 1215 assert_query_result [], @query
1216 1216 end
1217 1217
1218 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 1220 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1221 1221
1222 1222 assert_query_result [@issue2, @issue4, @issue5], @query
1223 1223 end
1224 1224
1225 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 1227 @query.add_filter('assigned_to_role', '!*', [''])
1228 1228
1229 1229 assert_query_result [@issue4, @issue5], @query
1230 1230 end
1231 1231
1232 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 1234 @query.add_filter('assigned_to_role', '*', [''])
1235 1235
1236 1236 assert_query_result [@issue1, @issue2, @issue3], @query
1237 1237 end
1238 1238
1239 1239 should "return issues with ! empty role" do
1240 1240 @empty_role = Role.generate!
1241 @query = Query.new(:name => '_', :project => @project)
1241 @query = IssueQuery.new(:name => '_', :project => @project)
1242 1242 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1243 1243
1244 1244 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1245 1245 end
1246 1246 end
1247 1247 end
1248
1249 1248 end
General Comments 0
You need to be logged in to leave comments. Login now