##// END OF EJS Templates
Adds an option for displaying the issue description on the issue list (#3447)....
Jean-Philippe Lang -
r10721:8201761e7732
parent child
Show More

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

@@ -1,407 +1,410
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def issue_list(issues, &block)
23 def issue_list(issues, &block)
24 ancestors = []
24 ancestors = []
25 issues.each do |issue|
25 issues.each do |issue|
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
26 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 ancestors.pop
27 ancestors.pop
28 end
28 end
29 yield issue, ancestors.size
29 yield issue, ancestors.size
30 ancestors << issue unless issue.leaf?
30 ancestors << issue unless issue.leaf?
31 end
31 end
32 end
32 end
33
33
34 # Renders a HTML/CSS tooltip
34 # Renders a HTML/CSS tooltip
35 #
35 #
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
36 # To use, a trigger div is needed. This is a div with the class of "tooltip"
37 # that contains this method wrapped in a span with the class of "tip"
37 # that contains this method wrapped in a span with the class of "tip"
38 #
38 #
39 # <div class="tooltip"><%= link_to_issue(issue) %>
39 # <div class="tooltip"><%= link_to_issue(issue) %>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
40 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
41 # </div>
41 # </div>
42 #
42 #
43 def render_issue_tooltip(issue)
43 def render_issue_tooltip(issue)
44 @cached_label_status ||= l(:field_status)
44 @cached_label_status ||= l(:field_status)
45 @cached_label_start_date ||= l(:field_start_date)
45 @cached_label_start_date ||= l(:field_start_date)
46 @cached_label_due_date ||= l(:field_due_date)
46 @cached_label_due_date ||= l(:field_due_date)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
47 @cached_label_assigned_to ||= l(:field_assigned_to)
48 @cached_label_priority ||= l(:field_priority)
48 @cached_label_priority ||= l(:field_priority)
49 @cached_label_project ||= l(:field_project)
49 @cached_label_project ||= l(:field_project)
50
50
51 link_to_issue(issue) + "<br /><br />".html_safe +
51 link_to_issue(issue) + "<br /><br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
52 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
53 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
54 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
55 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
56 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
57 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
58 end
58 end
59
59
60 def issue_heading(issue)
60 def issue_heading(issue)
61 h("#{issue.tracker} ##{issue.id}")
61 h("#{issue.tracker} ##{issue.id}")
62 end
62 end
63
63
64 def render_issue_subject_with_tree(issue)
64 def render_issue_subject_with_tree(issue)
65 s = ''
65 s = ''
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
66 ancestors = issue.root? ? [] : issue.ancestors.visible.all
67 ancestors.each do |ancestor|
67 ancestors.each do |ancestor|
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
68 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
69 end
69 end
70 s << '<div>'
70 s << '<div>'
71 subject = h(issue.subject)
71 subject = h(issue.subject)
72 if issue.is_private?
72 if issue.is_private?
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
73 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
74 end
74 end
75 s << content_tag('h3', subject)
75 s << content_tag('h3', subject)
76 s << '</div>' * (ancestors.size + 1)
76 s << '</div>' * (ancestors.size + 1)
77 s.html_safe
77 s.html_safe
78 end
78 end
79
79
80 def render_descendants_tree(issue)
80 def render_descendants_tree(issue)
81 s = '<form><table class="list issues">'
81 s = '<form><table class="list issues">'
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
82 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
83 css = "issue issue-#{child.id} hascontextmenu"
83 css = "issue issue-#{child.id} hascontextmenu"
84 css << " idnt idnt-#{level}" if level > 0
84 css << " idnt idnt-#{level}" if level > 0
85 s << content_tag('tr',
85 s << content_tag('tr',
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
86 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
87 content_tag('td', link_to_issue(child, :truncate => 60, :project => (issue.project_id != child.project_id)), :class => 'subject') +
88 content_tag('td', h(child.status)) +
88 content_tag('td', h(child.status)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
89 content_tag('td', link_to_user(child.assigned_to)) +
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
90 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
91 :class => css)
91 :class => css)
92 end
92 end
93 s << '</table></form>'
93 s << '</table></form>'
94 s.html_safe
94 s.html_safe
95 end
95 end
96
96
97 # Returns a link for adding a new subtask to the given issue
97 # Returns a link for adding a new subtask to the given issue
98 def link_to_new_subtask(issue)
98 def link_to_new_subtask(issue)
99 attrs = {
99 attrs = {
100 :tracker_id => issue.tracker,
100 :tracker_id => issue.tracker,
101 :parent_issue_id => issue
101 :parent_issue_id => issue
102 }
102 }
103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
103 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
104 end
104 end
105
105
106 class IssueFieldsRows
106 class IssueFieldsRows
107 include ActionView::Helpers::TagHelper
107 include ActionView::Helpers::TagHelper
108
108
109 def initialize
109 def initialize
110 @left = []
110 @left = []
111 @right = []
111 @right = []
112 end
112 end
113
113
114 def left(*args)
114 def left(*args)
115 args.any? ? @left << cells(*args) : @left
115 args.any? ? @left << cells(*args) : @left
116 end
116 end
117
117
118 def right(*args)
118 def right(*args)
119 args.any? ? @right << cells(*args) : @right
119 args.any? ? @right << cells(*args) : @right
120 end
120 end
121
121
122 def size
122 def size
123 @left.size > @right.size ? @left.size : @right.size
123 @left.size > @right.size ? @left.size : @right.size
124 end
124 end
125
125
126 def to_html
126 def to_html
127 html = ''.html_safe
127 html = ''.html_safe
128 blank = content_tag('th', '') + content_tag('td', '')
128 blank = content_tag('th', '') + content_tag('td', '')
129 size.times do |i|
129 size.times do |i|
130 left = @left[i] || blank
130 left = @left[i] || blank
131 right = @right[i] || blank
131 right = @right[i] || blank
132 html << content_tag('tr', left + right)
132 html << content_tag('tr', left + right)
133 end
133 end
134 html
134 html
135 end
135 end
136
136
137 def cells(label, text, options={})
137 def cells(label, text, options={})
138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
138 content_tag('th', "#{label}:", options) + content_tag('td', text, options)
139 end
139 end
140 end
140 end
141
141
142 def issue_fields_rows
142 def issue_fields_rows
143 r = IssueFieldsRows.new
143 r = IssueFieldsRows.new
144 yield r
144 yield r
145 r.to_html
145 r.to_html
146 end
146 end
147
147
148 def render_custom_fields_rows(issue)
148 def render_custom_fields_rows(issue)
149 return if issue.custom_field_values.empty?
149 return if issue.custom_field_values.empty?
150 ordered_values = []
150 ordered_values = []
151 half = (issue.custom_field_values.size / 2.0).ceil
151 half = (issue.custom_field_values.size / 2.0).ceil
152 half.times do |i|
152 half.times do |i|
153 ordered_values << issue.custom_field_values[i]
153 ordered_values << issue.custom_field_values[i]
154 ordered_values << issue.custom_field_values[i + half]
154 ordered_values << issue.custom_field_values[i + half]
155 end
155 end
156 s = "<tr>\n"
156 s = "<tr>\n"
157 n = 0
157 n = 0
158 ordered_values.compact.each do |value|
158 ordered_values.compact.each do |value|
159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
159 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
160 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
161 n += 1
161 n += 1
162 end
162 end
163 s << "</tr>\n"
163 s << "</tr>\n"
164 s.html_safe
164 s.html_safe
165 end
165 end
166
166
167 def issues_destroy_confirmation_message(issues)
167 def issues_destroy_confirmation_message(issues)
168 issues = [issues] unless issues.is_a?(Array)
168 issues = [issues] unless issues.is_a?(Array)
169 message = l(:text_issues_destroy_confirmation)
169 message = l(:text_issues_destroy_confirmation)
170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
170 descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
171 if descendant_count > 0
171 if descendant_count > 0
172 issues.each do |issue|
172 issues.each do |issue|
173 next if issue.root?
173 next if issue.root?
174 issues.each do |other_issue|
174 issues.each do |other_issue|
175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
175 descendant_count -= 1 if issue.is_descendant_of?(other_issue)
176 end
176 end
177 end
177 end
178 if descendant_count > 0
178 if descendant_count > 0
179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
179 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
180 end
180 end
181 end
181 end
182 message
182 message
183 end
183 end
184
184
185 def sidebar_queries
185 def sidebar_queries
186 unless @sidebar_queries
186 unless @sidebar_queries
187 @sidebar_queries = Query.visible.all(
187 @sidebar_queries = Query.visible.all(
188 :order => "#{Query.table_name}.name ASC",
188 :order => "#{Query.table_name}.name ASC",
189 # Project specific queries and global queries
189 # Project specific queries and global queries
190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
190 :conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
191 )
191 )
192 end
192 end
193 @sidebar_queries
193 @sidebar_queries
194 end
194 end
195
195
196 def query_links(title, queries)
196 def query_links(title, queries)
197 # links to #index on issues/show
197 # links to #index on issues/show
198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
198 url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
199
199
200 content_tag('h3', h(title)) +
200 content_tag('h3', h(title)) +
201 queries.collect {|query|
201 queries.collect {|query|
202 css = 'query'
202 css = 'query'
203 css << ' selected' if query == @query
203 css << ' selected' if query == @query
204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
204 link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
205 }.join('<br />').html_safe
205 }.join('<br />').html_safe
206 end
206 end
207
207
208 def render_sidebar_queries
208 def render_sidebar_queries
209 out = ''.html_safe
209 out = ''.html_safe
210 queries = sidebar_queries.select {|q| !q.is_public?}
210 queries = sidebar_queries.select {|q| !q.is_public?}
211 out << query_links(l(:label_my_queries), queries) if queries.any?
211 out << query_links(l(:label_my_queries), queries) if queries.any?
212 queries = sidebar_queries.select {|q| q.is_public?}
212 queries = sidebar_queries.select {|q| q.is_public?}
213 out << query_links(l(:label_query_plural), queries) if queries.any?
213 out << query_links(l(:label_query_plural), queries) if queries.any?
214 out
214 out
215 end
215 end
216
216
217 # Returns the textual representation of a journal details
217 # Returns the textual representation of a journal details
218 # as an array of strings
218 # as an array of strings
219 def details_to_strings(details, no_html=false, options={})
219 def details_to_strings(details, no_html=false, options={})
220 options[:only_path] = (options[:only_path] == false ? false : true)
220 options[:only_path] = (options[:only_path] == false ? false : true)
221 strings = []
221 strings = []
222 values_by_field = {}
222 values_by_field = {}
223 details.each do |detail|
223 details.each do |detail|
224 if detail.property == 'cf'
224 if detail.property == 'cf'
225 field_id = detail.prop_key
225 field_id = detail.prop_key
226 field = CustomField.find_by_id(field_id)
226 field = CustomField.find_by_id(field_id)
227 if field && field.multiple?
227 if field && field.multiple?
228 values_by_field[field_id] ||= {:added => [], :deleted => []}
228 values_by_field[field_id] ||= {:added => [], :deleted => []}
229 if detail.old_value
229 if detail.old_value
230 values_by_field[field_id][:deleted] << detail.old_value
230 values_by_field[field_id][:deleted] << detail.old_value
231 end
231 end
232 if detail.value
232 if detail.value
233 values_by_field[field_id][:added] << detail.value
233 values_by_field[field_id][:added] << detail.value
234 end
234 end
235 next
235 next
236 end
236 end
237 end
237 end
238 strings << show_detail(detail, no_html, options)
238 strings << show_detail(detail, no_html, options)
239 end
239 end
240 values_by_field.each do |field_id, changes|
240 values_by_field.each do |field_id, changes|
241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
241 detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
242 if changes[:added].any?
242 if changes[:added].any?
243 detail.value = changes[:added]
243 detail.value = changes[:added]
244 strings << show_detail(detail, no_html, options)
244 strings << show_detail(detail, no_html, options)
245 elsif changes[:deleted].any?
245 elsif changes[:deleted].any?
246 detail.old_value = changes[:deleted]
246 detail.old_value = changes[:deleted]
247 strings << show_detail(detail, no_html, options)
247 strings << show_detail(detail, no_html, options)
248 end
248 end
249 end
249 end
250 strings
250 strings
251 end
251 end
252
252
253 # Returns the textual representation of a single journal detail
253 # Returns the textual representation of a single journal detail
254 def show_detail(detail, no_html=false, options={})
254 def show_detail(detail, no_html=false, options={})
255 multiple = false
255 multiple = false
256 case detail.property
256 case detail.property
257 when 'attr'
257 when 'attr'
258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
258 field = detail.prop_key.to_s.gsub(/\_id$/, "")
259 label = l(("field_" + field).to_sym)
259 label = l(("field_" + field).to_sym)
260 case detail.prop_key
260 case detail.prop_key
261 when 'due_date', 'start_date'
261 when 'due_date', 'start_date'
262 value = format_date(detail.value.to_date) if detail.value
262 value = format_date(detail.value.to_date) if detail.value
263 old_value = format_date(detail.old_value.to_date) if detail.old_value
263 old_value = format_date(detail.old_value.to_date) if detail.old_value
264
264
265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
265 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
266 'priority_id', 'category_id', 'fixed_version_id'
266 'priority_id', 'category_id', 'fixed_version_id'
267 value = find_name_by_reflection(field, detail.value)
267 value = find_name_by_reflection(field, detail.value)
268 old_value = find_name_by_reflection(field, detail.old_value)
268 old_value = find_name_by_reflection(field, detail.old_value)
269
269
270 when 'estimated_hours'
270 when 'estimated_hours'
271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
271 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
272 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
273
273
274 when 'parent_id'
274 when 'parent_id'
275 label = l(:field_parent_issue)
275 label = l(:field_parent_issue)
276 value = "##{detail.value}" unless detail.value.blank?
276 value = "##{detail.value}" unless detail.value.blank?
277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
277 old_value = "##{detail.old_value}" unless detail.old_value.blank?
278
278
279 when 'is_private'
279 when 'is_private'
280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
280 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
281 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
282 end
282 end
283 when 'cf'
283 when 'cf'
284 custom_field = CustomField.find_by_id(detail.prop_key)
284 custom_field = CustomField.find_by_id(detail.prop_key)
285 if custom_field
285 if custom_field
286 multiple = custom_field.multiple?
286 multiple = custom_field.multiple?
287 label = custom_field.name
287 label = custom_field.name
288 value = format_value(detail.value, custom_field.field_format) if detail.value
288 value = format_value(detail.value, custom_field.field_format) if detail.value
289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
289 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
290 end
290 end
291 when 'attachment'
291 when 'attachment'
292 label = l(:label_attachment)
292 label = l(:label_attachment)
293 end
293 end
294 call_hook(:helper_issues_show_detail_after_setting,
294 call_hook(:helper_issues_show_detail_after_setting,
295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
295 {:detail => detail, :label => label, :value => value, :old_value => old_value })
296
296
297 label ||= detail.prop_key
297 label ||= detail.prop_key
298 value ||= detail.value
298 value ||= detail.value
299 old_value ||= detail.old_value
299 old_value ||= detail.old_value
300
300
301 unless no_html
301 unless no_html
302 label = content_tag('strong', label)
302 label = content_tag('strong', label)
303 old_value = content_tag("i", h(old_value)) if detail.old_value
303 old_value = content_tag("i", h(old_value)) if detail.old_value
304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
304 old_value = content_tag("del", old_value) if detail.old_value and detail.value.blank?
305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
305 if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
306 # Link to the attachment if it has not been removed
306 # Link to the attachment if it has not been removed
307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
307 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
308 if options[:only_path] != false && atta.is_text?
308 if options[:only_path] != false && atta.is_text?
309 value += link_to(
309 value += link_to(
310 image_tag('magnifier.png'),
310 image_tag('magnifier.png'),
311 :controller => 'attachments', :action => 'show',
311 :controller => 'attachments', :action => 'show',
312 :id => atta, :filename => atta.filename
312 :id => atta, :filename => atta.filename
313 )
313 )
314 end
314 end
315 else
315 else
316 value = content_tag("i", h(value)) if value
316 value = content_tag("i", h(value)) if value
317 end
317 end
318 end
318 end
319
319
320 if detail.property == 'attr' && detail.prop_key == 'description'
320 if detail.property == 'attr' && detail.prop_key == 'description'
321 s = l(:text_journal_changed_no_detail, :label => label)
321 s = l(:text_journal_changed_no_detail, :label => label)
322 unless no_html
322 unless no_html
323 diff_link = link_to 'diff',
323 diff_link = link_to 'diff',
324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
324 {:controller => 'journals', :action => 'diff', :id => detail.journal_id,
325 :detail_id => detail.id, :only_path => options[:only_path]},
325 :detail_id => detail.id, :only_path => options[:only_path]},
326 :title => l(:label_view_diff)
326 :title => l(:label_view_diff)
327 s << " (#{ diff_link })"
327 s << " (#{ diff_link })"
328 end
328 end
329 s.html_safe
329 s.html_safe
330 elsif detail.value.present?
330 elsif detail.value.present?
331 case detail.property
331 case detail.property
332 when 'attr', 'cf'
332 when 'attr', 'cf'
333 if detail.old_value.present?
333 if detail.old_value.present?
334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
334 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
335 elsif multiple
335 elsif multiple
336 l(:text_journal_added, :label => label, :value => value).html_safe
336 l(:text_journal_added, :label => label, :value => value).html_safe
337 else
337 else
338 l(:text_journal_set_to, :label => label, :value => value).html_safe
338 l(:text_journal_set_to, :label => label, :value => value).html_safe
339 end
339 end
340 when 'attachment'
340 when 'attachment'
341 l(:text_journal_added, :label => label, :value => value).html_safe
341 l(:text_journal_added, :label => label, :value => value).html_safe
342 end
342 end
343 else
343 else
344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
344 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
345 end
345 end
346 end
346 end
347
347
348 # Find the name of an associated record stored in the field attribute
348 # Find the name of an associated record stored in the field attribute
349 def find_name_by_reflection(field, id)
349 def find_name_by_reflection(field, id)
350 association = Issue.reflect_on_association(field.to_sym)
350 association = Issue.reflect_on_association(field.to_sym)
351 if association
351 if association
352 record = association.class_name.constantize.find_by_id(id)
352 record = association.class_name.constantize.find_by_id(id)
353 return record.name if record
353 return record.name if record
354 end
354 end
355 end
355 end
356
356
357 # Renders issue children recursively
357 # Renders issue children recursively
358 def render_api_issue_children(issue, api)
358 def render_api_issue_children(issue, api)
359 return if issue.leaf?
359 return if issue.leaf?
360 api.array :children do
360 api.array :children do
361 issue.children.each do |child|
361 issue.children.each do |child|
362 api.issue(:id => child.id) do
362 api.issue(:id => child.id) do
363 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
363 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
364 api.subject child.subject
364 api.subject child.subject
365 render_api_issue_children(child, api)
365 render_api_issue_children(child, api)
366 end
366 end
367 end
367 end
368 end
368 end
369 end
369 end
370
370
371 def issues_to_csv(issues, project, query, options={})
371 def issues_to_csv(issues, project, query, options={})
372 decimal_separator = l(:general_csv_decimal_separator)
372 decimal_separator = l(:general_csv_decimal_separator)
373 encoding = l(:general_csv_encoding)
373 encoding = l(:general_csv_encoding)
374 columns = (options[:columns] == 'all' ? query.available_columns : query.columns)
374 columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns)
375 if options[:description]
376 if description = query.available_columns.detect {|q| q.name == :description}
377 columns << description
378 end
379 end
375
380
376 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
381 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
377 # csv header fields
382 # csv header fields
378 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } +
383 csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) }
379 (options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : [])
380
384
381 # csv lines
385 # csv lines
382 issues.each do |issue|
386 issues.each do |issue|
383 col_values = columns.collect do |column|
387 col_values = columns.collect do |column|
384 s = if column.is_a?(QueryCustomFieldColumn)
388 s = if column.is_a?(QueryCustomFieldColumn)
385 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
389 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
386 show_value(cv)
390 show_value(cv)
387 else
391 else
388 value = column.value(issue)
392 value = column.value(issue)
389 if value.is_a?(Date)
393 if value.is_a?(Date)
390 format_date(value)
394 format_date(value)
391 elsif value.is_a?(Time)
395 elsif value.is_a?(Time)
392 format_time(value)
396 format_time(value)
393 elsif value.is_a?(Float)
397 elsif value.is_a?(Float)
394 ("%.2f" % value).gsub('.', decimal_separator)
398 ("%.2f" % value).gsub('.', decimal_separator)
395 else
399 else
396 value
400 value
397 end
401 end
398 end
402 end
399 s.to_s
403 s.to_s
400 end
404 end
401 csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } +
405 csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) }
402 (options[:description] ? [Redmine::CodesetUtil.from_utf8(issue.description, encoding)] : [])
403 end
406 end
404 end
407 end
405 export
408 export
406 end
409 end
407 end
410 end
@@ -1,163 +1,173
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 module QueriesHelper
20 module QueriesHelper
21 def filters_options_for_select(query)
21 def filters_options_for_select(query)
22 options_for_select(filters_options(query))
22 options_for_select(filters_options(query))
23 end
23 end
24
24
25 def filters_options(query)
25 def filters_options(query)
26 options = [[]]
26 options = [[]]
27 sorted_options = query.available_filters.sort do |a, b|
27 sorted_options = query.available_filters.sort do |a, b|
28 ord = 0
28 ord = 0
29 if !(a[1][:order] == 20 && b[1][:order] == 20)
29 if !(a[1][:order] == 20 && b[1][:order] == 20)
30 ord = a[1][:order] <=> b[1][:order]
30 ord = a[1][:order] <=> b[1][:order]
31 else
31 else
32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
32 cn = (CustomField::CUSTOM_FIELDS_NAMES.index(a[1][:field].class.name) <=>
33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
33 CustomField::CUSTOM_FIELDS_NAMES.index(b[1][:field].class.name))
34 if cn != 0
34 if cn != 0
35 ord = cn
35 ord = cn
36 else
36 else
37 f = (a[1][:field] <=> b[1][:field])
37 f = (a[1][:field] <=> b[1][:field])
38 if f != 0
38 if f != 0
39 ord = f
39 ord = f
40 else
40 else
41 # assigned_to or author
41 # assigned_to or author
42 ord = (a[0] <=> b[0])
42 ord = (a[0] <=> b[0])
43 end
43 end
44 end
44 end
45 end
45 end
46 ord
46 ord
47 end
47 end
48 options += sorted_options.map do |field, field_options|
48 options += sorted_options.map do |field, field_options|
49 [field_options[:name], field]
49 [field_options[:name], field]
50 end
50 end
51 end
51 end
52
52
53 def available_block_columns_tags(query)
54 tags = ''.html_safe
55 query.available_block_columns.each do |column|
56 tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline')
57 end
58 tags
59 end
60
53 def column_header(column)
61 def column_header(column)
54 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
62 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
55 :default_order => column.default_order) :
63 :default_order => column.default_order) :
56 content_tag('th', h(column.caption))
64 content_tag('th', h(column.caption))
57 end
65 end
58
66
59 def column_content(column, issue)
67 def column_content(column, issue)
60 value = column.value(issue)
68 value = column.value(issue)
61 if value.is_a?(Array)
69 if value.is_a?(Array)
62 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
70 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
63 else
71 else
64 column_value(column, issue, value)
72 column_value(column, issue, value)
65 end
73 end
66 end
74 end
67
75
68 def column_value(column, issue, value)
76 def column_value(column, issue, value)
69 case value.class.name
77 case value.class.name
70 when 'String'
78 when 'String'
71 if column.name == :subject
79 if column.name == :subject
72 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
80 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
81 elsif column.name == :description
82 issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : ''
73 else
83 else
74 h(value)
84 h(value)
75 end
85 end
76 when 'Time'
86 when 'Time'
77 format_time(value)
87 format_time(value)
78 when 'Date'
88 when 'Date'
79 format_date(value)
89 format_date(value)
80 when 'Fixnum', 'Float'
90 when 'Fixnum', 'Float'
81 if column.name == :done_ratio
91 if column.name == :done_ratio
82 progress_bar(value, :width => '80px')
92 progress_bar(value, :width => '80px')
83 elsif column.name == :spent_hours
93 elsif column.name == :spent_hours
84 sprintf "%.2f", value
94 sprintf "%.2f", value
85 else
95 else
86 h(value.to_s)
96 h(value.to_s)
87 end
97 end
88 when 'User'
98 when 'User'
89 link_to_user value
99 link_to_user value
90 when 'Project'
100 when 'Project'
91 link_to_project value
101 link_to_project value
92 when 'Version'
102 when 'Version'
93 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
103 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
94 when 'TrueClass'
104 when 'TrueClass'
95 l(:general_text_Yes)
105 l(:general_text_Yes)
96 when 'FalseClass'
106 when 'FalseClass'
97 l(:general_text_No)
107 l(:general_text_No)
98 when 'Issue'
108 when 'Issue'
99 link_to_issue(value, :subject => false)
109 link_to_issue(value, :subject => false)
100 when 'IssueRelation'
110 when 'IssueRelation'
101 other = value.other_issue(issue)
111 other = value.other_issue(issue)
102 content_tag('span',
112 content_tag('span',
103 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
113 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
104 :class => value.css_classes_for(issue))
114 :class => value.css_classes_for(issue))
105 else
115 else
106 h(value)
116 h(value)
107 end
117 end
108 end
118 end
109
119
110 # Retrieve query from session or build a new query
120 # Retrieve query from session or build a new query
111 def retrieve_query
121 def retrieve_query
112 if !params[:query_id].blank?
122 if !params[:query_id].blank?
113 cond = "project_id IS NULL"
123 cond = "project_id IS NULL"
114 cond << " OR project_id = #{@project.id}" if @project
124 cond << " OR project_id = #{@project.id}" if @project
115 @query = Query.find(params[:query_id], :conditions => cond)
125 @query = Query.find(params[:query_id], :conditions => cond)
116 raise ::Unauthorized unless @query.visible?
126 raise ::Unauthorized unless @query.visible?
117 @query.project = @project
127 @query.project = @project
118 session[:query] = {:id => @query.id, :project_id => @query.project_id}
128 session[:query] = {:id => @query.id, :project_id => @query.project_id}
119 sort_clear
129 sort_clear
120 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
130 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
121 # Give it a name, required to be valid
131 # Give it a name, required to be valid
122 @query = Query.new(:name => "_")
132 @query = Query.new(:name => "_")
123 @query.project = @project
133 @query.project = @project
124 build_query_from_params
134 build_query_from_params
125 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
135 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
126 else
136 else
127 # retrieve from session
137 # retrieve from session
128 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
138 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
129 @query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
139 @query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
130 @query.project = @project
140 @query.project = @project
131 end
141 end
132 end
142 end
133
143
134 def retrieve_query_from_session
144 def retrieve_query_from_session
135 if session[:query]
145 if session[:query]
136 if session[:query][:id]
146 if session[:query][:id]
137 @query = Query.find_by_id(session[:query][:id])
147 @query = Query.find_by_id(session[:query][:id])
138 return unless @query
148 return unless @query
139 else
149 else
140 @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
150 @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
141 end
151 end
142 if session[:query].has_key?(:project_id)
152 if session[:query].has_key?(:project_id)
143 @query.project_id = session[:query][:project_id]
153 @query.project_id = session[:query][:project_id]
144 else
154 else
145 @query.project = @project
155 @query.project = @project
146 end
156 end
147 @query
157 @query
148 end
158 end
149 end
159 end
150
160
151 def build_query_from_params
161 def build_query_from_params
152 if params[:fields] || params[:f]
162 if params[:fields] || params[:f]
153 @query.filters = {}
163 @query.filters = {}
154 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
164 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
155 else
165 else
156 @query.available_filters.keys.each do |field|
166 @query.available_filters.keys.each do |field|
157 @query.add_short_filter(field, params[field]) if params[field]
167 @query.add_short_filter(field, params[field]) if params[field]
158 end
168 end
159 end
169 end
160 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
170 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
161 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
171 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
162 end
172 end
163 end
173 end
@@ -1,1086 +1,1109
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @inline = options.key?(:inline) ? options[:inline] : true
30 @caption_key = options[:caption] || "field_#{name}"
31 @caption_key = options[:caption] || "field_#{name}"
31 end
32 end
32
33
33 def caption
34 def caption
34 l(@caption_key)
35 l(@caption_key)
35 end
36 end
36
37
37 # Returns true if the column is sortable, otherwise false
38 # Returns true if the column is sortable, otherwise false
38 def sortable?
39 def sortable?
39 !@sortable.nil?
40 !@sortable.nil?
40 end
41 end
41
42
42 def sortable
43 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
45 end
45
46
47 def inline?
48 @inline
49 end
50
46 def value(issue)
51 def value(issue)
47 issue.send name
52 issue.send name
48 end
53 end
49
54
50 def css_classes
55 def css_classes
51 name
56 name
52 end
57 end
53 end
58 end
54
59
55 class QueryCustomFieldColumn < QueryColumn
60 class QueryCustomFieldColumn < QueryColumn
56
61
57 def initialize(custom_field)
62 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
63 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
64 self.sortable = custom_field.order_statement || false
60 self.groupable = custom_field.group_statement || false
65 self.groupable = custom_field.group_statement || false
66 @inline = true
61 @cf = custom_field
67 @cf = custom_field
62 end
68 end
63
69
64 def caption
70 def caption
65 @cf.name
71 @cf.name
66 end
72 end
67
73
68 def custom_field
74 def custom_field
69 @cf
75 @cf
70 end
76 end
71
77
72 def value(issue)
78 def value(issue)
73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
79 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
74 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
75 end
81 end
76
82
77 def css_classes
83 def css_classes
78 @css_classes ||= "#{name} #{@cf.field_format}"
84 @css_classes ||= "#{name} #{@cf.field_format}"
79 end
85 end
80 end
86 end
81
87
82 class Query < ActiveRecord::Base
88 class Query < ActiveRecord::Base
83 class StatementInvalid < ::ActiveRecord::StatementInvalid
89 class StatementInvalid < ::ActiveRecord::StatementInvalid
84 end
90 end
85
91
86 belongs_to :project
92 belongs_to :project
87 belongs_to :user
93 belongs_to :user
88 serialize :filters
94 serialize :filters
89 serialize :column_names
95 serialize :column_names
90 serialize :sort_criteria, Array
96 serialize :sort_criteria, Array
91
97
92 attr_protected :project_id, :user_id
98 attr_protected :project_id, :user_id
93
99
94 validates_presence_of :name
100 validates_presence_of :name
95 validates_length_of :name, :maximum => 255
101 validates_length_of :name, :maximum => 255
96 validate :validate_query_filters
102 validate :validate_query_filters
97
103
98 @@operators = { "=" => :label_equals,
104 @@operators = { "=" => :label_equals,
99 "!" => :label_not_equals,
105 "!" => :label_not_equals,
100 "o" => :label_open_issues,
106 "o" => :label_open_issues,
101 "c" => :label_closed_issues,
107 "c" => :label_closed_issues,
102 "!*" => :label_none,
108 "!*" => :label_none,
103 "*" => :label_any,
109 "*" => :label_any,
104 ">=" => :label_greater_or_equal,
110 ">=" => :label_greater_or_equal,
105 "<=" => :label_less_or_equal,
111 "<=" => :label_less_or_equal,
106 "><" => :label_between,
112 "><" => :label_between,
107 "<t+" => :label_in_less_than,
113 "<t+" => :label_in_less_than,
108 ">t+" => :label_in_more_than,
114 ">t+" => :label_in_more_than,
109 "><t+"=> :label_in_the_next_days,
115 "><t+"=> :label_in_the_next_days,
110 "t+" => :label_in,
116 "t+" => :label_in,
111 "t" => :label_today,
117 "t" => :label_today,
112 "w" => :label_this_week,
118 "w" => :label_this_week,
113 ">t-" => :label_less_than_ago,
119 ">t-" => :label_less_than_ago,
114 "<t-" => :label_more_than_ago,
120 "<t-" => :label_more_than_ago,
115 "><t-"=> :label_in_the_past_days,
121 "><t-"=> :label_in_the_past_days,
116 "t-" => :label_ago,
122 "t-" => :label_ago,
117 "~" => :label_contains,
123 "~" => :label_contains,
118 "!~" => :label_not_contains,
124 "!~" => :label_not_contains,
119 "=p" => :label_any_issues_in_project,
125 "=p" => :label_any_issues_in_project,
120 "=!p" => :label_any_issues_not_in_project,
126 "=!p" => :label_any_issues_not_in_project,
121 "!p" => :label_no_issues_in_project}
127 "!p" => :label_no_issues_in_project}
122
128
123 cattr_reader :operators
129 cattr_reader :operators
124
130
125 @@operators_by_filter_type = { :list => [ "=", "!" ],
131 @@operators_by_filter_type = { :list => [ "=", "!" ],
126 :list_status => [ "o", "=", "!", "c", "*" ],
132 :list_status => [ "o", "=", "!", "c", "*" ],
127 :list_optional => [ "=", "!", "!*", "*" ],
133 :list_optional => [ "=", "!", "!*", "*" ],
128 :list_subprojects => [ "*", "!*", "=" ],
134 :list_subprojects => [ "*", "!*", "=" ],
129 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
135 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
130 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
136 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
131 :string => [ "=", "~", "!", "!~", "!*", "*" ],
137 :string => [ "=", "~", "!", "!~", "!*", "*" ],
132 :text => [ "~", "!~", "!*", "*" ],
138 :text => [ "~", "!~", "!*", "*" ],
133 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
139 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
134 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
140 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
135 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
141 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
136
142
137 cattr_reader :operators_by_filter_type
143 cattr_reader :operators_by_filter_type
138
144
139 @@available_columns = [
145 @@available_columns = [
140 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
146 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
141 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
147 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
142 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
148 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
143 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
149 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
144 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
150 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
145 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
151 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
146 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
152 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
147 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
153 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
148 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
154 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
149 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
155 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
150 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
156 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
151 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
157 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
152 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
158 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
153 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
159 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
154 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
160 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
155 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
161 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
156 QueryColumn.new(:relations, :caption => :label_related_issues)
162 QueryColumn.new(:relations, :caption => :label_related_issues),
163 QueryColumn.new(:description, :inline => false)
157 ]
164 ]
158 cattr_reader :available_columns
165 cattr_reader :available_columns
159
166
160 scope :visible, lambda {|*args|
167 scope :visible, lambda {|*args|
161 user = args.shift || User.current
168 user = args.shift || User.current
162 base = Project.allowed_to_condition(user, :view_issues, *args)
169 base = Project.allowed_to_condition(user, :view_issues, *args)
163 user_id = user.logged? ? user.id : 0
170 user_id = user.logged? ? user.id : 0
164 {
171 {
165 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
172 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
166 :include => :project
173 :include => :project
167 }
174 }
168 }
175 }
169
176
170 def initialize(attributes=nil, *args)
177 def initialize(attributes=nil, *args)
171 super attributes
178 super attributes
172 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
179 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
173 @is_for_all = project.nil?
180 @is_for_all = project.nil?
174 end
181 end
175
182
176 def validate_query_filters
183 def validate_query_filters
177 filters.each_key do |field|
184 filters.each_key do |field|
178 if values_for(field)
185 if values_for(field)
179 case type_for(field)
186 case type_for(field)
180 when :integer
187 when :integer
181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
188 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
182 when :float
189 when :float
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
190 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
184 when :date, :date_past
191 when :date, :date_past
185 case operator_for(field)
192 case operator_for(field)
186 when "=", ">=", "<=", "><"
193 when "=", ">=", "<=", "><"
187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
194 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
195 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
196 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
190 end
197 end
191 end
198 end
192 end
199 end
193
200
194 add_filter_error(field, :blank) unless
201 add_filter_error(field, :blank) unless
195 # filter requires one or more values
202 # filter requires one or more values
196 (values_for(field) and !values_for(field).first.blank?) or
203 (values_for(field) and !values_for(field).first.blank?) or
197 # filter doesn't require any value
204 # filter doesn't require any value
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
205 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
199 end if filters
206 end if filters
200 end
207 end
201
208
202 def add_filter_error(field, message)
209 def add_filter_error(field, message)
203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
210 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
204 errors.add(:base, m)
211 errors.add(:base, m)
205 end
212 end
206
213
207 # Returns true if the query is visible to +user+ or the current user.
214 # Returns true if the query is visible to +user+ or the current user.
208 def visible?(user=User.current)
215 def visible?(user=User.current)
209 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
216 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
210 end
217 end
211
218
212 def editable_by?(user)
219 def editable_by?(user)
213 return false unless user
220 return false unless user
214 # Admin can edit them all and regular users can edit their private queries
221 # Admin can edit them all and regular users can edit their private queries
215 return true if user.admin? || (!is_public && self.user_id == user.id)
222 return true if user.admin? || (!is_public && self.user_id == user.id)
216 # Members can not edit public queries that are for all project (only admin is allowed to)
223 # Members can not edit public queries that are for all project (only admin is allowed to)
217 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
224 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
218 end
225 end
219
226
220 def trackers
227 def trackers
221 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
228 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
222 end
229 end
223
230
224 # Returns a hash of localized labels for all filter operators
231 # Returns a hash of localized labels for all filter operators
225 def self.operators_labels
232 def self.operators_labels
226 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
233 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
227 end
234 end
228
235
229 def available_filters
236 def available_filters
230 return @available_filters if @available_filters
237 return @available_filters if @available_filters
231 @available_filters = {
238 @available_filters = {
232 "status_id" => {
239 "status_id" => {
233 :type => :list_status, :order => 0,
240 :type => :list_status, :order => 0,
234 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
241 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
235 },
242 },
236 "tracker_id" => {
243 "tracker_id" => {
237 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
244 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
238 },
245 },
239 "priority_id" => {
246 "priority_id" => {
240 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
247 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
241 },
248 },
242 "subject" => { :type => :text, :order => 8 },
249 "subject" => { :type => :text, :order => 8 },
243 "created_on" => { :type => :date_past, :order => 9 },
250 "created_on" => { :type => :date_past, :order => 9 },
244 "updated_on" => { :type => :date_past, :order => 10 },
251 "updated_on" => { :type => :date_past, :order => 10 },
245 "start_date" => { :type => :date, :order => 11 },
252 "start_date" => { :type => :date, :order => 11 },
246 "due_date" => { :type => :date, :order => 12 },
253 "due_date" => { :type => :date, :order => 12 },
247 "estimated_hours" => { :type => :float, :order => 13 },
254 "estimated_hours" => { :type => :float, :order => 13 },
248 "done_ratio" => { :type => :integer, :order => 14 }
255 "done_ratio" => { :type => :integer, :order => 14 }
249 }
256 }
250 IssueRelation::TYPES.each do |relation_type, options|
257 IssueRelation::TYPES.each do |relation_type, options|
251 @available_filters[relation_type] = {
258 @available_filters[relation_type] = {
252 :type => :relation, :order => @available_filters.size + 100,
259 :type => :relation, :order => @available_filters.size + 100,
253 :label => options[:name]
260 :label => options[:name]
254 }
261 }
255 end
262 end
256 principals = []
263 principals = []
257 if project
264 if project
258 principals += project.principals.sort
265 principals += project.principals.sort
259 unless project.leaf?
266 unless project.leaf?
260 subprojects = project.descendants.visible.all
267 subprojects = project.descendants.visible.all
261 if subprojects.any?
268 if subprojects.any?
262 @available_filters["subproject_id"] = {
269 @available_filters["subproject_id"] = {
263 :type => :list_subprojects, :order => 13,
270 :type => :list_subprojects, :order => 13,
264 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
271 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
265 }
272 }
266 principals += Principal.member_of(subprojects)
273 principals += Principal.member_of(subprojects)
267 end
274 end
268 end
275 end
269 else
276 else
270 if all_projects.any?
277 if all_projects.any?
271 # members of visible projects
278 # members of visible projects
272 principals += Principal.member_of(all_projects)
279 principals += Principal.member_of(all_projects)
273 # project filter
280 # project filter
274 project_values = []
281 project_values = []
275 if User.current.logged? && User.current.memberships.any?
282 if User.current.logged? && User.current.memberships.any?
276 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
283 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
277 end
284 end
278 project_values += all_projects_values
285 project_values += all_projects_values
279 @available_filters["project_id"] = {
286 @available_filters["project_id"] = {
280 :type => :list, :order => 1, :values => project_values
287 :type => :list, :order => 1, :values => project_values
281 } unless project_values.empty?
288 } unless project_values.empty?
282 end
289 end
283 end
290 end
284 principals.uniq!
291 principals.uniq!
285 principals.sort!
292 principals.sort!
286 users = principals.select {|p| p.is_a?(User)}
293 users = principals.select {|p| p.is_a?(User)}
287
294
288 assigned_to_values = []
295 assigned_to_values = []
289 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
296 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
290 assigned_to_values += (Setting.issue_group_assignment? ?
297 assigned_to_values += (Setting.issue_group_assignment? ?
291 principals : users).collect{|s| [s.name, s.id.to_s] }
298 principals : users).collect{|s| [s.name, s.id.to_s] }
292 @available_filters["assigned_to_id"] = {
299 @available_filters["assigned_to_id"] = {
293 :type => :list_optional, :order => 4, :values => assigned_to_values
300 :type => :list_optional, :order => 4, :values => assigned_to_values
294 } unless assigned_to_values.empty?
301 } unless assigned_to_values.empty?
295
302
296 author_values = []
303 author_values = []
297 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
304 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
298 author_values += users.collect{|s| [s.name, s.id.to_s] }
305 author_values += users.collect{|s| [s.name, s.id.to_s] }
299 @available_filters["author_id"] = {
306 @available_filters["author_id"] = {
300 :type => :list, :order => 5, :values => author_values
307 :type => :list, :order => 5, :values => author_values
301 } unless author_values.empty?
308 } unless author_values.empty?
302
309
303 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
310 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
304 @available_filters["member_of_group"] = {
311 @available_filters["member_of_group"] = {
305 :type => :list_optional, :order => 6, :values => group_values
312 :type => :list_optional, :order => 6, :values => group_values
306 } unless group_values.empty?
313 } unless group_values.empty?
307
314
308 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
315 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
309 @available_filters["assigned_to_role"] = {
316 @available_filters["assigned_to_role"] = {
310 :type => :list_optional, :order => 7, :values => role_values
317 :type => :list_optional, :order => 7, :values => role_values
311 } unless role_values.empty?
318 } unless role_values.empty?
312
319
313 if User.current.logged?
320 if User.current.logged?
314 @available_filters["watcher_id"] = {
321 @available_filters["watcher_id"] = {
315 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
322 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
316 }
323 }
317 end
324 end
318
325
319 if project
326 if project
320 # project specific filters
327 # project specific filters
321 categories = project.issue_categories.all
328 categories = project.issue_categories.all
322 unless categories.empty?
329 unless categories.empty?
323 @available_filters["category_id"] = {
330 @available_filters["category_id"] = {
324 :type => :list_optional, :order => 6,
331 :type => :list_optional, :order => 6,
325 :values => categories.collect{|s| [s.name, s.id.to_s] }
332 :values => categories.collect{|s| [s.name, s.id.to_s] }
326 }
333 }
327 end
334 end
328 versions = project.shared_versions.all
335 versions = project.shared_versions.all
329 unless versions.empty?
336 unless versions.empty?
330 @available_filters["fixed_version_id"] = {
337 @available_filters["fixed_version_id"] = {
331 :type => :list_optional, :order => 7,
338 :type => :list_optional, :order => 7,
332 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
339 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
333 }
340 }
334 end
341 end
335 add_custom_fields_filters(project.all_issue_custom_fields)
342 add_custom_fields_filters(project.all_issue_custom_fields)
336 else
343 else
337 # global filters for cross project issue list
344 # global filters for cross project issue list
338 system_shared_versions = Version.visible.find_all_by_sharing('system')
345 system_shared_versions = Version.visible.find_all_by_sharing('system')
339 unless system_shared_versions.empty?
346 unless system_shared_versions.empty?
340 @available_filters["fixed_version_id"] = {
347 @available_filters["fixed_version_id"] = {
341 :type => :list_optional, :order => 7,
348 :type => :list_optional, :order => 7,
342 :values => system_shared_versions.sort.collect{|s|
349 :values => system_shared_versions.sort.collect{|s|
343 ["#{s.project.name} - #{s.name}", s.id.to_s]
350 ["#{s.project.name} - #{s.name}", s.id.to_s]
344 }
351 }
345 }
352 }
346 end
353 end
347 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
354 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
348 end
355 end
349 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
356 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
350 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
357 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
351 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
358 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
352 @available_filters["is_private"] = {
359 @available_filters["is_private"] = {
353 :type => :list, :order => 16,
360 :type => :list, :order => 16,
354 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
361 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
355 }
362 }
356 end
363 end
357 Tracker.disabled_core_fields(trackers).each {|field|
364 Tracker.disabled_core_fields(trackers).each {|field|
358 @available_filters.delete field
365 @available_filters.delete field
359 }
366 }
360 @available_filters.each do |field, options|
367 @available_filters.each do |field, options|
361 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
362 end
369 end
363 @available_filters
370 @available_filters
364 end
371 end
365
372
366 # Returns a representation of the available filters for JSON serialization
373 # Returns a representation of the available filters for JSON serialization
367 def available_filters_as_json
374 def available_filters_as_json
368 json = {}
375 json = {}
369 available_filters.each do |field, options|
376 available_filters.each do |field, options|
370 json[field] = options.slice(:type, :name, :values).stringify_keys
377 json[field] = options.slice(:type, :name, :values).stringify_keys
371 end
378 end
372 json
379 json
373 end
380 end
374
381
375 def all_projects
382 def all_projects
376 @all_projects ||= Project.visible.all
383 @all_projects ||= Project.visible.all
377 end
384 end
378
385
379 def all_projects_values
386 def all_projects_values
380 return @all_projects_values if @all_projects_values
387 return @all_projects_values if @all_projects_values
381
388
382 values = []
389 values = []
383 Project.project_tree(all_projects) do |p, level|
390 Project.project_tree(all_projects) do |p, level|
384 prefix = (level > 0 ? ('--' * level + ' ') : '')
391 prefix = (level > 0 ? ('--' * level + ' ') : '')
385 values << ["#{prefix}#{p.name}", p.id.to_s]
392 values << ["#{prefix}#{p.name}", p.id.to_s]
386 end
393 end
387 @all_projects_values = values
394 @all_projects_values = values
388 end
395 end
389
396
390 def add_filter(field, operator, values)
397 def add_filter(field, operator, values)
391 # values must be an array
398 # values must be an array
392 return unless values.nil? || values.is_a?(Array)
399 return unless values.nil? || values.is_a?(Array)
393 # check if field is defined as an available filter
400 # check if field is defined as an available filter
394 if available_filters.has_key? field
401 if available_filters.has_key? field
395 filter_options = available_filters[field]
402 filter_options = available_filters[field]
396 # check if operator is allowed for that filter
403 # check if operator is allowed for that filter
397 #if @@operators_by_filter_type[filter_options[:type]].include? operator
404 #if @@operators_by_filter_type[filter_options[:type]].include? operator
398 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
405 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
399 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
406 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
400 #end
407 #end
401 filters[field] = {:operator => operator, :values => (values || [''])}
408 filters[field] = {:operator => operator, :values => (values || [''])}
402 end
409 end
403 end
410 end
404
411
405 def add_short_filter(field, expression)
412 def add_short_filter(field, expression)
406 return unless expression && available_filters.has_key?(field)
413 return unless expression && available_filters.has_key?(field)
407 field_type = available_filters[field][:type]
414 field_type = available_filters[field][:type]
408 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
415 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
409 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
416 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
410 add_filter field, operator, $1.present? ? $1.split('|') : ['']
417 add_filter field, operator, $1.present? ? $1.split('|') : ['']
411 end || add_filter(field, '=', expression.split('|'))
418 end || add_filter(field, '=', expression.split('|'))
412 end
419 end
413
420
414 # Add multiple filters using +add_filter+
421 # Add multiple filters using +add_filter+
415 def add_filters(fields, operators, values)
422 def add_filters(fields, operators, values)
416 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
423 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
417 fields.each do |field|
424 fields.each do |field|
418 add_filter(field, operators[field], values && values[field])
425 add_filter(field, operators[field], values && values[field])
419 end
426 end
420 end
427 end
421 end
428 end
422
429
423 def has_filter?(field)
430 def has_filter?(field)
424 filters and filters[field]
431 filters and filters[field]
425 end
432 end
426
433
427 def type_for(field)
434 def type_for(field)
428 available_filters[field][:type] if available_filters.has_key?(field)
435 available_filters[field][:type] if available_filters.has_key?(field)
429 end
436 end
430
437
431 def operator_for(field)
438 def operator_for(field)
432 has_filter?(field) ? filters[field][:operator] : nil
439 has_filter?(field) ? filters[field][:operator] : nil
433 end
440 end
434
441
435 def values_for(field)
442 def values_for(field)
436 has_filter?(field) ? filters[field][:values] : nil
443 has_filter?(field) ? filters[field][:values] : nil
437 end
444 end
438
445
439 def value_for(field, index=0)
446 def value_for(field, index=0)
440 (values_for(field) || [])[index]
447 (values_for(field) || [])[index]
441 end
448 end
442
449
443 def label_for(field)
450 def label_for(field)
444 label = available_filters[field][:name] if available_filters.has_key?(field)
451 label = available_filters[field][:name] if available_filters.has_key?(field)
445 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
452 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
446 end
453 end
447
454
448 def available_columns
455 def available_columns
449 return @available_columns if @available_columns
456 return @available_columns if @available_columns
450 @available_columns = ::Query.available_columns.dup
457 @available_columns = ::Query.available_columns.dup
451 @available_columns += (project ?
458 @available_columns += (project ?
452 project.all_issue_custom_fields :
459 project.all_issue_custom_fields :
453 IssueCustomField.all
460 IssueCustomField.all
454 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
461 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
455
462
456 if User.current.allowed_to?(:view_time_entries, project, :global => true)
463 if User.current.allowed_to?(:view_time_entries, project, :global => true)
457 index = nil
464 index = nil
458 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
465 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
459 index = (index ? index + 1 : -1)
466 index = (index ? index + 1 : -1)
460 # insert the column after estimated_hours or at the end
467 # insert the column after estimated_hours or at the end
461 @available_columns.insert index, QueryColumn.new(:spent_hours,
468 @available_columns.insert index, QueryColumn.new(:spent_hours,
462 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
469 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
463 :default_order => 'desc',
470 :default_order => 'desc',
464 :caption => :label_spent_time
471 :caption => :label_spent_time
465 )
472 )
466 end
473 end
467
474
468 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
475 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
469 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
476 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
470 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
477 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
471 end
478 end
472
479
473 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
480 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
474 @available_columns.reject! {|column|
481 @available_columns.reject! {|column|
475 disabled_fields.include?(column.name.to_s)
482 disabled_fields.include?(column.name.to_s)
476 }
483 }
477
484
478 @available_columns
485 @available_columns
479 end
486 end
480
487
481 def self.available_columns=(v)
488 def self.available_columns=(v)
482 self.available_columns = (v)
489 self.available_columns = (v)
483 end
490 end
484
491
485 def self.add_available_column(column)
492 def self.add_available_column(column)
486 self.available_columns << (column) if column.is_a?(QueryColumn)
493 self.available_columns << (column) if column.is_a?(QueryColumn)
487 end
494 end
488
495
489 # Returns an array of columns that can be used to group the results
496 # Returns an array of columns that can be used to group the results
490 def groupable_columns
497 def groupable_columns
491 available_columns.select {|c| c.groupable}
498 available_columns.select {|c| c.groupable}
492 end
499 end
493
500
494 # Returns a Hash of columns and the key for sorting
501 # Returns a Hash of columns and the key for sorting
495 def sortable_columns
502 def sortable_columns
496 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
503 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
497 h[column.name.to_s] = column.sortable
504 h[column.name.to_s] = column.sortable
498 h
505 h
499 })
506 })
500 end
507 end
501
508
502 def columns
509 def columns
503 # preserve the column_names order
510 # preserve the column_names order
504 (has_default_columns? ? default_columns_names : column_names).collect do |name|
511 (has_default_columns? ? default_columns_names : column_names).collect do |name|
505 available_columns.find { |col| col.name == name }
512 available_columns.find { |col| col.name == name }
506 end.compact
513 end.compact
507 end
514 end
508
515
516 def inline_columns
517 columns.select(&:inline?)
518 end
519
520 def block_columns
521 columns.reject(&:inline?)
522 end
523
524 def available_inline_columns
525 available_columns.select(&:inline?)
526 end
527
528 def available_block_columns
529 available_columns.reject(&:inline?)
530 end
531
509 def default_columns_names
532 def default_columns_names
510 @default_columns_names ||= begin
533 @default_columns_names ||= begin
511 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
534 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
512
535
513 project.present? ? default_columns : [:project] | default_columns
536 project.present? ? default_columns : [:project] | default_columns
514 end
537 end
515 end
538 end
516
539
517 def column_names=(names)
540 def column_names=(names)
518 if names
541 if names
519 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
542 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
520 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
543 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
521 # Set column_names to nil if default columns
544 # Set column_names to nil if default columns
522 if names == default_columns_names
545 if names == default_columns_names
523 names = nil
546 names = nil
524 end
547 end
525 end
548 end
526 write_attribute(:column_names, names)
549 write_attribute(:column_names, names)
527 end
550 end
528
551
529 def has_column?(column)
552 def has_column?(column)
530 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
553 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
531 end
554 end
532
555
533 def has_default_columns?
556 def has_default_columns?
534 column_names.nil? || column_names.empty?
557 column_names.nil? || column_names.empty?
535 end
558 end
536
559
537 def sort_criteria=(arg)
560 def sort_criteria=(arg)
538 c = []
561 c = []
539 if arg.is_a?(Hash)
562 if arg.is_a?(Hash)
540 arg = arg.keys.sort.collect {|k| arg[k]}
563 arg = arg.keys.sort.collect {|k| arg[k]}
541 end
564 end
542 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
565 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
543 write_attribute(:sort_criteria, c)
566 write_attribute(:sort_criteria, c)
544 end
567 end
545
568
546 def sort_criteria
569 def sort_criteria
547 read_attribute(:sort_criteria) || []
570 read_attribute(:sort_criteria) || []
548 end
571 end
549
572
550 def sort_criteria_key(arg)
573 def sort_criteria_key(arg)
551 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
574 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
552 end
575 end
553
576
554 def sort_criteria_order(arg)
577 def sort_criteria_order(arg)
555 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
578 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
556 end
579 end
557
580
558 def sort_criteria_order_for(key)
581 def sort_criteria_order_for(key)
559 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
582 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
560 end
583 end
561
584
562 # Returns the SQL sort order that should be prepended for grouping
585 # Returns the SQL sort order that should be prepended for grouping
563 def group_by_sort_order
586 def group_by_sort_order
564 if grouped? && (column = group_by_column)
587 if grouped? && (column = group_by_column)
565 order = sort_criteria_order_for(column.name) || column.default_order
588 order = sort_criteria_order_for(column.name) || column.default_order
566 column.sortable.is_a?(Array) ?
589 column.sortable.is_a?(Array) ?
567 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
590 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
568 "#{column.sortable} #{order}"
591 "#{column.sortable} #{order}"
569 end
592 end
570 end
593 end
571
594
572 # Returns true if the query is a grouped query
595 # Returns true if the query is a grouped query
573 def grouped?
596 def grouped?
574 !group_by_column.nil?
597 !group_by_column.nil?
575 end
598 end
576
599
577 def group_by_column
600 def group_by_column
578 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
601 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
579 end
602 end
580
603
581 def group_by_statement
604 def group_by_statement
582 group_by_column.try(:groupable)
605 group_by_column.try(:groupable)
583 end
606 end
584
607
585 def project_statement
608 def project_statement
586 project_clauses = []
609 project_clauses = []
587 if project && !project.descendants.active.empty?
610 if project && !project.descendants.active.empty?
588 ids = [project.id]
611 ids = [project.id]
589 if has_filter?("subproject_id")
612 if has_filter?("subproject_id")
590 case operator_for("subproject_id")
613 case operator_for("subproject_id")
591 when '='
614 when '='
592 # include the selected subprojects
615 # include the selected subprojects
593 ids += values_for("subproject_id").each(&:to_i)
616 ids += values_for("subproject_id").each(&:to_i)
594 when '!*'
617 when '!*'
595 # main project only
618 # main project only
596 else
619 else
597 # all subprojects
620 # all subprojects
598 ids += project.descendants.collect(&:id)
621 ids += project.descendants.collect(&:id)
599 end
622 end
600 elsif Setting.display_subprojects_issues?
623 elsif Setting.display_subprojects_issues?
601 ids += project.descendants.collect(&:id)
624 ids += project.descendants.collect(&:id)
602 end
625 end
603 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
626 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
604 elsif project
627 elsif project
605 project_clauses << "#{Project.table_name}.id = %d" % project.id
628 project_clauses << "#{Project.table_name}.id = %d" % project.id
606 end
629 end
607 project_clauses.any? ? project_clauses.join(' AND ') : nil
630 project_clauses.any? ? project_clauses.join(' AND ') : nil
608 end
631 end
609
632
610 def statement
633 def statement
611 # filters clauses
634 # filters clauses
612 filters_clauses = []
635 filters_clauses = []
613 filters.each_key do |field|
636 filters.each_key do |field|
614 next if field == "subproject_id"
637 next if field == "subproject_id"
615 v = values_for(field).clone
638 v = values_for(field).clone
616 next unless v and !v.empty?
639 next unless v and !v.empty?
617 operator = operator_for(field)
640 operator = operator_for(field)
618
641
619 # "me" value subsitution
642 # "me" value subsitution
620 if %w(assigned_to_id author_id watcher_id).include?(field)
643 if %w(assigned_to_id author_id watcher_id).include?(field)
621 if v.delete("me")
644 if v.delete("me")
622 if User.current.logged?
645 if User.current.logged?
623 v.push(User.current.id.to_s)
646 v.push(User.current.id.to_s)
624 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
647 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
625 else
648 else
626 v.push("0")
649 v.push("0")
627 end
650 end
628 end
651 end
629 end
652 end
630
653
631 if field == 'project_id'
654 if field == 'project_id'
632 if v.delete('mine')
655 if v.delete('mine')
633 v += User.current.memberships.map(&:project_id).map(&:to_s)
656 v += User.current.memberships.map(&:project_id).map(&:to_s)
634 end
657 end
635 end
658 end
636
659
637 if field =~ /cf_(\d+)$/
660 if field =~ /cf_(\d+)$/
638 # custom field
661 # custom field
639 filters_clauses << sql_for_custom_field(field, operator, v, $1)
662 filters_clauses << sql_for_custom_field(field, operator, v, $1)
640 elsif respond_to?("sql_for_#{field}_field")
663 elsif respond_to?("sql_for_#{field}_field")
641 # specific statement
664 # specific statement
642 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
665 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
643 else
666 else
644 # regular field
667 # regular field
645 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
668 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
646 end
669 end
647 end if filters and valid?
670 end if filters and valid?
648
671
649 filters_clauses << project_statement
672 filters_clauses << project_statement
650 filters_clauses.reject!(&:blank?)
673 filters_clauses.reject!(&:blank?)
651
674
652 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
675 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
653 end
676 end
654
677
655 # Returns the issue count
678 # Returns the issue count
656 def issue_count
679 def issue_count
657 Issue.visible.count(:include => [:status, :project], :conditions => statement)
680 Issue.visible.count(:include => [:status, :project], :conditions => statement)
658 rescue ::ActiveRecord::StatementInvalid => e
681 rescue ::ActiveRecord::StatementInvalid => e
659 raise StatementInvalid.new(e.message)
682 raise StatementInvalid.new(e.message)
660 end
683 end
661
684
662 # Returns the issue count by group or nil if query is not grouped
685 # Returns the issue count by group or nil if query is not grouped
663 def issue_count_by_group
686 def issue_count_by_group
664 r = nil
687 r = nil
665 if grouped?
688 if grouped?
666 begin
689 begin
667 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
690 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
668 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
691 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
669 rescue ActiveRecord::RecordNotFound
692 rescue ActiveRecord::RecordNotFound
670 r = {nil => issue_count}
693 r = {nil => issue_count}
671 end
694 end
672 c = group_by_column
695 c = group_by_column
673 if c.is_a?(QueryCustomFieldColumn)
696 if c.is_a?(QueryCustomFieldColumn)
674 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
697 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
675 end
698 end
676 end
699 end
677 r
700 r
678 rescue ::ActiveRecord::StatementInvalid => e
701 rescue ::ActiveRecord::StatementInvalid => e
679 raise StatementInvalid.new(e.message)
702 raise StatementInvalid.new(e.message)
680 end
703 end
681
704
682 # Returns the issues
705 # Returns the issues
683 # Valid options are :order, :offset, :limit, :include, :conditions
706 # Valid options are :order, :offset, :limit, :include, :conditions
684 def issues(options={})
707 def issues(options={})
685 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
708 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
686 order_option = nil if order_option.blank?
709 order_option = nil if order_option.blank?
687
710
688 issues = Issue.visible.where(options[:conditions]).all(
711 issues = Issue.visible.where(options[:conditions]).all(
689 :include => ([:status, :project] + (options[:include] || [])).uniq,
712 :include => ([:status, :project] + (options[:include] || [])).uniq,
690 :conditions => statement,
713 :conditions => statement,
691 :order => order_option,
714 :order => order_option,
692 :joins => joins_for_order_statement(order_option),
715 :joins => joins_for_order_statement(order_option),
693 :limit => options[:limit],
716 :limit => options[:limit],
694 :offset => options[:offset]
717 :offset => options[:offset]
695 )
718 )
696
719
697 if has_column?(:spent_hours)
720 if has_column?(:spent_hours)
698 Issue.load_visible_spent_hours(issues)
721 Issue.load_visible_spent_hours(issues)
699 end
722 end
700 if has_column?(:relations)
723 if has_column?(:relations)
701 Issue.load_visible_relations(issues)
724 Issue.load_visible_relations(issues)
702 end
725 end
703 issues
726 issues
704 rescue ::ActiveRecord::StatementInvalid => e
727 rescue ::ActiveRecord::StatementInvalid => e
705 raise StatementInvalid.new(e.message)
728 raise StatementInvalid.new(e.message)
706 end
729 end
707
730
708 # Returns the issues ids
731 # Returns the issues ids
709 def issue_ids(options={})
732 def issue_ids(options={})
710 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
733 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
711 order_option = nil if order_option.blank?
734 order_option = nil if order_option.blank?
712
735
713 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
736 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
714 :conditions => statement,
737 :conditions => statement,
715 :order => order_option,
738 :order => order_option,
716 :joins => joins_for_order_statement(order_option),
739 :joins => joins_for_order_statement(order_option),
717 :limit => options[:limit],
740 :limit => options[:limit],
718 :offset => options[:offset]).find_ids
741 :offset => options[:offset]).find_ids
719 rescue ::ActiveRecord::StatementInvalid => e
742 rescue ::ActiveRecord::StatementInvalid => e
720 raise StatementInvalid.new(e.message)
743 raise StatementInvalid.new(e.message)
721 end
744 end
722
745
723 # Returns the journals
746 # Returns the journals
724 # Valid options are :order, :offset, :limit
747 # Valid options are :order, :offset, :limit
725 def journals(options={})
748 def journals(options={})
726 Journal.visible.all(
749 Journal.visible.all(
727 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
750 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
728 :conditions => statement,
751 :conditions => statement,
729 :order => options[:order],
752 :order => options[:order],
730 :limit => options[:limit],
753 :limit => options[:limit],
731 :offset => options[:offset]
754 :offset => options[:offset]
732 )
755 )
733 rescue ::ActiveRecord::StatementInvalid => e
756 rescue ::ActiveRecord::StatementInvalid => e
734 raise StatementInvalid.new(e.message)
757 raise StatementInvalid.new(e.message)
735 end
758 end
736
759
737 # Returns the versions
760 # Returns the versions
738 # Valid options are :conditions
761 # Valid options are :conditions
739 def versions(options={})
762 def versions(options={})
740 Version.visible.where(options[:conditions]).all(
763 Version.visible.where(options[:conditions]).all(
741 :include => :project,
764 :include => :project,
742 :conditions => project_statement
765 :conditions => project_statement
743 )
766 )
744 rescue ::ActiveRecord::StatementInvalid => e
767 rescue ::ActiveRecord::StatementInvalid => e
745 raise StatementInvalid.new(e.message)
768 raise StatementInvalid.new(e.message)
746 end
769 end
747
770
748 def sql_for_watcher_id_field(field, operator, value)
771 def sql_for_watcher_id_field(field, operator, value)
749 db_table = Watcher.table_name
772 db_table = Watcher.table_name
750 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
773 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
751 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
774 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
752 end
775 end
753
776
754 def sql_for_member_of_group_field(field, operator, value)
777 def sql_for_member_of_group_field(field, operator, value)
755 if operator == '*' # Any group
778 if operator == '*' # Any group
756 groups = Group.all
779 groups = Group.all
757 operator = '=' # Override the operator since we want to find by assigned_to
780 operator = '=' # Override the operator since we want to find by assigned_to
758 elsif operator == "!*"
781 elsif operator == "!*"
759 groups = Group.all
782 groups = Group.all
760 operator = '!' # Override the operator since we want to find by assigned_to
783 operator = '!' # Override the operator since we want to find by assigned_to
761 else
784 else
762 groups = Group.find_all_by_id(value)
785 groups = Group.find_all_by_id(value)
763 end
786 end
764 groups ||= []
787 groups ||= []
765
788
766 members_of_groups = groups.inject([]) {|user_ids, group|
789 members_of_groups = groups.inject([]) {|user_ids, group|
767 if group && group.user_ids.present?
790 if group && group.user_ids.present?
768 user_ids << group.user_ids
791 user_ids << group.user_ids
769 end
792 end
770 user_ids.flatten.uniq.compact
793 user_ids.flatten.uniq.compact
771 }.sort.collect(&:to_s)
794 }.sort.collect(&:to_s)
772
795
773 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
796 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
774 end
797 end
775
798
776 def sql_for_assigned_to_role_field(field, operator, value)
799 def sql_for_assigned_to_role_field(field, operator, value)
777 case operator
800 case operator
778 when "*", "!*" # Member / Not member
801 when "*", "!*" # Member / Not member
779 sw = operator == "!*" ? 'NOT' : ''
802 sw = operator == "!*" ? 'NOT' : ''
780 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
803 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
781 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
804 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
782 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
805 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
783 when "=", "!"
806 when "=", "!"
784 role_cond = value.any? ?
807 role_cond = value.any? ?
785 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
808 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
786 "1=0"
809 "1=0"
787
810
788 sw = operator == "!" ? 'NOT' : ''
811 sw = operator == "!" ? 'NOT' : ''
789 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
812 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
790 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
813 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
791 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
814 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
792 end
815 end
793 end
816 end
794
817
795 def sql_for_is_private_field(field, operator, value)
818 def sql_for_is_private_field(field, operator, value)
796 op = (operator == "=" ? 'IN' : 'NOT IN')
819 op = (operator == "=" ? 'IN' : 'NOT IN')
797 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
820 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
798
821
799 "#{Issue.table_name}.is_private #{op} (#{va})"
822 "#{Issue.table_name}.is_private #{op} (#{va})"
800 end
823 end
801
824
802 def sql_for_relations(field, operator, value, options={})
825 def sql_for_relations(field, operator, value, options={})
803 relation_options = IssueRelation::TYPES[field]
826 relation_options = IssueRelation::TYPES[field]
804 return relation_options unless relation_options
827 return relation_options unless relation_options
805
828
806 relation_type = field
829 relation_type = field
807 join_column, target_join_column = "issue_from_id", "issue_to_id"
830 join_column, target_join_column = "issue_from_id", "issue_to_id"
808 if relation_options[:reverse] || options[:reverse]
831 if relation_options[:reverse] || options[:reverse]
809 relation_type = relation_options[:reverse] || relation_type
832 relation_type = relation_options[:reverse] || relation_type
810 join_column, target_join_column = target_join_column, join_column
833 join_column, target_join_column = target_join_column, join_column
811 end
834 end
812
835
813 sql = case operator
836 sql = case operator
814 when "*", "!*"
837 when "*", "!*"
815 op = (operator == "*" ? 'IN' : 'NOT IN')
838 op = (operator == "*" ? 'IN' : 'NOT IN')
816 "#{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)}')"
839 "#{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)}')"
817 when "=", "!"
840 when "=", "!"
818 op = (operator == "=" ? 'IN' : 'NOT IN')
841 op = (operator == "=" ? 'IN' : 'NOT IN')
819 "#{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})"
842 "#{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})"
820 when "=p", "=!p", "!p"
843 when "=p", "=!p", "!p"
821 op = (operator == "!p" ? 'NOT IN' : 'IN')
844 op = (operator == "!p" ? 'NOT IN' : 'IN')
822 comp = (operator == "=!p" ? '<>' : '=')
845 comp = (operator == "=!p" ? '<>' : '=')
823 "#{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})"
846 "#{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})"
824 end
847 end
825
848
826 if relation_options[:sym] == field && !options[:reverse]
849 if relation_options[:sym] == field && !options[:reverse]
827 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
850 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
828 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
851 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
829 else
852 else
830 sql
853 sql
831 end
854 end
832 end
855 end
833
856
834 IssueRelation::TYPES.keys.each do |relation_type|
857 IssueRelation::TYPES.keys.each do |relation_type|
835 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
858 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
836 end
859 end
837
860
838 private
861 private
839
862
840 def sql_for_custom_field(field, operator, value, custom_field_id)
863 def sql_for_custom_field(field, operator, value, custom_field_id)
841 db_table = CustomValue.table_name
864 db_table = CustomValue.table_name
842 db_field = 'value'
865 db_field = 'value'
843 filter = @available_filters[field]
866 filter = @available_filters[field]
844 return nil unless filter
867 return nil unless filter
845 if filter[:format] == 'user'
868 if filter[:format] == 'user'
846 if value.delete('me')
869 if value.delete('me')
847 value.push User.current.id.to_s
870 value.push User.current.id.to_s
848 end
871 end
849 end
872 end
850 not_in = nil
873 not_in = nil
851 if operator == '!'
874 if operator == '!'
852 # Makes ! operator work for custom fields with multiple values
875 # Makes ! operator work for custom fields with multiple values
853 operator = '='
876 operator = '='
854 not_in = 'NOT'
877 not_in = 'NOT'
855 end
878 end
856 customized_key = "id"
879 customized_key = "id"
857 customized_class = Issue
880 customized_class = Issue
858 if field =~ /^(.+)\.cf_/
881 if field =~ /^(.+)\.cf_/
859 assoc = $1
882 assoc = $1
860 customized_key = "#{assoc}_id"
883 customized_key = "#{assoc}_id"
861 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
884 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
862 raise "Unknown Issue association #{assoc}" unless customized_class
885 raise "Unknown Issue association #{assoc}" unless customized_class
863 end
886 end
864 "#{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 " +
887 "#{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 " +
865 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
888 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
866 end
889 end
867
890
868 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
891 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
869 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
892 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
870 sql = ''
893 sql = ''
871 case operator
894 case operator
872 when "="
895 when "="
873 if value.any?
896 if value.any?
874 case type_for(field)
897 case type_for(field)
875 when :date, :date_past
898 when :date, :date_past
876 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
899 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
877 when :integer
900 when :integer
878 if is_custom_filter
901 if is_custom_filter
879 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
902 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
880 else
903 else
881 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
904 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
882 end
905 end
883 when :float
906 when :float
884 if is_custom_filter
907 if is_custom_filter
885 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})"
908 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})"
886 else
909 else
887 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
910 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
888 end
911 end
889 else
912 else
890 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
913 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
891 end
914 end
892 else
915 else
893 # IN an empty set
916 # IN an empty set
894 sql = "1=0"
917 sql = "1=0"
895 end
918 end
896 when "!"
919 when "!"
897 if value.any?
920 if value.any?
898 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
921 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
899 else
922 else
900 # NOT IN an empty set
923 # NOT IN an empty set
901 sql = "1=1"
924 sql = "1=1"
902 end
925 end
903 when "!*"
926 when "!*"
904 sql = "#{db_table}.#{db_field} IS NULL"
927 sql = "#{db_table}.#{db_field} IS NULL"
905 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
928 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
906 when "*"
929 when "*"
907 sql = "#{db_table}.#{db_field} IS NOT NULL"
930 sql = "#{db_table}.#{db_field} IS NOT NULL"
908 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
931 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
909 when ">="
932 when ">="
910 if [:date, :date_past].include?(type_for(field))
933 if [:date, :date_past].include?(type_for(field))
911 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
934 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
912 else
935 else
913 if is_custom_filter
936 if is_custom_filter
914 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
937 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
915 else
938 else
916 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
939 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
917 end
940 end
918 end
941 end
919 when "<="
942 when "<="
920 if [:date, :date_past].include?(type_for(field))
943 if [:date, :date_past].include?(type_for(field))
921 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
944 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
922 else
945 else
923 if is_custom_filter
946 if is_custom_filter
924 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
947 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
925 else
948 else
926 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
949 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
927 end
950 end
928 end
951 end
929 when "><"
952 when "><"
930 if [:date, :date_past].include?(type_for(field))
953 if [:date, :date_past].include?(type_for(field))
931 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
954 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
932 else
955 else
933 if is_custom_filter
956 if is_custom_filter
934 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})"
957 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})"
935 else
958 else
936 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
959 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
937 end
960 end
938 end
961 end
939 when "o"
962 when "o"
940 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
963 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
941 when "c"
964 when "c"
942 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
965 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
943 when "><t-"
966 when "><t-"
944 # between today - n days and today
967 # between today - n days and today
945 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
968 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
946 when ">t-"
969 when ">t-"
947 # >= today - n days
970 # >= today - n days
948 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
971 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
949 when "<t-"
972 when "<t-"
950 # <= today - n days
973 # <= today - n days
951 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
974 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
952 when "t-"
975 when "t-"
953 # = n days in past
976 # = n days in past
954 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
977 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
955 when "><t+"
978 when "><t+"
956 # between today and today + n days
979 # between today and today + n days
957 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
980 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
958 when ">t+"
981 when ">t+"
959 # >= today + n days
982 # >= today + n days
960 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
983 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
961 when "<t+"
984 when "<t+"
962 # <= today + n days
985 # <= today + n days
963 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
986 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
964 when "t+"
987 when "t+"
965 # = today + n days
988 # = today + n days
966 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
989 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
967 when "t"
990 when "t"
968 # = today
991 # = today
969 sql = relative_date_clause(db_table, db_field, 0, 0)
992 sql = relative_date_clause(db_table, db_field, 0, 0)
970 when "w"
993 when "w"
971 # = this week
994 # = this week
972 first_day_of_week = l(:general_first_day_of_week).to_i
995 first_day_of_week = l(:general_first_day_of_week).to_i
973 day_of_week = Date.today.cwday
996 day_of_week = Date.today.cwday
974 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
997 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
975 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
998 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
976 when "~"
999 when "~"
977 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
1000 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
978 when "!~"
1001 when "!~"
979 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
1002 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
980 else
1003 else
981 raise "Unknown query operator #{operator}"
1004 raise "Unknown query operator #{operator}"
982 end
1005 end
983
1006
984 return sql
1007 return sql
985 end
1008 end
986
1009
987 def add_custom_fields_filters(custom_fields, assoc=nil)
1010 def add_custom_fields_filters(custom_fields, assoc=nil)
988 return unless custom_fields.present?
1011 return unless custom_fields.present?
989 @available_filters ||= {}
1012 @available_filters ||= {}
990
1013
991 custom_fields.select(&:is_filter?).each do |field|
1014 custom_fields.select(&:is_filter?).each do |field|
992 case field.field_format
1015 case field.field_format
993 when "text"
1016 when "text"
994 options = { :type => :text, :order => 20 }
1017 options = { :type => :text, :order => 20 }
995 when "list"
1018 when "list"
996 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
1019 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
997 when "date"
1020 when "date"
998 options = { :type => :date, :order => 20 }
1021 options = { :type => :date, :order => 20 }
999 when "bool"
1022 when "bool"
1000 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
1023 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
1001 when "int"
1024 when "int"
1002 options = { :type => :integer, :order => 20 }
1025 options = { :type => :integer, :order => 20 }
1003 when "float"
1026 when "float"
1004 options = { :type => :float, :order => 20 }
1027 options = { :type => :float, :order => 20 }
1005 when "user", "version"
1028 when "user", "version"
1006 next unless project
1029 next unless project
1007 values = field.possible_values_options(project)
1030 values = field.possible_values_options(project)
1008 if User.current.logged? && field.field_format == 'user'
1031 if User.current.logged? && field.field_format == 'user'
1009 values.unshift ["<< #{l(:label_me)} >>", "me"]
1032 values.unshift ["<< #{l(:label_me)} >>", "me"]
1010 end
1033 end
1011 options = { :type => :list_optional, :values => values, :order => 20}
1034 options = { :type => :list_optional, :values => values, :order => 20}
1012 else
1035 else
1013 options = { :type => :string, :order => 20 }
1036 options = { :type => :string, :order => 20 }
1014 end
1037 end
1015 filter_id = "cf_#{field.id}"
1038 filter_id = "cf_#{field.id}"
1016 filter_name = field.name
1039 filter_name = field.name
1017 if assoc.present?
1040 if assoc.present?
1018 filter_id = "#{assoc}.#{filter_id}"
1041 filter_id = "#{assoc}.#{filter_id}"
1019 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1042 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1020 end
1043 end
1021 @available_filters[filter_id] = options.merge({
1044 @available_filters[filter_id] = options.merge({
1022 :name => filter_name,
1045 :name => filter_name,
1023 :format => field.field_format,
1046 :format => field.field_format,
1024 :field => field
1047 :field => field
1025 })
1048 })
1026 end
1049 end
1027 end
1050 end
1028
1051
1029 def add_associations_custom_fields_filters(*associations)
1052 def add_associations_custom_fields_filters(*associations)
1030 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1053 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1031 associations.each do |assoc|
1054 associations.each do |assoc|
1032 association_klass = Issue.reflect_on_association(assoc).klass
1055 association_klass = Issue.reflect_on_association(assoc).klass
1033 fields_by_class.each do |field_class, fields|
1056 fields_by_class.each do |field_class, fields|
1034 if field_class.customized_class <= association_klass
1057 if field_class.customized_class <= association_klass
1035 add_custom_fields_filters(fields, assoc)
1058 add_custom_fields_filters(fields, assoc)
1036 end
1059 end
1037 end
1060 end
1038 end
1061 end
1039 end
1062 end
1040
1063
1041 # Returns a SQL clause for a date or datetime field.
1064 # Returns a SQL clause for a date or datetime field.
1042 def date_clause(table, field, from, to)
1065 def date_clause(table, field, from, to)
1043 s = []
1066 s = []
1044 if from
1067 if from
1045 from_yesterday = from - 1
1068 from_yesterday = from - 1
1046 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1069 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1047 if self.class.default_timezone == :utc
1070 if self.class.default_timezone == :utc
1048 from_yesterday_time = from_yesterday_time.utc
1071 from_yesterday_time = from_yesterday_time.utc
1049 end
1072 end
1050 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1073 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1051 end
1074 end
1052 if to
1075 if to
1053 to_time = Time.local(to.year, to.month, to.day)
1076 to_time = Time.local(to.year, to.month, to.day)
1054 if self.class.default_timezone == :utc
1077 if self.class.default_timezone == :utc
1055 to_time = to_time.utc
1078 to_time = to_time.utc
1056 end
1079 end
1057 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1080 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1058 end
1081 end
1059 s.join(' AND ')
1082 s.join(' AND ')
1060 end
1083 end
1061
1084
1062 # Returns a SQL clause for a date or datetime field using relative dates.
1085 # Returns a SQL clause for a date or datetime field using relative dates.
1063 def relative_date_clause(table, field, days_from, days_to)
1086 def relative_date_clause(table, field, days_from, days_to)
1064 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1087 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1065 end
1088 end
1066
1089
1067 # Additional joins required for the given sort options
1090 # Additional joins required for the given sort options
1068 def joins_for_order_statement(order_options)
1091 def joins_for_order_statement(order_options)
1069 joins = []
1092 joins = []
1070
1093
1071 if order_options
1094 if order_options
1072 if order_options.include?('authors')
1095 if order_options.include?('authors')
1073 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1096 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1074 end
1097 end
1075 order_options.scan(/cf_\d+/).uniq.each do |name|
1098 order_options.scan(/cf_\d+/).uniq.each do |name|
1076 column = available_columns.detect {|c| c.name.to_s == name}
1099 column = available_columns.detect {|c| c.name.to_s == name}
1077 join = column && column.custom_field.join_for_order_statement
1100 join = column && column.custom_field.join_for_order_statement
1078 if join
1101 if join
1079 joins << join
1102 joins << join
1080 end
1103 end
1081 end
1104 end
1082 end
1105 end
1083
1106
1084 joins.any? ? joins.join(' ') : nil
1107 joins.any? ? joins.join(' ') : nil
1085 end
1108 end
1086 end
1109 end
@@ -1,42 +1,49
1 <%= form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list issues">
4 <table class="list issues">
5 <thead>
5 <thead>
6 <tr>
6 <tr>
7 <th class="checkbox hide-when-print">
7 <th class="checkbox hide-when-print">
8 <%= link_to image_tag('toggle_check.png'), {},
8 <%= link_to image_tag('toggle_check.png'), {},
9 :onclick => 'toggleIssuesSelection(this); return false;',
9 :onclick => 'toggleIssuesSelection(this); return false;',
10 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
10 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
11 </th>
11 </th>
12 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
12 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
13 <% query.columns.each do |column| %>
13 <% query.inline_columns.each do |column| %>
14 <%= column_header(column) %>
14 <%= column_header(column) %>
15 <% end %>
15 <% end %>
16 </tr>
16 </tr>
17 </thead>
17 </thead>
18 <% previous_group = false %>
18 <% previous_group = false %>
19 <tbody>
19 <tbody>
20 <% issue_list(issues) do |issue, level| -%>
20 <% issue_list(issues) do |issue, level| -%>
21 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
21 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
22 <% reset_cycle %>
22 <% reset_cycle %>
23 <tr class="group open">
23 <tr class="group open">
24 <td colspan="<%= query.columns.size + 2 %>">
24 <td colspan="<%= query.inline_columns.size + 2 %>">
25 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
25 <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
26 <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span>
26 <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span>
27 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
27 <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
28 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
28 "toggleAllRowGroups(this)", :class => 'toggle-all') %>
29 </td>
29 </td>
30 </tr>
30 </tr>
31 <% previous_group = group %>
31 <% previous_group = group %>
32 <% end %>
32 <% end %>
33 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
33 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
34 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
34 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
35 <td class="id"><%= link_to issue.id, issue_path(issue) %></td>
35 <td class="id"><%= link_to issue.id, issue_path(issue) %></td>
36 <%= raw query.columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
36 <%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
37 </tr>
37 </tr>
38 <% @query.block_columns.each do |column|
39 if (text = column_content(column, issue)) && text.present? -%>
40 <tr class="<%= current_cycle %>">
41 <td colspan="<%= @query.inline_columns.size + 2 %>" class="<%= column.css_classes %>"><%= text %></td>
42 </tr>
43 <% end -%>
44 <% end -%>
38 <% end -%>
45 <% end -%>
39 </tbody>
46 </tbody>
40 </table>
47 </table>
41 </div>
48 </div>
42 <% end -%>
49 <% end -%>
@@ -1,104 +1,108
1 <div class="contextual">
1 <div class="contextual">
2 <% if !@query.new_record? && @query.editable_by?(User.current) %>
2 <% if !@query.new_record? && @query.editable_by?(User.current) %>
3 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
3 <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %>
4 <%= delete_link query_path(@query) %>
4 <%= delete_link query_path(@query) %>
5 <% end %>
5 <% end %>
6 </div>
6 </div>
7
7
8 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
8 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
9 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
9 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
10
10
11 <%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
11 <%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
12 :method => :get, :id => 'query_form') do %>
12 :method => :get, :id => 'query_form') do %>
13 <%= hidden_field_tag 'set_filter', '1' %>
13 <%= hidden_field_tag 'set_filter', '1' %>
14 <div id="query_form_content" class="hide-when-print">
14 <div id="query_form_content" class="hide-when-print">
15 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
15 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
16 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
16 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
17 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
17 <div style="<%= @query.new_record? ? "" : "display: none;" %>">
18 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
18 <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
19 </div>
19 </div>
20 </fieldset>
20 </fieldset>
21 <fieldset class="collapsible collapsed">
21 <fieldset class="collapsible collapsed">
22 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
22 <legend onclick="toggleFieldset(this);"><%= l(:label_options) %></legend>
23 <div style="display: none;">
23 <div style="display: none;">
24 <table>
24 <table>
25 <tr>
25 <tr>
26 <td><%= l(:field_column_names) %></td>
26 <td><%= l(:field_column_names) %></td>
27 <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
27 <td><%= render :partial => 'queries/columns', :locals => {:query => @query} %></td>
28 </tr>
28 </tr>
29 <tr>
29 <tr>
30 <td><label for='group_by'><%= l(:field_group_by) %></label></td>
30 <td><label for='group_by'><%= l(:field_group_by) %></label></td>
31 <td><%= select_tag('group_by',
31 <td><%= select_tag('group_by',
32 options_for_select(
32 options_for_select(
33 [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
33 [[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]},
34 @query.group_by)
34 @query.group_by)
35 ) %></td>
35 ) %></td>
36 </tr>
36 </tr>
37 <tr>
38 <td><%= l(:button_show) %></td>
39 <td><%= available_block_columns_tags(@query) %></td>
40 </tr>
37 </table>
41 </table>
38 </div>
42 </div>
39 </fieldset>
43 </fieldset>
40 </div>
44 </div>
41 <p class="buttons hide-when-print">
45 <p class="buttons hide-when-print">
42
46
43 <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
47 <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %>
44 <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
48 <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %>
45 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
49 <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %>
46 <%= link_to_function l(:button_save),
50 <%= link_to_function l(:button_save),
47 "$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }'); submit_query_form('query_form')",
51 "$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }'); submit_query_form('query_form')",
48 :class => 'icon icon-save' %>
52 :class => 'icon icon-save' %>
49 <% end %>
53 <% end %>
50 </p>
54 </p>
51 <% end %>
55 <% end %>
52
56
53 <%= error_messages_for 'query' %>
57 <%= error_messages_for 'query' %>
54 <% if @query.valid? %>
58 <% if @query.valid? %>
55 <% if @issues.empty? %>
59 <% if @issues.empty? %>
56 <p class="nodata"><%= l(:label_no_data) %></p>
60 <p class="nodata"><%= l(:label_no_data) %></p>
57 <% else %>
61 <% else %>
58 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
62 <%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
59 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
63 <p class="pagination"><%= pagination_links_full @issue_pages, @issue_count %></p>
60 <% end %>
64 <% end %>
61
65
62 <% other_formats_links do |f| %>
66 <% other_formats_links do |f| %>
63 <%= f.link_to 'Atom', :url => params.merge(:key => User.current.rss_key) %>
67 <%= f.link_to 'Atom', :url => params.merge(:key => User.current.rss_key) %>
64 <%= f.link_to 'CSV', :url => params, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
68 <%= f.link_to 'CSV', :url => params, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
65 <%= f.link_to 'PDF', :url => params %>
69 <%= f.link_to 'PDF', :url => params %>
66 <% end %>
70 <% end %>
67
71
68 <div id="csv-export-options" style="display:none;">
72 <div id="csv-export-options" style="display:none;">
69 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
73 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
70 <%= form_tag(params.merge({:format => 'csv',:page=>nil}), :method => :get, :id => 'csv-export-form') do %>
74 <%= form_tag(params.merge({:format => 'csv',:page=>nil}), :method => :get, :id => 'csv-export-form') do %>
71 <p>
75 <p>
72 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
76 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
73 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
77 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
74 </p>
78 </p>
75 <p>
79 <p>
76 <label><%= check_box_tag 'description', '1' %> <%= l(:field_description) %></label>
80 <label><%= check_box_tag 'description', '1', @query.has_column?(:description) %> <%= l(:field_description) %></label>
77 </p>
81 </p>
78 <p class="buttons">
82 <p class="buttons">
79 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
83 <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %>
80 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
84 <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %>
81 </p>
85 </p>
82 <% end %>
86 <% end %>
83 </div>
87 </div>
84
88
85 <% end %>
89 <% end %>
86 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
90 <%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>
87
91
88 <% content_for :sidebar do %>
92 <% content_for :sidebar do %>
89 <%= render :partial => 'issues/sidebar' %>
93 <%= render :partial => 'issues/sidebar' %>
90 <% end %>
94 <% end %>
91
95
92 <% content_for :header_tags do %>
96 <% content_for :header_tags do %>
93 <%= auto_discovery_link_tag(:atom,
97 <%= auto_discovery_link_tag(:atom,
94 {:query_id => @query, :format => 'atom',
98 {:query_id => @query, :format => 'atom',
95 :page => nil, :key => User.current.rss_key},
99 :page => nil, :key => User.current.rss_key},
96 :title => l(:label_issue_plural)) %>
100 :title => l(:label_issue_plural)) %>
97 <%= auto_discovery_link_tag(:atom,
101 <%= auto_discovery_link_tag(:atom,
98 {:controller => 'journals', :action => 'index',
102 {:controller => 'journals', :action => 'index',
99 :query_id => @query, :format => 'atom',
103 :query_id => @query, :format => 'atom',
100 :page => nil, :key => User.current.rss_key},
104 :page => nil, :key => User.current.rss_key},
101 :title => l(:label_changes_details)) %>
105 :title => l(:label_changes_details)) %>
102 <% end %>
106 <% end %>
103
107
104 <%= context_menu issues_context_menu_path %>
108 <%= context_menu issues_context_menu_path %>
@@ -1,34 +1,34
1 <table class="query-columns">
1 <table class="query-columns">
2 <tr>
2 <tr>
3 <td style="padding-left:0">
3 <td style="padding-left:0">
4 <%= label_tag "available_columns", l(:description_available_columns) %>
4 <%= label_tag "available_columns", l(:description_available_columns) %>
5 <br />
5 <br />
6 <%= select_tag 'available_columns',
6 <%= select_tag 'available_columns',
7 options_for_select((query.available_columns - query.columns).collect {|column| [column.caption, column.name]}),
7 options_for_select((query.available_inline_columns - query.columns).collect {|column| [column.caption, column.name]}),
8 :multiple => true, :size => 10, :style => "width:150px",
8 :multiple => true, :size => 10, :style => "width:150px",
9 :ondblclick => "moveOptions(this.form.available_columns, this.form.selected_columns);" %>
9 :ondblclick => "moveOptions(this.form.available_columns, this.form.selected_columns);" %>
10 </td>
10 </td>
11 <td class="buttons">
11 <td class="buttons">
12 <input type="button" value="&#8594;"
12 <input type="button" value="&#8594;"
13 onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br />
13 onclick="moveOptions(this.form.available_columns, this.form.selected_columns);" /><br />
14 <input type="button" value="&#8592;"
14 <input type="button" value="&#8592;"
15 onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" />
15 onclick="moveOptions(this.form.selected_columns, this.form.available_columns);" />
16 </td>
16 </td>
17 <td>
17 <td>
18 <%= label_tag "selected_columns", l(:description_selected_columns) %>
18 <%= label_tag "selected_columns", l(:description_selected_columns) %>
19 <br />
19 <br />
20 <%= select_tag((defined?(tag_name) ? tag_name : 'c[]'),
20 <%= select_tag((defined?(tag_name) ? tag_name : 'c[]'),
21 options_for_select(query.columns.collect {|column| [column.caption, column.name]}),
21 options_for_select(query.inline_columns.collect {|column| [column.caption, column.name]}),
22 :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px",
22 :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px",
23 :ondblclick => "moveOptions(this.form.selected_columns, this.form.available_columns);") %>
23 :ondblclick => "moveOptions(this.form.selected_columns, this.form.available_columns);") %>
24 </td>
24 </td>
25 <td class="buttons">
25 <td class="buttons">
26 <input type="button" value="&#8593;" onclick="moveOptionUp(this.form.selected_columns);" /><br />
26 <input type="button" value="&#8593;" onclick="moveOptionUp(this.form.selected_columns);" /><br />
27 <input type="button" value="&#8595;" onclick="moveOptionDown(this.form.selected_columns);" />
27 <input type="button" value="&#8595;" onclick="moveOptionDown(this.form.selected_columns);" />
28 </td>
28 </td>
29 </tr>
29 </tr>
30 </table>
30 </table>
31
31
32 <% content_for :header_tags do %>
32 <% content_for :header_tags do %>
33 <%= javascript_include_tag 'select_list_move' %>
33 <%= javascript_include_tag 'select_list_move' %>
34 <% end %>
34 <% end %>
@@ -1,52 +1,55
1 <%= error_messages_for 'query' %>
1 <%= error_messages_for 'query' %>
2
2
3 <div class="box">
3 <div class="box">
4 <div class="tabular">
4 <div class="tabular">
5 <p><label for="query_name"><%=l(:field_name)%></label>
5 <p><label for="query_name"><%=l(:field_name)%></label>
6 <%= text_field 'query', 'name', :size => 80 %></p>
6 <%= text_field 'query', 'name', :size => 80 %></p>
7
7
8 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
8 <% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
9 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
9 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
10 <%= check_box 'query', 'is_public',
10 <%= check_box 'query', 'is_public',
11 :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#query_is_for_all").removeAttr("checked"); $("#query_is_for_all").attr("disabled", true);} else {$("#query_is_for_all").removeAttr("disabled");}') %></p>
11 :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#query_is_for_all").removeAttr("checked"); $("#query_is_for_all").attr("disabled", true);} else {$("#query_is_for_all").removeAttr("disabled");}') %></p>
12 <% end %>
12 <% end %>
13
13
14 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
14 <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
15 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
15 <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?,
16 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
16 :disabled => (!@query.new_record? && (@query.project.nil? || (@query.is_public? && !User.current.admin?))) %></p>
17
17
18 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
18 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
19 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
19 <%= check_box_tag 'default_columns', 1, @query.has_default_columns?, :id => 'query_default_columns',
20 :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p>
20 :onclick => 'if (this.checked) {$("#columns").hide();} else {$("#columns").show();}' %></p>
21
21
22 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
22 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
23 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
23 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
24
25 <p><label><%= l(:button_show) %></label>
26 <%= available_block_columns_tags(@query) %></p>
24 </div>
27 </div>
25
28
26 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
29 <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend>
27 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
30 <%= render :partial => 'queries/filters', :locals => {:query => query}%>
28 </fieldset>
31 </fieldset>
29
32
30 <fieldset><legend><%= l(:label_sort) %></legend>
33 <fieldset><legend><%= l(:label_sort) %></legend>
31 <% 3.times do |i| %>
34 <% 3.times do |i| %>
32 <%= i+1 %>:
35 <%= i+1 %>:
33 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
36 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
34 l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %>
37 l(:description_query_sort_criteria_attribute), :class => "hidden-for-sighted" %>
35 <%= select_tag("query[sort_criteria][#{i}][]",
38 <%= select_tag("query[sort_criteria][#{i}][]",
36 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)),
39 options_for_select([[]] + query.available_columns.select(&:sortable?).collect {|column| [column.caption, column.name.to_s]}, @query.sort_criteria_key(i)),
37 :id => "query_sort_criteria_attribute_" + i.to_s)%>
40 :id => "query_sort_criteria_attribute_" + i.to_s)%>
38 <%= label_tag "query_sort_criteria_direction_" + i.to_s,
41 <%= label_tag "query_sort_criteria_direction_" + i.to_s,
39 l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
42 l(:description_query_sort_criteria_direction), :class => "hidden-for-sighted" %>
40 <%= select_tag("query[sort_criteria][#{i}][]",
43 <%= select_tag("query[sort_criteria][#{i}][]",
41 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
44 options_for_select([[], [l(:label_ascending), 'asc'], [l(:label_descending), 'desc']], @query.sort_criteria_order(i)),
42 :id => "query_sort_criteria_direction_" + i.to_s) %>
45 :id => "query_sort_criteria_direction_" + i.to_s) %>
43 <br />
46 <br />
44 <% end %>
47 <% end %>
45 </fieldset>
48 </fieldset>
46
49
47 <%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
50 <%= content_tag 'fieldset', :id => 'columns', :style => (query.has_default_columns? ? 'display:none;' : nil) do %>
48 <legend><%= l(:field_column_names) %></legend>
51 <legend><%= l(:field_column_names) %></legend>
49 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
52 <%= render :partial => 'queries/columns', :locals => {:query => query}%>
50 <% end %>
53 <% end %>
51
54
52 </div>
55 </div>
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,779 +1,792
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'iconv'
20 require 'iconv'
21 require 'tcpdf'
21 require 'tcpdf'
22 require 'fpdf/chinese'
22 require 'fpdf/chinese'
23 require 'fpdf/japanese'
23 require 'fpdf/japanese'
24 require 'fpdf/korean'
24 require 'fpdf/korean'
25
25
26 module Redmine
26 module Redmine
27 module Export
27 module Export
28 module PDF
28 module PDF
29 include ActionView::Helpers::TextHelper
29 include ActionView::Helpers::TextHelper
30 include ActionView::Helpers::NumberHelper
30 include ActionView::Helpers::NumberHelper
31 include IssuesHelper
31 include IssuesHelper
32
32
33 class ITCPDF < TCPDF
33 class ITCPDF < TCPDF
34 include Redmine::I18n
34 include Redmine::I18n
35 attr_accessor :footer_date
35 attr_accessor :footer_date
36
36
37 def initialize(lang)
37 def initialize(lang, orientation='P')
38 @@k_path_cache = Rails.root.join('tmp', 'pdf')
38 @@k_path_cache = Rails.root.join('tmp', 'pdf')
39 FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
39 FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
40 set_language_if_valid lang
40 set_language_if_valid lang
41 pdf_encoding = l(:general_pdf_encoding).upcase
41 pdf_encoding = l(:general_pdf_encoding).upcase
42 super('P', 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding)
42 super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding)
43 case current_language.to_s.downcase
43 case current_language.to_s.downcase
44 when 'vi'
44 when 'vi'
45 @font_for_content = 'DejaVuSans'
45 @font_for_content = 'DejaVuSans'
46 @font_for_footer = 'DejaVuSans'
46 @font_for_footer = 'DejaVuSans'
47 else
47 else
48 case pdf_encoding
48 case pdf_encoding
49 when 'UTF-8'
49 when 'UTF-8'
50 @font_for_content = 'FreeSans'
50 @font_for_content = 'FreeSans'
51 @font_for_footer = 'FreeSans'
51 @font_for_footer = 'FreeSans'
52 when 'CP949'
52 when 'CP949'
53 extend(PDF_Korean)
53 extend(PDF_Korean)
54 AddUHCFont()
54 AddUHCFont()
55 @font_for_content = 'UHC'
55 @font_for_content = 'UHC'
56 @font_for_footer = 'UHC'
56 @font_for_footer = 'UHC'
57 when 'CP932', 'SJIS', 'SHIFT_JIS'
57 when 'CP932', 'SJIS', 'SHIFT_JIS'
58 extend(PDF_Japanese)
58 extend(PDF_Japanese)
59 AddSJISFont()
59 AddSJISFont()
60 @font_for_content = 'SJIS'
60 @font_for_content = 'SJIS'
61 @font_for_footer = 'SJIS'
61 @font_for_footer = 'SJIS'
62 when 'GB18030'
62 when 'GB18030'
63 extend(PDF_Chinese)
63 extend(PDF_Chinese)
64 AddGBFont()
64 AddGBFont()
65 @font_for_content = 'GB'
65 @font_for_content = 'GB'
66 @font_for_footer = 'GB'
66 @font_for_footer = 'GB'
67 when 'BIG5'
67 when 'BIG5'
68 extend(PDF_Chinese)
68 extend(PDF_Chinese)
69 AddBig5Font()
69 AddBig5Font()
70 @font_for_content = 'Big5'
70 @font_for_content = 'Big5'
71 @font_for_footer = 'Big5'
71 @font_for_footer = 'Big5'
72 else
72 else
73 @font_for_content = 'Arial'
73 @font_for_content = 'Arial'
74 @font_for_footer = 'Helvetica'
74 @font_for_footer = 'Helvetica'
75 end
75 end
76 end
76 end
77 SetCreator(Redmine::Info.app_name)
77 SetCreator(Redmine::Info.app_name)
78 SetFont(@font_for_content)
78 SetFont(@font_for_content)
79 @outlines = []
79 @outlines = []
80 @outlineRoot = nil
80 @outlineRoot = nil
81 end
81 end
82
82
83 def SetFontStyle(style, size)
83 def SetFontStyle(style, size)
84 SetFont(@font_for_content, style, size)
84 SetFont(@font_for_content, style, size)
85 end
85 end
86
86
87 def SetTitle(txt)
87 def SetTitle(txt)
88 txt = begin
88 txt = begin
89 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
89 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
90 hextxt = "<FEFF" # FEFF is BOM
90 hextxt = "<FEFF" # FEFF is BOM
91 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
91 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
92 hextxt << ">"
92 hextxt << ">"
93 rescue
93 rescue
94 txt
94 txt
95 end || ''
95 end || ''
96 super(txt)
96 super(txt)
97 end
97 end
98
98
99 def textstring(s)
99 def textstring(s)
100 # Format a text string
100 # Format a text string
101 if s =~ /^</ # This means the string is hex-dumped.
101 if s =~ /^</ # This means the string is hex-dumped.
102 return s
102 return s
103 else
103 else
104 return '('+escape(s)+')'
104 return '('+escape(s)+')'
105 end
105 end
106 end
106 end
107
107
108 def fix_text_encoding(txt)
108 def fix_text_encoding(txt)
109 RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding))
109 RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding))
110 end
110 end
111
111
112 def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
112 def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
113 Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link)
113 Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link)
114 end
114 end
115
115
116 def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
116 def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
117 MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln)
117 MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln)
118 end
118 end
119
119
120 def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
120 def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
121 @attachments = attachments
121 @attachments = attachments
122 writeHTMLCell(w, h, x, y,
122 writeHTMLCell(w, h, x, y,
123 fix_text_encoding(
123 fix_text_encoding(
124 Redmine::WikiFormatting.to_html(Setting.text_formatting, txt)),
124 Redmine::WikiFormatting.to_html(Setting.text_formatting, txt)),
125 border, ln, fill)
125 border, ln, fill)
126 end
126 end
127
127
128 def getImageFilename(attrname)
128 def getImageFilename(attrname)
129 # attrname: general_pdf_encoding string file/uri name
129 # attrname: general_pdf_encoding string file/uri name
130 atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding))
130 atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding))
131 if atta
131 if atta
132 return atta.diskfile
132 return atta.diskfile
133 else
133 else
134 return nil
134 return nil
135 end
135 end
136 end
136 end
137
137
138 def Footer
138 def Footer
139 SetFont(@font_for_footer, 'I', 8)
139 SetFont(@font_for_footer, 'I', 8)
140 SetY(-15)
140 SetY(-15)
141 SetX(15)
141 SetX(15)
142 RDMCell(0, 5, @footer_date, 0, 0, 'L')
142 RDMCell(0, 5, @footer_date, 0, 0, 'L')
143 SetY(-15)
143 SetY(-15)
144 SetX(-30)
144 SetX(-30)
145 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
145 RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
146 end
146 end
147
147
148 def Bookmark(txt, level=0, y=0)
148 def Bookmark(txt, level=0, y=0)
149 if (y == -1)
149 if (y == -1)
150 y = GetY()
150 y = GetY()
151 end
151 end
152 @outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k}
152 @outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k}
153 end
153 end
154
154
155 def bookmark_title(txt)
155 def bookmark_title(txt)
156 txt = begin
156 txt = begin
157 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
157 utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
158 hextxt = "<FEFF" # FEFF is BOM
158 hextxt = "<FEFF" # FEFF is BOM
159 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
159 hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
160 hextxt << ">"
160 hextxt << ">"
161 rescue
161 rescue
162 txt
162 txt
163 end || ''
163 end || ''
164 end
164 end
165
165
166 def putbookmarks
166 def putbookmarks
167 nb=@outlines.size
167 nb=@outlines.size
168 return if (nb==0)
168 return if (nb==0)
169 lru=[]
169 lru=[]
170 level=0
170 level=0
171 @outlines.each_with_index do |o, i|
171 @outlines.each_with_index do |o, i|
172 if(o[:l]>0)
172 if(o[:l]>0)
173 parent=lru[o[:l]-1]
173 parent=lru[o[:l]-1]
174 #Set parent and last pointers
174 #Set parent and last pointers
175 @outlines[i][:parent]=parent
175 @outlines[i][:parent]=parent
176 @outlines[parent][:last]=i
176 @outlines[parent][:last]=i
177 if (o[:l]>level)
177 if (o[:l]>level)
178 #Level increasing: set first pointer
178 #Level increasing: set first pointer
179 @outlines[parent][:first]=i
179 @outlines[parent][:first]=i
180 end
180 end
181 else
181 else
182 @outlines[i][:parent]=nb
182 @outlines[i][:parent]=nb
183 end
183 end
184 if (o[:l]<=level && i>0)
184 if (o[:l]<=level && i>0)
185 #Set prev and next pointers
185 #Set prev and next pointers
186 prev=lru[o[:l]]
186 prev=lru[o[:l]]
187 @outlines[prev][:next]=i
187 @outlines[prev][:next]=i
188 @outlines[i][:prev]=prev
188 @outlines[i][:prev]=prev
189 end
189 end
190 lru[o[:l]]=i
190 lru[o[:l]]=i
191 level=o[:l]
191 level=o[:l]
192 end
192 end
193 #Outline items
193 #Outline items
194 n=self.n+1
194 n=self.n+1
195 @outlines.each_with_index do |o, i|
195 @outlines.each_with_index do |o, i|
196 newobj()
196 newobj()
197 out('<</Title '+bookmark_title(o[:t]))
197 out('<</Title '+bookmark_title(o[:t]))
198 out("/Parent #{n+o[:parent]} 0 R")
198 out("/Parent #{n+o[:parent]} 0 R")
199 if (o[:prev])
199 if (o[:prev])
200 out("/Prev #{n+o[:prev]} 0 R")
200 out("/Prev #{n+o[:prev]} 0 R")
201 end
201 end
202 if (o[:next])
202 if (o[:next])
203 out("/Next #{n+o[:next]} 0 R")
203 out("/Next #{n+o[:next]} 0 R")
204 end
204 end
205 if (o[:first])
205 if (o[:first])
206 out("/First #{n+o[:first]} 0 R")
206 out("/First #{n+o[:first]} 0 R")
207 end
207 end
208 if (o[:last])
208 if (o[:last])
209 out("/Last #{n+o[:last]} 0 R")
209 out("/Last #{n+o[:last]} 0 R")
210 end
210 end
211 out("/Dest [%d 0 R /XYZ 0 %.2f null]" % [1+2*o[:p], o[:y]])
211 out("/Dest [%d 0 R /XYZ 0 %.2f null]" % [1+2*o[:p], o[:y]])
212 out('/Count 0>>')
212 out('/Count 0>>')
213 out('endobj')
213 out('endobj')
214 end
214 end
215 #Outline root
215 #Outline root
216 newobj()
216 newobj()
217 @outlineRoot=self.n
217 @outlineRoot=self.n
218 out("<</Type /Outlines /First #{n} 0 R");
218 out("<</Type /Outlines /First #{n} 0 R");
219 out("/Last #{n+lru[0]} 0 R>>");
219 out("/Last #{n+lru[0]} 0 R>>");
220 out('endobj');
220 out('endobj');
221 end
221 end
222
222
223 def putresources()
223 def putresources()
224 super
224 super
225 putbookmarks()
225 putbookmarks()
226 end
226 end
227
227
228 def putcatalog()
228 def putcatalog()
229 super
229 super
230 if(@outlines.size > 0)
230 if(@outlines.size > 0)
231 out("/Outlines #{@outlineRoot} 0 R");
231 out("/Outlines #{@outlineRoot} 0 R");
232 out('/PageMode /UseOutlines');
232 out('/PageMode /UseOutlines');
233 end
233 end
234 end
234 end
235 end
235 end
236
236
237 # fetch row values
237 # fetch row values
238 def fetch_row_values(issue, query, level)
238 def fetch_row_values(issue, query, level)
239 query.columns.collect do |column|
239 query.inline_columns.collect do |column|
240 s = if column.is_a?(QueryCustomFieldColumn)
240 s = if column.is_a?(QueryCustomFieldColumn)
241 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
241 cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
242 show_value(cv)
242 show_value(cv)
243 else
243 else
244 value = issue.send(column.name)
244 value = issue.send(column.name)
245 if column.name == :subject
245 if column.name == :subject
246 value = " " * level + value
246 value = " " * level + value
247 end
247 end
248 if value.is_a?(Date)
248 if value.is_a?(Date)
249 format_date(value)
249 format_date(value)
250 elsif value.is_a?(Time)
250 elsif value.is_a?(Time)
251 format_time(value)
251 format_time(value)
252 else
252 else
253 value
253 value
254 end
254 end
255 end
255 end
256 s.to_s
256 s.to_s
257 end
257 end
258 end
258 end
259
259
260 # calculate columns width
260 # calculate columns width
261 def calc_col_width(issues, query, table_width, pdf)
261 def calc_col_width(issues, query, table_width, pdf)
262 # calculate statistics
262 # calculate statistics
263 # by captions
263 # by captions
264 pdf.SetFontStyle('B',8)
264 pdf.SetFontStyle('B',8)
265 col_padding = pdf.GetStringWidth('OO')
265 col_padding = pdf.GetStringWidth('OO')
266 col_width_min = query.columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding}
266 col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding}
267 col_width_max = Array.new(col_width_min)
267 col_width_max = Array.new(col_width_min)
268 col_width_avg = Array.new(col_width_min)
268 col_width_avg = Array.new(col_width_min)
269 word_width_max = query.columns.map {|c|
269 word_width_max = query.inline_columns.map {|c|
270 n = 10
270 n = 10
271 c.caption.split.each {|w|
271 c.caption.split.each {|w|
272 x = pdf.GetStringWidth(w) + col_padding
272 x = pdf.GetStringWidth(w) + col_padding
273 n = x if n < x
273 n = x if n < x
274 }
274 }
275 n
275 n
276 }
276 }
277
277
278 # by properties of issues
278 # by properties of issues
279 pdf.SetFontStyle('',8)
279 pdf.SetFontStyle('',8)
280 col_padding = pdf.GetStringWidth('OO')
280 col_padding = pdf.GetStringWidth('OO')
281 k = 1
281 k = 1
282 issue_list(issues) {|issue, level|
282 issue_list(issues) {|issue, level|
283 k += 1
283 k += 1
284 values = fetch_row_values(issue, query, level)
284 values = fetch_row_values(issue, query, level)
285 values.each_with_index {|v,i|
285 values.each_with_index {|v,i|
286 n = pdf.GetStringWidth(v) + col_padding
286 n = pdf.GetStringWidth(v) + col_padding
287 col_width_max[i] = n if col_width_max[i] < n
287 col_width_max[i] = n if col_width_max[i] < n
288 col_width_min[i] = n if col_width_min[i] > n
288 col_width_min[i] = n if col_width_min[i] > n
289 col_width_avg[i] += n
289 col_width_avg[i] += n
290 v.split.each {|w|
290 v.split.each {|w|
291 x = pdf.GetStringWidth(w) + col_padding
291 x = pdf.GetStringWidth(w) + col_padding
292 word_width_max[i] = x if word_width_max[i] < x
292 word_width_max[i] = x if word_width_max[i] < x
293 }
293 }
294 }
294 }
295 }
295 }
296 col_width_avg.map! {|x| x / k}
296 col_width_avg.map! {|x| x / k}
297
297
298 # calculate columns width
298 # calculate columns width
299 ratio = table_width / col_width_avg.inject(0) {|s,w| s += w}
299 ratio = table_width / col_width_avg.inject(0) {|s,w| s += w}
300 col_width = col_width_avg.map {|w| w * ratio}
300 col_width = col_width_avg.map {|w| w * ratio}
301
301
302 # correct max word width if too many columns
302 # correct max word width if too many columns
303 ratio = table_width / word_width_max.inject(0) {|s,w| s += w}
303 ratio = table_width / word_width_max.inject(0) {|s,w| s += w}
304 word_width_max.map! {|v| v * ratio} if ratio < 1
304 word_width_max.map! {|v| v * ratio} if ratio < 1
305
305
306 # correct and lock width of some columns
306 # correct and lock width of some columns
307 done = 1
307 done = 1
308 col_fix = []
308 col_fix = []
309 col_width.each_with_index do |w,i|
309 col_width.each_with_index do |w,i|
310 if w > col_width_max[i]
310 if w > col_width_max[i]
311 col_width[i] = col_width_max[i]
311 col_width[i] = col_width_max[i]
312 col_fix[i] = 1
312 col_fix[i] = 1
313 done = 0
313 done = 0
314 elsif w < word_width_max[i]
314 elsif w < word_width_max[i]
315 col_width[i] = word_width_max[i]
315 col_width[i] = word_width_max[i]
316 col_fix[i] = 1
316 col_fix[i] = 1
317 done = 0
317 done = 0
318 else
318 else
319 col_fix[i] = 0
319 col_fix[i] = 0
320 end
320 end
321 end
321 end
322
322
323 # iterate while need to correct and lock coluns width
323 # iterate while need to correct and lock coluns width
324 while done == 0
324 while done == 0
325 # calculate free & locked columns width
325 # calculate free & locked columns width
326 done = 1
326 done = 1
327 fix_col_width = 0
327 fix_col_width = 0
328 free_col_width = 0
328 free_col_width = 0
329 col_width.each_with_index do |w,i|
329 col_width.each_with_index do |w,i|
330 if col_fix[i] == 1
330 if col_fix[i] == 1
331 fix_col_width += w
331 fix_col_width += w
332 else
332 else
333 free_col_width += w
333 free_col_width += w
334 end
334 end
335 end
335 end
336
336
337 # calculate column normalizing ratio
337 # calculate column normalizing ratio
338 if free_col_width == 0
338 if free_col_width == 0
339 ratio = table_width / col_width.inject(0) {|s,w| s += w}
339 ratio = table_width / col_width.inject(0) {|s,w| s += w}
340 else
340 else
341 ratio = (table_width - fix_col_width) / free_col_width
341 ratio = (table_width - fix_col_width) / free_col_width
342 end
342 end
343
343
344 # correct columns width
344 # correct columns width
345 col_width.each_with_index do |w,i|
345 col_width.each_with_index do |w,i|
346 if col_fix[i] == 0
346 if col_fix[i] == 0
347 col_width[i] = w * ratio
347 col_width[i] = w * ratio
348
348
349 # check if column width less then max word width
349 # check if column width less then max word width
350 if col_width[i] < word_width_max[i]
350 if col_width[i] < word_width_max[i]
351 col_width[i] = word_width_max[i]
351 col_width[i] = word_width_max[i]
352 col_fix[i] = 1
352 col_fix[i] = 1
353 done = 0
353 done = 0
354 elsif col_width[i] > col_width_max[i]
354 elsif col_width[i] > col_width_max[i]
355 col_width[i] = col_width_max[i]
355 col_width[i] = col_width_max[i]
356 col_fix[i] = 1
356 col_fix[i] = 1
357 done = 0
357 done = 0
358 end
358 end
359 end
359 end
360 end
360 end
361 end
361 end
362 col_width
362 col_width
363 end
363 end
364
364
365 def render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
365 def render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
366 # headers
366 # headers
367 pdf.SetFontStyle('B',8)
367 pdf.SetFontStyle('B',8)
368 pdf.SetFillColor(230, 230, 230)
368 pdf.SetFillColor(230, 230, 230)
369
369
370 # render it background to find the max height used
370 # render it background to find the max height used
371 base_x = pdf.GetX
371 base_x = pdf.GetX
372 base_y = pdf.GetY
372 base_y = pdf.GetY
373 max_height = issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
373 max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true)
374 pdf.Rect(base_x, base_y, table_width + col_id_width, max_height, 'FD');
374 pdf.Rect(base_x, base_y, table_width + col_id_width, max_height, 'FD');
375 pdf.SetXY(base_x, base_y);
375 pdf.SetXY(base_x, base_y);
376
376
377 # write the cells on page
377 # write the cells on page
378 pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1)
378 pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1)
379 issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
379 issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true)
380 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
380 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
381 pdf.SetY(base_y + max_height);
381 pdf.SetY(base_y + max_height);
382
382
383 # rows
383 # rows
384 pdf.SetFontStyle('',8)
384 pdf.SetFontStyle('',8)
385 pdf.SetFillColor(255, 255, 255)
385 pdf.SetFillColor(255, 255, 255)
386 end
386 end
387
387
388 # Returns a PDF string of a list of issues
388 # Returns a PDF string of a list of issues
389 def issues_to_pdf(issues, project, query)
389 def issues_to_pdf(issues, project, query)
390 pdf = ITCPDF.new(current_language)
390 pdf = ITCPDF.new(current_language, "L")
391 title = query.new_record? ? l(:label_issue_plural) : query.name
391 title = query.new_record? ? l(:label_issue_plural) : query.name
392 title = "#{project} - #{title}" if project
392 title = "#{project} - #{title}" if project
393 pdf.SetTitle(title)
393 pdf.SetTitle(title)
394 pdf.alias_nb_pages
394 pdf.alias_nb_pages
395 pdf.footer_date = format_date(Date.today)
395 pdf.footer_date = format_date(Date.today)
396 pdf.SetAutoPageBreak(false)
396 pdf.SetAutoPageBreak(false)
397 pdf.AddPage("L")
397 pdf.AddPage("L")
398
398
399 # Landscape A4 = 210 x 297 mm
399 # Landscape A4 = 210 x 297 mm
400 page_height = 210
400 page_height = 210
401 page_width = 297
401 page_width = 297
402 right_margin = 10
402 right_margin = 10
403 bottom_margin = 20
403 bottom_margin = 20
404 col_id_width = 10
404 col_id_width = 10
405 row_height = 4
405 row_height = 4
406
406
407 # column widths
407 # column widths
408 table_width = page_width - right_margin - 10 # fixed left margin
408 table_width = page_width - right_margin - 10 # fixed left margin
409 col_width = []
409 col_width = []
410 unless query.columns.empty?
410 unless query.inline_columns.empty?
411 col_width = calc_col_width(issues, query, table_width - col_id_width, pdf)
411 col_width = calc_col_width(issues, query, table_width - col_id_width, pdf)
412 table_width = col_width.inject(0) {|s,v| s += v}
412 table_width = col_width.inject(0) {|s,v| s += v}
413 end
413 end
414
414
415 # use full width if the description is displayed
416 if table_width > 0 && query.has_column?(:description)
417 col_width = col_width.map {|w| w = w * (page_width - right_margin - 10 - col_id_width) / table_width}
418 table_width = col_width.inject(0) {|s,v| s += v}
419 end
420
415 # title
421 # title
416 pdf.SetFontStyle('B',11)
422 pdf.SetFontStyle('B',11)
417 pdf.RDMCell(190,10, title)
423 pdf.RDMCell(190,10, title)
418 pdf.Ln
424 pdf.Ln
419 render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
425 render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
420 previous_group = false
426 previous_group = false
421 issue_list(issues) do |issue, level|
427 issue_list(issues) do |issue, level|
422 if query.grouped? &&
428 if query.grouped? &&
423 (group = query.group_by_column.value(issue)) != previous_group
429 (group = query.group_by_column.value(issue)) != previous_group
424 pdf.SetFontStyle('B',10)
430 pdf.SetFontStyle('B',10)
425 group_label = group.blank? ? 'None' : group.to_s
431 group_label = group.blank? ? 'None' : group.to_s
426 group_label << " (#{query.issue_count_by_group[group]})"
432 group_label << " (#{query.issue_count_by_group[group]})"
427 pdf.Bookmark group_label, 0, -1
433 pdf.Bookmark group_label, 0, -1
428 pdf.RDMCell(table_width + col_id_width, row_height * 2, group_label, 1, 1, 'L')
434 pdf.RDMCell(table_width + col_id_width, row_height * 2, group_label, 1, 1, 'L')
429 pdf.SetFontStyle('',8)
435 pdf.SetFontStyle('',8)
430 previous_group = group
436 previous_group = group
431 end
437 end
432
438
433 # fetch row values
439 # fetch row values
434 col_values = fetch_row_values(issue, query, level)
440 col_values = fetch_row_values(issue, query, level)
435
441
436 # render it off-page to find the max height used
442 # render it off-page to find the max height used
437 base_x = pdf.GetX
443 base_x = pdf.GetX
438 base_y = pdf.GetY
444 base_y = pdf.GetY
439 pdf.SetY(2 * page_height)
445 pdf.SetY(2 * page_height)
440 max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
446 max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
441 pdf.SetXY(base_x, base_y)
447 pdf.SetXY(base_x, base_y)
442
448
443 # make new page if it doesn't fit on the current one
449 # make new page if it doesn't fit on the current one
444 space_left = page_height - base_y - bottom_margin
450 space_left = page_height - base_y - bottom_margin
445 if max_height > space_left
451 if max_height > space_left
446 pdf.AddPage("L")
452 pdf.AddPage("L")
447 render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
453 render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
448 base_x = pdf.GetX
454 base_x = pdf.GetX
449 base_y = pdf.GetY
455 base_y = pdf.GetY
450 end
456 end
451
457
452 # write the cells on page
458 # write the cells on page
453 pdf.RDMCell(col_id_width, row_height, issue.id.to_s, "T", 0, 'C', 1)
459 pdf.RDMCell(col_id_width, row_height, issue.id.to_s, "T", 0, 'C', 1)
454 issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
460 issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
455 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
461 issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
456 pdf.SetY(base_y + max_height);
462 pdf.SetY(base_y + max_height);
463
464 if query.has_column?(:description) && issue.description?
465 pdf.SetX(10)
466 pdf.SetAutoPageBreak(true, 20)
467 pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT")
468 pdf.SetAutoPageBreak(false)
469 end
457 end
470 end
458
471
459 if issues.size == Setting.issues_export_limit.to_i
472 if issues.size == Setting.issues_export_limit.to_i
460 pdf.SetFontStyle('B',10)
473 pdf.SetFontStyle('B',10)
461 pdf.RDMCell(0, row_height, '...')
474 pdf.RDMCell(0, row_height, '...')
462 end
475 end
463 pdf.Output
476 pdf.Output
464 end
477 end
465
478
466 # Renders MultiCells and returns the maximum height used
479 # Renders MultiCells and returns the maximum height used
467 def issues_to_pdf_write_cells(pdf, col_values, col_widths,
480 def issues_to_pdf_write_cells(pdf, col_values, col_widths,
468 row_height, head=false)
481 row_height, head=false)
469 base_y = pdf.GetY
482 base_y = pdf.GetY
470 max_height = row_height
483 max_height = row_height
471 col_values.each_with_index do |column, i|
484 col_values.each_with_index do |column, i|
472 col_x = pdf.GetX
485 col_x = pdf.GetX
473 if head == true
486 if head == true
474 pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1)
487 pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1)
475 else
488 else
476 pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1)
489 pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1)
477 end
490 end
478 max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height
491 max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height
479 pdf.SetXY(col_x + col_widths[i], base_y);
492 pdf.SetXY(col_x + col_widths[i], base_y);
480 end
493 end
481 return max_height
494 return max_height
482 end
495 end
483
496
484 # Draw lines to close the row (MultiCell border drawing in not uniform)
497 # Draw lines to close the row (MultiCell border drawing in not uniform)
485 def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
498 def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
486 id_width, col_widths)
499 id_width, col_widths)
487 col_x = top_x + id_width
500 col_x = top_x + id_width
488 pdf.Line(col_x, top_y, col_x, lower_y) # id right border
501 pdf.Line(col_x, top_y, col_x, lower_y) # id right border
489 col_widths.each do |width|
502 col_widths.each do |width|
490 col_x += width
503 col_x += width
491 pdf.Line(col_x, top_y, col_x, lower_y) # columns right border
504 pdf.Line(col_x, top_y, col_x, lower_y) # columns right border
492 end
505 end
493 pdf.Line(top_x, top_y, top_x, lower_y) # left border
506 pdf.Line(top_x, top_y, top_x, lower_y) # left border
494 pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border
507 pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border
495 end
508 end
496
509
497 # Returns a PDF string of a single issue
510 # Returns a PDF string of a single issue
498 def issue_to_pdf(issue, assoc={})
511 def issue_to_pdf(issue, assoc={})
499 pdf = ITCPDF.new(current_language)
512 pdf = ITCPDF.new(current_language)
500 pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}")
513 pdf.SetTitle("#{issue.project} - #{issue.tracker} ##{issue.id}")
501 pdf.alias_nb_pages
514 pdf.alias_nb_pages
502 pdf.footer_date = format_date(Date.today)
515 pdf.footer_date = format_date(Date.today)
503 pdf.AddPage
516 pdf.AddPage
504 pdf.SetFontStyle('B',11)
517 pdf.SetFontStyle('B',11)
505 buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
518 buf = "#{issue.project} - #{issue.tracker} ##{issue.id}"
506 pdf.RDMMultiCell(190, 5, buf)
519 pdf.RDMMultiCell(190, 5, buf)
507 pdf.SetFontStyle('',8)
520 pdf.SetFontStyle('',8)
508 base_x = pdf.GetX
521 base_x = pdf.GetX
509 i = 1
522 i = 1
510 issue.ancestors.visible.each do |ancestor|
523 issue.ancestors.visible.each do |ancestor|
511 pdf.SetX(base_x + i)
524 pdf.SetX(base_x + i)
512 buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
525 buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
513 pdf.RDMMultiCell(190 - i, 5, buf)
526 pdf.RDMMultiCell(190 - i, 5, buf)
514 i += 1 if i < 35
527 i += 1 if i < 35
515 end
528 end
516 pdf.SetFontStyle('B',11)
529 pdf.SetFontStyle('B',11)
517 pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
530 pdf.RDMMultiCell(190 - i, 5, issue.subject.to_s)
518 pdf.SetFontStyle('',8)
531 pdf.SetFontStyle('',8)
519 pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
532 pdf.RDMMultiCell(190, 5, "#{format_time(issue.created_on)} - #{issue.author}")
520 pdf.Ln
533 pdf.Ln
521
534
522 left = []
535 left = []
523 left << [l(:field_status), issue.status]
536 left << [l(:field_status), issue.status]
524 left << [l(:field_priority), issue.priority]
537 left << [l(:field_priority), issue.priority]
525 left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
538 left << [l(:field_assigned_to), issue.assigned_to] unless issue.disabled_core_fields.include?('assigned_to_id')
526 left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
539 left << [l(:field_category), issue.category] unless issue.disabled_core_fields.include?('category_id')
527 left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
540 left << [l(:field_fixed_version), issue.fixed_version] unless issue.disabled_core_fields.include?('fixed_version_id')
528
541
529 right = []
542 right = []
530 right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
543 right << [l(:field_start_date), format_date(issue.start_date)] unless issue.disabled_core_fields.include?('start_date')
531 right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
544 right << [l(:field_due_date), format_date(issue.due_date)] unless issue.disabled_core_fields.include?('due_date')
532 right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
545 right << [l(:field_done_ratio), "#{issue.done_ratio}%"] unless issue.disabled_core_fields.include?('done_ratio')
533 right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
546 right << [l(:field_estimated_hours), l_hours(issue.estimated_hours)] unless issue.disabled_core_fields.include?('estimated_hours')
534 right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
547 right << [l(:label_spent_time), l_hours(issue.total_spent_hours)] if User.current.allowed_to?(:view_time_entries, issue.project)
535
548
536 rows = left.size > right.size ? left.size : right.size
549 rows = left.size > right.size ? left.size : right.size
537 while left.size < rows
550 while left.size < rows
538 left << nil
551 left << nil
539 end
552 end
540 while right.size < rows
553 while right.size < rows
541 right << nil
554 right << nil
542 end
555 end
543
556
544 half = (issue.custom_field_values.size / 2.0).ceil
557 half = (issue.custom_field_values.size / 2.0).ceil
545 issue.custom_field_values.each_with_index do |custom_value, i|
558 issue.custom_field_values.each_with_index do |custom_value, i|
546 (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value)]
559 (i < half ? left : right) << [custom_value.custom_field.name, show_value(custom_value)]
547 end
560 end
548
561
549 rows = left.size > right.size ? left.size : right.size
562 rows = left.size > right.size ? left.size : right.size
550 rows.times do |i|
563 rows.times do |i|
551 item = left[i]
564 item = left[i]
552 pdf.SetFontStyle('B',9)
565 pdf.SetFontStyle('B',9)
553 pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
566 pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
554 pdf.SetFontStyle('',9)
567 pdf.SetFontStyle('',9)
555 pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
568 pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
556
569
557 item = right[i]
570 item = right[i]
558 pdf.SetFontStyle('B',9)
571 pdf.SetFontStyle('B',9)
559 pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
572 pdf.RDMCell(35,5, item ? "#{item.first}:" : "", i == 0 ? "LT" : "L")
560 pdf.SetFontStyle('',9)
573 pdf.SetFontStyle('',9)
561 pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
574 pdf.RDMCell(60,5, item ? item.last.to_s : "", i == 0 ? "RT" : "R")
562 pdf.Ln
575 pdf.Ln
563 end
576 end
564
577
565 pdf.SetFontStyle('B',9)
578 pdf.SetFontStyle('B',9)
566 pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
579 pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
567 pdf.SetFontStyle('',9)
580 pdf.SetFontStyle('',9)
568
581
569 # Set resize image scale
582 # Set resize image scale
570 pdf.SetImageScale(1.6)
583 pdf.SetImageScale(1.6)
571 pdf.RDMwriteHTMLCell(35+155, 5, 0, 0,
584 pdf.RDMwriteHTMLCell(35+155, 5, 0, 0,
572 issue.description.to_s, issue.attachments, "LRB")
585 issue.description.to_s, issue.attachments, "LRB")
573
586
574 unless issue.leaf?
587 unless issue.leaf?
575 # for CJK
588 # for CJK
576 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 )
589 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 )
577
590
578 pdf.SetFontStyle('B',9)
591 pdf.SetFontStyle('B',9)
579 pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
592 pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
580 pdf.Ln
593 pdf.Ln
581 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
594 issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
582 buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}",
595 buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}",
583 :length => truncate_length)
596 :length => truncate_length)
584 level = 10 if level >= 10
597 level = 10 if level >= 10
585 pdf.SetFontStyle('',8)
598 pdf.SetFontStyle('',8)
586 pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L")
599 pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L")
587 pdf.SetFontStyle('B',8)
600 pdf.SetFontStyle('B',8)
588 pdf.RDMCell(20,5, child.status.to_s, "R")
601 pdf.RDMCell(20,5, child.status.to_s, "R")
589 pdf.Ln
602 pdf.Ln
590 end
603 end
591 end
604 end
592
605
593 relations = issue.relations.select { |r| r.other_issue(issue).visible? }
606 relations = issue.relations.select { |r| r.other_issue(issue).visible? }
594 unless relations.empty?
607 unless relations.empty?
595 # for CJK
608 # for CJK
596 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 )
609 truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 )
597
610
598 pdf.SetFontStyle('B',9)
611 pdf.SetFontStyle('B',9)
599 pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
612 pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
600 pdf.Ln
613 pdf.Ln
601 relations.each do |relation|
614 relations.each do |relation|
602 buf = ""
615 buf = ""
603 buf += "#{l(relation.label_for(issue))} "
616 buf += "#{l(relation.label_for(issue))} "
604 if relation.delay && relation.delay != 0
617 if relation.delay && relation.delay != 0
605 buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
618 buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
606 end
619 end
607 if Setting.cross_project_issue_relations?
620 if Setting.cross_project_issue_relations?
608 buf += "#{relation.other_issue(issue).project} - "
621 buf += "#{relation.other_issue(issue).project} - "
609 end
622 end
610 buf += "#{relation.other_issue(issue).tracker}" +
623 buf += "#{relation.other_issue(issue).tracker}" +
611 " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
624 " # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
612 buf = truncate(buf, :length => truncate_length)
625 buf = truncate(buf, :length => truncate_length)
613 pdf.SetFontStyle('', 8)
626 pdf.SetFontStyle('', 8)
614 pdf.RDMCell(35+155-60, 5, buf, "L")
627 pdf.RDMCell(35+155-60, 5, buf, "L")
615 pdf.SetFontStyle('B',8)
628 pdf.SetFontStyle('B',8)
616 pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
629 pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
617 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
630 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
618 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R")
631 pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R")
619 pdf.Ln
632 pdf.Ln
620 end
633 end
621 end
634 end
622 pdf.RDMCell(190,5, "", "T")
635 pdf.RDMCell(190,5, "", "T")
623 pdf.Ln
636 pdf.Ln
624
637
625 if issue.changesets.any? &&
638 if issue.changesets.any? &&
626 User.current.allowed_to?(:view_changesets, issue.project)
639 User.current.allowed_to?(:view_changesets, issue.project)
627 pdf.SetFontStyle('B',9)
640 pdf.SetFontStyle('B',9)
628 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
641 pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
629 pdf.Ln
642 pdf.Ln
630 for changeset in issue.changesets
643 for changeset in issue.changesets
631 pdf.SetFontStyle('B',8)
644 pdf.SetFontStyle('B',8)
632 csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
645 csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
633 csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
646 csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
634 pdf.RDMCell(190, 5, csstr)
647 pdf.RDMCell(190, 5, csstr)
635 pdf.Ln
648 pdf.Ln
636 unless changeset.comments.blank?
649 unless changeset.comments.blank?
637 pdf.SetFontStyle('',8)
650 pdf.SetFontStyle('',8)
638 pdf.RDMwriteHTMLCell(190,5,0,0,
651 pdf.RDMwriteHTMLCell(190,5,0,0,
639 changeset.comments.to_s, issue.attachments, "")
652 changeset.comments.to_s, issue.attachments, "")
640 end
653 end
641 pdf.Ln
654 pdf.Ln
642 end
655 end
643 end
656 end
644
657
645 if assoc[:journals].present?
658 if assoc[:journals].present?
646 pdf.SetFontStyle('B',9)
659 pdf.SetFontStyle('B',9)
647 pdf.RDMCell(190,5, l(:label_history), "B")
660 pdf.RDMCell(190,5, l(:label_history), "B")
648 pdf.Ln
661 pdf.Ln
649 assoc[:journals].each do |journal|
662 assoc[:journals].each do |journal|
650 pdf.SetFontStyle('B',8)
663 pdf.SetFontStyle('B',8)
651 title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
664 title = "##{journal.indice} - #{format_time(journal.created_on)} - #{journal.user}"
652 title << " (#{l(:field_private_notes)})" if journal.private_notes?
665 title << " (#{l(:field_private_notes)})" if journal.private_notes?
653 pdf.RDMCell(190,5, title)
666 pdf.RDMCell(190,5, title)
654 pdf.Ln
667 pdf.Ln
655 pdf.SetFontStyle('I',8)
668 pdf.SetFontStyle('I',8)
656 details_to_strings(journal.details, true).each do |string|
669 details_to_strings(journal.details, true).each do |string|
657 pdf.RDMMultiCell(190,5, "- " + string)
670 pdf.RDMMultiCell(190,5, "- " + string)
658 end
671 end
659 if journal.notes?
672 if journal.notes?
660 pdf.Ln unless journal.details.empty?
673 pdf.Ln unless journal.details.empty?
661 pdf.SetFontStyle('',8)
674 pdf.SetFontStyle('',8)
662 pdf.RDMwriteHTMLCell(190,5,0,0,
675 pdf.RDMwriteHTMLCell(190,5,0,0,
663 journal.notes.to_s, issue.attachments, "")
676 journal.notes.to_s, issue.attachments, "")
664 end
677 end
665 pdf.Ln
678 pdf.Ln
666 end
679 end
667 end
680 end
668
681
669 if issue.attachments.any?
682 if issue.attachments.any?
670 pdf.SetFontStyle('B',9)
683 pdf.SetFontStyle('B',9)
671 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
684 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
672 pdf.Ln
685 pdf.Ln
673 for attachment in issue.attachments
686 for attachment in issue.attachments
674 pdf.SetFontStyle('',8)
687 pdf.SetFontStyle('',8)
675 pdf.RDMCell(80,5, attachment.filename)
688 pdf.RDMCell(80,5, attachment.filename)
676 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
689 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
677 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
690 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
678 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
691 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
679 pdf.Ln
692 pdf.Ln
680 end
693 end
681 end
694 end
682 pdf.Output
695 pdf.Output
683 end
696 end
684
697
685 # Returns a PDF string of a set of wiki pages
698 # Returns a PDF string of a set of wiki pages
686 def wiki_pages_to_pdf(pages, project)
699 def wiki_pages_to_pdf(pages, project)
687 pdf = ITCPDF.new(current_language)
700 pdf = ITCPDF.new(current_language)
688 pdf.SetTitle(project.name)
701 pdf.SetTitle(project.name)
689 pdf.alias_nb_pages
702 pdf.alias_nb_pages
690 pdf.footer_date = format_date(Date.today)
703 pdf.footer_date = format_date(Date.today)
691 pdf.AddPage
704 pdf.AddPage
692 pdf.SetFontStyle('B',11)
705 pdf.SetFontStyle('B',11)
693 pdf.RDMMultiCell(190,5, project.name)
706 pdf.RDMMultiCell(190,5, project.name)
694 pdf.Ln
707 pdf.Ln
695 # Set resize image scale
708 # Set resize image scale
696 pdf.SetImageScale(1.6)
709 pdf.SetImageScale(1.6)
697 pdf.SetFontStyle('',9)
710 pdf.SetFontStyle('',9)
698 write_page_hierarchy(pdf, pages.group_by(&:parent_id))
711 write_page_hierarchy(pdf, pages.group_by(&:parent_id))
699 pdf.Output
712 pdf.Output
700 end
713 end
701
714
702 # Returns a PDF string of a single wiki page
715 # Returns a PDF string of a single wiki page
703 def wiki_page_to_pdf(page, project)
716 def wiki_page_to_pdf(page, project)
704 pdf = ITCPDF.new(current_language)
717 pdf = ITCPDF.new(current_language)
705 pdf.SetTitle("#{project} - #{page.title}")
718 pdf.SetTitle("#{project} - #{page.title}")
706 pdf.alias_nb_pages
719 pdf.alias_nb_pages
707 pdf.footer_date = format_date(Date.today)
720 pdf.footer_date = format_date(Date.today)
708 pdf.AddPage
721 pdf.AddPage
709 pdf.SetFontStyle('B',11)
722 pdf.SetFontStyle('B',11)
710 pdf.RDMMultiCell(190,5,
723 pdf.RDMMultiCell(190,5,
711 "#{project} - #{page.title} - # #{page.content.version}")
724 "#{project} - #{page.title} - # #{page.content.version}")
712 pdf.Ln
725 pdf.Ln
713 # Set resize image scale
726 # Set resize image scale
714 pdf.SetImageScale(1.6)
727 pdf.SetImageScale(1.6)
715 pdf.SetFontStyle('',9)
728 pdf.SetFontStyle('',9)
716 write_wiki_page(pdf, page)
729 write_wiki_page(pdf, page)
717 pdf.Output
730 pdf.Output
718 end
731 end
719
732
720 def write_page_hierarchy(pdf, pages, node=nil, level=0)
733 def write_page_hierarchy(pdf, pages, node=nil, level=0)
721 if pages[node]
734 if pages[node]
722 pages[node].each do |page|
735 pages[node].each do |page|
723 if @new_page
736 if @new_page
724 pdf.AddPage
737 pdf.AddPage
725 else
738 else
726 @new_page = true
739 @new_page = true
727 end
740 end
728 pdf.Bookmark page.title, level
741 pdf.Bookmark page.title, level
729 write_wiki_page(pdf, page)
742 write_wiki_page(pdf, page)
730 write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id]
743 write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id]
731 end
744 end
732 end
745 end
733 end
746 end
734
747
735 def write_wiki_page(pdf, page)
748 def write_wiki_page(pdf, page)
736 pdf.RDMwriteHTMLCell(190,5,0,0,
749 pdf.RDMwriteHTMLCell(190,5,0,0,
737 page.content.text.to_s, page.attachments, 0)
750 page.content.text.to_s, page.attachments, 0)
738 if page.attachments.any?
751 if page.attachments.any?
739 pdf.Ln
752 pdf.Ln
740 pdf.SetFontStyle('B',9)
753 pdf.SetFontStyle('B',9)
741 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
754 pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
742 pdf.Ln
755 pdf.Ln
743 for attachment in page.attachments
756 for attachment in page.attachments
744 pdf.SetFontStyle('',8)
757 pdf.SetFontStyle('',8)
745 pdf.RDMCell(80,5, attachment.filename)
758 pdf.RDMCell(80,5, attachment.filename)
746 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
759 pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
747 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
760 pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
748 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
761 pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
749 pdf.Ln
762 pdf.Ln
750 end
763 end
751 end
764 end
752 end
765 end
753
766
754 class RDMPdfEncoding
767 class RDMPdfEncoding
755 def self.rdm_from_utf8(txt, encoding)
768 def self.rdm_from_utf8(txt, encoding)
756 txt ||= ''
769 txt ||= ''
757 txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
770 txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
758 if txt.respond_to?(:force_encoding)
771 if txt.respond_to?(:force_encoding)
759 txt.force_encoding('ASCII-8BIT')
772 txt.force_encoding('ASCII-8BIT')
760 end
773 end
761 txt
774 txt
762 end
775 end
763
776
764 def self.attach(attachments, filename, encoding)
777 def self.attach(attachments, filename, encoding)
765 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
778 filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
766 atta = nil
779 atta = nil
767 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
780 if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
768 atta = Attachment.latest_attach(attachments, filename_utf8)
781 atta = Attachment.latest_attach(attachments, filename_utf8)
769 end
782 end
770 if atta && atta.readable? && atta.visible?
783 if atta && atta.readable? && atta.visible?
771 return atta
784 return atta
772 else
785 else
773 return nil
786 return nil
774 end
787 end
775 end
788 end
776 end
789 end
777 end
790 end
778 end
791 end
779 end
792 end
@@ -1,1136 +1,1138
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
3
3
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 #content h1, h2, h3, h4 {color: #555;}
5 #content h1, h2, h3, h4 {color: #555;}
6 h2, .wiki h1 {font-size: 20px;}
6 h2, .wiki h1 {font-size: 20px;}
7 h3, .wiki h2 {font-size: 16px;}
7 h3, .wiki h2 {font-size: 16px;}
8 h4, .wiki h3 {font-size: 13px;}
8 h4, .wiki h3 {font-size: 13px;}
9 h4 {border-bottom: 1px dotted #bbb;}
9 h4 {border-bottom: 1px dotted #bbb;}
10
10
11 /***** Layout *****/
11 /***** Layout *****/
12 #wrapper {background: white;}
12 #wrapper {background: white;}
13
13
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
14 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 #top-menu ul {margin: 0; padding: 0;}
15 #top-menu ul {margin: 0; padding: 0;}
16 #top-menu li {
16 #top-menu li {
17 float:left;
17 float:left;
18 list-style-type:none;
18 list-style-type:none;
19 margin: 0px 0px 0px 0px;
19 margin: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
20 padding: 0px 0px 0px 0px;
21 white-space:nowrap;
21 white-space:nowrap;
22 }
22 }
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
23 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
24 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25
25
26 #account {float:right;}
26 #account {float:right;}
27
27
28 #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
29 #header a {color:#f8f8f8;}
29 #header a {color:#f8f8f8;}
30 #header h1 a.ancestor { font-size: 80%; }
30 #header h1 a.ancestor { font-size: 80%; }
31 #quick-search {float:right;}
31 #quick-search {float:right;}
32
32
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
33 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
34 #main-menu ul {margin: 0; padding: 0;}
34 #main-menu ul {margin: 0; padding: 0;}
35 #main-menu li {
35 #main-menu li {
36 float:left;
36 float:left;
37 list-style-type:none;
37 list-style-type:none;
38 margin: 0px 2px 0px 0px;
38 margin: 0px 2px 0px 0px;
39 padding: 0px 0px 0px 0px;
39 padding: 0px 0px 0px 0px;
40 white-space:nowrap;
40 white-space:nowrap;
41 }
41 }
42 #main-menu li a {
42 #main-menu li a {
43 display: block;
43 display: block;
44 color: #fff;
44 color: #fff;
45 text-decoration: none;
45 text-decoration: none;
46 font-weight: bold;
46 font-weight: bold;
47 margin: 0;
47 margin: 0;
48 padding: 4px 10px 4px 10px;
48 padding: 4px 10px 4px 10px;
49 }
49 }
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
50 #main-menu li a:hover {background:#759FCF; color:#fff;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
51 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
52
52
53 #admin-menu ul {margin: 0; padding: 0;}
53 #admin-menu ul {margin: 0; padding: 0;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
54 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
55
55
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
56 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
57 #admin-menu a.projects { background-image: url(../images/projects.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
58 #admin-menu a.users { background-image: url(../images/user.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
59 #admin-menu a.groups { background-image: url(../images/group.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
60 #admin-menu a.roles { background-image: url(../images/database_key.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
61 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
62 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
63 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
64 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
65 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
66 #admin-menu a.settings { background-image: url(../images/changeset.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
67 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
68 #admin-menu a.info { background-image: url(../images/help.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
69 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
70
70
71 #main {background-color:#EEEEEE;}
71 #main {background-color:#EEEEEE;}
72
72
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
73 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
74 * html #sidebar{ width: 22%; }
74 * html #sidebar{ width: 22%; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
75 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
76 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
77 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
78 #sidebar .contextual { margin-right: 1em; }
78 #sidebar .contextual { margin-right: 1em; }
79
79
80 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
81 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
82 html>body #content { min-height: 600px; }
82 html>body #content { min-height: 600px; }
83 * html body #content { height: 600px; } /* IE */
83 * html body #content { height: 600px; } /* IE */
84
84
85 #main.nosidebar #sidebar{ display: none; }
85 #main.nosidebar #sidebar{ display: none; }
86 #main.nosidebar #content{ width: auto; border-right: 0; }
86 #main.nosidebar #content{ width: auto; border-right: 0; }
87
87
88 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
89
89
90 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
91 #login-form table td {padding: 6px;}
91 #login-form table td {padding: 6px;}
92 #login-form label {font-weight: bold;}
92 #login-form label {font-weight: bold;}
93 #login-form input#username, #login-form input#password { width: 300px; }
93 #login-form input#username, #login-form input#password { width: 300px; }
94
94
95 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
95 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
96 div.modal h3.title {display:none;}
96 div.modal h3.title {display:none;}
97 div.modal p.buttons {text-align:right; margin-bottom:0;}
97 div.modal p.buttons {text-align:right; margin-bottom:0;}
98
98
99 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
99 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
100
100
101 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
101 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
102
102
103 /***** Links *****/
103 /***** Links *****/
104 a, a:link, a:visited{ color: #169; text-decoration: none; }
104 a, a:link, a:visited{ color: #169; text-decoration: none; }
105 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
105 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
106 a img{ border: 0; }
106 a img{ border: 0; }
107
107
108 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
108 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
109 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
109 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
110 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
110 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
111
111
112 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
112 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
113 #sidebar a.selected:hover {text-decoration:none;}
113 #sidebar a.selected:hover {text-decoration:none;}
114 #admin-menu a {line-height:1.7em;}
114 #admin-menu a {line-height:1.7em;}
115 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
115 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
116
116
117 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
117 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
118 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
118 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
119
119
120 a#toggle-completed-versions {color:#999;}
120 a#toggle-completed-versions {color:#999;}
121 /***** Tables *****/
121 /***** Tables *****/
122 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
122 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
123 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
123 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
124 table.list td { vertical-align: top; padding-right:10px; }
124 table.list td { vertical-align: top; padding-right:10px; }
125 table.list td.id { width: 2%; text-align: center;}
125 table.list td.id { width: 2%; text-align: center;}
126 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
126 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
127 table.list td.checkbox input {padding:0px;}
127 table.list td.checkbox input {padding:0px;}
128 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
128 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
129 table.list td.buttons a { padding-right: 0.6em; }
129 table.list td.buttons a { padding-right: 0.6em; }
130 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
130 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
131
131
132 tr.project td.name a { white-space:nowrap; }
132 tr.project td.name a { white-space:nowrap; }
133 tr.project.closed, tr.project.archived { color: #aaa; }
133 tr.project.closed, tr.project.archived { color: #aaa; }
134 tr.project.closed a, tr.project.archived a { color: #aaa; }
134 tr.project.closed a, tr.project.archived a { color: #aaa; }
135
135
136 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
136 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
137 tr.project.idnt-1 td.name {padding-left: 0.5em;}
137 tr.project.idnt-1 td.name {padding-left: 0.5em;}
138 tr.project.idnt-2 td.name {padding-left: 2em;}
138 tr.project.idnt-2 td.name {padding-left: 2em;}
139 tr.project.idnt-3 td.name {padding-left: 3.5em;}
139 tr.project.idnt-3 td.name {padding-left: 3.5em;}
140 tr.project.idnt-4 td.name {padding-left: 5em;}
140 tr.project.idnt-4 td.name {padding-left: 5em;}
141 tr.project.idnt-5 td.name {padding-left: 6.5em;}
141 tr.project.idnt-5 td.name {padding-left: 6.5em;}
142 tr.project.idnt-6 td.name {padding-left: 8em;}
142 tr.project.idnt-6 td.name {padding-left: 8em;}
143 tr.project.idnt-7 td.name {padding-left: 9.5em;}
143 tr.project.idnt-7 td.name {padding-left: 9.5em;}
144 tr.project.idnt-8 td.name {padding-left: 11em;}
144 tr.project.idnt-8 td.name {padding-left: 11em;}
145 tr.project.idnt-9 td.name {padding-left: 12.5em;}
145 tr.project.idnt-9 td.name {padding-left: 12.5em;}
146
146
147 tr.issue { text-align: center; white-space: nowrap; }
147 tr.issue { text-align: center; white-space: nowrap; }
148 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
148 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; }
149 tr.issue td.subject, tr.issue td.relations { text-align: left; }
149 tr.issue td.subject, tr.issue td.relations { text-align: left; }
150 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
150 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
151 tr.issue td.relations span {white-space: nowrap;}
151 tr.issue td.relations span {white-space: nowrap;}
152 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
153 table.issues td.description pre {white-space:normal;}
152
154
153 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
155 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
154 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
156 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
155 tr.issue.idnt-2 td.subject {padding-left: 2em;}
157 tr.issue.idnt-2 td.subject {padding-left: 2em;}
156 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
158 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
157 tr.issue.idnt-4 td.subject {padding-left: 5em;}
159 tr.issue.idnt-4 td.subject {padding-left: 5em;}
158 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
160 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
159 tr.issue.idnt-6 td.subject {padding-left: 8em;}
161 tr.issue.idnt-6 td.subject {padding-left: 8em;}
160 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
162 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
161 tr.issue.idnt-8 td.subject {padding-left: 11em;}
163 tr.issue.idnt-8 td.subject {padding-left: 11em;}
162 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
164 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
163
165
164 tr.entry { border: 1px solid #f8f8f8; }
166 tr.entry { border: 1px solid #f8f8f8; }
165 tr.entry td { white-space: nowrap; }
167 tr.entry td { white-space: nowrap; }
166 tr.entry td.filename { width: 30%; }
168 tr.entry td.filename { width: 30%; }
167 tr.entry td.filename_no_report { width: 70%; }
169 tr.entry td.filename_no_report { width: 70%; }
168 tr.entry td.size { text-align: right; font-size: 90%; }
170 tr.entry td.size { text-align: right; font-size: 90%; }
169 tr.entry td.revision, tr.entry td.author { text-align: center; }
171 tr.entry td.revision, tr.entry td.author { text-align: center; }
170 tr.entry td.age { text-align: right; }
172 tr.entry td.age { text-align: right; }
171 tr.entry.file td.filename a { margin-left: 16px; }
173 tr.entry.file td.filename a { margin-left: 16px; }
172 tr.entry.file td.filename_no_report a { margin-left: 16px; }
174 tr.entry.file td.filename_no_report a { margin-left: 16px; }
173
175
174 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
176 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
175 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
177 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
176
178
177 tr.changeset { height: 20px }
179 tr.changeset { height: 20px }
178 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
180 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
179 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
181 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
180 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
182 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
181 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
183 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
182
184
183 table.files tr.file td { text-align: center; }
185 table.files tr.file td { text-align: center; }
184 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
186 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
185 table.files tr.file td.digest { font-size: 80%; }
187 table.files tr.file td.digest { font-size: 80%; }
186
188
187 table.members td.roles, table.memberships td.roles { width: 45%; }
189 table.members td.roles, table.memberships td.roles { width: 45%; }
188
190
189 tr.message { height: 2.6em; }
191 tr.message { height: 2.6em; }
190 tr.message td.subject { padding-left: 20px; }
192 tr.message td.subject { padding-left: 20px; }
191 tr.message td.created_on { white-space: nowrap; }
193 tr.message td.created_on { white-space: nowrap; }
192 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
194 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
193 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
195 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
194 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
196 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
195
197
196 tr.version.closed, tr.version.closed a { color: #999; }
198 tr.version.closed, tr.version.closed a { color: #999; }
197 tr.version td.name { padding-left: 20px; }
199 tr.version td.name { padding-left: 20px; }
198 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
200 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
199 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
201 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
200
202
201 tr.user td { width:13%; }
203 tr.user td { width:13%; }
202 tr.user td.email { width:18%; }
204 tr.user td.email { width:18%; }
203 tr.user td { white-space: nowrap; }
205 tr.user td { white-space: nowrap; }
204 tr.user.locked, tr.user.registered { color: #aaa; }
206 tr.user.locked, tr.user.registered { color: #aaa; }
205 tr.user.locked a, tr.user.registered a { color: #aaa; }
207 tr.user.locked a, tr.user.registered a { color: #aaa; }
206
208
207 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
209 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
208
210
209 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
211 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
210
212
211 tr.time-entry { text-align: center; white-space: nowrap; }
213 tr.time-entry { text-align: center; white-space: nowrap; }
212 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
214 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
213 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
215 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
214 td.hours .hours-dec { font-size: 0.9em; }
216 td.hours .hours-dec { font-size: 0.9em; }
215
217
216 table.plugins td { vertical-align: middle; }
218 table.plugins td { vertical-align: middle; }
217 table.plugins td.configure { text-align: right; padding-right: 1em; }
219 table.plugins td.configure { text-align: right; padding-right: 1em; }
218 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
220 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
219 table.plugins span.description { display: block; font-size: 0.9em; }
221 table.plugins span.description { display: block; font-size: 0.9em; }
220 table.plugins span.url { display: block; font-size: 0.9em; }
222 table.plugins span.url { display: block; font-size: 0.9em; }
221
223
222 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
224 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
223 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
225 table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
224 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
226 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
225 tr.group:hover a.toggle-all { display:inline;}
227 tr.group:hover a.toggle-all { display:inline;}
226 a.toggle-all:hover {text-decoration:none;}
228 a.toggle-all:hover {text-decoration:none;}
227
229
228 table.list tbody tr:hover { background-color:#ffffdd; }
230 table.list tbody tr:hover { background-color:#ffffdd; }
229 table.list tbody tr.group:hover { background-color:inherit; }
231 table.list tbody tr.group:hover { background-color:inherit; }
230 table td {padding:2px;}
232 table td {padding:2px;}
231 table p {margin:0;}
233 table p {margin:0;}
232 .odd {background-color:#f6f7f8;}
234 .odd {background-color:#f6f7f8;}
233 .even {background-color: #fff;}
235 .even {background-color: #fff;}
234
236
235 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
237 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
236 a.sort.asc { background-image: url(../images/sort_asc.png); }
238 a.sort.asc { background-image: url(../images/sort_asc.png); }
237 a.sort.desc { background-image: url(../images/sort_desc.png); }
239 a.sort.desc { background-image: url(../images/sort_desc.png); }
238
240
239 table.attributes { width: 100% }
241 table.attributes { width: 100% }
240 table.attributes th { vertical-align: top; text-align: left; }
242 table.attributes th { vertical-align: top; text-align: left; }
241 table.attributes td { vertical-align: top; }
243 table.attributes td { vertical-align: top; }
242
244
243 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
245 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
244 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
246 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
245 table.boards td.last-message {font-size:80%;}
247 table.boards td.last-message {font-size:80%;}
246
248
247 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
249 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
248
250
249 table.query-columns {
251 table.query-columns {
250 border-collapse: collapse;
252 border-collapse: collapse;
251 border: 0;
253 border: 0;
252 }
254 }
253
255
254 table.query-columns td.buttons {
256 table.query-columns td.buttons {
255 vertical-align: middle;
257 vertical-align: middle;
256 text-align: center;
258 text-align: center;
257 }
259 }
258
260
259 td.center {text-align:center;}
261 td.center {text-align:center;}
260
262
261 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
263 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
262
264
263 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
265 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
264 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
266 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
265 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
267 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
266 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
268 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
267
269
268 #watchers ul {margin: 0; padding: 0;}
270 #watchers ul {margin: 0; padding: 0;}
269 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
271 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
270 #watchers select {width: 95%; display: block;}
272 #watchers select {width: 95%; display: block;}
271 #watchers a.delete {opacity: 0.4;}
273 #watchers a.delete {opacity: 0.4;}
272 #watchers a.delete:hover {opacity: 1;}
274 #watchers a.delete:hover {opacity: 1;}
273 #watchers img.gravatar {margin: 0 4px 2px 0;}
275 #watchers img.gravatar {margin: 0 4px 2px 0;}
274
276
275 span#watchers_inputs {overflow:auto; display:block;}
277 span#watchers_inputs {overflow:auto; display:block;}
276 span.search_for_watchers {display:block;}
278 span.search_for_watchers {display:block;}
277 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
279 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
278 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
280 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
279
281
280
282
281 .highlight { background-color: #FCFD8D;}
283 .highlight { background-color: #FCFD8D;}
282 .highlight.token-1 { background-color: #faa;}
284 .highlight.token-1 { background-color: #faa;}
283 .highlight.token-2 { background-color: #afa;}
285 .highlight.token-2 { background-color: #afa;}
284 .highlight.token-3 { background-color: #aaf;}
286 .highlight.token-3 { background-color: #aaf;}
285
287
286 .box{
288 .box{
287 padding:6px;
289 padding:6px;
288 margin-bottom: 10px;
290 margin-bottom: 10px;
289 background-color:#f6f6f6;
291 background-color:#f6f6f6;
290 color:#505050;
292 color:#505050;
291 line-height:1.5em;
293 line-height:1.5em;
292 border: 1px solid #e4e4e4;
294 border: 1px solid #e4e4e4;
293 }
295 }
294
296
295 div.square {
297 div.square {
296 border: 1px solid #999;
298 border: 1px solid #999;
297 float: left;
299 float: left;
298 margin: .3em .4em 0 .4em;
300 margin: .3em .4em 0 .4em;
299 overflow: hidden;
301 overflow: hidden;
300 width: .6em; height: .6em;
302 width: .6em; height: .6em;
301 }
303 }
302 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
304 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
303 .contextual input, .contextual select {font-size:0.9em;}
305 .contextual input, .contextual select {font-size:0.9em;}
304 .message .contextual { margin-top: 0; }
306 .message .contextual { margin-top: 0; }
305
307
306 .splitcontent {overflow:auto;}
308 .splitcontent {overflow:auto;}
307 .splitcontentleft{float:left; width:49%;}
309 .splitcontentleft{float:left; width:49%;}
308 .splitcontentright{float:right; width:49%;}
310 .splitcontentright{float:right; width:49%;}
309 form {display: inline;}
311 form {display: inline;}
310 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
312 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
311 fieldset {border: 1px solid #e4e4e4; margin:0;}
313 fieldset {border: 1px solid #e4e4e4; margin:0;}
312 legend {color: #484848;}
314 legend {color: #484848;}
313 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
315 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
314 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
316 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
315 blockquote blockquote { margin-left: 0;}
317 blockquote blockquote { margin-left: 0;}
316 acronym { border-bottom: 1px dotted; cursor: help; }
318 acronym { border-bottom: 1px dotted; cursor: help; }
317 textarea.wiki-edit {width:99%; resize:vertical;}
319 textarea.wiki-edit {width:99%; resize:vertical;}
318 li p {margin-top: 0;}
320 li p {margin-top: 0;}
319 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
321 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
320 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
322 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
321 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
323 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
322 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
324 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
323
325
324 div.issue div.subject div div { padding-left: 16px; }
326 div.issue div.subject div div { padding-left: 16px; }
325 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
327 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
326 div.issue div.subject>div>p { margin-top: 0.5em; }
328 div.issue div.subject>div>p { margin-top: 0.5em; }
327 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
329 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
328 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
330 div.issue span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
329 div.issue .next-prev-links {color:#999;}
331 div.issue .next-prev-links {color:#999;}
330 div.issue table.attributes th {width:22%;}
332 div.issue table.attributes th {width:22%;}
331 div.issue table.attributes td {width:28%;}
333 div.issue table.attributes td {width:28%;}
332
334
333 #issue_tree table.issues, #relations table.issues { border: 0; }
335 #issue_tree table.issues, #relations table.issues { border: 0; }
334 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
336 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
335 #relations td.buttons {padding:0;}
337 #relations td.buttons {padding:0;}
336
338
337 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
339 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
338 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
340 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
339 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
341 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
340
342
341 fieldset#date-range p { margin: 2px 0 2px 0; }
343 fieldset#date-range p { margin: 2px 0 2px 0; }
342 fieldset#filters table { border-collapse: collapse; }
344 fieldset#filters table { border-collapse: collapse; }
343 fieldset#filters table td { padding: 0; vertical-align: middle; }
345 fieldset#filters table td { padding: 0; vertical-align: middle; }
344 fieldset#filters tr.filter { height: 2.1em; }
346 fieldset#filters tr.filter { height: 2.1em; }
345 fieldset#filters td.field { width:230px; }
347 fieldset#filters td.field { width:230px; }
346 fieldset#filters td.operator { width:180px; }
348 fieldset#filters td.operator { width:180px; }
347 fieldset#filters td.operator select {max-width:170px;}
349 fieldset#filters td.operator select {max-width:170px;}
348 fieldset#filters td.values { white-space:nowrap; }
350 fieldset#filters td.values { white-space:nowrap; }
349 fieldset#filters td.values select {min-width:130px;}
351 fieldset#filters td.values select {min-width:130px;}
350 fieldset#filters td.values input {height:1em;}
352 fieldset#filters td.values input {height:1em;}
351 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
353 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
352
354
353 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
355 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
354 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
356 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
355
357
356 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
358 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
357 div#issue-changesets div.changeset { padding: 4px;}
359 div#issue-changesets div.changeset { padding: 4px;}
358 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
360 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
359 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
361 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
360
362
361 .journal ul.details img {margin:0 0 -3px 4px;}
363 .journal ul.details img {margin:0 0 -3px 4px;}
362 div.journal {overflow:auto;}
364 div.journal {overflow:auto;}
363 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
365 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
364
366
365 div#activity dl, #search-results { margin-left: 2em; }
367 div#activity dl, #search-results { margin-left: 2em; }
366 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
368 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
367 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
369 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
368 div#activity dt.me .time { border-bottom: 1px solid #999; }
370 div#activity dt.me .time { border-bottom: 1px solid #999; }
369 div#activity dt .time { color: #777; font-size: 80%; }
371 div#activity dt .time { color: #777; font-size: 80%; }
370 div#activity dd .description, #search-results dd .description { font-style: italic; }
372 div#activity dd .description, #search-results dd .description { font-style: italic; }
371 div#activity span.project:after, #search-results span.project:after { content: " -"; }
373 div#activity span.project:after, #search-results span.project:after { content: " -"; }
372 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
374 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
373
375
374 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
376 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
375
377
376 div#search-results-counts {float:right;}
378 div#search-results-counts {float:right;}
377 div#search-results-counts ul { margin-top: 0.5em; }
379 div#search-results-counts ul { margin-top: 0.5em; }
378 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
380 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
379
381
380 dt.issue { background-image: url(../images/ticket.png); }
382 dt.issue { background-image: url(../images/ticket.png); }
381 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
383 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
382 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
384 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
383 dt.issue-note { background-image: url(../images/ticket_note.png); }
385 dt.issue-note { background-image: url(../images/ticket_note.png); }
384 dt.changeset { background-image: url(../images/changeset.png); }
386 dt.changeset { background-image: url(../images/changeset.png); }
385 dt.news { background-image: url(../images/news.png); }
387 dt.news { background-image: url(../images/news.png); }
386 dt.message { background-image: url(../images/message.png); }
388 dt.message { background-image: url(../images/message.png); }
387 dt.reply { background-image: url(../images/comments.png); }
389 dt.reply { background-image: url(../images/comments.png); }
388 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
390 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
389 dt.attachment { background-image: url(../images/attachment.png); }
391 dt.attachment { background-image: url(../images/attachment.png); }
390 dt.document { background-image: url(../images/document.png); }
392 dt.document { background-image: url(../images/document.png); }
391 dt.project { background-image: url(../images/projects.png); }
393 dt.project { background-image: url(../images/projects.png); }
392 dt.time-entry { background-image: url(../images/time.png); }
394 dt.time-entry { background-image: url(../images/time.png); }
393
395
394 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
396 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
395
397
396 div#roadmap .related-issues { margin-bottom: 1em; }
398 div#roadmap .related-issues { margin-bottom: 1em; }
397 div#roadmap .related-issues td.checkbox { display: none; }
399 div#roadmap .related-issues td.checkbox { display: none; }
398 div#roadmap .wiki h1:first-child { display: none; }
400 div#roadmap .wiki h1:first-child { display: none; }
399 div#roadmap .wiki h1 { font-size: 120%; }
401 div#roadmap .wiki h1 { font-size: 120%; }
400 div#roadmap .wiki h2 { font-size: 110%; }
402 div#roadmap .wiki h2 { font-size: 110%; }
401 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
403 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
402
404
403 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
405 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
404 div#version-summary fieldset { margin-bottom: 1em; }
406 div#version-summary fieldset { margin-bottom: 1em; }
405 div#version-summary fieldset.time-tracking table { width:100%; }
407 div#version-summary fieldset.time-tracking table { width:100%; }
406 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
408 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
407
409
408 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
410 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
409 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
411 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
410 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
412 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
411 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
413 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
412 table#time-report .hours-dec { font-size: 0.9em; }
414 table#time-report .hours-dec { font-size: 0.9em; }
413
415
414 div.wiki-page .contextual a {opacity: 0.4}
416 div.wiki-page .contextual a {opacity: 0.4}
415 div.wiki-page .contextual a:hover {opacity: 1}
417 div.wiki-page .contextual a:hover {opacity: 1}
416
418
417 form .attributes select { width: 60%; }
419 form .attributes select { width: 60%; }
418 input#issue_subject { width: 99%; }
420 input#issue_subject { width: 99%; }
419 select#issue_done_ratio { width: 95px; }
421 select#issue_done_ratio { width: 95px; }
420
422
421 ul.projects {margin:0; padding-left:1em;}
423 ul.projects {margin:0; padding-left:1em;}
422 ul.projects ul {padding-left:1.6em;}
424 ul.projects ul {padding-left:1.6em;}
423 ul.projects.root {margin:0; padding:0;}
425 ul.projects.root {margin:0; padding:0;}
424 ul.projects li {list-style-type:none;}
426 ul.projects li {list-style-type:none;}
425
427
426 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
428 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
427 #projects-index ul.projects li.root {margin-bottom: 1em;}
429 #projects-index ul.projects li.root {margin-bottom: 1em;}
428 #projects-index ul.projects li.child {margin-top: 1em;}
430 #projects-index ul.projects li.child {margin-top: 1em;}
429 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
431 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
430 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
432 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
431
433
432 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
434 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
433
435
434 #related-issues li img {vertical-align:middle;}
436 #related-issues li img {vertical-align:middle;}
435
437
436 ul.properties {padding:0; font-size: 0.9em; color: #777;}
438 ul.properties {padding:0; font-size: 0.9em; color: #777;}
437 ul.properties li {list-style-type:none;}
439 ul.properties li {list-style-type:none;}
438 ul.properties li span {font-style:italic;}
440 ul.properties li span {font-style:italic;}
439
441
440 .total-hours { font-size: 110%; font-weight: bold; }
442 .total-hours { font-size: 110%; font-weight: bold; }
441 .total-hours span.hours-int { font-size: 120%; }
443 .total-hours span.hours-int { font-size: 120%; }
442
444
443 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
445 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
444 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
446 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
445
447
446 #workflow_copy_form select { width: 200px; }
448 #workflow_copy_form select { width: 200px; }
447 table.transitions td.enabled {background: #bfb;}
449 table.transitions td.enabled {background: #bfb;}
448 table.fields_permissions select {font-size:90%}
450 table.fields_permissions select {font-size:90%}
449 table.fields_permissions td.readonly {background:#ddd;}
451 table.fields_permissions td.readonly {background:#ddd;}
450 table.fields_permissions td.required {background:#d88;}
452 table.fields_permissions td.required {background:#d88;}
451
453
452 textarea#custom_field_possible_values {width: 99%}
454 textarea#custom_field_possible_values {width: 99%}
453 input#content_comments {width: 99%}
455 input#content_comments {width: 99%}
454
456
455 .pagination {font-size: 90%}
457 .pagination {font-size: 90%}
456 p.pagination {margin-top:8px;}
458 p.pagination {margin-top:8px;}
457
459
458 /***** Tabular forms ******/
460 /***** Tabular forms ******/
459 .tabular p{
461 .tabular p{
460 margin: 0;
462 margin: 0;
461 padding: 3px 0 3px 0;
463 padding: 3px 0 3px 0;
462 padding-left: 180px; /* width of left column containing the label elements */
464 padding-left: 180px; /* width of left column containing the label elements */
463 min-height: 1.8em;
465 min-height: 1.8em;
464 clear:left;
466 clear:left;
465 }
467 }
466
468
467 html>body .tabular p {overflow:hidden;}
469 html>body .tabular p {overflow:hidden;}
468
470
469 .tabular label{
471 .tabular label{
470 font-weight: bold;
472 font-weight: bold;
471 float: left;
473 float: left;
472 text-align: right;
474 text-align: right;
473 /* width of left column */
475 /* width of left column */
474 margin-left: -180px;
476 margin-left: -180px;
475 /* width of labels. Should be smaller than left column to create some right margin */
477 /* width of labels. Should be smaller than left column to create some right margin */
476 width: 175px;
478 width: 175px;
477 }
479 }
478
480
479 .tabular label.floating{
481 .tabular label.floating{
480 font-weight: normal;
482 font-weight: normal;
481 margin-left: 0px;
483 margin-left: 0px;
482 text-align: left;
484 text-align: left;
483 width: 270px;
485 width: 270px;
484 }
486 }
485
487
486 .tabular label.block{
488 .tabular label.block{
487 font-weight: normal;
489 font-weight: normal;
488 margin-left: 0px !important;
490 margin-left: 0px !important;
489 text-align: left;
491 text-align: left;
490 float: none;
492 float: none;
491 display: block;
493 display: block;
492 width: auto;
494 width: auto;
493 }
495 }
494
496
495 .tabular label.inline{
497 .tabular label.inline{
496 font-weight: normal;
498 font-weight: normal;
497 float:none;
499 float:none;
498 margin-left: 5px !important;
500 margin-left: 5px !important;
499 width: auto;
501 width: auto;
500 }
502 }
501
503
502 label.no-css {
504 label.no-css {
503 font-weight: inherit;
505 font-weight: inherit;
504 float:none;
506 float:none;
505 text-align:left;
507 text-align:left;
506 margin-left:0px;
508 margin-left:0px;
507 width:auto;
509 width:auto;
508 }
510 }
509 input#time_entry_comments { width: 90%;}
511 input#time_entry_comments { width: 90%;}
510
512
511 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
513 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
512
514
513 .tabular.settings p{ padding-left: 300px; }
515 .tabular.settings p{ padding-left: 300px; }
514 .tabular.settings label{ margin-left: -300px; width: 295px; }
516 .tabular.settings label{ margin-left: -300px; width: 295px; }
515 .tabular.settings textarea { width: 99%; }
517 .tabular.settings textarea { width: 99%; }
516
518
517 .settings.enabled_scm table {width:100%}
519 .settings.enabled_scm table {width:100%}
518 .settings.enabled_scm td.scm_name{ font-weight: bold; }
520 .settings.enabled_scm td.scm_name{ font-weight: bold; }
519
521
520 fieldset.settings label { display: block; }
522 fieldset.settings label { display: block; }
521 fieldset#notified_events .parent { padding-left: 20px; }
523 fieldset#notified_events .parent { padding-left: 20px; }
522
524
523 span.required {color: #bb0000;}
525 span.required {color: #bb0000;}
524 .summary {font-style: italic;}
526 .summary {font-style: italic;}
525
527
526 #attachments_fields input.description {margin-left: 8px; width:340px;}
528 #attachments_fields input.description {margin-left: 8px; width:340px;}
527 #attachments_fields span {display:block; white-space:nowrap;}
529 #attachments_fields span {display:block; white-space:nowrap;}
528 #attachments_fields img {vertical-align: middle;}
530 #attachments_fields img {vertical-align: middle;}
529
531
530 div.attachments { margin-top: 12px; }
532 div.attachments { margin-top: 12px; }
531 div.attachments p { margin:4px 0 2px 0; }
533 div.attachments p { margin:4px 0 2px 0; }
532 div.attachments img { vertical-align: middle; }
534 div.attachments img { vertical-align: middle; }
533 div.attachments span.author { font-size: 0.9em; color: #888; }
535 div.attachments span.author { font-size: 0.9em; color: #888; }
534
536
535 div.thumbnails {margin-top:0.6em;}
537 div.thumbnails {margin-top:0.6em;}
536 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
538 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
537 div.thumbnails img {margin: 3px;}
539 div.thumbnails img {margin: 3px;}
538
540
539 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
541 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
540 .other-formats span + span:before { content: "| "; }
542 .other-formats span + span:before { content: "| "; }
541
543
542 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
544 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
543
545
544 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
546 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
545 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
547 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
546
548
547 textarea.text_cf {width:90%;}
549 textarea.text_cf {width:90%;}
548
550
549 /* Project members tab */
551 /* Project members tab */
550 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
552 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
551 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
553 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
552 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
554 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
553 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
555 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
554 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
556 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
555 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
557 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
556
558
557 #users_for_watcher {height: 200px; overflow:auto;}
559 #users_for_watcher {height: 200px; overflow:auto;}
558 #users_for_watcher label {display: block;}
560 #users_for_watcher label {display: block;}
559
561
560 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
562 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
561
563
562 input#principal_search, input#user_search {width:100%}
564 input#principal_search, input#user_search {width:100%}
563 input#principal_search, input#user_search {
565 input#principal_search, input#user_search {
564 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
566 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
565 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
567 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
566 }
568 }
567 input#principal_search.ajax-loading, input#user_search.ajax-loading {
569 input#principal_search.ajax-loading, input#user_search.ajax-loading {
568 background-image: url(../images/loading.gif);
570 background-image: url(../images/loading.gif);
569 }
571 }
570
572
571 * html div#tab-content-members fieldset div { height: 450px; }
573 * html div#tab-content-members fieldset div { height: 450px; }
572
574
573 /***** Flash & error messages ****/
575 /***** Flash & error messages ****/
574 #errorExplanation, div.flash, .nodata, .warning, .conflict {
576 #errorExplanation, div.flash, .nodata, .warning, .conflict {
575 padding: 4px 4px 4px 30px;
577 padding: 4px 4px 4px 30px;
576 margin-bottom: 12px;
578 margin-bottom: 12px;
577 font-size: 1.1em;
579 font-size: 1.1em;
578 border: 2px solid;
580 border: 2px solid;
579 }
581 }
580
582
581 div.flash {margin-top: 8px;}
583 div.flash {margin-top: 8px;}
582
584
583 div.flash.error, #errorExplanation {
585 div.flash.error, #errorExplanation {
584 background: url(../images/exclamation.png) 8px 50% no-repeat;
586 background: url(../images/exclamation.png) 8px 50% no-repeat;
585 background-color: #ffe3e3;
587 background-color: #ffe3e3;
586 border-color: #dd0000;
588 border-color: #dd0000;
587 color: #880000;
589 color: #880000;
588 }
590 }
589
591
590 div.flash.notice {
592 div.flash.notice {
591 background: url(../images/true.png) 8px 5px no-repeat;
593 background: url(../images/true.png) 8px 5px no-repeat;
592 background-color: #dfffdf;
594 background-color: #dfffdf;
593 border-color: #9fcf9f;
595 border-color: #9fcf9f;
594 color: #005f00;
596 color: #005f00;
595 }
597 }
596
598
597 div.flash.warning, .conflict {
599 div.flash.warning, .conflict {
598 background: url(../images/warning.png) 8px 5px no-repeat;
600 background: url(../images/warning.png) 8px 5px no-repeat;
599 background-color: #FFEBC1;
601 background-color: #FFEBC1;
600 border-color: #FDBF3B;
602 border-color: #FDBF3B;
601 color: #A6750C;
603 color: #A6750C;
602 text-align: left;
604 text-align: left;
603 }
605 }
604
606
605 .nodata, .warning {
607 .nodata, .warning {
606 text-align: center;
608 text-align: center;
607 background-color: #FFEBC1;
609 background-color: #FFEBC1;
608 border-color: #FDBF3B;
610 border-color: #FDBF3B;
609 color: #A6750C;
611 color: #A6750C;
610 }
612 }
611
613
612 #errorExplanation ul { font-size: 0.9em;}
614 #errorExplanation ul { font-size: 0.9em;}
613 #errorExplanation h2, #errorExplanation p { display: none; }
615 #errorExplanation h2, #errorExplanation p { display: none; }
614
616
615 .conflict-details {font-size:80%;}
617 .conflict-details {font-size:80%;}
616
618
617 /***** Ajax indicator ******/
619 /***** Ajax indicator ******/
618 #ajax-indicator {
620 #ajax-indicator {
619 position: absolute; /* fixed not supported by IE */
621 position: absolute; /* fixed not supported by IE */
620 background-color:#eee;
622 background-color:#eee;
621 border: 1px solid #bbb;
623 border: 1px solid #bbb;
622 top:35%;
624 top:35%;
623 left:40%;
625 left:40%;
624 width:20%;
626 width:20%;
625 font-weight:bold;
627 font-weight:bold;
626 text-align:center;
628 text-align:center;
627 padding:0.6em;
629 padding:0.6em;
628 z-index:100;
630 z-index:100;
629 opacity: 0.5;
631 opacity: 0.5;
630 }
632 }
631
633
632 html>body #ajax-indicator { position: fixed; }
634 html>body #ajax-indicator { position: fixed; }
633
635
634 #ajax-indicator span {
636 #ajax-indicator span {
635 background-position: 0% 40%;
637 background-position: 0% 40%;
636 background-repeat: no-repeat;
638 background-repeat: no-repeat;
637 background-image: url(../images/loading.gif);
639 background-image: url(../images/loading.gif);
638 padding-left: 26px;
640 padding-left: 26px;
639 vertical-align: bottom;
641 vertical-align: bottom;
640 }
642 }
641
643
642 /***** Calendar *****/
644 /***** Calendar *****/
643 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
645 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
644 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
646 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
645 table.cal thead th.week-number {width: auto;}
647 table.cal thead th.week-number {width: auto;}
646 table.cal tbody tr {height: 100px;}
648 table.cal tbody tr {height: 100px;}
647 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
649 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
648 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
650 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
649 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
651 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
650 table.cal td.odd p.day-num {color: #bbb;}
652 table.cal td.odd p.day-num {color: #bbb;}
651 table.cal td.today {background:#ffffdd;}
653 table.cal td.today {background:#ffffdd;}
652 table.cal td.today p.day-num {font-weight: bold;}
654 table.cal td.today p.day-num {font-weight: bold;}
653 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
655 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
654 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
656 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
655 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
657 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
656 p.cal.legend span {display:block;}
658 p.cal.legend span {display:block;}
657
659
658 /***** Tooltips ******/
660 /***** Tooltips ******/
659 .tooltip{position:relative;z-index:24;}
661 .tooltip{position:relative;z-index:24;}
660 .tooltip:hover{z-index:25;color:#000;}
662 .tooltip:hover{z-index:25;color:#000;}
661 .tooltip span.tip{display: none; text-align:left;}
663 .tooltip span.tip{display: none; text-align:left;}
662
664
663 div.tooltip:hover span.tip{
665 div.tooltip:hover span.tip{
664 display:block;
666 display:block;
665 position:absolute;
667 position:absolute;
666 top:12px; left:24px; width:270px;
668 top:12px; left:24px; width:270px;
667 border:1px solid #555;
669 border:1px solid #555;
668 background-color:#fff;
670 background-color:#fff;
669 padding: 4px;
671 padding: 4px;
670 font-size: 0.8em;
672 font-size: 0.8em;
671 color:#505050;
673 color:#505050;
672 }
674 }
673
675
674 img.ui-datepicker-trigger {
676 img.ui-datepicker-trigger {
675 cursor: pointer;
677 cursor: pointer;
676 vertical-align: middle;
678 vertical-align: middle;
677 margin-left: 4px;
679 margin-left: 4px;
678 }
680 }
679
681
680 /***** Progress bar *****/
682 /***** Progress bar *****/
681 table.progress {
683 table.progress {
682 border-collapse: collapse;
684 border-collapse: collapse;
683 border-spacing: 0pt;
685 border-spacing: 0pt;
684 empty-cells: show;
686 empty-cells: show;
685 text-align: center;
687 text-align: center;
686 float:left;
688 float:left;
687 margin: 1px 6px 1px 0px;
689 margin: 1px 6px 1px 0px;
688 }
690 }
689
691
690 table.progress td { height: 1em; }
692 table.progress td { height: 1em; }
691 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
693 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
692 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
694 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
693 table.progress td.todo { background: #eee none repeat scroll 0%; }
695 table.progress td.todo { background: #eee none repeat scroll 0%; }
694 p.pourcent {font-size: 80%;}
696 p.pourcent {font-size: 80%;}
695 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
697 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
696
698
697 #roadmap table.progress td { height: 1.2em; }
699 #roadmap table.progress td { height: 1.2em; }
698 /***** Tabs *****/
700 /***** Tabs *****/
699 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
701 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
700 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
702 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
701 #content .tabs ul li {
703 #content .tabs ul li {
702 float:left;
704 float:left;
703 list-style-type:none;
705 list-style-type:none;
704 white-space:nowrap;
706 white-space:nowrap;
705 margin-right:4px;
707 margin-right:4px;
706 background:#fff;
708 background:#fff;
707 position:relative;
709 position:relative;
708 margin-bottom:-1px;
710 margin-bottom:-1px;
709 }
711 }
710 #content .tabs ul li a{
712 #content .tabs ul li a{
711 display:block;
713 display:block;
712 font-size: 0.9em;
714 font-size: 0.9em;
713 text-decoration:none;
715 text-decoration:none;
714 line-height:1.3em;
716 line-height:1.3em;
715 padding:4px 6px 4px 6px;
717 padding:4px 6px 4px 6px;
716 border: 1px solid #ccc;
718 border: 1px solid #ccc;
717 border-bottom: 1px solid #bbbbbb;
719 border-bottom: 1px solid #bbbbbb;
718 background-color: #f6f6f6;
720 background-color: #f6f6f6;
719 color:#999;
721 color:#999;
720 font-weight:bold;
722 font-weight:bold;
721 border-top-left-radius:3px;
723 border-top-left-radius:3px;
722 border-top-right-radius:3px;
724 border-top-right-radius:3px;
723 }
725 }
724
726
725 #content .tabs ul li a:hover {
727 #content .tabs ul li a:hover {
726 background-color: #ffffdd;
728 background-color: #ffffdd;
727 text-decoration:none;
729 text-decoration:none;
728 }
730 }
729
731
730 #content .tabs ul li a.selected {
732 #content .tabs ul li a.selected {
731 background-color: #fff;
733 background-color: #fff;
732 border: 1px solid #bbbbbb;
734 border: 1px solid #bbbbbb;
733 border-bottom: 1px solid #fff;
735 border-bottom: 1px solid #fff;
734 color:#444;
736 color:#444;
735 }
737 }
736
738
737 #content .tabs ul li a.selected:hover {background-color: #fff;}
739 #content .tabs ul li a.selected:hover {background-color: #fff;}
738
740
739 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
741 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
740
742
741 button.tab-left, button.tab-right {
743 button.tab-left, button.tab-right {
742 font-size: 0.9em;
744 font-size: 0.9em;
743 cursor: pointer;
745 cursor: pointer;
744 height:24px;
746 height:24px;
745 border: 1px solid #ccc;
747 border: 1px solid #ccc;
746 border-bottom: 1px solid #bbbbbb;
748 border-bottom: 1px solid #bbbbbb;
747 position:absolute;
749 position:absolute;
748 padding:4px;
750 padding:4px;
749 width: 20px;
751 width: 20px;
750 bottom: -1px;
752 bottom: -1px;
751 }
753 }
752
754
753 button.tab-left {
755 button.tab-left {
754 right: 20px;
756 right: 20px;
755 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
757 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
756 border-top-left-radius:3px;
758 border-top-left-radius:3px;
757 }
759 }
758
760
759 button.tab-right {
761 button.tab-right {
760 right: 0;
762 right: 0;
761 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
763 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
762 border-top-right-radius:3px;
764 border-top-right-radius:3px;
763 }
765 }
764
766
765 /***** Diff *****/
767 /***** Diff *****/
766 .diff_out { background: #fcc; }
768 .diff_out { background: #fcc; }
767 .diff_out span { background: #faa; }
769 .diff_out span { background: #faa; }
768 .diff_in { background: #cfc; }
770 .diff_in { background: #cfc; }
769 .diff_in span { background: #afa; }
771 .diff_in span { background: #afa; }
770
772
771 .text-diff {
773 .text-diff {
772 padding: 1em;
774 padding: 1em;
773 background-color:#f6f6f6;
775 background-color:#f6f6f6;
774 color:#505050;
776 color:#505050;
775 border: 1px solid #e4e4e4;
777 border: 1px solid #e4e4e4;
776 }
778 }
777
779
778 /***** Wiki *****/
780 /***** Wiki *****/
779 div.wiki table {
781 div.wiki table {
780 border-collapse: collapse;
782 border-collapse: collapse;
781 margin-bottom: 1em;
783 margin-bottom: 1em;
782 }
784 }
783
785
784 div.wiki table, div.wiki td, div.wiki th {
786 div.wiki table, div.wiki td, div.wiki th {
785 border: 1px solid #bbb;
787 border: 1px solid #bbb;
786 padding: 4px;
788 padding: 4px;
787 }
789 }
788
790
789 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
791 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
790
792
791 div.wiki .external {
793 div.wiki .external {
792 background-position: 0% 60%;
794 background-position: 0% 60%;
793 background-repeat: no-repeat;
795 background-repeat: no-repeat;
794 padding-left: 12px;
796 padding-left: 12px;
795 background-image: url(../images/external.png);
797 background-image: url(../images/external.png);
796 }
798 }
797
799
798 div.wiki a.new {color: #b73535;}
800 div.wiki a.new {color: #b73535;}
799
801
800 div.wiki ul, div.wiki ol {margin-bottom:1em;}
802 div.wiki ul, div.wiki ol {margin-bottom:1em;}
801
803
802 div.wiki pre {
804 div.wiki pre {
803 margin: 1em 1em 1em 1.6em;
805 margin: 1em 1em 1em 1.6em;
804 padding: 8px;
806 padding: 8px;
805 background-color: #fafafa;
807 background-color: #fafafa;
806 border: 1px solid #e2e2e2;
808 border: 1px solid #e2e2e2;
807 width:auto;
809 width:auto;
808 overflow-x: auto;
810 overflow-x: auto;
809 overflow-y: hidden;
811 overflow-y: hidden;
810 }
812 }
811
813
812 div.wiki ul.toc {
814 div.wiki ul.toc {
813 background-color: #ffffdd;
815 background-color: #ffffdd;
814 border: 1px solid #e4e4e4;
816 border: 1px solid #e4e4e4;
815 padding: 4px;
817 padding: 4px;
816 line-height: 1.2em;
818 line-height: 1.2em;
817 margin-bottom: 12px;
819 margin-bottom: 12px;
818 margin-right: 12px;
820 margin-right: 12px;
819 margin-left: 0;
821 margin-left: 0;
820 display: table
822 display: table
821 }
823 }
822 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
824 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
823
825
824 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
826 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
825 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
827 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
826 div.wiki ul.toc ul { margin: 0; padding: 0; }
828 div.wiki ul.toc ul { margin: 0; padding: 0; }
827 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
829 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
828 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
830 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
829 div.wiki ul.toc a {
831 div.wiki ul.toc a {
830 font-size: 0.9em;
832 font-size: 0.9em;
831 font-weight: normal;
833 font-weight: normal;
832 text-decoration: none;
834 text-decoration: none;
833 color: #606060;
835 color: #606060;
834 }
836 }
835 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
837 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
836
838
837 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
839 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
838 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
840 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
839 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
841 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
840
842
841 div.wiki img { vertical-align: middle; }
843 div.wiki img { vertical-align: middle; }
842
844
843 /***** My page layout *****/
845 /***** My page layout *****/
844 .block-receiver {
846 .block-receiver {
845 border:1px dashed #c0c0c0;
847 border:1px dashed #c0c0c0;
846 margin-bottom: 20px;
848 margin-bottom: 20px;
847 padding: 15px 0 15px 0;
849 padding: 15px 0 15px 0;
848 }
850 }
849
851
850 .mypage-box {
852 .mypage-box {
851 margin:0 0 20px 0;
853 margin:0 0 20px 0;
852 color:#505050;
854 color:#505050;
853 line-height:1.5em;
855 line-height:1.5em;
854 }
856 }
855
857
856 .handle {cursor: move;}
858 .handle {cursor: move;}
857
859
858 a.close-icon {
860 a.close-icon {
859 display:block;
861 display:block;
860 margin-top:3px;
862 margin-top:3px;
861 overflow:hidden;
863 overflow:hidden;
862 width:12px;
864 width:12px;
863 height:12px;
865 height:12px;
864 background-repeat: no-repeat;
866 background-repeat: no-repeat;
865 cursor:pointer;
867 cursor:pointer;
866 background-image:url('../images/close.png');
868 background-image:url('../images/close.png');
867 }
869 }
868 a.close-icon:hover {background-image:url('../images/close_hl.png');}
870 a.close-icon:hover {background-image:url('../images/close_hl.png');}
869
871
870 /***** Gantt chart *****/
872 /***** Gantt chart *****/
871 .gantt_hdr {
873 .gantt_hdr {
872 position:absolute;
874 position:absolute;
873 top:0;
875 top:0;
874 height:16px;
876 height:16px;
875 border-top: 1px solid #c0c0c0;
877 border-top: 1px solid #c0c0c0;
876 border-bottom: 1px solid #c0c0c0;
878 border-bottom: 1px solid #c0c0c0;
877 border-right: 1px solid #c0c0c0;
879 border-right: 1px solid #c0c0c0;
878 text-align: center;
880 text-align: center;
879 overflow: hidden;
881 overflow: hidden;
880 }
882 }
881
883
882 .gantt_hdr.nwday {background-color:#f1f1f1;}
884 .gantt_hdr.nwday {background-color:#f1f1f1;}
883
885
884 .gantt_subjects { font-size: 0.8em; }
886 .gantt_subjects { font-size: 0.8em; }
885 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
887 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
886
888
887 .task {
889 .task {
888 position: absolute;
890 position: absolute;
889 height:8px;
891 height:8px;
890 font-size:0.8em;
892 font-size:0.8em;
891 color:#888;
893 color:#888;
892 padding:0;
894 padding:0;
893 margin:0;
895 margin:0;
894 line-height:16px;
896 line-height:16px;
895 white-space:nowrap;
897 white-space:nowrap;
896 }
898 }
897
899
898 .task.label {width:100%;}
900 .task.label {width:100%;}
899 .task.label.project, .task.label.version { font-weight: bold; }
901 .task.label.project, .task.label.version { font-weight: bold; }
900
902
901 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
903 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
902 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
904 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
903 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
905 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
904
906
905 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
907 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
906 .task_late.parent, .task_done.parent { height: 3px;}
908 .task_late.parent, .task_done.parent { height: 3px;}
907 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
909 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
908 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
910 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
909
911
910 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
912 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
911 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
913 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
912 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
914 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
913 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
915 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
914
916
915 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
917 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
916 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
918 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
917 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
919 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
918 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
920 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
919
921
920 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
922 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
921 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
923 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
922
924
923 /***** Icons *****/
925 /***** Icons *****/
924 .icon {
926 .icon {
925 background-position: 0% 50%;
927 background-position: 0% 50%;
926 background-repeat: no-repeat;
928 background-repeat: no-repeat;
927 padding-left: 20px;
929 padding-left: 20px;
928 padding-top: 2px;
930 padding-top: 2px;
929 padding-bottom: 3px;
931 padding-bottom: 3px;
930 }
932 }
931
933
932 .icon-add { background-image: url(../images/add.png); }
934 .icon-add { background-image: url(../images/add.png); }
933 .icon-edit { background-image: url(../images/edit.png); }
935 .icon-edit { background-image: url(../images/edit.png); }
934 .icon-copy { background-image: url(../images/copy.png); }
936 .icon-copy { background-image: url(../images/copy.png); }
935 .icon-duplicate { background-image: url(../images/duplicate.png); }
937 .icon-duplicate { background-image: url(../images/duplicate.png); }
936 .icon-del { background-image: url(../images/delete.png); }
938 .icon-del { background-image: url(../images/delete.png); }
937 .icon-move { background-image: url(../images/move.png); }
939 .icon-move { background-image: url(../images/move.png); }
938 .icon-save { background-image: url(../images/save.png); }
940 .icon-save { background-image: url(../images/save.png); }
939 .icon-cancel { background-image: url(../images/cancel.png); }
941 .icon-cancel { background-image: url(../images/cancel.png); }
940 .icon-multiple { background-image: url(../images/table_multiple.png); }
942 .icon-multiple { background-image: url(../images/table_multiple.png); }
941 .icon-folder { background-image: url(../images/folder.png); }
943 .icon-folder { background-image: url(../images/folder.png); }
942 .open .icon-folder { background-image: url(../images/folder_open.png); }
944 .open .icon-folder { background-image: url(../images/folder_open.png); }
943 .icon-package { background-image: url(../images/package.png); }
945 .icon-package { background-image: url(../images/package.png); }
944 .icon-user { background-image: url(../images/user.png); }
946 .icon-user { background-image: url(../images/user.png); }
945 .icon-projects { background-image: url(../images/projects.png); }
947 .icon-projects { background-image: url(../images/projects.png); }
946 .icon-help { background-image: url(../images/help.png); }
948 .icon-help { background-image: url(../images/help.png); }
947 .icon-attachment { background-image: url(../images/attachment.png); }
949 .icon-attachment { background-image: url(../images/attachment.png); }
948 .icon-history { background-image: url(../images/history.png); }
950 .icon-history { background-image: url(../images/history.png); }
949 .icon-time { background-image: url(../images/time.png); }
951 .icon-time { background-image: url(../images/time.png); }
950 .icon-time-add { background-image: url(../images/time_add.png); }
952 .icon-time-add { background-image: url(../images/time_add.png); }
951 .icon-stats { background-image: url(../images/stats.png); }
953 .icon-stats { background-image: url(../images/stats.png); }
952 .icon-warning { background-image: url(../images/warning.png); }
954 .icon-warning { background-image: url(../images/warning.png); }
953 .icon-fav { background-image: url(../images/fav.png); }
955 .icon-fav { background-image: url(../images/fav.png); }
954 .icon-fav-off { background-image: url(../images/fav_off.png); }
956 .icon-fav-off { background-image: url(../images/fav_off.png); }
955 .icon-reload { background-image: url(../images/reload.png); }
957 .icon-reload { background-image: url(../images/reload.png); }
956 .icon-lock { background-image: url(../images/locked.png); }
958 .icon-lock { background-image: url(../images/locked.png); }
957 .icon-unlock { background-image: url(../images/unlock.png); }
959 .icon-unlock { background-image: url(../images/unlock.png); }
958 .icon-checked { background-image: url(../images/true.png); }
960 .icon-checked { background-image: url(../images/true.png); }
959 .icon-details { background-image: url(../images/zoom_in.png); }
961 .icon-details { background-image: url(../images/zoom_in.png); }
960 .icon-report { background-image: url(../images/report.png); }
962 .icon-report { background-image: url(../images/report.png); }
961 .icon-comment { background-image: url(../images/comment.png); }
963 .icon-comment { background-image: url(../images/comment.png); }
962 .icon-summary { background-image: url(../images/lightning.png); }
964 .icon-summary { background-image: url(../images/lightning.png); }
963 .icon-server-authentication { background-image: url(../images/server_key.png); }
965 .icon-server-authentication { background-image: url(../images/server_key.png); }
964 .icon-issue { background-image: url(../images/ticket.png); }
966 .icon-issue { background-image: url(../images/ticket.png); }
965 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
967 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
966 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
968 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
967 .icon-passwd { background-image: url(../images/textfield_key.png); }
969 .icon-passwd { background-image: url(../images/textfield_key.png); }
968 .icon-test { background-image: url(../images/bullet_go.png); }
970 .icon-test { background-image: url(../images/bullet_go.png); }
969
971
970 .icon-file { background-image: url(../images/files/default.png); }
972 .icon-file { background-image: url(../images/files/default.png); }
971 .icon-file.text-plain { background-image: url(../images/files/text.png); }
973 .icon-file.text-plain { background-image: url(../images/files/text.png); }
972 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
974 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
973 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
975 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
974 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
976 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
975 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
977 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
976 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
978 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
977 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
979 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
978 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
980 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
979 .icon-file.text-css { background-image: url(../images/files/css.png); }
981 .icon-file.text-css { background-image: url(../images/files/css.png); }
980 .icon-file.text-html { background-image: url(../images/files/html.png); }
982 .icon-file.text-html { background-image: url(../images/files/html.png); }
981 .icon-file.image-gif { background-image: url(../images/files/image.png); }
983 .icon-file.image-gif { background-image: url(../images/files/image.png); }
982 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
984 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
983 .icon-file.image-png { background-image: url(../images/files/image.png); }
985 .icon-file.image-png { background-image: url(../images/files/image.png); }
984 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
986 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
985 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
987 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
986 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
988 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
987 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
989 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
988
990
989 img.gravatar {
991 img.gravatar {
990 padding: 2px;
992 padding: 2px;
991 border: solid 1px #d5d5d5;
993 border: solid 1px #d5d5d5;
992 background: #fff;
994 background: #fff;
993 vertical-align: middle;
995 vertical-align: middle;
994 }
996 }
995
997
996 div.issue img.gravatar {
998 div.issue img.gravatar {
997 float: left;
999 float: left;
998 margin: 0 6px 0 0;
1000 margin: 0 6px 0 0;
999 padding: 5px;
1001 padding: 5px;
1000 }
1002 }
1001
1003
1002 div.issue table img.gravatar {
1004 div.issue table img.gravatar {
1003 height: 14px;
1005 height: 14px;
1004 width: 14px;
1006 width: 14px;
1005 padding: 2px;
1007 padding: 2px;
1006 float: left;
1008 float: left;
1007 margin: 0 0.5em 0 0;
1009 margin: 0 0.5em 0 0;
1008 }
1010 }
1009
1011
1010 h2 img.gravatar {margin: -2px 4px -4px 0;}
1012 h2 img.gravatar {margin: -2px 4px -4px 0;}
1011 h3 img.gravatar {margin: -4px 4px -4px 0;}
1013 h3 img.gravatar {margin: -4px 4px -4px 0;}
1012 h4 img.gravatar {margin: -6px 4px -4px 0;}
1014 h4 img.gravatar {margin: -6px 4px -4px 0;}
1013 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1015 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1014 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1016 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1015 /* Used on 12px Gravatar img tags without the icon background */
1017 /* Used on 12px Gravatar img tags without the icon background */
1016 .icon-gravatar {float: left; margin-right: 4px;}
1018 .icon-gravatar {float: left; margin-right: 4px;}
1017
1019
1018 #activity dt, .journal {clear: left;}
1020 #activity dt, .journal {clear: left;}
1019
1021
1020 .journal-link {float: right;}
1022 .journal-link {float: right;}
1021
1023
1022 h2 img { vertical-align:middle; }
1024 h2 img { vertical-align:middle; }
1023
1025
1024 .hascontextmenu { cursor: context-menu; }
1026 .hascontextmenu { cursor: context-menu; }
1025
1027
1026 /************* CodeRay styles *************/
1028 /************* CodeRay styles *************/
1027 .syntaxhl div {display: inline;}
1029 .syntaxhl div {display: inline;}
1028 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1030 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1029 .syntaxhl .code pre { overflow: auto }
1031 .syntaxhl .code pre { overflow: auto }
1030 .syntaxhl .debug { color: white !important; background: blue !important; }
1032 .syntaxhl .debug { color: white !important; background: blue !important; }
1031
1033
1032 .syntaxhl .annotation { color:#007 }
1034 .syntaxhl .annotation { color:#007 }
1033 .syntaxhl .attribute-name { color:#b48 }
1035 .syntaxhl .attribute-name { color:#b48 }
1034 .syntaxhl .attribute-value { color:#700 }
1036 .syntaxhl .attribute-value { color:#700 }
1035 .syntaxhl .binary { color:#509 }
1037 .syntaxhl .binary { color:#509 }
1036 .syntaxhl .char .content { color:#D20 }
1038 .syntaxhl .char .content { color:#D20 }
1037 .syntaxhl .char .delimiter { color:#710 }
1039 .syntaxhl .char .delimiter { color:#710 }
1038 .syntaxhl .char { color:#D20 }
1040 .syntaxhl .char { color:#D20 }
1039 .syntaxhl .class { color:#258; font-weight:bold }
1041 .syntaxhl .class { color:#258; font-weight:bold }
1040 .syntaxhl .class-variable { color:#369 }
1042 .syntaxhl .class-variable { color:#369 }
1041 .syntaxhl .color { color:#0A0 }
1043 .syntaxhl .color { color:#0A0 }
1042 .syntaxhl .comment { color:#385 }
1044 .syntaxhl .comment { color:#385 }
1043 .syntaxhl .comment .char { color:#385 }
1045 .syntaxhl .comment .char { color:#385 }
1044 .syntaxhl .comment .delimiter { color:#385 }
1046 .syntaxhl .comment .delimiter { color:#385 }
1045 .syntaxhl .complex { color:#A08 }
1047 .syntaxhl .complex { color:#A08 }
1046 .syntaxhl .constant { color:#258; font-weight:bold }
1048 .syntaxhl .constant { color:#258; font-weight:bold }
1047 .syntaxhl .decorator { color:#B0B }
1049 .syntaxhl .decorator { color:#B0B }
1048 .syntaxhl .definition { color:#099; font-weight:bold }
1050 .syntaxhl .definition { color:#099; font-weight:bold }
1049 .syntaxhl .delimiter { color:black }
1051 .syntaxhl .delimiter { color:black }
1050 .syntaxhl .directive { color:#088; font-weight:bold }
1052 .syntaxhl .directive { color:#088; font-weight:bold }
1051 .syntaxhl .doc { color:#970 }
1053 .syntaxhl .doc { color:#970 }
1052 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1054 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1053 .syntaxhl .doctype { color:#34b }
1055 .syntaxhl .doctype { color:#34b }
1054 .syntaxhl .entity { color:#800; font-weight:bold }
1056 .syntaxhl .entity { color:#800; font-weight:bold }
1055 .syntaxhl .error { color:#F00; background-color:#FAA }
1057 .syntaxhl .error { color:#F00; background-color:#FAA }
1056 .syntaxhl .escape { color:#666 }
1058 .syntaxhl .escape { color:#666 }
1057 .syntaxhl .exception { color:#C00; font-weight:bold }
1059 .syntaxhl .exception { color:#C00; font-weight:bold }
1058 .syntaxhl .float { color:#06D }
1060 .syntaxhl .float { color:#06D }
1059 .syntaxhl .function { color:#06B; font-weight:bold }
1061 .syntaxhl .function { color:#06B; font-weight:bold }
1060 .syntaxhl .global-variable { color:#d70 }
1062 .syntaxhl .global-variable { color:#d70 }
1061 .syntaxhl .hex { color:#02b }
1063 .syntaxhl .hex { color:#02b }
1062 .syntaxhl .imaginary { color:#f00 }
1064 .syntaxhl .imaginary { color:#f00 }
1063 .syntaxhl .include { color:#B44; font-weight:bold }
1065 .syntaxhl .include { color:#B44; font-weight:bold }
1064 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1066 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1065 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1067 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1066 .syntaxhl .instance-variable { color:#33B }
1068 .syntaxhl .instance-variable { color:#33B }
1067 .syntaxhl .integer { color:#06D }
1069 .syntaxhl .integer { color:#06D }
1068 .syntaxhl .key .char { color: #60f }
1070 .syntaxhl .key .char { color: #60f }
1069 .syntaxhl .key .delimiter { color: #404 }
1071 .syntaxhl .key .delimiter { color: #404 }
1070 .syntaxhl .key { color: #606 }
1072 .syntaxhl .key { color: #606 }
1071 .syntaxhl .keyword { color:#939; font-weight:bold }
1073 .syntaxhl .keyword { color:#939; font-weight:bold }
1072 .syntaxhl .label { color:#970; font-weight:bold }
1074 .syntaxhl .label { color:#970; font-weight:bold }
1073 .syntaxhl .local-variable { color:#963 }
1075 .syntaxhl .local-variable { color:#963 }
1074 .syntaxhl .namespace { color:#707; font-weight:bold }
1076 .syntaxhl .namespace { color:#707; font-weight:bold }
1075 .syntaxhl .octal { color:#40E }
1077 .syntaxhl .octal { color:#40E }
1076 .syntaxhl .operator { }
1078 .syntaxhl .operator { }
1077 .syntaxhl .predefined { color:#369; font-weight:bold }
1079 .syntaxhl .predefined { color:#369; font-weight:bold }
1078 .syntaxhl .predefined-constant { color:#069 }
1080 .syntaxhl .predefined-constant { color:#069 }
1079 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1081 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1080 .syntaxhl .preprocessor { color:#579 }
1082 .syntaxhl .preprocessor { color:#579 }
1081 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1083 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1082 .syntaxhl .regexp .content { color:#808 }
1084 .syntaxhl .regexp .content { color:#808 }
1083 .syntaxhl .regexp .delimiter { color:#404 }
1085 .syntaxhl .regexp .delimiter { color:#404 }
1084 .syntaxhl .regexp .modifier { color:#C2C }
1086 .syntaxhl .regexp .modifier { color:#C2C }
1085 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1087 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1086 .syntaxhl .reserved { color:#080; font-weight:bold }
1088 .syntaxhl .reserved { color:#080; font-weight:bold }
1087 .syntaxhl .shell .content { color:#2B2 }
1089 .syntaxhl .shell .content { color:#2B2 }
1088 .syntaxhl .shell .delimiter { color:#161 }
1090 .syntaxhl .shell .delimiter { color:#161 }
1089 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1091 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1090 .syntaxhl .string .char { color: #46a }
1092 .syntaxhl .string .char { color: #46a }
1091 .syntaxhl .string .content { color: #46a }
1093 .syntaxhl .string .content { color: #46a }
1092 .syntaxhl .string .delimiter { color: #46a }
1094 .syntaxhl .string .delimiter { color: #46a }
1093 .syntaxhl .string .modifier { color: #46a }
1095 .syntaxhl .string .modifier { color: #46a }
1094 .syntaxhl .symbol .content { color:#d33 }
1096 .syntaxhl .symbol .content { color:#d33 }
1095 .syntaxhl .symbol .delimiter { color:#d33 }
1097 .syntaxhl .symbol .delimiter { color:#d33 }
1096 .syntaxhl .symbol { color:#d33 }
1098 .syntaxhl .symbol { color:#d33 }
1097 .syntaxhl .tag { color:#070 }
1099 .syntaxhl .tag { color:#070 }
1098 .syntaxhl .type { color:#339; font-weight:bold }
1100 .syntaxhl .type { color:#339; font-weight:bold }
1099 .syntaxhl .value { color: #088; }
1101 .syntaxhl .value { color: #088; }
1100 .syntaxhl .variable { color:#037 }
1102 .syntaxhl .variable { color:#037 }
1101
1103
1102 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1104 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1103 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1105 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1104 .syntaxhl .change { color: #bbf; background: #007; }
1106 .syntaxhl .change { color: #bbf; background: #007; }
1105 .syntaxhl .head { color: #f8f; background: #505 }
1107 .syntaxhl .head { color: #f8f; background: #505 }
1106 .syntaxhl .head .filename { color: white; }
1108 .syntaxhl .head .filename { color: white; }
1107
1109
1108 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1110 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1109 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1111 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1110
1112
1111 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1113 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1112 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1114 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1113 .syntaxhl .change .change { color: #88f }
1115 .syntaxhl .change .change { color: #88f }
1114 .syntaxhl .head .head { color: #f4f }
1116 .syntaxhl .head .head { color: #f4f }
1115
1117
1116 /***** Media print specific styles *****/
1118 /***** Media print specific styles *****/
1117 @media print {
1119 @media print {
1118 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1120 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1119 #main { background: #fff; }
1121 #main { background: #fff; }
1120 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1122 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1121 #wiki_add_attachment { display:none; }
1123 #wiki_add_attachment { display:none; }
1122 .hide-when-print { display: none; }
1124 .hide-when-print { display: none; }
1123 .autoscroll {overflow-x: visible;}
1125 .autoscroll {overflow-x: visible;}
1124 table.list {margin-top:0.5em;}
1126 table.list {margin-top:0.5em;}
1125 table.list th, table.list td {border: 1px solid #aaa;}
1127 table.list th, table.list td {border: 1px solid #aaa;}
1126 }
1128 }
1127
1129
1128 /* Accessibility specific styles */
1130 /* Accessibility specific styles */
1129 .hidden-for-sighted {
1131 .hidden-for-sighted {
1130 position:absolute;
1132 position:absolute;
1131 left:-10000px;
1133 left:-10000px;
1132 top:auto;
1134 top:auto;
1133 width:1px;
1135 width:1px;
1134 height:1px;
1136 height:1px;
1135 overflow:hidden;
1137 overflow:hidden;
1136 }
1138 }
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1232 +1,1249
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 fixtures :projects, :enabled_modules, :users, :members,
23 fixtures :projects, :enabled_modules, :users, :members,
24 :member_roles, :roles, :trackers, :issue_statuses,
24 :member_roles, :roles, :trackers, :issue_statuses,
25 :issue_categories, :enumerations, :issues,
25 :issue_categories, :enumerations, :issues,
26 :watchers, :custom_fields, :custom_values, :versions,
26 :watchers, :custom_fields, :custom_values, :versions,
27 :queries,
27 :queries,
28 :projects_trackers,
28 :projects_trackers,
29 :custom_fields_trackers
29 :custom_fields_trackers
30
30
31 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
31 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
32 query = Query.new(:project => nil, :name => '_')
32 query = Query.new(:project => nil, :name => '_')
33 assert query.available_filters.has_key?('cf_1')
33 assert query.available_filters.has_key?('cf_1')
34 assert !query.available_filters.has_key?('cf_3')
34 assert !query.available_filters.has_key?('cf_3')
35 end
35 end
36
36
37 def test_system_shared_versions_should_be_available_in_global_queries
37 def test_system_shared_versions_should_be_available_in_global_queries
38 Version.find(2).update_attribute :sharing, 'system'
38 Version.find(2).update_attribute :sharing, 'system'
39 query = Query.new(:project => nil, :name => '_')
39 query = Query.new(:project => nil, :name => '_')
40 assert query.available_filters.has_key?('fixed_version_id')
40 assert query.available_filters.has_key?('fixed_version_id')
41 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
41 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
42 end
42 end
43
43
44 def test_project_filter_in_global_queries
44 def test_project_filter_in_global_queries
45 query = Query.new(:project => nil, :name => '_')
45 query = Query.new(:project => nil, :name => '_')
46 project_filter = query.available_filters["project_id"]
46 project_filter = query.available_filters["project_id"]
47 assert_not_nil project_filter
47 assert_not_nil project_filter
48 project_ids = project_filter[:values].map{|p| p[1]}
48 project_ids = project_filter[:values].map{|p| p[1]}
49 assert project_ids.include?("1") #public project
49 assert project_ids.include?("1") #public project
50 assert !project_ids.include?("2") #private project user cannot see
50 assert !project_ids.include?("2") #private project user cannot see
51 end
51 end
52
52
53 def find_issues_with_query(query)
53 def find_issues_with_query(query)
54 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
54 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
55 query.statement
55 query.statement
56 ).all
56 ).all
57 end
57 end
58
58
59 def assert_find_issues_with_query_is_successful(query)
59 def assert_find_issues_with_query_is_successful(query)
60 assert_nothing_raised do
60 assert_nothing_raised do
61 find_issues_with_query(query)
61 find_issues_with_query(query)
62 end
62 end
63 end
63 end
64
64
65 def assert_query_statement_includes(query, condition)
65 def assert_query_statement_includes(query, condition)
66 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
66 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
67 end
67 end
68
68
69 def assert_query_result(expected, query)
69 def assert_query_result(expected, query)
70 assert_nothing_raised do
70 assert_nothing_raised do
71 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
71 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
72 assert_equal expected.size, query.issue_count
72 assert_equal expected.size, query.issue_count
73 end
73 end
74 end
74 end
75
75
76 def test_query_should_allow_shared_versions_for_a_project_query
76 def test_query_should_allow_shared_versions_for_a_project_query
77 subproject_version = Version.find(4)
77 subproject_version = Version.find(4)
78 query = Query.new(:project => Project.find(1), :name => '_')
78 query = Query.new(:project => Project.find(1), :name => '_')
79 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
79 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
80
80
81 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
81 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
82 end
82 end
83
83
84 def test_query_with_multiple_custom_fields
84 def test_query_with_multiple_custom_fields
85 query = Query.find(1)
85 query = Query.find(1)
86 assert query.valid?
86 assert query.valid?
87 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
87 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
88 issues = find_issues_with_query(query)
88 issues = find_issues_with_query(query)
89 assert_equal 1, issues.length
89 assert_equal 1, issues.length
90 assert_equal Issue.find(3), issues.first
90 assert_equal Issue.find(3), issues.first
91 end
91 end
92
92
93 def test_operator_none
93 def test_operator_none
94 query = Query.new(:project => Project.find(1), :name => '_')
94 query = Query.new(:project => Project.find(1), :name => '_')
95 query.add_filter('fixed_version_id', '!*', [''])
95 query.add_filter('fixed_version_id', '!*', [''])
96 query.add_filter('cf_1', '!*', [''])
96 query.add_filter('cf_1', '!*', [''])
97 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
97 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
98 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
98 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
99 find_issues_with_query(query)
99 find_issues_with_query(query)
100 end
100 end
101
101
102 def test_operator_none_for_integer
102 def test_operator_none_for_integer
103 query = Query.new(:project => Project.find(1), :name => '_')
103 query = Query.new(:project => Project.find(1), :name => '_')
104 query.add_filter('estimated_hours', '!*', [''])
104 query.add_filter('estimated_hours', '!*', [''])
105 issues = find_issues_with_query(query)
105 issues = find_issues_with_query(query)
106 assert !issues.empty?
106 assert !issues.empty?
107 assert issues.all? {|i| !i.estimated_hours}
107 assert issues.all? {|i| !i.estimated_hours}
108 end
108 end
109
109
110 def test_operator_none_for_date
110 def test_operator_none_for_date
111 query = Query.new(:project => Project.find(1), :name => '_')
111 query = Query.new(:project => Project.find(1), :name => '_')
112 query.add_filter('start_date', '!*', [''])
112 query.add_filter('start_date', '!*', [''])
113 issues = find_issues_with_query(query)
113 issues = find_issues_with_query(query)
114 assert !issues.empty?
114 assert !issues.empty?
115 assert issues.all? {|i| i.start_date.nil?}
115 assert issues.all? {|i| i.start_date.nil?}
116 end
116 end
117
117
118 def test_operator_none_for_string_custom_field
118 def test_operator_none_for_string_custom_field
119 query = Query.new(:project => Project.find(1), :name => '_')
119 query = Query.new(:project => Project.find(1), :name => '_')
120 query.add_filter('cf_2', '!*', [''])
120 query.add_filter('cf_2', '!*', [''])
121 assert query.has_filter?('cf_2')
121 assert query.has_filter?('cf_2')
122 issues = find_issues_with_query(query)
122 issues = find_issues_with_query(query)
123 assert !issues.empty?
123 assert !issues.empty?
124 assert issues.all? {|i| i.custom_field_value(2).blank?}
124 assert issues.all? {|i| i.custom_field_value(2).blank?}
125 end
125 end
126
126
127 def test_operator_all
127 def test_operator_all
128 query = Query.new(:project => Project.find(1), :name => '_')
128 query = Query.new(:project => Project.find(1), :name => '_')
129 query.add_filter('fixed_version_id', '*', [''])
129 query.add_filter('fixed_version_id', '*', [''])
130 query.add_filter('cf_1', '*', [''])
130 query.add_filter('cf_1', '*', [''])
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
131 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
132 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
132 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
133 find_issues_with_query(query)
133 find_issues_with_query(query)
134 end
134 end
135
135
136 def test_operator_all_for_date
136 def test_operator_all_for_date
137 query = Query.new(:project => Project.find(1), :name => '_')
137 query = Query.new(:project => Project.find(1), :name => '_')
138 query.add_filter('start_date', '*', [''])
138 query.add_filter('start_date', '*', [''])
139 issues = find_issues_with_query(query)
139 issues = find_issues_with_query(query)
140 assert !issues.empty?
140 assert !issues.empty?
141 assert issues.all? {|i| i.start_date.present?}
141 assert issues.all? {|i| i.start_date.present?}
142 end
142 end
143
143
144 def test_operator_all_for_string_custom_field
144 def test_operator_all_for_string_custom_field
145 query = Query.new(:project => Project.find(1), :name => '_')
145 query = Query.new(:project => Project.find(1), :name => '_')
146 query.add_filter('cf_2', '*', [''])
146 query.add_filter('cf_2', '*', [''])
147 assert query.has_filter?('cf_2')
147 assert query.has_filter?('cf_2')
148 issues = find_issues_with_query(query)
148 issues = find_issues_with_query(query)
149 assert !issues.empty?
149 assert !issues.empty?
150 assert issues.all? {|i| i.custom_field_value(2).present?}
150 assert issues.all? {|i| i.custom_field_value(2).present?}
151 end
151 end
152
152
153 def test_numeric_filter_should_not_accept_non_numeric_values
153 def test_numeric_filter_should_not_accept_non_numeric_values
154 query = Query.new(:name => '_')
154 query = Query.new(:name => '_')
155 query.add_filter('estimated_hours', '=', ['a'])
155 query.add_filter('estimated_hours', '=', ['a'])
156
156
157 assert query.has_filter?('estimated_hours')
157 assert query.has_filter?('estimated_hours')
158 assert !query.valid?
158 assert !query.valid?
159 end
159 end
160
160
161 def test_operator_is_on_float
161 def test_operator_is_on_float
162 Issue.update_all("estimated_hours = 171.2", "id=2")
162 Issue.update_all("estimated_hours = 171.2", "id=2")
163
163
164 query = Query.new(:name => '_')
164 query = Query.new(:name => '_')
165 query.add_filter('estimated_hours', '=', ['171.20'])
165 query.add_filter('estimated_hours', '=', ['171.20'])
166 issues = find_issues_with_query(query)
166 issues = find_issues_with_query(query)
167 assert_equal 1, issues.size
167 assert_equal 1, issues.size
168 assert_equal 2, issues.first.id
168 assert_equal 2, issues.first.id
169 end
169 end
170
170
171 def test_operator_is_on_integer_custom_field
171 def test_operator_is_on_integer_custom_field
172 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
172 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
173 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
173 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
174 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
174 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
175 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
175 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
176
176
177 query = Query.new(:name => '_')
177 query = Query.new(:name => '_')
178 query.add_filter("cf_#{f.id}", '=', ['12'])
178 query.add_filter("cf_#{f.id}", '=', ['12'])
179 issues = find_issues_with_query(query)
179 issues = find_issues_with_query(query)
180 assert_equal 1, issues.size
180 assert_equal 1, issues.size
181 assert_equal 2, issues.first.id
181 assert_equal 2, issues.first.id
182 end
182 end
183
183
184 def test_operator_is_on_integer_custom_field_should_accept_negative_value
184 def test_operator_is_on_integer_custom_field_should_accept_negative_value
185 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
185 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
186 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
186 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
187 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
187 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
188 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
188 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
189
189
190 query = Query.new(:name => '_')
190 query = Query.new(:name => '_')
191 query.add_filter("cf_#{f.id}", '=', ['-12'])
191 query.add_filter("cf_#{f.id}", '=', ['-12'])
192 assert query.valid?
192 assert query.valid?
193 issues = find_issues_with_query(query)
193 issues = find_issues_with_query(query)
194 assert_equal 1, issues.size
194 assert_equal 1, issues.size
195 assert_equal 2, issues.first.id
195 assert_equal 2, issues.first.id
196 end
196 end
197
197
198 def test_operator_is_on_float_custom_field
198 def test_operator_is_on_float_custom_field
199 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
199 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
200 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
200 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
201 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
201 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
202 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
202 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
203
203
204 query = Query.new(:name => '_')
204 query = Query.new(:name => '_')
205 query.add_filter("cf_#{f.id}", '=', ['12.7'])
205 query.add_filter("cf_#{f.id}", '=', ['12.7'])
206 issues = find_issues_with_query(query)
206 issues = find_issues_with_query(query)
207 assert_equal 1, issues.size
207 assert_equal 1, issues.size
208 assert_equal 2, issues.first.id
208 assert_equal 2, issues.first.id
209 end
209 end
210
210
211 def test_operator_is_on_float_custom_field_should_accept_negative_value
211 def test_operator_is_on_float_custom_field_should_accept_negative_value
212 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
212 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
213 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
213 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
214 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
214 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
215 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
215 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
216
216
217 query = Query.new(:name => '_')
217 query = Query.new(:name => '_')
218 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
218 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
219 assert query.valid?
219 assert query.valid?
220 issues = find_issues_with_query(query)
220 issues = find_issues_with_query(query)
221 assert_equal 1, issues.size
221 assert_equal 1, issues.size
222 assert_equal 2, issues.first.id
222 assert_equal 2, issues.first.id
223 end
223 end
224
224
225 def test_operator_is_on_multi_list_custom_field
225 def test_operator_is_on_multi_list_custom_field
226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
227 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
227 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
228 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
228 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
229 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
229 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
230 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
230 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
231
231
232 query = Query.new(:name => '_')
232 query = Query.new(:name => '_')
233 query.add_filter("cf_#{f.id}", '=', ['value1'])
233 query.add_filter("cf_#{f.id}", '=', ['value1'])
234 issues = find_issues_with_query(query)
234 issues = find_issues_with_query(query)
235 assert_equal [1, 3], issues.map(&:id).sort
235 assert_equal [1, 3], issues.map(&:id).sort
236
236
237 query = Query.new(:name => '_')
237 query = Query.new(:name => '_')
238 query.add_filter("cf_#{f.id}", '=', ['value2'])
238 query.add_filter("cf_#{f.id}", '=', ['value2'])
239 issues = find_issues_with_query(query)
239 issues = find_issues_with_query(query)
240 assert_equal [1], issues.map(&:id).sort
240 assert_equal [1], issues.map(&:id).sort
241 end
241 end
242
242
243 def test_operator_is_not_on_multi_list_custom_field
243 def test_operator_is_not_on_multi_list_custom_field
244 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
244 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
245 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
245 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
246 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
246 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
247 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
247 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
248 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
248 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
249
249
250 query = Query.new(:name => '_')
250 query = Query.new(:name => '_')
251 query.add_filter("cf_#{f.id}", '!', ['value1'])
251 query.add_filter("cf_#{f.id}", '!', ['value1'])
252 issues = find_issues_with_query(query)
252 issues = find_issues_with_query(query)
253 assert !issues.map(&:id).include?(1)
253 assert !issues.map(&:id).include?(1)
254 assert !issues.map(&:id).include?(3)
254 assert !issues.map(&:id).include?(3)
255
255
256 query = Query.new(:name => '_')
256 query = Query.new(:name => '_')
257 query.add_filter("cf_#{f.id}", '!', ['value2'])
257 query.add_filter("cf_#{f.id}", '!', ['value2'])
258 issues = find_issues_with_query(query)
258 issues = find_issues_with_query(query)
259 assert !issues.map(&:id).include?(1)
259 assert !issues.map(&:id).include?(1)
260 assert issues.map(&:id).include?(3)
260 assert issues.map(&:id).include?(3)
261 end
261 end
262
262
263 def test_operator_is_on_is_private_field
263 def test_operator_is_on_is_private_field
264 # is_private filter only available for those who can set issues private
264 # is_private filter only available for those who can set issues private
265 User.current = User.find(2)
265 User.current = User.find(2)
266
266
267 query = Query.new(:name => '_')
267 query = Query.new(:name => '_')
268 assert query.available_filters.key?('is_private')
268 assert query.available_filters.key?('is_private')
269
269
270 query.add_filter("is_private", '=', ['1'])
270 query.add_filter("is_private", '=', ['1'])
271 issues = find_issues_with_query(query)
271 issues = find_issues_with_query(query)
272 assert issues.any?
272 assert issues.any?
273 assert_nil issues.detect {|issue| !issue.is_private?}
273 assert_nil issues.detect {|issue| !issue.is_private?}
274 ensure
274 ensure
275 User.current = nil
275 User.current = nil
276 end
276 end
277
277
278 def test_operator_is_not_on_is_private_field
278 def test_operator_is_not_on_is_private_field
279 # is_private filter only available for those who can set issues private
279 # is_private filter only available for those who can set issues private
280 User.current = User.find(2)
280 User.current = User.find(2)
281
281
282 query = Query.new(:name => '_')
282 query = Query.new(:name => '_')
283 assert query.available_filters.key?('is_private')
283 assert query.available_filters.key?('is_private')
284
284
285 query.add_filter("is_private", '!', ['1'])
285 query.add_filter("is_private", '!', ['1'])
286 issues = find_issues_with_query(query)
286 issues = find_issues_with_query(query)
287 assert issues.any?
287 assert issues.any?
288 assert_nil issues.detect {|issue| issue.is_private?}
288 assert_nil issues.detect {|issue| issue.is_private?}
289 ensure
289 ensure
290 User.current = nil
290 User.current = nil
291 end
291 end
292
292
293 def test_operator_greater_than
293 def test_operator_greater_than
294 query = Query.new(:project => Project.find(1), :name => '_')
294 query = Query.new(:project => Project.find(1), :name => '_')
295 query.add_filter('done_ratio', '>=', ['40'])
295 query.add_filter('done_ratio', '>=', ['40'])
296 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
296 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
297 find_issues_with_query(query)
297 find_issues_with_query(query)
298 end
298 end
299
299
300 def test_operator_greater_than_a_float
300 def test_operator_greater_than_a_float
301 query = Query.new(:project => Project.find(1), :name => '_')
301 query = Query.new(:project => Project.find(1), :name => '_')
302 query.add_filter('estimated_hours', '>=', ['40.5'])
302 query.add_filter('estimated_hours', '>=', ['40.5'])
303 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
303 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
304 find_issues_with_query(query)
304 find_issues_with_query(query)
305 end
305 end
306
306
307 def test_operator_greater_than_on_int_custom_field
307 def test_operator_greater_than_on_int_custom_field
308 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
308 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
309 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
309 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
310 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
310 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
311 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
312
312
313 query = Query.new(:project => Project.find(1), :name => '_')
313 query = Query.new(:project => Project.find(1), :name => '_')
314 query.add_filter("cf_#{f.id}", '>=', ['8'])
314 query.add_filter("cf_#{f.id}", '>=', ['8'])
315 issues = find_issues_with_query(query)
315 issues = find_issues_with_query(query)
316 assert_equal 1, issues.size
316 assert_equal 1, issues.size
317 assert_equal 2, issues.first.id
317 assert_equal 2, issues.first.id
318 end
318 end
319
319
320 def test_operator_lesser_than
320 def test_operator_lesser_than
321 query = Query.new(:project => Project.find(1), :name => '_')
321 query = Query.new(:project => Project.find(1), :name => '_')
322 query.add_filter('done_ratio', '<=', ['30'])
322 query.add_filter('done_ratio', '<=', ['30'])
323 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
323 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
324 find_issues_with_query(query)
324 find_issues_with_query(query)
325 end
325 end
326
326
327 def test_operator_lesser_than_on_custom_field
327 def test_operator_lesser_than_on_custom_field
328 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
328 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
329 query = Query.new(:project => Project.find(1), :name => '_')
329 query = Query.new(:project => Project.find(1), :name => '_')
330 query.add_filter("cf_#{f.id}", '<=', ['30'])
330 query.add_filter("cf_#{f.id}", '<=', ['30'])
331 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
331 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
332 find_issues_with_query(query)
332 find_issues_with_query(query)
333 end
333 end
334
334
335 def test_operator_between
335 def test_operator_between
336 query = Query.new(:project => Project.find(1), :name => '_')
336 query = Query.new(:project => Project.find(1), :name => '_')
337 query.add_filter('done_ratio', '><', ['30', '40'])
337 query.add_filter('done_ratio', '><', ['30', '40'])
338 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
338 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
339 find_issues_with_query(query)
339 find_issues_with_query(query)
340 end
340 end
341
341
342 def test_operator_between_on_custom_field
342 def test_operator_between_on_custom_field
343 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
343 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
344 query = Query.new(:project => Project.find(1), :name => '_')
344 query = Query.new(:project => Project.find(1), :name => '_')
345 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
345 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
346 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
346 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
347 find_issues_with_query(query)
347 find_issues_with_query(query)
348 end
348 end
349
349
350 def test_date_filter_should_not_accept_non_date_values
350 def test_date_filter_should_not_accept_non_date_values
351 query = Query.new(:name => '_')
351 query = Query.new(:name => '_')
352 query.add_filter('created_on', '=', ['a'])
352 query.add_filter('created_on', '=', ['a'])
353
353
354 assert query.has_filter?('created_on')
354 assert query.has_filter?('created_on')
355 assert !query.valid?
355 assert !query.valid?
356 end
356 end
357
357
358 def test_date_filter_should_not_accept_invalid_date_values
358 def test_date_filter_should_not_accept_invalid_date_values
359 query = Query.new(:name => '_')
359 query = Query.new(:name => '_')
360 query.add_filter('created_on', '=', ['2011-01-34'])
360 query.add_filter('created_on', '=', ['2011-01-34'])
361
361
362 assert query.has_filter?('created_on')
362 assert query.has_filter?('created_on')
363 assert !query.valid?
363 assert !query.valid?
364 end
364 end
365
365
366 def test_relative_date_filter_should_not_accept_non_integer_values
366 def test_relative_date_filter_should_not_accept_non_integer_values
367 query = Query.new(:name => '_')
367 query = Query.new(:name => '_')
368 query.add_filter('created_on', '>t-', ['a'])
368 query.add_filter('created_on', '>t-', ['a'])
369
369
370 assert query.has_filter?('created_on')
370 assert query.has_filter?('created_on')
371 assert !query.valid?
371 assert !query.valid?
372 end
372 end
373
373
374 def test_operator_date_equals
374 def test_operator_date_equals
375 query = Query.new(:name => '_')
375 query = Query.new(:name => '_')
376 query.add_filter('due_date', '=', ['2011-07-10'])
376 query.add_filter('due_date', '=', ['2011-07-10'])
377 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
377 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
378 find_issues_with_query(query)
378 find_issues_with_query(query)
379 end
379 end
380
380
381 def test_operator_date_lesser_than
381 def test_operator_date_lesser_than
382 query = Query.new(:name => '_')
382 query = Query.new(:name => '_')
383 query.add_filter('due_date', '<=', ['2011-07-10'])
383 query.add_filter('due_date', '<=', ['2011-07-10'])
384 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
384 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
385 find_issues_with_query(query)
385 find_issues_with_query(query)
386 end
386 end
387
387
388 def test_operator_date_greater_than
388 def test_operator_date_greater_than
389 query = Query.new(:name => '_')
389 query = Query.new(:name => '_')
390 query.add_filter('due_date', '>=', ['2011-07-10'])
390 query.add_filter('due_date', '>=', ['2011-07-10'])
391 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
391 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
392 find_issues_with_query(query)
392 find_issues_with_query(query)
393 end
393 end
394
394
395 def test_operator_date_between
395 def test_operator_date_between
396 query = Query.new(:name => '_')
396 query = Query.new(:name => '_')
397 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
397 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
398 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
398 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
399 find_issues_with_query(query)
399 find_issues_with_query(query)
400 end
400 end
401
401
402 def test_operator_in_more_than
402 def test_operator_in_more_than
403 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
403 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
404 query = Query.new(:project => Project.find(1), :name => '_')
404 query = Query.new(:project => Project.find(1), :name => '_')
405 query.add_filter('due_date', '>t+', ['15'])
405 query.add_filter('due_date', '>t+', ['15'])
406 issues = find_issues_with_query(query)
406 issues = find_issues_with_query(query)
407 assert !issues.empty?
407 assert !issues.empty?
408 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
408 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
409 end
409 end
410
410
411 def test_operator_in_less_than
411 def test_operator_in_less_than
412 query = Query.new(:project => Project.find(1), :name => '_')
412 query = Query.new(:project => Project.find(1), :name => '_')
413 query.add_filter('due_date', '<t+', ['15'])
413 query.add_filter('due_date', '<t+', ['15'])
414 issues = find_issues_with_query(query)
414 issues = find_issues_with_query(query)
415 assert !issues.empty?
415 assert !issues.empty?
416 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
416 issues.each {|issue| assert(issue.due_date <= (Date.today + 15))}
417 end
417 end
418
418
419 def test_operator_in_the_next_days
419 def test_operator_in_the_next_days
420 query = Query.new(:project => Project.find(1), :name => '_')
420 query = Query.new(:project => Project.find(1), :name => '_')
421 query.add_filter('due_date', '><t+', ['15'])
421 query.add_filter('due_date', '><t+', ['15'])
422 issues = find_issues_with_query(query)
422 issues = find_issues_with_query(query)
423 assert !issues.empty?
423 assert !issues.empty?
424 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
424 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
425 end
425 end
426
426
427 def test_operator_less_than_ago
427 def test_operator_less_than_ago
428 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
428 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
429 query = Query.new(:project => Project.find(1), :name => '_')
429 query = Query.new(:project => Project.find(1), :name => '_')
430 query.add_filter('due_date', '>t-', ['3'])
430 query.add_filter('due_date', '>t-', ['3'])
431 issues = find_issues_with_query(query)
431 issues = find_issues_with_query(query)
432 assert !issues.empty?
432 assert !issues.empty?
433 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
433 issues.each {|issue| assert(issue.due_date >= (Date.today - 3))}
434 end
434 end
435
435
436 def test_operator_in_the_past_days
436 def test_operator_in_the_past_days
437 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
437 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
438 query = Query.new(:project => Project.find(1), :name => '_')
438 query = Query.new(:project => Project.find(1), :name => '_')
439 query.add_filter('due_date', '><t-', ['3'])
439 query.add_filter('due_date', '><t-', ['3'])
440 issues = find_issues_with_query(query)
440 issues = find_issues_with_query(query)
441 assert !issues.empty?
441 assert !issues.empty?
442 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
442 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
443 end
443 end
444
444
445 def test_operator_more_than_ago
445 def test_operator_more_than_ago
446 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
446 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
447 query = Query.new(:project => Project.find(1), :name => '_')
447 query = Query.new(:project => Project.find(1), :name => '_')
448 query.add_filter('due_date', '<t-', ['10'])
448 query.add_filter('due_date', '<t-', ['10'])
449 assert query.statement.include?("#{Issue.table_name}.due_date <=")
449 assert query.statement.include?("#{Issue.table_name}.due_date <=")
450 issues = find_issues_with_query(query)
450 issues = find_issues_with_query(query)
451 assert !issues.empty?
451 assert !issues.empty?
452 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
452 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
453 end
453 end
454
454
455 def test_operator_in
455 def test_operator_in
456 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
456 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
457 query = Query.new(:project => Project.find(1), :name => '_')
457 query = Query.new(:project => Project.find(1), :name => '_')
458 query.add_filter('due_date', 't+', ['2'])
458 query.add_filter('due_date', 't+', ['2'])
459 issues = find_issues_with_query(query)
459 issues = find_issues_with_query(query)
460 assert !issues.empty?
460 assert !issues.empty?
461 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
461 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
462 end
462 end
463
463
464 def test_operator_ago
464 def test_operator_ago
465 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
465 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
466 query = Query.new(:project => Project.find(1), :name => '_')
466 query = Query.new(:project => Project.find(1), :name => '_')
467 query.add_filter('due_date', 't-', ['3'])
467 query.add_filter('due_date', 't-', ['3'])
468 issues = find_issues_with_query(query)
468 issues = find_issues_with_query(query)
469 assert !issues.empty?
469 assert !issues.empty?
470 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
470 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
471 end
471 end
472
472
473 def test_operator_today
473 def test_operator_today
474 query = Query.new(:project => Project.find(1), :name => '_')
474 query = Query.new(:project => Project.find(1), :name => '_')
475 query.add_filter('due_date', 't', [''])
475 query.add_filter('due_date', 't', [''])
476 issues = find_issues_with_query(query)
476 issues = find_issues_with_query(query)
477 assert !issues.empty?
477 assert !issues.empty?
478 issues.each {|issue| assert_equal Date.today, issue.due_date}
478 issues.each {|issue| assert_equal Date.today, issue.due_date}
479 end
479 end
480
480
481 def test_operator_this_week_on_date
481 def test_operator_this_week_on_date
482 query = Query.new(:project => Project.find(1), :name => '_')
482 query = Query.new(:project => Project.find(1), :name => '_')
483 query.add_filter('due_date', 'w', [''])
483 query.add_filter('due_date', 'w', [''])
484 find_issues_with_query(query)
484 find_issues_with_query(query)
485 end
485 end
486
486
487 def test_operator_this_week_on_datetime
487 def test_operator_this_week_on_datetime
488 query = Query.new(:project => Project.find(1), :name => '_')
488 query = Query.new(:project => Project.find(1), :name => '_')
489 query.add_filter('created_on', 'w', [''])
489 query.add_filter('created_on', 'w', [''])
490 find_issues_with_query(query)
490 find_issues_with_query(query)
491 end
491 end
492
492
493 def test_operator_contains
493 def test_operator_contains
494 query = Query.new(:project => Project.find(1), :name => '_')
494 query = Query.new(:project => Project.find(1), :name => '_')
495 query.add_filter('subject', '~', ['uNable'])
495 query.add_filter('subject', '~', ['uNable'])
496 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
496 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
497 result = find_issues_with_query(query)
497 result = find_issues_with_query(query)
498 assert result.empty?
498 assert result.empty?
499 result.each {|issue| assert issue.subject.downcase.include?('unable') }
499 result.each {|issue| assert issue.subject.downcase.include?('unable') }
500 end
500 end
501
501
502 def test_range_for_this_week_with_week_starting_on_monday
502 def test_range_for_this_week_with_week_starting_on_monday
503 I18n.locale = :fr
503 I18n.locale = :fr
504 assert_equal '1', I18n.t(:general_first_day_of_week)
504 assert_equal '1', I18n.t(:general_first_day_of_week)
505
505
506 Date.stubs(:today).returns(Date.parse('2011-04-29'))
506 Date.stubs(:today).returns(Date.parse('2011-04-29'))
507
507
508 query = Query.new(:project => Project.find(1), :name => '_')
508 query = Query.new(:project => Project.find(1), :name => '_')
509 query.add_filter('due_date', 'w', [''])
509 query.add_filter('due_date', 'w', [''])
510 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
510 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
511 I18n.locale = :en
511 I18n.locale = :en
512 end
512 end
513
513
514 def test_range_for_this_week_with_week_starting_on_sunday
514 def test_range_for_this_week_with_week_starting_on_sunday
515 I18n.locale = :en
515 I18n.locale = :en
516 assert_equal '7', I18n.t(:general_first_day_of_week)
516 assert_equal '7', I18n.t(:general_first_day_of_week)
517
517
518 Date.stubs(:today).returns(Date.parse('2011-04-29'))
518 Date.stubs(:today).returns(Date.parse('2011-04-29'))
519
519
520 query = Query.new(:project => Project.find(1), :name => '_')
520 query = Query.new(:project => Project.find(1), :name => '_')
521 query.add_filter('due_date', 'w', [''])
521 query.add_filter('due_date', 'w', [''])
522 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
522 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
523 end
523 end
524
524
525 def test_operator_does_not_contains
525 def test_operator_does_not_contains
526 query = Query.new(:project => Project.find(1), :name => '_')
526 query = Query.new(:project => Project.find(1), :name => '_')
527 query.add_filter('subject', '!~', ['uNable'])
527 query.add_filter('subject', '!~', ['uNable'])
528 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
528 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
529 find_issues_with_query(query)
529 find_issues_with_query(query)
530 end
530 end
531
531
532 def test_filter_assigned_to_me
532 def test_filter_assigned_to_me
533 user = User.find(2)
533 user = User.find(2)
534 group = Group.find(10)
534 group = Group.find(10)
535 User.current = user
535 User.current = user
536 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
536 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
537 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
537 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
538 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
538 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
539 group.users << user
539 group.users << user
540
540
541 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
541 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
542 result = query.issues
542 result = query.issues
543 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
543 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
544
544
545 assert result.include?(i1)
545 assert result.include?(i1)
546 assert result.include?(i2)
546 assert result.include?(i2)
547 assert !result.include?(i3)
547 assert !result.include?(i3)
548 end
548 end
549
549
550 def test_user_custom_field_filtered_on_me
550 def test_user_custom_field_filtered_on_me
551 User.current = User.find(2)
551 User.current = User.find(2)
552 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
552 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
553 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
553 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
554 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
554 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
555
555
556 query = Query.new(:name => '_', :project => Project.find(1))
556 query = Query.new(:name => '_', :project => Project.find(1))
557 filter = query.available_filters["cf_#{cf.id}"]
557 filter = query.available_filters["cf_#{cf.id}"]
558 assert_not_nil filter
558 assert_not_nil filter
559 assert_include 'me', filter[:values].map{|v| v[1]}
559 assert_include 'me', filter[:values].map{|v| v[1]}
560
560
561 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
561 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
562 result = query.issues
562 result = query.issues
563 assert_equal 1, result.size
563 assert_equal 1, result.size
564 assert_equal issue1, result.first
564 assert_equal issue1, result.first
565 end
565 end
566
566
567 def test_filter_my_projects
567 def test_filter_my_projects
568 User.current = User.find(2)
568 User.current = User.find(2)
569 query = Query.new(:name => '_')
569 query = Query.new(:name => '_')
570 filter = query.available_filters['project_id']
570 filter = query.available_filters['project_id']
571 assert_not_nil filter
571 assert_not_nil filter
572 assert_include 'mine', filter[:values].map{|v| v[1]}
572 assert_include 'mine', filter[:values].map{|v| v[1]}
573
573
574 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
574 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
575 result = query.issues
575 result = query.issues
576 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
576 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
577 end
577 end
578
578
579 def test_filter_watched_issues
579 def test_filter_watched_issues
580 User.current = User.find(1)
580 User.current = User.find(1)
581 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
581 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
582 result = find_issues_with_query(query)
582 result = find_issues_with_query(query)
583 assert_not_nil result
583 assert_not_nil result
584 assert !result.empty?
584 assert !result.empty?
585 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
585 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
586 User.current = nil
586 User.current = nil
587 end
587 end
588
588
589 def test_filter_unwatched_issues
589 def test_filter_unwatched_issues
590 User.current = User.find(1)
590 User.current = User.find(1)
591 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
591 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
592 result = find_issues_with_query(query)
592 result = find_issues_with_query(query)
593 assert_not_nil result
593 assert_not_nil result
594 assert !result.empty?
594 assert !result.empty?
595 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
595 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
596 User.current = nil
596 User.current = nil
597 end
597 end
598
598
599 def test_filter_on_project_custom_field
599 def test_filter_on_project_custom_field
600 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
600 field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
601 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
601 CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
602 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
602 CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
603
603
604 query = Query.new(:name => '_')
604 query = Query.new(:name => '_')
605 filter_name = "project.cf_#{field.id}"
605 filter_name = "project.cf_#{field.id}"
606 assert_include filter_name, query.available_filters.keys
606 assert_include filter_name, query.available_filters.keys
607 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
607 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
608 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
608 assert_equal [3, 5], find_issues_with_query(query).map(&:project_id).uniq.sort
609 end
609 end
610
610
611 def test_filter_on_author_custom_field
611 def test_filter_on_author_custom_field
612 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
612 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
613 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
613 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
614
614
615 query = Query.new(:name => '_')
615 query = Query.new(:name => '_')
616 filter_name = "author.cf_#{field.id}"
616 filter_name = "author.cf_#{field.id}"
617 assert_include filter_name, query.available_filters.keys
617 assert_include filter_name, query.available_filters.keys
618 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
618 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
619 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
619 assert_equal [3], find_issues_with_query(query).map(&:author_id).uniq.sort
620 end
620 end
621
621
622 def test_filter_on_assigned_to_custom_field
622 def test_filter_on_assigned_to_custom_field
623 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
623 field = UserCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
624 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
624 CustomValue.create!(:custom_field => field, :customized => User.find(3), :value => 'Foo')
625
625
626 query = Query.new(:name => '_')
626 query = Query.new(:name => '_')
627 filter_name = "assigned_to.cf_#{field.id}"
627 filter_name = "assigned_to.cf_#{field.id}"
628 assert_include filter_name, query.available_filters.keys
628 assert_include filter_name, query.available_filters.keys
629 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
629 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
630 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
630 assert_equal [3], find_issues_with_query(query).map(&:assigned_to_id).uniq.sort
631 end
631 end
632
632
633 def test_filter_on_fixed_version_custom_field
633 def test_filter_on_fixed_version_custom_field
634 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
634 field = VersionCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
635 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
635 CustomValue.create!(:custom_field => field, :customized => Version.find(2), :value => 'Foo')
636
636
637 query = Query.new(:name => '_')
637 query = Query.new(:name => '_')
638 filter_name = "fixed_version.cf_#{field.id}"
638 filter_name = "fixed_version.cf_#{field.id}"
639 assert_include filter_name, query.available_filters.keys
639 assert_include filter_name, query.available_filters.keys
640 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
640 query.filters = {filter_name => {:operator => '=', :values => ['Foo']}}
641 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
641 assert_equal [2], find_issues_with_query(query).map(&:fixed_version_id).uniq.sort
642 end
642 end
643
643
644 def test_filter_on_relations_with_a_specific_issue
644 def test_filter_on_relations_with_a_specific_issue
645 IssueRelation.delete_all
645 IssueRelation.delete_all
646 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
646 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
647 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
647 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
648
648
649 query = Query.new(:name => '_')
649 query = Query.new(:name => '_')
650 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
650 query.filters = {"relates" => {:operator => '=', :values => ['1']}}
651 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
651 assert_equal [2, 3], find_issues_with_query(query).map(&:id).sort
652
652
653 query = Query.new(:name => '_')
653 query = Query.new(:name => '_')
654 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
654 query.filters = {"relates" => {:operator => '=', :values => ['2']}}
655 assert_equal [1], find_issues_with_query(query).map(&:id).sort
655 assert_equal [1], find_issues_with_query(query).map(&:id).sort
656 end
656 end
657
657
658 def test_filter_on_relations_with_any_issues_in_a_project
658 def test_filter_on_relations_with_any_issues_in_a_project
659 IssueRelation.delete_all
659 IssueRelation.delete_all
660 with_settings :cross_project_issue_relations => '1' do
660 with_settings :cross_project_issue_relations => '1' do
661 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
661 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
662 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
662 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(2).issues.first)
663 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
663 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
664 end
664 end
665
665
666 query = Query.new(:name => '_')
666 query = Query.new(:name => '_')
667 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
667 query.filters = {"relates" => {:operator => '=p', :values => ['2']}}
668 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
668 assert_equal [1, 2], find_issues_with_query(query).map(&:id).sort
669
669
670 query = Query.new(:name => '_')
670 query = Query.new(:name => '_')
671 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
671 query.filters = {"relates" => {:operator => '=p', :values => ['3']}}
672 assert_equal [1], find_issues_with_query(query).map(&:id).sort
672 assert_equal [1], find_issues_with_query(query).map(&:id).sort
673
673
674 query = Query.new(:name => '_')
674 query = Query.new(:name => '_')
675 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
675 query.filters = {"relates" => {:operator => '=p', :values => ['4']}}
676 assert_equal [], find_issues_with_query(query).map(&:id).sort
676 assert_equal [], find_issues_with_query(query).map(&:id).sort
677 end
677 end
678
678
679 def test_filter_on_relations_with_any_issues_not_in_a_project
679 def test_filter_on_relations_with_any_issues_not_in_a_project
680 IssueRelation.delete_all
680 IssueRelation.delete_all
681 with_settings :cross_project_issue_relations => '1' do
681 with_settings :cross_project_issue_relations => '1' do
682 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
682 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
683 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
683 #IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(1).issues.first)
684 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
684 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(3).issues.first)
685 end
685 end
686
686
687 query = Query.new(:name => '_')
687 query = Query.new(:name => '_')
688 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
688 query.filters = {"relates" => {:operator => '=!p', :values => ['1']}}
689 assert_equal [1], find_issues_with_query(query).map(&:id).sort
689 assert_equal [1], find_issues_with_query(query).map(&:id).sort
690 end
690 end
691
691
692 def test_filter_on_relations_with_no_issues_in_a_project
692 def test_filter_on_relations_with_no_issues_in_a_project
693 IssueRelation.delete_all
693 IssueRelation.delete_all
694 with_settings :cross_project_issue_relations => '1' do
694 with_settings :cross_project_issue_relations => '1' do
695 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
695 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Project.find(2).issues.first)
696 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
696 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(2), :issue_to => Project.find(3).issues.first)
697 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
697 IssueRelation.create!(:relation_type => "relates", :issue_to => Project.find(2).issues.first, :issue_from => Issue.find(3))
698 end
698 end
699
699
700 query = Query.new(:name => '_')
700 query = Query.new(:name => '_')
701 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
701 query.filters = {"relates" => {:operator => '!p', :values => ['2']}}
702 ids = find_issues_with_query(query).map(&:id).sort
702 ids = find_issues_with_query(query).map(&:id).sort
703 assert_include 2, ids
703 assert_include 2, ids
704 assert_not_include 1, ids
704 assert_not_include 1, ids
705 assert_not_include 3, ids
705 assert_not_include 3, ids
706 end
706 end
707
707
708 def test_filter_on_relations_with_no_issues
708 def test_filter_on_relations_with_no_issues
709 IssueRelation.delete_all
709 IssueRelation.delete_all
710 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
710 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
711 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
711 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
712
712
713 query = Query.new(:name => '_')
713 query = Query.new(:name => '_')
714 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
714 query.filters = {"relates" => {:operator => '!*', :values => ['']}}
715 ids = find_issues_with_query(query).map(&:id)
715 ids = find_issues_with_query(query).map(&:id)
716 assert_equal [], ids & [1, 2, 3]
716 assert_equal [], ids & [1, 2, 3]
717 assert_include 4, ids
717 assert_include 4, ids
718 end
718 end
719
719
720 def test_filter_on_relations_with_any_issues
720 def test_filter_on_relations_with_any_issues
721 IssueRelation.delete_all
721 IssueRelation.delete_all
722 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
722 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(2))
723 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
723 IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(3), :issue_to => Issue.find(1))
724
724
725 query = Query.new(:name => '_')
725 query = Query.new(:name => '_')
726 query.filters = {"relates" => {:operator => '*', :values => ['']}}
726 query.filters = {"relates" => {:operator => '*', :values => ['']}}
727 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
727 assert_equal [1, 2, 3], find_issues_with_query(query).map(&:id).sort
728 end
728 end
729
729
730 def test_statement_should_be_nil_with_no_filters
730 def test_statement_should_be_nil_with_no_filters
731 q = Query.new(:name => '_')
731 q = Query.new(:name => '_')
732 q.filters = {}
732 q.filters = {}
733
733
734 assert q.valid?
734 assert q.valid?
735 assert_nil q.statement
735 assert_nil q.statement
736 end
736 end
737
737
738 def test_default_columns
738 def test_default_columns
739 q = Query.new
739 q = Query.new
740 assert !q.columns.empty?
740 assert q.columns.any?
741 assert q.inline_columns.any?
742 assert q.block_columns.empty?
741 end
743 end
742
744
743 def test_set_column_names
745 def test_set_column_names
744 q = Query.new
746 q = Query.new
745 q.column_names = ['tracker', :subject, '', 'unknonw_column']
747 q.column_names = ['tracker', :subject, '', 'unknonw_column']
746 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
748 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
747 c = q.columns.first
749 c = q.columns.first
748 assert q.has_column?(c)
750 assert q.has_column?(c)
749 end
751 end
750
752
753 def test_inline_and_block_columns
754 q = Query.new
755 q.column_names = ['subject', 'description', 'tracker']
756
757 assert_equal [:subject, :tracker], q.inline_columns.map(&:name)
758 assert_equal [:description], q.block_columns.map(&:name)
759 end
760
761 def test_custom_field_columns_should_be_inline
762 q = Query.new
763 columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn}
764 assert columns.any?
765 assert_nil columns.detect {|column| !column.inline?}
766 end
767
751 def test_query_should_preload_spent_hours
768 def test_query_should_preload_spent_hours
752 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
769 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
753 assert q.has_column?(:spent_hours)
770 assert q.has_column?(:spent_hours)
754 issues = q.issues
771 issues = q.issues
755 assert_not_nil issues.first.instance_variable_get("@spent_hours")
772 assert_not_nil issues.first.instance_variable_get("@spent_hours")
756 end
773 end
757
774
758 def test_groupable_columns_should_include_custom_fields
775 def test_groupable_columns_should_include_custom_fields
759 q = Query.new
776 q = Query.new
760 column = q.groupable_columns.detect {|c| c.name == :cf_1}
777 column = q.groupable_columns.detect {|c| c.name == :cf_1}
761 assert_not_nil column
778 assert_not_nil column
762 assert_kind_of QueryCustomFieldColumn, column
779 assert_kind_of QueryCustomFieldColumn, column
763 end
780 end
764
781
765 def test_groupable_columns_should_not_include_multi_custom_fields
782 def test_groupable_columns_should_not_include_multi_custom_fields
766 field = CustomField.find(1)
783 field = CustomField.find(1)
767 field.update_attribute :multiple, true
784 field.update_attribute :multiple, true
768
785
769 q = Query.new
786 q = Query.new
770 column = q.groupable_columns.detect {|c| c.name == :cf_1}
787 column = q.groupable_columns.detect {|c| c.name == :cf_1}
771 assert_nil column
788 assert_nil column
772 end
789 end
773
790
774 def test_groupable_columns_should_include_user_custom_fields
791 def test_groupable_columns_should_include_user_custom_fields
775 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
792 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'user')
776
793
777 q = Query.new
794 q = Query.new
778 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
795 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
779 end
796 end
780
797
781 def test_groupable_columns_should_include_version_custom_fields
798 def test_groupable_columns_should_include_version_custom_fields
782 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
799 cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1], :field_format => 'version')
783
800
784 q = Query.new
801 q = Query.new
785 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
802 assert q.groupable_columns.detect {|c| c.name == "cf_#{cf.id}".to_sym}
786 end
803 end
787
804
788 def test_grouped_with_valid_column
805 def test_grouped_with_valid_column
789 q = Query.new(:group_by => 'status')
806 q = Query.new(:group_by => 'status')
790 assert q.grouped?
807 assert q.grouped?
791 assert_not_nil q.group_by_column
808 assert_not_nil q.group_by_column
792 assert_equal :status, q.group_by_column.name
809 assert_equal :status, q.group_by_column.name
793 assert_not_nil q.group_by_statement
810 assert_not_nil q.group_by_statement
794 assert_equal 'status', q.group_by_statement
811 assert_equal 'status', q.group_by_statement
795 end
812 end
796
813
797 def test_grouped_with_invalid_column
814 def test_grouped_with_invalid_column
798 q = Query.new(:group_by => 'foo')
815 q = Query.new(:group_by => 'foo')
799 assert !q.grouped?
816 assert !q.grouped?
800 assert_nil q.group_by_column
817 assert_nil q.group_by_column
801 assert_nil q.group_by_statement
818 assert_nil q.group_by_statement
802 end
819 end
803
820
804 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
821 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
805 with_settings :user_format => 'lastname_coma_firstname' do
822 with_settings :user_format => 'lastname_coma_firstname' do
806 q = Query.new
823 q = Query.new
807 assert q.sortable_columns.has_key?('assigned_to')
824 assert q.sortable_columns.has_key?('assigned_to')
808 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
825 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
809 end
826 end
810 end
827 end
811
828
812 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
829 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
813 with_settings :user_format => 'lastname_coma_firstname' do
830 with_settings :user_format => 'lastname_coma_firstname' do
814 q = Query.new
831 q = Query.new
815 assert q.sortable_columns.has_key?('author')
832 assert q.sortable_columns.has_key?('author')
816 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
833 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
817 end
834 end
818 end
835 end
819
836
820 def test_sortable_columns_should_include_custom_field
837 def test_sortable_columns_should_include_custom_field
821 q = Query.new
838 q = Query.new
822 assert q.sortable_columns['cf_1']
839 assert q.sortable_columns['cf_1']
823 end
840 end
824
841
825 def test_sortable_columns_should_not_include_multi_custom_field
842 def test_sortable_columns_should_not_include_multi_custom_field
826 field = CustomField.find(1)
843 field = CustomField.find(1)
827 field.update_attribute :multiple, true
844 field.update_attribute :multiple, true
828
845
829 q = Query.new
846 q = Query.new
830 assert !q.sortable_columns['cf_1']
847 assert !q.sortable_columns['cf_1']
831 end
848 end
832
849
833 def test_default_sort
850 def test_default_sort
834 q = Query.new
851 q = Query.new
835 assert_equal [], q.sort_criteria
852 assert_equal [], q.sort_criteria
836 end
853 end
837
854
838 def test_set_sort_criteria_with_hash
855 def test_set_sort_criteria_with_hash
839 q = Query.new
856 q = Query.new
840 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
857 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
841 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
858 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
842 end
859 end
843
860
844 def test_set_sort_criteria_with_array
861 def test_set_sort_criteria_with_array
845 q = Query.new
862 q = Query.new
846 q.sort_criteria = [['priority', 'desc'], 'tracker']
863 q.sort_criteria = [['priority', 'desc'], 'tracker']
847 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
864 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
848 end
865 end
849
866
850 def test_create_query_with_sort
867 def test_create_query_with_sort
851 q = Query.new(:name => 'Sorted')
868 q = Query.new(:name => 'Sorted')
852 q.sort_criteria = [['priority', 'desc'], 'tracker']
869 q.sort_criteria = [['priority', 'desc'], 'tracker']
853 assert q.save
870 assert q.save
854 q.reload
871 q.reload
855 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
872 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
856 end
873 end
857
874
858 def test_sort_by_string_custom_field_asc
875 def test_sort_by_string_custom_field_asc
859 q = Query.new
876 q = Query.new
860 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
877 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
861 assert c
878 assert c
862 assert c.sortable
879 assert c.sortable
863 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
880 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
864 q.statement
881 q.statement
865 ).order("#{c.sortable} ASC").all
882 ).order("#{c.sortable} ASC").all
866 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
883 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
867 assert !values.empty?
884 assert !values.empty?
868 assert_equal values.sort, values
885 assert_equal values.sort, values
869 end
886 end
870
887
871 def test_sort_by_string_custom_field_desc
888 def test_sort_by_string_custom_field_desc
872 q = Query.new
889 q = Query.new
873 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
890 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
874 assert c
891 assert c
875 assert c.sortable
892 assert c.sortable
876 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
893 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
877 q.statement
894 q.statement
878 ).order("#{c.sortable} DESC").all
895 ).order("#{c.sortable} DESC").all
879 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
896 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
880 assert !values.empty?
897 assert !values.empty?
881 assert_equal values.sort.reverse, values
898 assert_equal values.sort.reverse, values
882 end
899 end
883
900
884 def test_sort_by_float_custom_field_asc
901 def test_sort_by_float_custom_field_asc
885 q = Query.new
902 q = Query.new
886 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
903 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
887 assert c
904 assert c
888 assert c.sortable
905 assert c.sortable
889 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
906 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
890 q.statement
907 q.statement
891 ).order("#{c.sortable} ASC").all
908 ).order("#{c.sortable} ASC").all
892 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
909 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
893 assert !values.empty?
910 assert !values.empty?
894 assert_equal values.sort, values
911 assert_equal values.sort, values
895 end
912 end
896
913
897 def test_invalid_query_should_raise_query_statement_invalid_error
914 def test_invalid_query_should_raise_query_statement_invalid_error
898 q = Query.new
915 q = Query.new
899 assert_raise Query::StatementInvalid do
916 assert_raise Query::StatementInvalid do
900 q.issues(:conditions => "foo = 1")
917 q.issues(:conditions => "foo = 1")
901 end
918 end
902 end
919 end
903
920
904 def test_issue_count
921 def test_issue_count
905 q = Query.new(:name => '_')
922 q = Query.new(:name => '_')
906 issue_count = q.issue_count
923 issue_count = q.issue_count
907 assert_equal q.issues.size, issue_count
924 assert_equal q.issues.size, issue_count
908 end
925 end
909
926
910 def test_issue_count_with_archived_issues
927 def test_issue_count_with_archived_issues
911 p = Project.generate! do |project|
928 p = Project.generate! do |project|
912 project.status = Project::STATUS_ARCHIVED
929 project.status = Project::STATUS_ARCHIVED
913 end
930 end
914 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
931 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
915 assert !i.visible?
932 assert !i.visible?
916
933
917 test_issue_count
934 test_issue_count
918 end
935 end
919
936
920 def test_issue_count_by_association_group
937 def test_issue_count_by_association_group
921 q = Query.new(:name => '_', :group_by => 'assigned_to')
938 q = Query.new(:name => '_', :group_by => 'assigned_to')
922 count_by_group = q.issue_count_by_group
939 count_by_group = q.issue_count_by_group
923 assert_kind_of Hash, count_by_group
940 assert_kind_of Hash, count_by_group
924 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
941 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
925 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
942 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
926 assert count_by_group.has_key?(User.find(3))
943 assert count_by_group.has_key?(User.find(3))
927 end
944 end
928
945
929 def test_issue_count_by_list_custom_field_group
946 def test_issue_count_by_list_custom_field_group
930 q = Query.new(:name => '_', :group_by => 'cf_1')
947 q = Query.new(:name => '_', :group_by => 'cf_1')
931 count_by_group = q.issue_count_by_group
948 count_by_group = q.issue_count_by_group
932 assert_kind_of Hash, count_by_group
949 assert_kind_of Hash, count_by_group
933 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
950 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
934 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
951 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
935 assert count_by_group.has_key?('MySQL')
952 assert count_by_group.has_key?('MySQL')
936 end
953 end
937
954
938 def test_issue_count_by_date_custom_field_group
955 def test_issue_count_by_date_custom_field_group
939 q = Query.new(:name => '_', :group_by => 'cf_8')
956 q = Query.new(:name => '_', :group_by => 'cf_8')
940 count_by_group = q.issue_count_by_group
957 count_by_group = q.issue_count_by_group
941 assert_kind_of Hash, count_by_group
958 assert_kind_of Hash, count_by_group
942 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
959 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
943 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
960 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
944 end
961 end
945
962
946 def test_issue_count_with_nil_group_only
963 def test_issue_count_with_nil_group_only
947 Issue.update_all("assigned_to_id = NULL")
964 Issue.update_all("assigned_to_id = NULL")
948
965
949 q = Query.new(:name => '_', :group_by => 'assigned_to')
966 q = Query.new(:name => '_', :group_by => 'assigned_to')
950 count_by_group = q.issue_count_by_group
967 count_by_group = q.issue_count_by_group
951 assert_kind_of Hash, count_by_group
968 assert_kind_of Hash, count_by_group
952 assert_equal 1, count_by_group.keys.size
969 assert_equal 1, count_by_group.keys.size
953 assert_nil count_by_group.keys.first
970 assert_nil count_by_group.keys.first
954 end
971 end
955
972
956 def test_issue_ids
973 def test_issue_ids
957 q = Query.new(:name => '_')
974 q = Query.new(:name => '_')
958 order = "issues.subject, issues.id"
975 order = "issues.subject, issues.id"
959 issues = q.issues(:order => order)
976 issues = q.issues(:order => order)
960 assert_equal issues.map(&:id), q.issue_ids(:order => order)
977 assert_equal issues.map(&:id), q.issue_ids(:order => order)
961 end
978 end
962
979
963 def test_label_for
980 def test_label_for
964 set_language_if_valid 'en'
981 set_language_if_valid 'en'
965 q = Query.new
982 q = Query.new
966 assert_equal 'Assignee', q.label_for('assigned_to_id')
983 assert_equal 'Assignee', q.label_for('assigned_to_id')
967 end
984 end
968
985
969 def test_label_for_fr
986 def test_label_for_fr
970 set_language_if_valid 'fr'
987 set_language_if_valid 'fr'
971 q = Query.new
988 q = Query.new
972 s = "Assign\xc3\xa9 \xc3\xa0"
989 s = "Assign\xc3\xa9 \xc3\xa0"
973 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
990 s.force_encoding('UTF-8') if s.respond_to?(:force_encoding)
974 assert_equal s, q.label_for('assigned_to_id')
991 assert_equal s, q.label_for('assigned_to_id')
975 end
992 end
976
993
977 def test_editable_by
994 def test_editable_by
978 admin = User.find(1)
995 admin = User.find(1)
979 manager = User.find(2)
996 manager = User.find(2)
980 developer = User.find(3)
997 developer = User.find(3)
981
998
982 # Public query on project 1
999 # Public query on project 1
983 q = Query.find(1)
1000 q = Query.find(1)
984 assert q.editable_by?(admin)
1001 assert q.editable_by?(admin)
985 assert q.editable_by?(manager)
1002 assert q.editable_by?(manager)
986 assert !q.editable_by?(developer)
1003 assert !q.editable_by?(developer)
987
1004
988 # Private query on project 1
1005 # Private query on project 1
989 q = Query.find(2)
1006 q = Query.find(2)
990 assert q.editable_by?(admin)
1007 assert q.editable_by?(admin)
991 assert !q.editable_by?(manager)
1008 assert !q.editable_by?(manager)
992 assert q.editable_by?(developer)
1009 assert q.editable_by?(developer)
993
1010
994 # Private query for all projects
1011 # Private query for all projects
995 q = Query.find(3)
1012 q = Query.find(3)
996 assert q.editable_by?(admin)
1013 assert q.editable_by?(admin)
997 assert !q.editable_by?(manager)
1014 assert !q.editable_by?(manager)
998 assert q.editable_by?(developer)
1015 assert q.editable_by?(developer)
999
1016
1000 # Public query for all projects
1017 # Public query for all projects
1001 q = Query.find(4)
1018 q = Query.find(4)
1002 assert q.editable_by?(admin)
1019 assert q.editable_by?(admin)
1003 assert !q.editable_by?(manager)
1020 assert !q.editable_by?(manager)
1004 assert !q.editable_by?(developer)
1021 assert !q.editable_by?(developer)
1005 end
1022 end
1006
1023
1007 def test_visible_scope
1024 def test_visible_scope
1008 query_ids = Query.visible(User.anonymous).map(&:id)
1025 query_ids = Query.visible(User.anonymous).map(&:id)
1009
1026
1010 assert query_ids.include?(1), 'public query on public project was not visible'
1027 assert query_ids.include?(1), 'public query on public project was not visible'
1011 assert query_ids.include?(4), 'public query for all projects was not visible'
1028 assert query_ids.include?(4), 'public query for all projects was not visible'
1012 assert !query_ids.include?(2), 'private query on public project was visible'
1029 assert !query_ids.include?(2), 'private query on public project was visible'
1013 assert !query_ids.include?(3), 'private query for all projects was visible'
1030 assert !query_ids.include?(3), 'private query for all projects was visible'
1014 assert !query_ids.include?(7), 'public query on private project was visible'
1031 assert !query_ids.include?(7), 'public query on private project was visible'
1015 end
1032 end
1016
1033
1017 context "#available_filters" do
1034 context "#available_filters" do
1018 setup do
1035 setup do
1019 @query = Query.new(:name => "_")
1036 @query = Query.new(:name => "_")
1020 end
1037 end
1021
1038
1022 should "include users of visible projects in cross-project view" do
1039 should "include users of visible projects in cross-project view" do
1023 users = @query.available_filters["assigned_to_id"]
1040 users = @query.available_filters["assigned_to_id"]
1024 assert_not_nil users
1041 assert_not_nil users
1025 assert users[:values].map{|u|u[1]}.include?("3")
1042 assert users[:values].map{|u|u[1]}.include?("3")
1026 end
1043 end
1027
1044
1028 should "include users of subprojects" do
1045 should "include users of subprojects" do
1029 user1 = User.generate!
1046 user1 = User.generate!
1030 user2 = User.generate!
1047 user2 = User.generate!
1031 project = Project.find(1)
1048 project = Project.find(1)
1032 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1049 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
1033 @query.project = project
1050 @query.project = project
1034
1051
1035 users = @query.available_filters["assigned_to_id"]
1052 users = @query.available_filters["assigned_to_id"]
1036 assert_not_nil users
1053 assert_not_nil users
1037 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1054 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
1038 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1055 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
1039 end
1056 end
1040
1057
1041 should "include visible projects in cross-project view" do
1058 should "include visible projects in cross-project view" do
1042 projects = @query.available_filters["project_id"]
1059 projects = @query.available_filters["project_id"]
1043 assert_not_nil projects
1060 assert_not_nil projects
1044 assert projects[:values].map{|u|u[1]}.include?("1")
1061 assert projects[:values].map{|u|u[1]}.include?("1")
1045 end
1062 end
1046
1063
1047 context "'member_of_group' filter" do
1064 context "'member_of_group' filter" do
1048 should "be present" do
1065 should "be present" do
1049 assert @query.available_filters.keys.include?("member_of_group")
1066 assert @query.available_filters.keys.include?("member_of_group")
1050 end
1067 end
1051
1068
1052 should "be an optional list" do
1069 should "be an optional list" do
1053 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
1070 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
1054 end
1071 end
1055
1072
1056 should "have a list of the groups as values" do
1073 should "have a list of the groups as values" do
1057 Group.destroy_all # No fixtures
1074 Group.destroy_all # No fixtures
1058 group1 = Group.generate!.reload
1075 group1 = Group.generate!.reload
1059 group2 = Group.generate!.reload
1076 group2 = Group.generate!.reload
1060
1077
1061 expected_group_list = [
1078 expected_group_list = [
1062 [group1.name, group1.id.to_s],
1079 [group1.name, group1.id.to_s],
1063 [group2.name, group2.id.to_s]
1080 [group2.name, group2.id.to_s]
1064 ]
1081 ]
1065 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
1082 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
1066 end
1083 end
1067
1084
1068 end
1085 end
1069
1086
1070 context "'assigned_to_role' filter" do
1087 context "'assigned_to_role' filter" do
1071 should "be present" do
1088 should "be present" do
1072 assert @query.available_filters.keys.include?("assigned_to_role")
1089 assert @query.available_filters.keys.include?("assigned_to_role")
1073 end
1090 end
1074
1091
1075 should "be an optional list" do
1092 should "be an optional list" do
1076 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
1093 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
1077 end
1094 end
1078
1095
1079 should "have a list of the Roles as values" do
1096 should "have a list of the Roles as values" do
1080 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1097 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
1081 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1098 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
1082 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1099 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
1083 end
1100 end
1084
1101
1085 should "not include the built in Roles as values" do
1102 should "not include the built in Roles as values" do
1086 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1103 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
1087 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1104 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
1088 end
1105 end
1089
1106
1090 end
1107 end
1091
1108
1092 end
1109 end
1093
1110
1094 context "#statement" do
1111 context "#statement" do
1095 context "with 'member_of_group' filter" do
1112 context "with 'member_of_group' filter" do
1096 setup do
1113 setup do
1097 Group.destroy_all # No fixtures
1114 Group.destroy_all # No fixtures
1098 @user_in_group = User.generate!
1115 @user_in_group = User.generate!
1099 @second_user_in_group = User.generate!
1116 @second_user_in_group = User.generate!
1100 @user_in_group2 = User.generate!
1117 @user_in_group2 = User.generate!
1101 @user_not_in_group = User.generate!
1118 @user_not_in_group = User.generate!
1102
1119
1103 @group = Group.generate!.reload
1120 @group = Group.generate!.reload
1104 @group.users << @user_in_group
1121 @group.users << @user_in_group
1105 @group.users << @second_user_in_group
1122 @group.users << @second_user_in_group
1106
1123
1107 @group2 = Group.generate!.reload
1124 @group2 = Group.generate!.reload
1108 @group2.users << @user_in_group2
1125 @group2.users << @user_in_group2
1109
1126
1110 end
1127 end
1111
1128
1112 should "search assigned to for users in the group" do
1129 should "search assigned to for users in the group" do
1113 @query = Query.new(:name => '_')
1130 @query = Query.new(:name => '_')
1114 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1131 @query.add_filter('member_of_group', '=', [@group.id.to_s])
1115
1132
1116 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
1133 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
1117 assert_find_issues_with_query_is_successful @query
1134 assert_find_issues_with_query_is_successful @query
1118 end
1135 end
1119
1136
1120 should "search not assigned to any group member (none)" do
1137 should "search not assigned to any group member (none)" do
1121 @query = Query.new(:name => '_')
1138 @query = Query.new(:name => '_')
1122 @query.add_filter('member_of_group', '!*', [''])
1139 @query.add_filter('member_of_group', '!*', [''])
1123
1140
1124 # Users not in a group
1141 # Users not in a group
1125 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1142 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1126 assert_find_issues_with_query_is_successful @query
1143 assert_find_issues_with_query_is_successful @query
1127 end
1144 end
1128
1145
1129 should "search assigned to any group member (all)" do
1146 should "search assigned to any group member (all)" do
1130 @query = Query.new(:name => '_')
1147 @query = Query.new(:name => '_')
1131 @query.add_filter('member_of_group', '*', [''])
1148 @query.add_filter('member_of_group', '*', [''])
1132
1149
1133 # Only users in a group
1150 # Only users in a group
1134 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1151 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
1135 assert_find_issues_with_query_is_successful @query
1152 assert_find_issues_with_query_is_successful @query
1136 end
1153 end
1137
1154
1138 should "return an empty set with = empty group" do
1155 should "return an empty set with = empty group" do
1139 @empty_group = Group.generate!
1156 @empty_group = Group.generate!
1140 @query = Query.new(:name => '_')
1157 @query = Query.new(:name => '_')
1141 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1158 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
1142
1159
1143 assert_equal [], find_issues_with_query(@query)
1160 assert_equal [], find_issues_with_query(@query)
1144 end
1161 end
1145
1162
1146 should "return issues with ! empty group" do
1163 should "return issues with ! empty group" do
1147 @empty_group = Group.generate!
1164 @empty_group = Group.generate!
1148 @query = Query.new(:name => '_')
1165 @query = Query.new(:name => '_')
1149 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1166 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
1150
1167
1151 assert_find_issues_with_query_is_successful @query
1168 assert_find_issues_with_query_is_successful @query
1152 end
1169 end
1153 end
1170 end
1154
1171
1155 context "with 'assigned_to_role' filter" do
1172 context "with 'assigned_to_role' filter" do
1156 setup do
1173 setup do
1157 @manager_role = Role.find_by_name('Manager')
1174 @manager_role = Role.find_by_name('Manager')
1158 @developer_role = Role.find_by_name('Developer')
1175 @developer_role = Role.find_by_name('Developer')
1159
1176
1160 @project = Project.generate!
1177 @project = Project.generate!
1161 @manager = User.generate!
1178 @manager = User.generate!
1162 @developer = User.generate!
1179 @developer = User.generate!
1163 @boss = User.generate!
1180 @boss = User.generate!
1164 @guest = User.generate!
1181 @guest = User.generate!
1165 User.add_to_project(@manager, @project, @manager_role)
1182 User.add_to_project(@manager, @project, @manager_role)
1166 User.add_to_project(@developer, @project, @developer_role)
1183 User.add_to_project(@developer, @project, @developer_role)
1167 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1184 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
1168
1185
1169 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1186 @issue1 = Issue.generate!(:project => @project, :assigned_to_id => @manager.id)
1170 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1187 @issue2 = Issue.generate!(:project => @project, :assigned_to_id => @developer.id)
1171 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1188 @issue3 = Issue.generate!(:project => @project, :assigned_to_id => @boss.id)
1172 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1189 @issue4 = Issue.generate!(:project => @project, :assigned_to_id => @guest.id)
1173 @issue5 = Issue.generate!(:project => @project)
1190 @issue5 = Issue.generate!(:project => @project)
1174 end
1191 end
1175
1192
1176 should "search assigned to for users with the Role" do
1193 should "search assigned to for users with the Role" do
1177 @query = Query.new(:name => '_', :project => @project)
1194 @query = Query.new(:name => '_', :project => @project)
1178 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1195 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1179
1196
1180 assert_query_result [@issue1, @issue3], @query
1197 assert_query_result [@issue1, @issue3], @query
1181 end
1198 end
1182
1199
1183 should "search assigned to for users with the Role on the issue project" do
1200 should "search assigned to for users with the Role on the issue project" do
1184 other_project = Project.generate!
1201 other_project = Project.generate!
1185 User.add_to_project(@developer, other_project, @manager_role)
1202 User.add_to_project(@developer, other_project, @manager_role)
1186
1203
1187 @query = Query.new(:name => '_', :project => @project)
1204 @query = Query.new(:name => '_', :project => @project)
1188 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1205 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
1189
1206
1190 assert_query_result [@issue1, @issue3], @query
1207 assert_query_result [@issue1, @issue3], @query
1191 end
1208 end
1192
1209
1193 should "return an empty set with empty role" do
1210 should "return an empty set with empty role" do
1194 @empty_role = Role.generate!
1211 @empty_role = Role.generate!
1195 @query = Query.new(:name => '_', :project => @project)
1212 @query = Query.new(:name => '_', :project => @project)
1196 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1213 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
1197
1214
1198 assert_query_result [], @query
1215 assert_query_result [], @query
1199 end
1216 end
1200
1217
1201 should "search assigned to for users without the Role" do
1218 should "search assigned to for users without the Role" do
1202 @query = Query.new(:name => '_', :project => @project)
1219 @query = Query.new(:name => '_', :project => @project)
1203 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1220 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1204
1221
1205 assert_query_result [@issue2, @issue4, @issue5], @query
1222 assert_query_result [@issue2, @issue4, @issue5], @query
1206 end
1223 end
1207
1224
1208 should "search assigned to for users not assigned to any Role (none)" do
1225 should "search assigned to for users not assigned to any Role (none)" do
1209 @query = Query.new(:name => '_', :project => @project)
1226 @query = Query.new(:name => '_', :project => @project)
1210 @query.add_filter('assigned_to_role', '!*', [''])
1227 @query.add_filter('assigned_to_role', '!*', [''])
1211
1228
1212 assert_query_result [@issue4, @issue5], @query
1229 assert_query_result [@issue4, @issue5], @query
1213 end
1230 end
1214
1231
1215 should "search assigned to for users assigned to any Role (all)" do
1232 should "search assigned to for users assigned to any Role (all)" do
1216 @query = Query.new(:name => '_', :project => @project)
1233 @query = Query.new(:name => '_', :project => @project)
1217 @query.add_filter('assigned_to_role', '*', [''])
1234 @query.add_filter('assigned_to_role', '*', [''])
1218
1235
1219 assert_query_result [@issue1, @issue2, @issue3], @query
1236 assert_query_result [@issue1, @issue2, @issue3], @query
1220 end
1237 end
1221
1238
1222 should "return issues with ! empty role" do
1239 should "return issues with ! empty role" do
1223 @empty_role = Role.generate!
1240 @empty_role = Role.generate!
1224 @query = Query.new(:name => '_', :project => @project)
1241 @query = Query.new(:name => '_', :project => @project)
1225 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1242 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1226
1243
1227 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1244 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1228 end
1245 end
1229 end
1246 end
1230 end
1247 end
1231
1248
1232 end
1249 end
General Comments 0
You need to be logged in to leave comments. Login now