##// END OF EJS Templates
Issue list improvements for subtasking (#5196):...
Jean-Philippe Lang -
r3504:4524bc04e962
parent child
Show More
@@ -1,228 +1,236
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 module IssuesHelper
18 module IssuesHelper
19 include ApplicationHelper
19 include ApplicationHelper
20
20
21 def issue_list(issues, &block)
22 ancestors = []
23 issues.each do |issue|
24 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
25 ancestors.pop
26 end
27 yield issue, ancestors.size
28 ancestors << issue
29 end
30 end
31
21 def render_issue_tooltip(issue)
32 def render_issue_tooltip(issue)
22 @cached_label_start_date ||= l(:field_start_date)
33 @cached_label_start_date ||= l(:field_start_date)
23 @cached_label_due_date ||= l(:field_due_date)
34 @cached_label_due_date ||= l(:field_due_date)
24 @cached_label_assigned_to ||= l(:field_assigned_to)
35 @cached_label_assigned_to ||= l(:field_assigned_to)
25 @cached_label_priority ||= l(:field_priority)
36 @cached_label_priority ||= l(:field_priority)
26
37
27 link_to_issue(issue) + "<br /><br />" +
38 link_to_issue(issue) + "<br /><br />" +
28 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
39 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
29 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
40 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +
30 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
41 "<strong>#{@cached_label_assigned_to}</strong>: #{issue.assigned_to}<br />" +
31 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
42 "<strong>#{@cached_label_priority}</strong>: #{issue.priority.name}"
32 end
43 end
33
44
34 def render_issue_subject_with_tree(issue)
45 def render_issue_subject_with_tree(issue)
35 s = ''
46 s = ''
36 issue.ancestors.each do |ancestor|
47 issue.ancestors.each do |ancestor|
37 s << '<div>' + content_tag('p', link_to_issue(ancestor))
48 s << '<div>' + content_tag('p', link_to_issue(ancestor))
38 end
49 end
39 s << '<div>' + content_tag('h3', h(issue.subject))
50 s << '<div>' + content_tag('h3', h(issue.subject))
40 s << '</div>' * (issue.ancestors.size + 1)
51 s << '</div>' * (issue.ancestors.size + 1)
41 s
52 s
42 end
53 end
43
54
44 def render_descendants_tree(issue)
55 def render_descendants_tree(issue)
45 s = '<form><table class="list issues">'
56 s = '<form><table class="list issues">'
46 ancestors = []
57 issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
47 issue.descendants.sort_by(&:lft).each do |child|
48 level = child.level - issue.level - 1
49 s << content_tag('tr',
58 s << content_tag('tr',
50 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
59 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
51 content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject',
60 content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
52 :style => "padding-left: #{level * 20}px") +
53 content_tag('td', h(child.status)) +
61 content_tag('td', h(child.status)) +
54 content_tag('td', link_to_user(child.assigned_to)) +
62 content_tag('td', link_to_user(child.assigned_to)) +
55 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
63 content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
56 :class => "issue-#{child.id} hascontextmenu")
64 :class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
57 end
65 end
58 s << '</form></table>'
66 s << '</form></table>'
59 s
67 s
60 end
68 end
61
69
62 def render_custom_fields_rows(issue)
70 def render_custom_fields_rows(issue)
63 return if issue.custom_field_values.empty?
71 return if issue.custom_field_values.empty?
64 ordered_values = []
72 ordered_values = []
65 half = (issue.custom_field_values.size / 2.0).ceil
73 half = (issue.custom_field_values.size / 2.0).ceil
66 half.times do |i|
74 half.times do |i|
67 ordered_values << issue.custom_field_values[i]
75 ordered_values << issue.custom_field_values[i]
68 ordered_values << issue.custom_field_values[i + half]
76 ordered_values << issue.custom_field_values[i + half]
69 end
77 end
70 s = "<tr>\n"
78 s = "<tr>\n"
71 n = 0
79 n = 0
72 ordered_values.compact.each do |value|
80 ordered_values.compact.each do |value|
73 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
81 s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
74 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
82 s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
75 n += 1
83 n += 1
76 end
84 end
77 s << "</tr>\n"
85 s << "</tr>\n"
78 s
86 s
79 end
87 end
80
88
81 def sidebar_queries
89 def sidebar_queries
82 unless @sidebar_queries
90 unless @sidebar_queries
83 # User can see public queries and his own queries
91 # User can see public queries and his own queries
84 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
92 visible = ARCondition.new(["is_public = ? OR user_id = ?", true, (User.current.logged? ? User.current.id : 0)])
85 # Project specific queries and global queries
93 # Project specific queries and global queries
86 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
94 visible << (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
87 @sidebar_queries = Query.find(:all,
95 @sidebar_queries = Query.find(:all,
88 :select => 'id, name',
96 :select => 'id, name',
89 :order => "name ASC",
97 :order => "name ASC",
90 :conditions => visible.conditions)
98 :conditions => visible.conditions)
91 end
99 end
92 @sidebar_queries
100 @sidebar_queries
93 end
101 end
94
102
95 def show_detail(detail, no_html=false)
103 def show_detail(detail, no_html=false)
96 case detail.property
104 case detail.property
97 when 'attr'
105 when 'attr'
98 field = detail.prop_key.to_s.gsub(/\_id$/, "")
106 field = detail.prop_key.to_s.gsub(/\_id$/, "")
99 label = l(("field_" + field).to_sym)
107 label = l(("field_" + field).to_sym)
100 case
108 case
101 when ['due_date', 'start_date'].include?(detail.prop_key)
109 when ['due_date', 'start_date'].include?(detail.prop_key)
102 value = format_date(detail.value.to_date) if detail.value
110 value = format_date(detail.value.to_date) if detail.value
103 old_value = format_date(detail.old_value.to_date) if detail.old_value
111 old_value = format_date(detail.old_value.to_date) if detail.old_value
104
112
105 when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
113 when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
106 value = find_name_by_reflection(field, detail.value)
114 value = find_name_by_reflection(field, detail.value)
107 old_value = find_name_by_reflection(field, detail.old_value)
115 old_value = find_name_by_reflection(field, detail.old_value)
108
116
109 when detail.prop_key == 'estimated_hours'
117 when detail.prop_key == 'estimated_hours'
110 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
118 value = "%0.02f" % detail.value.to_f unless detail.value.blank?
111 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
119 old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
112
120
113 when detail.prop_key == 'parent_id'
121 when detail.prop_key == 'parent_id'
114 label = l(:field_parent_issue)
122 label = l(:field_parent_issue)
115 value = "##{detail.value}" unless detail.value.blank?
123 value = "##{detail.value}" unless detail.value.blank?
116 old_value = "##{detail.old_value}" unless detail.old_value.blank?
124 old_value = "##{detail.old_value}" unless detail.old_value.blank?
117 end
125 end
118 when 'cf'
126 when 'cf'
119 custom_field = CustomField.find_by_id(detail.prop_key)
127 custom_field = CustomField.find_by_id(detail.prop_key)
120 if custom_field
128 if custom_field
121 label = custom_field.name
129 label = custom_field.name
122 value = format_value(detail.value, custom_field.field_format) if detail.value
130 value = format_value(detail.value, custom_field.field_format) if detail.value
123 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
131 old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
124 end
132 end
125 when 'attachment'
133 when 'attachment'
126 label = l(:label_attachment)
134 label = l(:label_attachment)
127 end
135 end
128 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
136 call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
129
137
130 label ||= detail.prop_key
138 label ||= detail.prop_key
131 value ||= detail.value
139 value ||= detail.value
132 old_value ||= detail.old_value
140 old_value ||= detail.old_value
133
141
134 unless no_html
142 unless no_html
135 label = content_tag('strong', label)
143 label = content_tag('strong', label)
136 old_value = content_tag("i", h(old_value)) if detail.old_value
144 old_value = content_tag("i", h(old_value)) if detail.old_value
137 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
145 old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
138 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
146 if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
139 # Link to the attachment if it has not been removed
147 # Link to the attachment if it has not been removed
140 value = link_to_attachment(a)
148 value = link_to_attachment(a)
141 else
149 else
142 value = content_tag("i", h(value)) if value
150 value = content_tag("i", h(value)) if value
143 end
151 end
144 end
152 end
145
153
146 if !detail.value.blank?
154 if !detail.value.blank?
147 case detail.property
155 case detail.property
148 when 'attr', 'cf'
156 when 'attr', 'cf'
149 if !detail.old_value.blank?
157 if !detail.old_value.blank?
150 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
158 l(:text_journal_changed, :label => label, :old => old_value, :new => value)
151 else
159 else
152 l(:text_journal_set_to, :label => label, :value => value)
160 l(:text_journal_set_to, :label => label, :value => value)
153 end
161 end
154 when 'attachment'
162 when 'attachment'
155 l(:text_journal_added, :label => label, :value => value)
163 l(:text_journal_added, :label => label, :value => value)
156 end
164 end
157 else
165 else
158 l(:text_journal_deleted, :label => label, :old => old_value)
166 l(:text_journal_deleted, :label => label, :old => old_value)
159 end
167 end
160 end
168 end
161
169
162 # Find the name of an associated record stored in the field attribute
170 # Find the name of an associated record stored in the field attribute
163 def find_name_by_reflection(field, id)
171 def find_name_by_reflection(field, id)
164 association = Issue.reflect_on_association(field.to_sym)
172 association = Issue.reflect_on_association(field.to_sym)
165 if association
173 if association
166 record = association.class_name.constantize.find_by_id(id)
174 record = association.class_name.constantize.find_by_id(id)
167 return record.name if record
175 return record.name if record
168 end
176 end
169 end
177 end
170
178
171 def issues_to_csv(issues, project = nil)
179 def issues_to_csv(issues, project = nil)
172 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
180 ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
173 decimal_separator = l(:general_csv_decimal_separator)
181 decimal_separator = l(:general_csv_decimal_separator)
174 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
182 export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
175 # csv header fields
183 # csv header fields
176 headers = [ "#",
184 headers = [ "#",
177 l(:field_status),
185 l(:field_status),
178 l(:field_project),
186 l(:field_project),
179 l(:field_tracker),
187 l(:field_tracker),
180 l(:field_priority),
188 l(:field_priority),
181 l(:field_subject),
189 l(:field_subject),
182 l(:field_assigned_to),
190 l(:field_assigned_to),
183 l(:field_category),
191 l(:field_category),
184 l(:field_fixed_version),
192 l(:field_fixed_version),
185 l(:field_author),
193 l(:field_author),
186 l(:field_start_date),
194 l(:field_start_date),
187 l(:field_due_date),
195 l(:field_due_date),
188 l(:field_done_ratio),
196 l(:field_done_ratio),
189 l(:field_estimated_hours),
197 l(:field_estimated_hours),
190 l(:field_parent_issue),
198 l(:field_parent_issue),
191 l(:field_created_on),
199 l(:field_created_on),
192 l(:field_updated_on)
200 l(:field_updated_on)
193 ]
201 ]
194 # Export project custom fields if project is given
202 # Export project custom fields if project is given
195 # otherwise export custom fields marked as "For all projects"
203 # otherwise export custom fields marked as "For all projects"
196 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
204 custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
197 custom_fields.each {|f| headers << f.name}
205 custom_fields.each {|f| headers << f.name}
198 # Description in the last column
206 # Description in the last column
199 headers << l(:field_description)
207 headers << l(:field_description)
200 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
208 csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
201 # csv lines
209 # csv lines
202 issues.each do |issue|
210 issues.each do |issue|
203 fields = [issue.id,
211 fields = [issue.id,
204 issue.status.name,
212 issue.status.name,
205 issue.project.name,
213 issue.project.name,
206 issue.tracker.name,
214 issue.tracker.name,
207 issue.priority.name,
215 issue.priority.name,
208 issue.subject,
216 issue.subject,
209 issue.assigned_to,
217 issue.assigned_to,
210 issue.category,
218 issue.category,
211 issue.fixed_version,
219 issue.fixed_version,
212 issue.author.name,
220 issue.author.name,
213 format_date(issue.start_date),
221 format_date(issue.start_date),
214 format_date(issue.due_date),
222 format_date(issue.due_date),
215 issue.done_ratio,
223 issue.done_ratio,
216 issue.estimated_hours.to_s.gsub('.', decimal_separator),
224 issue.estimated_hours.to_s.gsub('.', decimal_separator),
217 issue.parent_id,
225 issue.parent_id,
218 format_time(issue.created_on),
226 format_time(issue.created_on),
219 format_time(issue.updated_on)
227 format_time(issue.updated_on)
220 ]
228 ]
221 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
229 custom_fields.each {|f| fields << show_value(issue.custom_value_for(f)) }
222 fields << issue.description
230 fields << issue.description
223 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
231 csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
224 end
232 end
225 end
233 end
226 export
234 export
227 end
235 end
228 end
236 end
@@ -1,64 +1,66
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 module QueriesHelper
18 module QueriesHelper
19
19
20 def operators_for_select(filter_type)
20 def operators_for_select(filter_type)
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
22 end
22 end
23
23
24 def column_header(column)
24 def column_header(column)
25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
26 :default_order => column.default_order) :
26 :default_order => column.default_order) :
27 content_tag('th', column.caption)
27 content_tag('th', column.caption)
28 end
28 end
29
29
30 def column_content(column, issue)
30 def column_content(column, issue)
31 value = column.value(issue)
31 value = column.value(issue)
32
32
33 case value.class.name
33 case value.class.name
34 when 'String'
34 when 'String'
35 if column.name == :subject
35 if column.name == :subject
36 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
36 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
37 else
37 else
38 h(value)
38 h(value)
39 end
39 end
40 when 'Time'
40 when 'Time'
41 format_time(value)
41 format_time(value)
42 when 'Date'
42 when 'Date'
43 format_date(value)
43 format_date(value)
44 when 'Fixnum', 'Float'
44 when 'Fixnum', 'Float'
45 if column.name == :done_ratio
45 if column.name == :done_ratio
46 progress_bar(value, :width => '80px')
46 progress_bar(value, :width => '80px')
47 else
47 else
48 value.to_s
48 value.to_s
49 end
49 end
50 when 'User'
50 when 'User'
51 link_to_user value
51 link_to_user value
52 when 'Project'
52 when 'Project'
53 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
53 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
54 when 'Version'
54 when 'Version'
55 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
55 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
56 when 'TrueClass'
56 when 'TrueClass'
57 l(:general_text_Yes)
57 l(:general_text_Yes)
58 when 'FalseClass'
58 when 'FalseClass'
59 l(:general_text_No)
59 l(:general_text_No)
60 when 'Issue'
61 link_to_issue(value, :subject => false)
60 else
62 else
61 h(value)
63 h(value)
62 end
64 end
63 end
65 end
64 end
66 end
@@ -1,226 +1,235
1 # Helpers to sort tables using clickable column headers.
1 # Helpers to sort tables using clickable column headers.
2 #
2 #
3 # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
3 # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
4 # Jean-Philippe Lang, 2009
4 # Jean-Philippe Lang, 2009
5 # License: This source code is released under the MIT license.
5 # License: This source code is released under the MIT license.
6 #
6 #
7 # - Consecutive clicks toggle the column's sort order.
7 # - Consecutive clicks toggle the column's sort order.
8 # - Sort state is maintained by a session hash entry.
8 # - Sort state is maintained by a session hash entry.
9 # - CSS classes identify sort column and state.
9 # - CSS classes identify sort column and state.
10 # - Typically used in conjunction with the Pagination module.
10 # - Typically used in conjunction with the Pagination module.
11 #
11 #
12 # Example code snippets:
12 # Example code snippets:
13 #
13 #
14 # Controller:
14 # Controller:
15 #
15 #
16 # helper :sort
16 # helper :sort
17 # include SortHelper
17 # include SortHelper
18 #
18 #
19 # def list
19 # def list
20 # sort_init 'last_name'
20 # sort_init 'last_name'
21 # sort_update %w(first_name last_name)
21 # sort_update %w(first_name last_name)
22 # @items = Contact.find_all nil, sort_clause
22 # @items = Contact.find_all nil, sort_clause
23 # end
23 # end
24 #
24 #
25 # Controller (using Pagination module):
25 # Controller (using Pagination module):
26 #
26 #
27 # helper :sort
27 # helper :sort
28 # include SortHelper
28 # include SortHelper
29 #
29 #
30 # def list
30 # def list
31 # sort_init 'last_name'
31 # sort_init 'last_name'
32 # sort_update %w(first_name last_name)
32 # sort_update %w(first_name last_name)
33 # @contact_pages, @items = paginate :contacts,
33 # @contact_pages, @items = paginate :contacts,
34 # :order_by => sort_clause,
34 # :order_by => sort_clause,
35 # :per_page => 10
35 # :per_page => 10
36 # end
36 # end
37 #
37 #
38 # View (table header in list.rhtml):
38 # View (table header in list.rhtml):
39 #
39 #
40 # <thead>
40 # <thead>
41 # <tr>
41 # <tr>
42 # <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
42 # <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
43 # <%= sort_header_tag('last_name', :caption => 'Name') %>
43 # <%= sort_header_tag('last_name', :caption => 'Name') %>
44 # <%= sort_header_tag('phone') %>
44 # <%= sort_header_tag('phone') %>
45 # <%= sort_header_tag('address', :width => 200) %>
45 # <%= sort_header_tag('address', :width => 200) %>
46 # </tr>
46 # </tr>
47 # </thead>
47 # </thead>
48 #
48 #
49 # - Introduces instance variables: @sort_default, @sort_criteria
49 # - Introduces instance variables: @sort_default, @sort_criteria
50 # - Introduces param :sort
50 # - Introduces param :sort
51 #
51 #
52
52
53 module SortHelper
53 module SortHelper
54 class SortCriteria
54 class SortCriteria
55
55
56 def initialize
56 def initialize
57 @criteria = []
57 @criteria = []
58 end
58 end
59
59
60 def available_criteria=(criteria)
60 def available_criteria=(criteria)
61 unless criteria.is_a?(Hash)
61 unless criteria.is_a?(Hash)
62 criteria = criteria.inject({}) {|h,k| h[k] = k; h}
62 criteria = criteria.inject({}) {|h,k| h[k] = k; h}
63 end
63 end
64 @available_criteria = criteria
64 @available_criteria = criteria
65 end
65 end
66
66
67 def from_param(param)
67 def from_param(param)
68 @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
68 @criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
69 normalize!
69 normalize!
70 end
70 end
71
71
72 def criteria=(arg)
72 def criteria=(arg)
73 @criteria = arg
73 @criteria = arg
74 normalize!
74 normalize!
75 end
75 end
76
76
77 def to_param
77 def to_param
78 @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
78 @criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
79 end
79 end
80
80
81 def to_sql
81 def to_sql
82 sql = @criteria.collect do |k,o|
82 sql = @criteria.collect do |k,o|
83 if s = @available_criteria[k]
83 if s = @available_criteria[k]
84 (o ? s.to_a : s.to_a.collect {|c| "#{c} DESC"}).join(', ')
84 (o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ')
85 end
85 end
86 end.compact.join(', ')
86 end.compact.join(', ')
87 sql.blank? ? nil : sql
87 sql.blank? ? nil : sql
88 end
88 end
89
89
90 def add!(key, asc)
90 def add!(key, asc)
91 @criteria.delete_if {|k,o| k == key}
91 @criteria.delete_if {|k,o| k == key}
92 @criteria = [[key, asc]] + @criteria
92 @criteria = [[key, asc]] + @criteria
93 normalize!
93 normalize!
94 end
94 end
95
95
96 def add(*args)
96 def add(*args)
97 r = self.class.new.from_param(to_param)
97 r = self.class.new.from_param(to_param)
98 r.add!(*args)
98 r.add!(*args)
99 r
99 r
100 end
100 end
101
101
102 def first_key
102 def first_key
103 @criteria.first && @criteria.first.first
103 @criteria.first && @criteria.first.first
104 end
104 end
105
105
106 def first_asc?
106 def first_asc?
107 @criteria.first && @criteria.first.last
107 @criteria.first && @criteria.first.last
108 end
108 end
109
109
110 def empty?
110 def empty?
111 @criteria.empty?
111 @criteria.empty?
112 end
112 end
113
113
114 private
114 private
115
115
116 def normalize!
116 def normalize!
117 @criteria ||= []
117 @criteria ||= []
118 @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
118 @criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
119 @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
119 @criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
120 @criteria.slice!(3)
120 @criteria.slice!(3)
121 self
121 self
122 end
122 end
123
124 # Appends DESC to the sort criterion unless it has a fixed order
125 def append_desc(criterion)
126 if criterion =~ / (asc|desc)$/i
127 criterion
128 else
129 "#{criterion} DESC"
130 end
131 end
123 end
132 end
124
133
125 def sort_name
134 def sort_name
126 controller_name + '_' + action_name + '_sort'
135 controller_name + '_' + action_name + '_sort'
127 end
136 end
128
137
129 # Initializes the default sort.
138 # Initializes the default sort.
130 # Examples:
139 # Examples:
131 #
140 #
132 # sort_init 'name'
141 # sort_init 'name'
133 # sort_init 'id', 'desc'
142 # sort_init 'id', 'desc'
134 # sort_init ['name', ['id', 'desc']]
143 # sort_init ['name', ['id', 'desc']]
135 # sort_init [['name', 'desc'], ['id', 'desc']]
144 # sort_init [['name', 'desc'], ['id', 'desc']]
136 #
145 #
137 def sort_init(*args)
146 def sort_init(*args)
138 case args.size
147 case args.size
139 when 1
148 when 1
140 @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
149 @sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
141 when 2
150 when 2
142 @sort_default = [[args.first, args.last]]
151 @sort_default = [[args.first, args.last]]
143 else
152 else
144 raise ArgumentError
153 raise ArgumentError
145 end
154 end
146 end
155 end
147
156
148 # Updates the sort state. Call this in the controller prior to calling
157 # Updates the sort state. Call this in the controller prior to calling
149 # sort_clause.
158 # sort_clause.
150 # - criteria can be either an array or a hash of allowed keys
159 # - criteria can be either an array or a hash of allowed keys
151 #
160 #
152 def sort_update(criteria)
161 def sort_update(criteria)
153 @sort_criteria = SortCriteria.new
162 @sort_criteria = SortCriteria.new
154 @sort_criteria.available_criteria = criteria
163 @sort_criteria.available_criteria = criteria
155 @sort_criteria.from_param(params[:sort] || session[sort_name])
164 @sort_criteria.from_param(params[:sort] || session[sort_name])
156 @sort_criteria.criteria = @sort_default if @sort_criteria.empty?
165 @sort_criteria.criteria = @sort_default if @sort_criteria.empty?
157 session[sort_name] = @sort_criteria.to_param
166 session[sort_name] = @sort_criteria.to_param
158 end
167 end
159
168
160 # Clears the sort criteria session data
169 # Clears the sort criteria session data
161 #
170 #
162 def sort_clear
171 def sort_clear
163 session[sort_name] = nil
172 session[sort_name] = nil
164 end
173 end
165
174
166 # Returns an SQL sort clause corresponding to the current sort state.
175 # Returns an SQL sort clause corresponding to the current sort state.
167 # Use this to sort the controller's table items collection.
176 # Use this to sort the controller's table items collection.
168 #
177 #
169 def sort_clause()
178 def sort_clause()
170 @sort_criteria.to_sql
179 @sort_criteria.to_sql
171 end
180 end
172
181
173 # Returns a link which sorts by the named column.
182 # Returns a link which sorts by the named column.
174 #
183 #
175 # - column is the name of an attribute in the sorted record collection.
184 # - column is the name of an attribute in the sorted record collection.
176 # - the optional caption explicitly specifies the displayed link text.
185 # - the optional caption explicitly specifies the displayed link text.
177 # - 2 CSS classes reflect the state of the link: sort and asc or desc
186 # - 2 CSS classes reflect the state of the link: sort and asc or desc
178 #
187 #
179 def sort_link(column, caption, default_order)
188 def sort_link(column, caption, default_order)
180 css, order = nil, default_order
189 css, order = nil, default_order
181
190
182 if column.to_s == @sort_criteria.first_key
191 if column.to_s == @sort_criteria.first_key
183 if @sort_criteria.first_asc?
192 if @sort_criteria.first_asc?
184 css = 'sort asc'
193 css = 'sort asc'
185 order = 'desc'
194 order = 'desc'
186 else
195 else
187 css = 'sort desc'
196 css = 'sort desc'
188 order = 'asc'
197 order = 'asc'
189 end
198 end
190 end
199 end
191 caption = column.to_s.humanize unless caption
200 caption = column.to_s.humanize unless caption
192
201
193 sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
202 sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
194 # don't reuse params if filters are present
203 # don't reuse params if filters are present
195 url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
204 url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
196
205
197 # Add project_id to url_options
206 # Add project_id to url_options
198 url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
207 url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
199
208
200 link_to_remote(caption,
209 link_to_remote(caption,
201 {:update => "content", :url => url_options, :method => :get},
210 {:update => "content", :url => url_options, :method => :get},
202 {:href => url_for(url_options),
211 {:href => url_for(url_options),
203 :class => css})
212 :class => css})
204 end
213 end
205
214
206 # Returns a table header <th> tag with a sort link for the named column
215 # Returns a table header <th> tag with a sort link for the named column
207 # attribute.
216 # attribute.
208 #
217 #
209 # Options:
218 # Options:
210 # :caption The displayed link name (defaults to titleized column name).
219 # :caption The displayed link name (defaults to titleized column name).
211 # :title The tag's 'title' attribute (defaults to 'Sort by :caption').
220 # :title The tag's 'title' attribute (defaults to 'Sort by :caption').
212 #
221 #
213 # Other options hash entries generate additional table header tag attributes.
222 # Other options hash entries generate additional table header tag attributes.
214 #
223 #
215 # Example:
224 # Example:
216 #
225 #
217 # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
226 # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
218 #
227 #
219 def sort_header_tag(column, options = {})
228 def sort_header_tag(column, options = {})
220 caption = options.delete(:caption) || column.to_s.humanize
229 caption = options.delete(:caption) || column.to_s.humanize
221 default_order = options.delete(:default_order) || 'asc'
230 default_order = options.delete(:default_order) || 'asc'
222 options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
231 options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
223 content_tag('th', sort_link(column, caption, default_order), options)
232 content_tag('th', sort_link(column, caption, default_order), options)
224 end
233 end
225 end
234 end
226
235
@@ -1,573 +1,575
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 @caption_key = options[:caption] || "field_#{name}"
30 end
31 end
31
32
32 def caption
33 def caption
33 l("field_#{name}")
34 l(@caption_key)
34 end
35 end
35
36
36 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
37 def sortable?
38 def sortable?
38 !sortable.nil?
39 !sortable.nil?
39 end
40 end
40
41
41 def value(issue)
42 def value(issue)
42 issue.send name
43 issue.send name
43 end
44 end
44 end
45 end
45
46
46 class QueryCustomFieldColumn < QueryColumn
47 class QueryCustomFieldColumn < QueryColumn
47
48
48 def initialize(custom_field)
49 def initialize(custom_field)
49 self.name = "cf_#{custom_field.id}".to_sym
50 self.name = "cf_#{custom_field.id}".to_sym
50 self.sortable = custom_field.order_statement || false
51 self.sortable = custom_field.order_statement || false
51 if %w(list date bool int).include?(custom_field.field_format)
52 if %w(list date bool int).include?(custom_field.field_format)
52 self.groupable = custom_field.order_statement
53 self.groupable = custom_field.order_statement
53 end
54 end
54 self.groupable ||= false
55 self.groupable ||= false
55 @cf = custom_field
56 @cf = custom_field
56 end
57 end
57
58
58 def caption
59 def caption
59 @cf.name
60 @cf.name
60 end
61 end
61
62
62 def custom_field
63 def custom_field
63 @cf
64 @cf
64 end
65 end
65
66
66 def value(issue)
67 def value(issue)
67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv && @cf.cast_value(cv.value)
69 cv && @cf.cast_value(cv.value)
69 end
70 end
70 end
71 end
71
72
72 class Query < ActiveRecord::Base
73 class Query < ActiveRecord::Base
73 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 class StatementInvalid < ::ActiveRecord::StatementInvalid
74 end
75 end
75
76
76 belongs_to :project
77 belongs_to :project
77 belongs_to :user
78 belongs_to :user
78 serialize :filters
79 serialize :filters
79 serialize :column_names
80 serialize :column_names
80 serialize :sort_criteria, Array
81 serialize :sort_criteria, Array
81
82
82 attr_protected :project_id, :user_id
83 attr_protected :project_id, :user_id
83
84
84 validates_presence_of :name, :on => :save
85 validates_presence_of :name, :on => :save
85 validates_length_of :name, :maximum => 255
86 validates_length_of :name, :maximum => 255
86
87
87 @@operators = { "=" => :label_equals,
88 @@operators = { "=" => :label_equals,
88 "!" => :label_not_equals,
89 "!" => :label_not_equals,
89 "o" => :label_open_issues,
90 "o" => :label_open_issues,
90 "c" => :label_closed_issues,
91 "c" => :label_closed_issues,
91 "!*" => :label_none,
92 "!*" => :label_none,
92 "*" => :label_all,
93 "*" => :label_all,
93 ">=" => :label_greater_or_equal,
94 ">=" => :label_greater_or_equal,
94 "<=" => :label_less_or_equal,
95 "<=" => :label_less_or_equal,
95 "<t+" => :label_in_less_than,
96 "<t+" => :label_in_less_than,
96 ">t+" => :label_in_more_than,
97 ">t+" => :label_in_more_than,
97 "t+" => :label_in,
98 "t+" => :label_in,
98 "t" => :label_today,
99 "t" => :label_today,
99 "w" => :label_this_week,
100 "w" => :label_this_week,
100 ">t-" => :label_less_than_ago,
101 ">t-" => :label_less_than_ago,
101 "<t-" => :label_more_than_ago,
102 "<t-" => :label_more_than_ago,
102 "t-" => :label_ago,
103 "t-" => :label_ago,
103 "~" => :label_contains,
104 "~" => :label_contains,
104 "!~" => :label_not_contains }
105 "!~" => :label_not_contains }
105
106
106 cattr_reader :operators
107 cattr_reader :operators
107
108
108 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 @@operators_by_filter_type = { :list => [ "=", "!" ],
109 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_status => [ "o", "=", "!", "c", "*" ],
110 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_optional => [ "=", "!", "!*", "*" ],
111 :list_subprojects => [ "*", "!*", "=" ],
112 :list_subprojects => [ "*", "!*", "=" ],
112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
114 :string => [ "=", "~", "!", "!~" ],
115 :string => [ "=", "~", "!", "!~" ],
115 :text => [ "~", "!~" ],
116 :text => [ "~", "!~" ],
116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117 :integer => [ "=", ">=", "<=", "!*", "*" ] }
117
118
118 cattr_reader :operators_by_filter_type
119 cattr_reader :operators_by_filter_type
119
120
120 @@available_columns = [
121 @@available_columns = [
121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
123 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
125 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
126 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
127 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
126 QueryColumn.new(:author),
128 QueryColumn.new(:author),
127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
129 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
130 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
131 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
132 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
133 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
134 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
135 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
136 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
137 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
136 ]
138 ]
137 cattr_reader :available_columns
139 cattr_reader :available_columns
138
140
139 def initialize(attributes = nil)
141 def initialize(attributes = nil)
140 super attributes
142 super attributes
141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
143 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
142 end
144 end
143
145
144 def after_initialize
146 def after_initialize
145 # Store the fact that project is nil (used in #editable_by?)
147 # Store the fact that project is nil (used in #editable_by?)
146 @is_for_all = project.nil?
148 @is_for_all = project.nil?
147 end
149 end
148
150
149 def validate
151 def validate
150 filters.each_key do |field|
152 filters.each_key do |field|
151 errors.add label_for(field), :blank unless
153 errors.add label_for(field), :blank unless
152 # filter requires one or more values
154 # filter requires one or more values
153 (values_for(field) and !values_for(field).first.blank?) or
155 (values_for(field) and !values_for(field).first.blank?) or
154 # filter doesn't require any value
156 # filter doesn't require any value
155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
157 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
156 end if filters
158 end if filters
157 end
159 end
158
160
159 def editable_by?(user)
161 def editable_by?(user)
160 return false unless user
162 return false unless user
161 # Admin can edit them all and regular users can edit their private queries
163 # Admin can edit them all and regular users can edit their private queries
162 return true if user.admin? || (!is_public && self.user_id == user.id)
164 return true if user.admin? || (!is_public && self.user_id == user.id)
163 # Members can not edit public queries that are for all project (only admin is allowed to)
165 # Members can not edit public queries that are for all project (only admin is allowed to)
164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
166 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
165 end
167 end
166
168
167 def available_filters
169 def available_filters
168 return @available_filters if @available_filters
170 return @available_filters if @available_filters
169
171
170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
172 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
171
173
172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
174 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
175 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
176 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
175 "subject" => { :type => :text, :order => 8 },
177 "subject" => { :type => :text, :order => 8 },
176 "created_on" => { :type => :date_past, :order => 9 },
178 "created_on" => { :type => :date_past, :order => 9 },
177 "updated_on" => { :type => :date_past, :order => 10 },
179 "updated_on" => { :type => :date_past, :order => 10 },
178 "start_date" => { :type => :date, :order => 11 },
180 "start_date" => { :type => :date, :order => 11 },
179 "due_date" => { :type => :date, :order => 12 },
181 "due_date" => { :type => :date, :order => 12 },
180 "estimated_hours" => { :type => :integer, :order => 13 },
182 "estimated_hours" => { :type => :integer, :order => 13 },
181 "done_ratio" => { :type => :integer, :order => 14 }}
183 "done_ratio" => { :type => :integer, :order => 14 }}
182
184
183 user_values = []
185 user_values = []
184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
186 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
185 if project
187 if project
186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
188 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
187 else
189 else
188 project_ids = User.current.projects.collect(&:id)
190 project_ids = User.current.projects.collect(&:id)
189 if project_ids.any?
191 if project_ids.any?
190 # members of the user's projects
192 # members of the user's projects
191 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
193 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
192 end
194 end
193 end
195 end
194 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
196 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
195 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
197 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
196
198
197 if User.current.logged?
199 if User.current.logged?
198 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
200 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
199 end
201 end
200
202
201 if project
203 if project
202 # project specific filters
204 # project specific filters
203 unless @project.issue_categories.empty?
205 unless @project.issue_categories.empty?
204 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
206 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
205 end
207 end
206 unless @project.shared_versions.empty?
208 unless @project.shared_versions.empty?
207 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
209 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
208 end
210 end
209 unless @project.descendants.active.empty?
211 unless @project.descendants.active.empty?
210 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
212 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
211 end
213 end
212 add_custom_fields_filters(@project.all_issue_custom_fields)
214 add_custom_fields_filters(@project.all_issue_custom_fields)
213 else
215 else
214 # global filters for cross project issue list
216 # global filters for cross project issue list
215 system_shared_versions = Version.visible.find_all_by_sharing('system')
217 system_shared_versions = Version.visible.find_all_by_sharing('system')
216 unless system_shared_versions.empty?
218 unless system_shared_versions.empty?
217 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
219 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
218 end
220 end
219 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
221 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
220 end
222 end
221 @available_filters
223 @available_filters
222 end
224 end
223
225
224 def add_filter(field, operator, values)
226 def add_filter(field, operator, values)
225 # values must be an array
227 # values must be an array
226 return unless values and values.is_a? Array # and !values.first.empty?
228 return unless values and values.is_a? Array # and !values.first.empty?
227 # check if field is defined as an available filter
229 # check if field is defined as an available filter
228 if available_filters.has_key? field
230 if available_filters.has_key? field
229 filter_options = available_filters[field]
231 filter_options = available_filters[field]
230 # check if operator is allowed for that filter
232 # check if operator is allowed for that filter
231 #if @@operators_by_filter_type[filter_options[:type]].include? operator
233 #if @@operators_by_filter_type[filter_options[:type]].include? operator
232 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
234 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
233 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
235 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
234 #end
236 #end
235 filters[field] = {:operator => operator, :values => values }
237 filters[field] = {:operator => operator, :values => values }
236 end
238 end
237 end
239 end
238
240
239 def add_short_filter(field, expression)
241 def add_short_filter(field, expression)
240 return unless expression
242 return unless expression
241 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
243 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
242 add_filter field, (parms[0] || "="), [parms[1] || ""]
244 add_filter field, (parms[0] || "="), [parms[1] || ""]
243 end
245 end
244
246
245 def has_filter?(field)
247 def has_filter?(field)
246 filters and filters[field]
248 filters and filters[field]
247 end
249 end
248
250
249 def operator_for(field)
251 def operator_for(field)
250 has_filter?(field) ? filters[field][:operator] : nil
252 has_filter?(field) ? filters[field][:operator] : nil
251 end
253 end
252
254
253 def values_for(field)
255 def values_for(field)
254 has_filter?(field) ? filters[field][:values] : nil
256 has_filter?(field) ? filters[field][:values] : nil
255 end
257 end
256
258
257 def label_for(field)
259 def label_for(field)
258 label = available_filters[field][:name] if available_filters.has_key?(field)
260 label = available_filters[field][:name] if available_filters.has_key?(field)
259 label ||= field.gsub(/\_id$/, "")
261 label ||= field.gsub(/\_id$/, "")
260 end
262 end
261
263
262 def available_columns
264 def available_columns
263 return @available_columns if @available_columns
265 return @available_columns if @available_columns
264 @available_columns = Query.available_columns
266 @available_columns = Query.available_columns
265 @available_columns += (project ?
267 @available_columns += (project ?
266 project.all_issue_custom_fields :
268 project.all_issue_custom_fields :
267 IssueCustomField.find(:all)
269 IssueCustomField.find(:all)
268 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
270 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
269 end
271 end
270
272
271 # Returns an array of columns that can be used to group the results
273 # Returns an array of columns that can be used to group the results
272 def groupable_columns
274 def groupable_columns
273 available_columns.select {|c| c.groupable}
275 available_columns.select {|c| c.groupable}
274 end
276 end
275
277
276 # Returns a Hash of columns and the key for sorting
278 # Returns a Hash of columns and the key for sorting
277 def sortable_columns
279 def sortable_columns
278 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
280 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
279 h[column.name.to_s] = column.sortable
281 h[column.name.to_s] = column.sortable
280 h
282 h
281 })
283 })
282 end
284 end
283
285
284 def columns
286 def columns
285 if has_default_columns?
287 if has_default_columns?
286 available_columns.select do |c|
288 available_columns.select do |c|
287 # Adds the project column by default for cross-project lists
289 # Adds the project column by default for cross-project lists
288 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
290 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
289 end
291 end
290 else
292 else
291 # preserve the column_names order
293 # preserve the column_names order
292 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
294 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
293 end
295 end
294 end
296 end
295
297
296 def column_names=(names)
298 def column_names=(names)
297 if names
299 if names
298 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
300 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
299 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
301 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
300 # Set column_names to nil if default columns
302 # Set column_names to nil if default columns
301 if names.map(&:to_s) == Setting.issue_list_default_columns
303 if names.map(&:to_s) == Setting.issue_list_default_columns
302 names = nil
304 names = nil
303 end
305 end
304 end
306 end
305 write_attribute(:column_names, names)
307 write_attribute(:column_names, names)
306 end
308 end
307
309
308 def has_column?(column)
310 def has_column?(column)
309 column_names && column_names.include?(column.name)
311 column_names && column_names.include?(column.name)
310 end
312 end
311
313
312 def has_default_columns?
314 def has_default_columns?
313 column_names.nil? || column_names.empty?
315 column_names.nil? || column_names.empty?
314 end
316 end
315
317
316 def sort_criteria=(arg)
318 def sort_criteria=(arg)
317 c = []
319 c = []
318 if arg.is_a?(Hash)
320 if arg.is_a?(Hash)
319 arg = arg.keys.sort.collect {|k| arg[k]}
321 arg = arg.keys.sort.collect {|k| arg[k]}
320 end
322 end
321 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
323 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
322 write_attribute(:sort_criteria, c)
324 write_attribute(:sort_criteria, c)
323 end
325 end
324
326
325 def sort_criteria
327 def sort_criteria
326 read_attribute(:sort_criteria) || []
328 read_attribute(:sort_criteria) || []
327 end
329 end
328
330
329 def sort_criteria_key(arg)
331 def sort_criteria_key(arg)
330 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
332 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
331 end
333 end
332
334
333 def sort_criteria_order(arg)
335 def sort_criteria_order(arg)
334 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
336 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
335 end
337 end
336
338
337 # Returns the SQL sort order that should be prepended for grouping
339 # Returns the SQL sort order that should be prepended for grouping
338 def group_by_sort_order
340 def group_by_sort_order
339 if grouped? && (column = group_by_column)
341 if grouped? && (column = group_by_column)
340 column.sortable.is_a?(Array) ?
342 column.sortable.is_a?(Array) ?
341 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
343 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
342 "#{column.sortable} #{column.default_order}"
344 "#{column.sortable} #{column.default_order}"
343 end
345 end
344 end
346 end
345
347
346 # Returns true if the query is a grouped query
348 # Returns true if the query is a grouped query
347 def grouped?
349 def grouped?
348 !group_by.blank?
350 !group_by.blank?
349 end
351 end
350
352
351 def group_by_column
353 def group_by_column
352 groupable_columns.detect {|c| c.name.to_s == group_by}
354 groupable_columns.detect {|c| c.name.to_s == group_by}
353 end
355 end
354
356
355 def group_by_statement
357 def group_by_statement
356 group_by_column.groupable
358 group_by_column.groupable
357 end
359 end
358
360
359 def project_statement
361 def project_statement
360 project_clauses = []
362 project_clauses = []
361 if project && !@project.descendants.active.empty?
363 if project && !@project.descendants.active.empty?
362 ids = [project.id]
364 ids = [project.id]
363 if has_filter?("subproject_id")
365 if has_filter?("subproject_id")
364 case operator_for("subproject_id")
366 case operator_for("subproject_id")
365 when '='
367 when '='
366 # include the selected subprojects
368 # include the selected subprojects
367 ids += values_for("subproject_id").each(&:to_i)
369 ids += values_for("subproject_id").each(&:to_i)
368 when '!*'
370 when '!*'
369 # main project only
371 # main project only
370 else
372 else
371 # all subprojects
373 # all subprojects
372 ids += project.descendants.collect(&:id)
374 ids += project.descendants.collect(&:id)
373 end
375 end
374 elsif Setting.display_subprojects_issues?
376 elsif Setting.display_subprojects_issues?
375 ids += project.descendants.collect(&:id)
377 ids += project.descendants.collect(&:id)
376 end
378 end
377 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
379 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
378 elsif project
380 elsif project
379 project_clauses << "#{Project.table_name}.id = %d" % project.id
381 project_clauses << "#{Project.table_name}.id = %d" % project.id
380 end
382 end
381 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
383 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
382 project_clauses.join(' AND ')
384 project_clauses.join(' AND ')
383 end
385 end
384
386
385 def statement
387 def statement
386 # filters clauses
388 # filters clauses
387 filters_clauses = []
389 filters_clauses = []
388 filters.each_key do |field|
390 filters.each_key do |field|
389 next if field == "subproject_id"
391 next if field == "subproject_id"
390 v = values_for(field).clone
392 v = values_for(field).clone
391 next unless v and !v.empty?
393 next unless v and !v.empty?
392 operator = operator_for(field)
394 operator = operator_for(field)
393
395
394 # "me" value subsitution
396 # "me" value subsitution
395 if %w(assigned_to_id author_id watcher_id).include?(field)
397 if %w(assigned_to_id author_id watcher_id).include?(field)
396 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
398 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
397 end
399 end
398
400
399 sql = ''
401 sql = ''
400 if field =~ /^cf_(\d+)$/
402 if field =~ /^cf_(\d+)$/
401 # custom field
403 # custom field
402 db_table = CustomValue.table_name
404 db_table = CustomValue.table_name
403 db_field = 'value'
405 db_field = 'value'
404 is_custom_filter = true
406 is_custom_filter = true
405 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
407 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
406 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
408 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
407 elsif field == 'watcher_id'
409 elsif field == 'watcher_id'
408 db_table = Watcher.table_name
410 db_table = Watcher.table_name
409 db_field = 'user_id'
411 db_field = 'user_id'
410 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
412 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
411 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
413 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
412 else
414 else
413 # regular field
415 # regular field
414 db_table = Issue.table_name
416 db_table = Issue.table_name
415 db_field = field
417 db_field = field
416 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
418 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
417 end
419 end
418 filters_clauses << sql
420 filters_clauses << sql
419
421
420 end if filters and valid?
422 end if filters and valid?
421
423
422 (filters_clauses << project_statement).join(' AND ')
424 (filters_clauses << project_statement).join(' AND ')
423 end
425 end
424
426
425 # Returns the issue count
427 # Returns the issue count
426 def issue_count
428 def issue_count
427 Issue.count(:include => [:status, :project], :conditions => statement)
429 Issue.count(:include => [:status, :project], :conditions => statement)
428 rescue ::ActiveRecord::StatementInvalid => e
430 rescue ::ActiveRecord::StatementInvalid => e
429 raise StatementInvalid.new(e.message)
431 raise StatementInvalid.new(e.message)
430 end
432 end
431
433
432 # Returns the issue count by group or nil if query is not grouped
434 # Returns the issue count by group or nil if query is not grouped
433 def issue_count_by_group
435 def issue_count_by_group
434 r = nil
436 r = nil
435 if grouped?
437 if grouped?
436 begin
438 begin
437 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
439 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
438 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
440 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
439 rescue ActiveRecord::RecordNotFound
441 rescue ActiveRecord::RecordNotFound
440 r = {nil => issue_count}
442 r = {nil => issue_count}
441 end
443 end
442 c = group_by_column
444 c = group_by_column
443 if c.is_a?(QueryCustomFieldColumn)
445 if c.is_a?(QueryCustomFieldColumn)
444 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
446 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
445 end
447 end
446 end
448 end
447 r
449 r
448 rescue ::ActiveRecord::StatementInvalid => e
450 rescue ::ActiveRecord::StatementInvalid => e
449 raise StatementInvalid.new(e.message)
451 raise StatementInvalid.new(e.message)
450 end
452 end
451
453
452 # Returns the issues
454 # Returns the issues
453 # Valid options are :order, :offset, :limit, :include, :conditions
455 # Valid options are :order, :offset, :limit, :include, :conditions
454 def issues(options={})
456 def issues(options={})
455 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
457 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
456 order_option = nil if order_option.blank?
458 order_option = nil if order_option.blank?
457
459
458 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
460 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
459 :conditions => Query.merge_conditions(statement, options[:conditions]),
461 :conditions => Query.merge_conditions(statement, options[:conditions]),
460 :order => order_option,
462 :order => order_option,
461 :limit => options[:limit],
463 :limit => options[:limit],
462 :offset => options[:offset]
464 :offset => options[:offset]
463 rescue ::ActiveRecord::StatementInvalid => e
465 rescue ::ActiveRecord::StatementInvalid => e
464 raise StatementInvalid.new(e.message)
466 raise StatementInvalid.new(e.message)
465 end
467 end
466
468
467 # Returns the journals
469 # Returns the journals
468 # Valid options are :order, :offset, :limit
470 # Valid options are :order, :offset, :limit
469 def journals(options={})
471 def journals(options={})
470 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
472 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
471 :conditions => statement,
473 :conditions => statement,
472 :order => options[:order],
474 :order => options[:order],
473 :limit => options[:limit],
475 :limit => options[:limit],
474 :offset => options[:offset]
476 :offset => options[:offset]
475 rescue ::ActiveRecord::StatementInvalid => e
477 rescue ::ActiveRecord::StatementInvalid => e
476 raise StatementInvalid.new(e.message)
478 raise StatementInvalid.new(e.message)
477 end
479 end
478
480
479 # Returns the versions
481 # Returns the versions
480 # Valid options are :conditions
482 # Valid options are :conditions
481 def versions(options={})
483 def versions(options={})
482 Version.find :all, :include => :project,
484 Version.find :all, :include => :project,
483 :conditions => Query.merge_conditions(project_statement, options[:conditions])
485 :conditions => Query.merge_conditions(project_statement, options[:conditions])
484 rescue ::ActiveRecord::StatementInvalid => e
486 rescue ::ActiveRecord::StatementInvalid => e
485 raise StatementInvalid.new(e.message)
487 raise StatementInvalid.new(e.message)
486 end
488 end
487
489
488 private
490 private
489
491
490 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
492 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
491 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
493 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
492 sql = ''
494 sql = ''
493 case operator
495 case operator
494 when "="
496 when "="
495 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
497 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
496 when "!"
498 when "!"
497 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
499 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
498 when "!*"
500 when "!*"
499 sql = "#{db_table}.#{db_field} IS NULL"
501 sql = "#{db_table}.#{db_field} IS NULL"
500 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
502 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
501 when "*"
503 when "*"
502 sql = "#{db_table}.#{db_field} IS NOT NULL"
504 sql = "#{db_table}.#{db_field} IS NOT NULL"
503 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
505 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
504 when ">="
506 when ">="
505 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
507 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
506 when "<="
508 when "<="
507 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
509 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
508 when "o"
510 when "o"
509 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
511 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
510 when "c"
512 when "c"
511 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
513 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
512 when ">t-"
514 when ">t-"
513 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
515 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
514 when "<t-"
516 when "<t-"
515 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
517 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
516 when "t-"
518 when "t-"
517 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
519 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
518 when ">t+"
520 when ">t+"
519 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
521 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
520 when "<t+"
522 when "<t+"
521 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
523 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
522 when "t+"
524 when "t+"
523 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
525 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
524 when "t"
526 when "t"
525 sql = date_range_clause(db_table, db_field, 0, 0)
527 sql = date_range_clause(db_table, db_field, 0, 0)
526 when "w"
528 when "w"
527 from = l(:general_first_day_of_week) == '7' ?
529 from = l(:general_first_day_of_week) == '7' ?
528 # week starts on sunday
530 # week starts on sunday
529 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
531 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
530 # week starts on monday (Rails default)
532 # week starts on monday (Rails default)
531 Time.now.at_beginning_of_week
533 Time.now.at_beginning_of_week
532 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
534 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
533 when "~"
535 when "~"
534 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
536 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
535 when "!~"
537 when "!~"
536 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
538 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
537 end
539 end
538
540
539 return sql
541 return sql
540 end
542 end
541
543
542 def add_custom_fields_filters(custom_fields)
544 def add_custom_fields_filters(custom_fields)
543 @available_filters ||= {}
545 @available_filters ||= {}
544
546
545 custom_fields.select(&:is_filter?).each do |field|
547 custom_fields.select(&:is_filter?).each do |field|
546 case field.field_format
548 case field.field_format
547 when "text"
549 when "text"
548 options = { :type => :text, :order => 20 }
550 options = { :type => :text, :order => 20 }
549 when "list"
551 when "list"
550 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
552 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
551 when "date"
553 when "date"
552 options = { :type => :date, :order => 20 }
554 options = { :type => :date, :order => 20 }
553 when "bool"
555 when "bool"
554 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
556 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
555 else
557 else
556 options = { :type => :string, :order => 20 }
558 options = { :type => :string, :order => 20 }
557 end
559 end
558 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
560 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
559 end
561 end
560 end
562 end
561
563
562 # Returns a SQL clause for a date or datetime field.
564 # Returns a SQL clause for a date or datetime field.
563 def date_range_clause(table, field, from, to)
565 def date_range_clause(table, field, from, to)
564 s = []
566 s = []
565 if from
567 if from
566 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
568 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
567 end
569 end
568 if to
570 if to
569 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
571 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
570 end
572 end
571 s.join(' AND ')
573 s.join(' AND ')
572 end
574 end
573 end
575 end
@@ -1,36 +1,36
1 <% form_tag({}) do -%>
1 <% form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list issues">
4 <table class="list issues">
5 <thead><tr>
5 <thead><tr>
6 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
6 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
7 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
7 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
8 </th>
8 </th>
9 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
9 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
10 <% query.columns.each do |column| %>
10 <% query.columns.each do |column| %>
11 <%= column_header(column) %>
11 <%= column_header(column) %>
12 <% end %>
12 <% end %>
13 </tr></thead>
13 </tr></thead>
14 <% previous_group = false %>
14 <% previous_group = false %>
15 <tbody>
15 <tbody>
16 <% issues.each do |issue| -%>
16 <% issue_list(issues) do |issue, level| -%>
17 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
17 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
18 <% reset_cycle %>
18 <% reset_cycle %>
19 <tr class="group open">
19 <tr class="group open">
20 <td colspan="<%= query.columns.size + 2 %>">
20 <td colspan="<%= query.columns.size + 2 %>">
21 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
21 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
22 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
22 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
23 </td>
23 </td>
24 </tr>
24 </tr>
25 <% previous_group = group %>
25 <% previous_group = group %>
26 <% end %>
26 <% end %>
27 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
27 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
28 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
28 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
29 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
29 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
30 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
30 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
31 </tr>
31 </tr>
32 <% end -%>
32 <% end -%>
33 </tbody>
33 </tbody>
34 </table>
34 </table>
35 </div>
35 </div>
36 <% end -%>
36 <% end -%>
@@ -1,884 +1,895
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
1 body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; }
2
2
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
3 h1, h2, h3, h4 { font-family: "Trebuchet MS", Verdana, sans-serif;}
4 h1 {margin:0; padding:0; font-size: 24px;}
4 h1 {margin:0; padding:0; font-size: 24px;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
5 h2, .wiki h1 {font-size: 20px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
6 h3, .wiki h2 {font-size: 16px;padding: 2px 10px 1px 0px;margin: 0 0 10px 0; border-bottom: 1px solid #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
7 h4, .wiki h3 {font-size: 13px;padding: 2px 10px 1px 0px;margin-bottom: 5px; border-bottom: 1px dotted #bbbbbb; color: #444;}
8
8
9 /***** Layout *****/
9 /***** Layout *****/
10 #wrapper {background: white;}
10 #wrapper {background: white;}
11
11
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
12 #top-menu {background: #2C4056; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
13 #top-menu ul {margin: 0; padding: 0;}
13 #top-menu ul {margin: 0; padding: 0;}
14 #top-menu li {
14 #top-menu li {
15 float:left;
15 float:left;
16 list-style-type:none;
16 list-style-type:none;
17 margin: 0px 0px 0px 0px;
17 margin: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
18 padding: 0px 0px 0px 0px;
19 white-space:nowrap;
19 white-space:nowrap;
20 }
20 }
21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
21 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
22 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
23
23
24 #account {float:right;}
24 #account {float:right;}
25
25
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
26 #header {height:5.3em;margin:0;background-color:#507AAA;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
27 #header a {color:#f8f8f8;}
27 #header a {color:#f8f8f8;}
28 #header h1 a.ancestor { font-size: 80%; }
28 #header h1 a.ancestor { font-size: 80%; }
29 #quick-search {float:right;}
29 #quick-search {float:right;}
30
30
31 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
31 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;}
32 #main-menu ul {margin: 0; padding: 0;}
32 #main-menu ul {margin: 0; padding: 0;}
33 #main-menu li {
33 #main-menu li {
34 float:left;
34 float:left;
35 list-style-type:none;
35 list-style-type:none;
36 margin: 0px 2px 0px 0px;
36 margin: 0px 2px 0px 0px;
37 padding: 0px 0px 0px 0px;
37 padding: 0px 0px 0px 0px;
38 white-space:nowrap;
38 white-space:nowrap;
39 }
39 }
40 #main-menu li a {
40 #main-menu li a {
41 display: block;
41 display: block;
42 color: #fff;
42 color: #fff;
43 text-decoration: none;
43 text-decoration: none;
44 font-weight: bold;
44 font-weight: bold;
45 margin: 0;
45 margin: 0;
46 padding: 4px 10px 4px 10px;
46 padding: 4px 10px 4px 10px;
47 }
47 }
48 #main-menu li a:hover {background:#759FCF; color:#fff;}
48 #main-menu li a:hover {background:#759FCF; color:#fff;}
49 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
49 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
50
50
51 #admin-menu ul {margin: 0; padding: 0;}
51 #admin-menu ul {margin: 0; padding: 0;}
52 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
52 #admin-menu li {margin: 0; padding: 0 0 12px 0; list-style-type:none;}
53
53
54 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
54 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
55 #admin-menu a.projects { background-image: url(../images/projects.png); }
55 #admin-menu a.projects { background-image: url(../images/projects.png); }
56 #admin-menu a.users { background-image: url(../images/user.png); }
56 #admin-menu a.users { background-image: url(../images/user.png); }
57 #admin-menu a.groups { background-image: url(../images/group.png); }
57 #admin-menu a.groups { background-image: url(../images/group.png); }
58 #admin-menu a.roles { background-image: url(../images/database_key.png); }
58 #admin-menu a.roles { background-image: url(../images/database_key.png); }
59 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
59 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
60 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
60 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
61 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
61 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
62 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
62 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
63 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
63 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
64 #admin-menu a.settings { background-image: url(../images/changeset.png); }
64 #admin-menu a.settings { background-image: url(../images/changeset.png); }
65 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
65 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
66 #admin-menu a.info { background-image: url(../images/help.png); }
66 #admin-menu a.info { background-image: url(../images/help.png); }
67
67
68 #main {background-color:#EEEEEE;}
68 #main {background-color:#EEEEEE;}
69
69
70 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
70 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
71 * html #sidebar{ width: 22%; }
71 * html #sidebar{ width: 22%; }
72 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
72 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
73 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
73 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
74 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
74 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
75 #sidebar .contextual { margin-right: 1em; }
75 #sidebar .contextual { margin-right: 1em; }
76
76
77 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
77 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
78 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
78 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
79 html>body #content { min-height: 600px; }
79 html>body #content { min-height: 600px; }
80 * html body #content { height: 600px; } /* IE */
80 * html body #content { height: 600px; } /* IE */
81
81
82 #main.nosidebar #sidebar{ display: none; }
82 #main.nosidebar #sidebar{ display: none; }
83 #main.nosidebar #content{ width: auto; border-right: 0; }
83 #main.nosidebar #content{ width: auto; border-right: 0; }
84
84
85 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
85 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
86
86
87 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
87 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
88 #login-form table td {padding: 6px;}
88 #login-form table td {padding: 6px;}
89 #login-form label {font-weight: bold;}
89 #login-form label {font-weight: bold;}
90 #login-form input#username, #login-form input#password { width: 300px; }
90 #login-form input#username, #login-form input#password { width: 300px; }
91
91
92 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
92 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
93
93
94 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
94 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
95
95
96 /***** Links *****/
96 /***** Links *****/
97 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
97 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
98 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
98 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
99 a img{ border: 0; }
99 a img{ border: 0; }
100
100
101 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
101 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
102
102
103 /***** Tables *****/
103 /***** Tables *****/
104 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
104 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
105 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
105 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
106 table.list td { vertical-align: top; }
106 table.list td { vertical-align: top; }
107 table.list td.id { width: 2%; text-align: center;}
107 table.list td.id { width: 2%; text-align: center;}
108 table.list td.checkbox { width: 15px; padding: 0px;}
108 table.list td.checkbox { width: 15px; padding: 0px;}
109 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
109 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
110 table.list td.buttons a { padding-right: 0.6em; }
110 table.list td.buttons a { padding-right: 0.6em; }
111 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
111 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
112
112
113 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
113 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
114 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
114 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
115
115
116 tr.issue { text-align: center; white-space: nowrap; }
116 tr.issue { text-align: center; white-space: nowrap; }
117 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
117 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
118 tr.issue td.subject { text-align: left; }
118 tr.issue td.subject { text-align: left; }
119 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
119 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
120
120
121 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
122 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
123 tr.issue.idnt-2 td.subject {padding-left: 2em;}
124 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
125 tr.issue.idnt-4 td.subject {padding-left: 5em;}
126 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
127 tr.issue.idnt-6 td.subject {padding-left: 8em;}
128 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
129 tr.issue.idnt-8 td.subject {padding-left: 11em;}
130 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
131
121 tr.entry { border: 1px solid #f8f8f8; }
132 tr.entry { border: 1px solid #f8f8f8; }
122 tr.entry td { white-space: nowrap; }
133 tr.entry td { white-space: nowrap; }
123 tr.entry td.filename { width: 30%; }
134 tr.entry td.filename { width: 30%; }
124 tr.entry td.size { text-align: right; font-size: 90%; }
135 tr.entry td.size { text-align: right; font-size: 90%; }
125 tr.entry td.revision, tr.entry td.author { text-align: center; }
136 tr.entry td.revision, tr.entry td.author { text-align: center; }
126 tr.entry td.age { text-align: right; }
137 tr.entry td.age { text-align: right; }
127 tr.entry.file td.filename a { margin-left: 16px; }
138 tr.entry.file td.filename a { margin-left: 16px; }
128
139
129 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
140 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
130 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
141 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
131
142
132 tr.changeset td.author { text-align: center; width: 15%; }
143 tr.changeset td.author { text-align: center; width: 15%; }
133 tr.changeset td.committed_on { text-align: center; width: 15%; }
144 tr.changeset td.committed_on { text-align: center; width: 15%; }
134
145
135 table.files tr.file td { text-align: center; }
146 table.files tr.file td { text-align: center; }
136 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
147 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
137 table.files tr.file td.digest { font-size: 80%; }
148 table.files tr.file td.digest { font-size: 80%; }
138
149
139 table.members td.roles, table.memberships td.roles { width: 45%; }
150 table.members td.roles, table.memberships td.roles { width: 45%; }
140
151
141 tr.message { height: 2.6em; }
152 tr.message { height: 2.6em; }
142 tr.message td.subject { padding-left: 20px; }
153 tr.message td.subject { padding-left: 20px; }
143 tr.message td.created_on { white-space: nowrap; }
154 tr.message td.created_on { white-space: nowrap; }
144 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
155 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
145 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
156 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
146 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
157 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
147
158
148 tr.version.closed, tr.version.closed a { color: #999; }
159 tr.version.closed, tr.version.closed a { color: #999; }
149 tr.version td.name { padding-left: 20px; }
160 tr.version td.name { padding-left: 20px; }
150 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
161 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
151 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; }
162 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; }
152
163
153 tr.user td { width:13%; }
164 tr.user td { width:13%; }
154 tr.user td.email { width:18%; }
165 tr.user td.email { width:18%; }
155 tr.user td { white-space: nowrap; }
166 tr.user td { white-space: nowrap; }
156 tr.user.locked, tr.user.registered { color: #aaa; }
167 tr.user.locked, tr.user.registered { color: #aaa; }
157 tr.user.locked a, tr.user.registered a { color: #aaa; }
168 tr.user.locked a, tr.user.registered a { color: #aaa; }
158
169
159 tr.time-entry { text-align: center; white-space: nowrap; }
170 tr.time-entry { text-align: center; white-space: nowrap; }
160 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
171 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
161 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
172 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
162 td.hours .hours-dec { font-size: 0.9em; }
173 td.hours .hours-dec { font-size: 0.9em; }
163
174
164 table.plugins td { vertical-align: middle; }
175 table.plugins td { vertical-align: middle; }
165 table.plugins td.configure { text-align: right; padding-right: 1em; }
176 table.plugins td.configure { text-align: right; padding-right: 1em; }
166 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
177 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
167 table.plugins span.description { display: block; font-size: 0.9em; }
178 table.plugins span.description { display: block; font-size: 0.9em; }
168 table.plugins span.url { display: block; font-size: 0.9em; }
179 table.plugins span.url { display: block; font-size: 0.9em; }
169
180
170 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
181 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
171 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
182 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
172
183
173 table.list tbody tr:hover { background-color:#ffffdd; }
184 table.list tbody tr:hover { background-color:#ffffdd; }
174 table.list tbody tr.group:hover { background-color:inherit; }
185 table.list tbody tr.group:hover { background-color:inherit; }
175 table td {padding:2px;}
186 table td {padding:2px;}
176 table p {margin:0;}
187 table p {margin:0;}
177 .odd {background-color:#f6f7f8;}
188 .odd {background-color:#f6f7f8;}
178 .even {background-color: #fff;}
189 .even {background-color: #fff;}
179
190
180 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
191 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
181 a.sort.asc { background-image: url(../images/sort_asc.png); }
192 a.sort.asc { background-image: url(../images/sort_asc.png); }
182 a.sort.desc { background-image: url(../images/sort_desc.png); }
193 a.sort.desc { background-image: url(../images/sort_desc.png); }
183
194
184 table.attributes { width: 100% }
195 table.attributes { width: 100% }
185 table.attributes th { vertical-align: top; text-align: left; }
196 table.attributes th { vertical-align: top; text-align: left; }
186 table.attributes td { vertical-align: top; }
197 table.attributes td { vertical-align: top; }
187
198
188 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
199 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
189
200
190 td.center {text-align:center;}
201 td.center {text-align:center;}
191
202
192 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
203 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
193
204
194 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
205 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
195 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
206 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
196 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
207 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
197 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
208 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
198
209
199 #watchers ul {margin: 0; padding: 0;}
210 #watchers ul {margin: 0; padding: 0;}
200 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
211 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
201 #watchers select {width: 95%; display: block;}
212 #watchers select {width: 95%; display: block;}
202 #watchers a.delete {opacity: 0.4;}
213 #watchers a.delete {opacity: 0.4;}
203 #watchers a.delete:hover {opacity: 1;}
214 #watchers a.delete:hover {opacity: 1;}
204 #watchers img.gravatar {vertical-align: middle;margin-right: 4px;}
215 #watchers img.gravatar {vertical-align: middle;margin-right: 4px;}
205
216
206 .highlight { background-color: #FCFD8D;}
217 .highlight { background-color: #FCFD8D;}
207 .highlight.token-1 { background-color: #faa;}
218 .highlight.token-1 { background-color: #faa;}
208 .highlight.token-2 { background-color: #afa;}
219 .highlight.token-2 { background-color: #afa;}
209 .highlight.token-3 { background-color: #aaf;}
220 .highlight.token-3 { background-color: #aaf;}
210
221
211 .box{
222 .box{
212 padding:6px;
223 padding:6px;
213 margin-bottom: 10px;
224 margin-bottom: 10px;
214 background-color:#f6f6f6;
225 background-color:#f6f6f6;
215 color:#505050;
226 color:#505050;
216 line-height:1.5em;
227 line-height:1.5em;
217 border: 1px solid #e4e4e4;
228 border: 1px solid #e4e4e4;
218 }
229 }
219
230
220 div.square {
231 div.square {
221 border: 1px solid #999;
232 border: 1px solid #999;
222 float: left;
233 float: left;
223 margin: .3em .4em 0 .4em;
234 margin: .3em .4em 0 .4em;
224 overflow: hidden;
235 overflow: hidden;
225 width: .6em; height: .6em;
236 width: .6em; height: .6em;
226 }
237 }
227 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
238 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
228 .contextual input, .contextual select {font-size:0.9em;}
239 .contextual input, .contextual select {font-size:0.9em;}
229 .message .contextual { margin-top: 0; }
240 .message .contextual { margin-top: 0; }
230
241
231 .splitcontentleft{float:left; width:49%;}
242 .splitcontentleft{float:left; width:49%;}
232 .splitcontentright{float:right; width:49%;}
243 .splitcontentright{float:right; width:49%;}
233 form {display: inline;}
244 form {display: inline;}
234 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
245 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
235 fieldset {border: 1px solid #e4e4e4; margin:0;}
246 fieldset {border: 1px solid #e4e4e4; margin:0;}
236 legend {color: #484848;}
247 legend {color: #484848;}
237 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
248 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
238 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
249 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
239 blockquote blockquote { margin-left: 0;}
250 blockquote blockquote { margin-left: 0;}
240 acronym { border-bottom: 1px dotted; cursor: help; }
251 acronym { border-bottom: 1px dotted; cursor: help; }
241 textarea.wiki-edit { width: 99%; }
252 textarea.wiki-edit { width: 99%; }
242 li p {margin-top: 0;}
253 li p {margin-top: 0;}
243 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
254 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
244 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
255 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
245 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
256 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
246 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
257 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
247
258
248 div.issue div.subject div div { padding-left: 16px; }
259 div.issue div.subject div div { padding-left: 16px; }
249 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
260 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
250 div.issue div.subject>div>p { margin-top: 0.5em; }
261 div.issue div.subject>div>p { margin-top: 0.5em; }
251 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
262 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
252
263
253 #issue_tree table.issues { border: 0; }
264 #issue_tree table.issues { border: 0; }
254 #issue_tree td.checkbox {display:none;}
265 #issue_tree td.checkbox {display:none;}
255
266
256 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
267 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
257 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
268 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
258 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
269 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
259
270
260 fieldset#date-range p { margin: 2px 0 2px 0; }
271 fieldset#date-range p { margin: 2px 0 2px 0; }
261 fieldset#filters table { border-collapse: collapse; }
272 fieldset#filters table { border-collapse: collapse; }
262 fieldset#filters table td { padding: 0; vertical-align: middle; }
273 fieldset#filters table td { padding: 0; vertical-align: middle; }
263 fieldset#filters tr.filter { height: 2em; }
274 fieldset#filters tr.filter { height: 2em; }
264 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
275 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
265 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
276 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
266
277
267 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
278 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
268 div#issue-changesets .changeset { padding: 4px;}
279 div#issue-changesets .changeset { padding: 4px;}
269 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
280 div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
270 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
281 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
271
282
272 div#activity dl, #search-results { margin-left: 2em; }
283 div#activity dl, #search-results { margin-left: 2em; }
273 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
284 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
274 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
285 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
275 div#activity dt.me .time { border-bottom: 1px solid #999; }
286 div#activity dt.me .time { border-bottom: 1px solid #999; }
276 div#activity dt .time { color: #777; font-size: 80%; }
287 div#activity dt .time { color: #777; font-size: 80%; }
277 div#activity dd .description, #search-results dd .description { font-style: italic; }
288 div#activity dd .description, #search-results dd .description { font-style: italic; }
278 div#activity span.project:after, #search-results span.project:after { content: " -"; }
289 div#activity span.project:after, #search-results span.project:after { content: " -"; }
279 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
290 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
280
291
281 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
292 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
282
293
283 div#search-results-counts {float:right;}
294 div#search-results-counts {float:right;}
284 div#search-results-counts ul { margin-top: 0.5em; }
295 div#search-results-counts ul { margin-top: 0.5em; }
285 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
296 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
286
297
287 dt.issue { background-image: url(../images/ticket.png); }
298 dt.issue { background-image: url(../images/ticket.png); }
288 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
299 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
289 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
300 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
290 dt.issue-note { background-image: url(../images/ticket_note.png); }
301 dt.issue-note { background-image: url(../images/ticket_note.png); }
291 dt.changeset { background-image: url(../images/changeset.png); }
302 dt.changeset { background-image: url(../images/changeset.png); }
292 dt.news { background-image: url(../images/news.png); }
303 dt.news { background-image: url(../images/news.png); }
293 dt.message { background-image: url(../images/message.png); }
304 dt.message { background-image: url(../images/message.png); }
294 dt.reply { background-image: url(../images/comments.png); }
305 dt.reply { background-image: url(../images/comments.png); }
295 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
306 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
296 dt.attachment { background-image: url(../images/attachment.png); }
307 dt.attachment { background-image: url(../images/attachment.png); }
297 dt.document { background-image: url(../images/document.png); }
308 dt.document { background-image: url(../images/document.png); }
298 dt.project { background-image: url(../images/projects.png); }
309 dt.project { background-image: url(../images/projects.png); }
299 dt.time-entry { background-image: url(../images/time.png); }
310 dt.time-entry { background-image: url(../images/time.png); }
300
311
301 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
312 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
302
313
303 div#roadmap .related-issues { margin-bottom: 1em; }
314 div#roadmap .related-issues { margin-bottom: 1em; }
304 div#roadmap .related-issues td.checkbox { display: none; }
315 div#roadmap .related-issues td.checkbox { display: none; }
305 div#roadmap .wiki h1:first-child { display: none; }
316 div#roadmap .wiki h1:first-child { display: none; }
306 div#roadmap .wiki h1 { font-size: 120%; }
317 div#roadmap .wiki h1 { font-size: 120%; }
307 div#roadmap .wiki h2 { font-size: 110%; }
318 div#roadmap .wiki h2 { font-size: 110%; }
308
319
309 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
320 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
310 div#version-summary fieldset { margin-bottom: 1em; }
321 div#version-summary fieldset { margin-bottom: 1em; }
311 div#version-summary .total-hours { text-align: right; }
322 div#version-summary .total-hours { text-align: right; }
312
323
313 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
324 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
314 table#time-report tbody tr { font-style: italic; color: #777; }
325 table#time-report tbody tr { font-style: italic; color: #777; }
315 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
326 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
316 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
327 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
317 table#time-report .hours-dec { font-size: 0.9em; }
328 table#time-report .hours-dec { font-size: 0.9em; }
318
329
319 form .attributes { margin-bottom: 8px; }
330 form .attributes { margin-bottom: 8px; }
320 form .attributes p { padding-top: 1px; padding-bottom: 2px; }
331 form .attributes p { padding-top: 1px; padding-bottom: 2px; }
321 form .attributes select { min-width: 50%; }
332 form .attributes select { min-width: 50%; }
322
333
323 ul.projects { margin: 0; padding-left: 1em; }
334 ul.projects { margin: 0; padding-left: 1em; }
324 ul.projects.root { margin: 0; padding: 0; }
335 ul.projects.root { margin: 0; padding: 0; }
325 ul.projects ul { border-left: 3px solid #e0e0e0; }
336 ul.projects ul { border-left: 3px solid #e0e0e0; }
326 ul.projects li { list-style-type:none; }
337 ul.projects li { list-style-type:none; }
327 ul.projects li.root { margin-bottom: 1em; }
338 ul.projects li.root { margin-bottom: 1em; }
328 ul.projects li.child { margin-top: 1em;}
339 ul.projects li.child { margin-top: 1em;}
329 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
340 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
330 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
341 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
331
342
332 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
343 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
333 #tracker_project_ids li { list-style-type:none; }
344 #tracker_project_ids li { list-style-type:none; }
334
345
335 ul.properties {padding:0; font-size: 0.9em; color: #777;}
346 ul.properties {padding:0; font-size: 0.9em; color: #777;}
336 ul.properties li {list-style-type:none;}
347 ul.properties li {list-style-type:none;}
337 ul.properties li span {font-style:italic;}
348 ul.properties li span {font-style:italic;}
338
349
339 .total-hours { font-size: 110%; font-weight: bold; }
350 .total-hours { font-size: 110%; font-weight: bold; }
340 .total-hours span.hours-int { font-size: 120%; }
351 .total-hours span.hours-int { font-size: 120%; }
341
352
342 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
353 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
343 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
354 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
344
355
345 #workflow_copy_form select { width: 200px; }
356 #workflow_copy_form select { width: 200px; }
346
357
347 .pagination {font-size: 90%}
358 .pagination {font-size: 90%}
348 p.pagination {margin-top:8px;}
359 p.pagination {margin-top:8px;}
349
360
350 /***** Tabular forms ******/
361 /***** Tabular forms ******/
351 .tabular p{
362 .tabular p{
352 margin: 0;
363 margin: 0;
353 padding: 5px 0 8px 0;
364 padding: 5px 0 8px 0;
354 padding-left: 180px; /*width of left column containing the label elements*/
365 padding-left: 180px; /*width of left column containing the label elements*/
355 height: 1%;
366 height: 1%;
356 clear:left;
367 clear:left;
357 }
368 }
358
369
359 html>body .tabular p {overflow:hidden;}
370 html>body .tabular p {overflow:hidden;}
360
371
361 .tabular label{
372 .tabular label{
362 font-weight: bold;
373 font-weight: bold;
363 float: left;
374 float: left;
364 text-align: right;
375 text-align: right;
365 margin-left: -180px; /*width of left column*/
376 margin-left: -180px; /*width of left column*/
366 width: 175px; /*width of labels. Should be smaller than left column to create some right
377 width: 175px; /*width of labels. Should be smaller than left column to create some right
367 margin*/
378 margin*/
368 }
379 }
369
380
370 .tabular label.floating{
381 .tabular label.floating{
371 font-weight: normal;
382 font-weight: normal;
372 margin-left: 0px;
383 margin-left: 0px;
373 text-align: left;
384 text-align: left;
374 width: 270px;
385 width: 270px;
375 }
386 }
376
387
377 .tabular label.block{
388 .tabular label.block{
378 font-weight: normal;
389 font-weight: normal;
379 margin-left: 0px !important;
390 margin-left: 0px !important;
380 text-align: left;
391 text-align: left;
381 float: none;
392 float: none;
382 display: block;
393 display: block;
383 width: auto;
394 width: auto;
384 }
395 }
385
396
386 input#time_entry_comments { width: 90%;}
397 input#time_entry_comments { width: 90%;}
387
398
388 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
399 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
389
400
390 .tabular.settings p{ padding-left: 300px; }
401 .tabular.settings p{ padding-left: 300px; }
391 .tabular.settings label{ margin-left: -300px; width: 295px; }
402 .tabular.settings label{ margin-left: -300px; width: 295px; }
392 .tabular.settings textarea { width: 99%; }
403 .tabular.settings textarea { width: 99%; }
393
404
394 fieldset.settings label { display: block; }
405 fieldset.settings label { display: block; }
395
406
396 .required {color: #bb0000;}
407 .required {color: #bb0000;}
397 .summary {font-style: italic;}
408 .summary {font-style: italic;}
398
409
399 #attachments_fields input[type=text] {margin-left: 8px; }
410 #attachments_fields input[type=text] {margin-left: 8px; }
400
411
401 div.attachments { margin-top: 12px; }
412 div.attachments { margin-top: 12px; }
402 div.attachments p { margin:4px 0 2px 0; }
413 div.attachments p { margin:4px 0 2px 0; }
403 div.attachments img { vertical-align: middle; }
414 div.attachments img { vertical-align: middle; }
404 div.attachments span.author { font-size: 0.9em; color: #888; }
415 div.attachments span.author { font-size: 0.9em; color: #888; }
405
416
406 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
417 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
407 .other-formats span + span:before { content: "| "; }
418 .other-formats span + span:before { content: "| "; }
408
419
409 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
420 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
410
421
411 /* Project members tab */
422 /* Project members tab */
412 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
423 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
413 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
424 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
414 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
425 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
415 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
426 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
416 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
427 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
417 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
428 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
418
429
419 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
430 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
420
431
421 * html div#tab-content-members fieldset div { height: 450px; }
432 * html div#tab-content-members fieldset div { height: 450px; }
422
433
423 /***** Flash & error messages ****/
434 /***** Flash & error messages ****/
424 #errorExplanation, div.flash, .nodata, .warning {
435 #errorExplanation, div.flash, .nodata, .warning {
425 padding: 4px 4px 4px 30px;
436 padding: 4px 4px 4px 30px;
426 margin-bottom: 12px;
437 margin-bottom: 12px;
427 font-size: 1.1em;
438 font-size: 1.1em;
428 border: 2px solid;
439 border: 2px solid;
429 }
440 }
430
441
431 div.flash {margin-top: 8px;}
442 div.flash {margin-top: 8px;}
432
443
433 div.flash.error, #errorExplanation {
444 div.flash.error, #errorExplanation {
434 background: url(../images/exclamation.png) 8px 50% no-repeat;
445 background: url(../images/exclamation.png) 8px 50% no-repeat;
435 background-color: #ffe3e3;
446 background-color: #ffe3e3;
436 border-color: #dd0000;
447 border-color: #dd0000;
437 color: #880000;
448 color: #880000;
438 }
449 }
439
450
440 div.flash.notice {
451 div.flash.notice {
441 background: url(../images/true.png) 8px 5px no-repeat;
452 background: url(../images/true.png) 8px 5px no-repeat;
442 background-color: #dfffdf;
453 background-color: #dfffdf;
443 border-color: #9fcf9f;
454 border-color: #9fcf9f;
444 color: #005f00;
455 color: #005f00;
445 }
456 }
446
457
447 div.flash.warning {
458 div.flash.warning {
448 background: url(../images/warning.png) 8px 5px no-repeat;
459 background: url(../images/warning.png) 8px 5px no-repeat;
449 background-color: #FFEBC1;
460 background-color: #FFEBC1;
450 border-color: #FDBF3B;
461 border-color: #FDBF3B;
451 color: #A6750C;
462 color: #A6750C;
452 text-align: left;
463 text-align: left;
453 }
464 }
454
465
455 .nodata, .warning {
466 .nodata, .warning {
456 text-align: center;
467 text-align: center;
457 background-color: #FFEBC1;
468 background-color: #FFEBC1;
458 border-color: #FDBF3B;
469 border-color: #FDBF3B;
459 color: #A6750C;
470 color: #A6750C;
460 }
471 }
461
472
462 #errorExplanation ul { font-size: 0.9em;}
473 #errorExplanation ul { font-size: 0.9em;}
463 #errorExplanation h2, #errorExplanation p { display: none; }
474 #errorExplanation h2, #errorExplanation p { display: none; }
464
475
465 /***** Ajax indicator ******/
476 /***** Ajax indicator ******/
466 #ajax-indicator {
477 #ajax-indicator {
467 position: absolute; /* fixed not supported by IE */
478 position: absolute; /* fixed not supported by IE */
468 background-color:#eee;
479 background-color:#eee;
469 border: 1px solid #bbb;
480 border: 1px solid #bbb;
470 top:35%;
481 top:35%;
471 left:40%;
482 left:40%;
472 width:20%;
483 width:20%;
473 font-weight:bold;
484 font-weight:bold;
474 text-align:center;
485 text-align:center;
475 padding:0.6em;
486 padding:0.6em;
476 z-index:100;
487 z-index:100;
477 filter:alpha(opacity=50);
488 filter:alpha(opacity=50);
478 opacity: 0.5;
489 opacity: 0.5;
479 }
490 }
480
491
481 html>body #ajax-indicator { position: fixed; }
492 html>body #ajax-indicator { position: fixed; }
482
493
483 #ajax-indicator span {
494 #ajax-indicator span {
484 background-position: 0% 40%;
495 background-position: 0% 40%;
485 background-repeat: no-repeat;
496 background-repeat: no-repeat;
486 background-image: url(../images/loading.gif);
497 background-image: url(../images/loading.gif);
487 padding-left: 26px;
498 padding-left: 26px;
488 vertical-align: bottom;
499 vertical-align: bottom;
489 }
500 }
490
501
491 /***** Calendar *****/
502 /***** Calendar *****/
492 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
503 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
493 table.cal thead th {width: 14%;}
504 table.cal thead th {width: 14%;}
494 table.cal tbody tr {height: 100px;}
505 table.cal tbody tr {height: 100px;}
495 table.cal th { background-color:#EEEEEE; padding: 4px; }
506 table.cal th { background-color:#EEEEEE; padding: 4px; }
496 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
507 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
497 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
508 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
498 table.cal td.odd p.day-num {color: #bbb;}
509 table.cal td.odd p.day-num {color: #bbb;}
499 table.cal td.today {background:#ffffdd;}
510 table.cal td.today {background:#ffffdd;}
500 table.cal td.today p.day-num {font-weight: bold;}
511 table.cal td.today p.day-num {font-weight: bold;}
501 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
512 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
502 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
513 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
503 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
514 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
504 p.cal.legend span {display:block;}
515 p.cal.legend span {display:block;}
505
516
506 /***** Tooltips ******/
517 /***** Tooltips ******/
507 .tooltip{position:relative;z-index:24;}
518 .tooltip{position:relative;z-index:24;}
508 .tooltip:hover{z-index:25;color:#000;}
519 .tooltip:hover{z-index:25;color:#000;}
509 .tooltip span.tip{display: none; text-align:left;}
520 .tooltip span.tip{display: none; text-align:left;}
510
521
511 div.tooltip:hover span.tip{
522 div.tooltip:hover span.tip{
512 display:block;
523 display:block;
513 position:absolute;
524 position:absolute;
514 top:12px; left:24px; width:270px;
525 top:12px; left:24px; width:270px;
515 border:1px solid #555;
526 border:1px solid #555;
516 background-color:#fff;
527 background-color:#fff;
517 padding: 4px;
528 padding: 4px;
518 font-size: 0.8em;
529 font-size: 0.8em;
519 color:#505050;
530 color:#505050;
520 }
531 }
521
532
522 /***** Progress bar *****/
533 /***** Progress bar *****/
523 table.progress {
534 table.progress {
524 border: 1px solid #D7D7D7;
535 border: 1px solid #D7D7D7;
525 border-collapse: collapse;
536 border-collapse: collapse;
526 border-spacing: 0pt;
537 border-spacing: 0pt;
527 empty-cells: show;
538 empty-cells: show;
528 text-align: center;
539 text-align: center;
529 float:left;
540 float:left;
530 margin: 1px 6px 1px 0px;
541 margin: 1px 6px 1px 0px;
531 }
542 }
532
543
533 table.progress td { height: 0.9em; }
544 table.progress td { height: 0.9em; }
534 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
545 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
535 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
546 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
536 table.progress td.open { background: #FFF none repeat scroll 0%; }
547 table.progress td.open { background: #FFF none repeat scroll 0%; }
537 p.pourcent {font-size: 80%;}
548 p.pourcent {font-size: 80%;}
538 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
549 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
539
550
540 /***** Tabs *****/
551 /***** Tabs *****/
541 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
552 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
542 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
553 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
543 #content .tabs ul li {
554 #content .tabs ul li {
544 float:left;
555 float:left;
545 list-style-type:none;
556 list-style-type:none;
546 white-space:nowrap;
557 white-space:nowrap;
547 margin-right:8px;
558 margin-right:8px;
548 background:#fff;
559 background:#fff;
549 position:relative;
560 position:relative;
550 margin-bottom:-1px;
561 margin-bottom:-1px;
551 }
562 }
552 #content .tabs ul li a{
563 #content .tabs ul li a{
553 display:block;
564 display:block;
554 font-size: 0.9em;
565 font-size: 0.9em;
555 text-decoration:none;
566 text-decoration:none;
556 line-height:1.3em;
567 line-height:1.3em;
557 padding:4px 6px 4px 6px;
568 padding:4px 6px 4px 6px;
558 border: 1px solid #ccc;
569 border: 1px solid #ccc;
559 border-bottom: 1px solid #bbbbbb;
570 border-bottom: 1px solid #bbbbbb;
560 background-color: #eeeeee;
571 background-color: #eeeeee;
561 color:#777;
572 color:#777;
562 font-weight:bold;
573 font-weight:bold;
563 }
574 }
564
575
565 #content .tabs ul li a:hover {
576 #content .tabs ul li a:hover {
566 background-color: #ffffdd;
577 background-color: #ffffdd;
567 text-decoration:none;
578 text-decoration:none;
568 }
579 }
569
580
570 #content .tabs ul li a.selected {
581 #content .tabs ul li a.selected {
571 background-color: #fff;
582 background-color: #fff;
572 border: 1px solid #bbbbbb;
583 border: 1px solid #bbbbbb;
573 border-bottom: 1px solid #fff;
584 border-bottom: 1px solid #fff;
574 }
585 }
575
586
576 #content .tabs ul li a.selected:hover {
587 #content .tabs ul li a.selected:hover {
577 background-color: #fff;
588 background-color: #fff;
578 }
589 }
579
590
580 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
591 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
581
592
582 button.tab-left, button.tab-right {
593 button.tab-left, button.tab-right {
583 font-size: 0.9em;
594 font-size: 0.9em;
584 cursor: pointer;
595 cursor: pointer;
585 height:24px;
596 height:24px;
586 border: 1px solid #ccc;
597 border: 1px solid #ccc;
587 border-bottom: 1px solid #bbbbbb;
598 border-bottom: 1px solid #bbbbbb;
588 position:absolute;
599 position:absolute;
589 padding:4px;
600 padding:4px;
590 width: 20px;
601 width: 20px;
591 bottom: -1px;
602 bottom: -1px;
592 }
603 }
593
604
594 button.tab-left {
605 button.tab-left {
595 right: 20px;
606 right: 20px;
596 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
607 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
597 }
608 }
598
609
599 button.tab-right {
610 button.tab-right {
600 right: 0;
611 right: 0;
601 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
612 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
602 }
613 }
603
614
604 /***** Auto-complete *****/
615 /***** Auto-complete *****/
605 div.autocomplete {
616 div.autocomplete {
606 position:absolute;
617 position:absolute;
607 width:400px;
618 width:400px;
608 margin:0;
619 margin:0;
609 padding:0;
620 padding:0;
610 }
621 }
611 div.autocomplete ul {
622 div.autocomplete ul {
612 list-style-type:none;
623 list-style-type:none;
613 margin:0;
624 margin:0;
614 padding:0;
625 padding:0;
615 }
626 }
616 div.autocomplete ul li {
627 div.autocomplete ul li {
617 list-style-type:none;
628 list-style-type:none;
618 display:block;
629 display:block;
619 margin:-1px 0 0 0;
630 margin:-1px 0 0 0;
620 padding:2px;
631 padding:2px;
621 cursor:pointer;
632 cursor:pointer;
622 font-size: 90%;
633 font-size: 90%;
623 border: 1px solid #ccc;
634 border: 1px solid #ccc;
624 border-left: 1px solid #ccc;
635 border-left: 1px solid #ccc;
625 border-right: 1px solid #ccc;
636 border-right: 1px solid #ccc;
626 background-color:white;
637 background-color:white;
627 }
638 }
628 div.autocomplete ul li.selected { background-color: #ffb;}
639 div.autocomplete ul li.selected { background-color: #ffb;}
629 div.autocomplete ul li span.informal {
640 div.autocomplete ul li span.informal {
630 font-size: 80%;
641 font-size: 80%;
631 color: #aaa;
642 color: #aaa;
632 }
643 }
633
644
634 #parent_issue_candidates ul li {width: 500px;}
645 #parent_issue_candidates ul li {width: 500px;}
635
646
636 /***** Diff *****/
647 /***** Diff *****/
637 .diff_out { background: #fcc; }
648 .diff_out { background: #fcc; }
638 .diff_in { background: #cfc; }
649 .diff_in { background: #cfc; }
639
650
640 /***** Wiki *****/
651 /***** Wiki *****/
641 div.wiki table {
652 div.wiki table {
642 border: 1px solid #505050;
653 border: 1px solid #505050;
643 border-collapse: collapse;
654 border-collapse: collapse;
644 margin-bottom: 1em;
655 margin-bottom: 1em;
645 }
656 }
646
657
647 div.wiki table, div.wiki td, div.wiki th {
658 div.wiki table, div.wiki td, div.wiki th {
648 border: 1px solid #bbb;
659 border: 1px solid #bbb;
649 padding: 4px;
660 padding: 4px;
650 }
661 }
651
662
652 div.wiki .external {
663 div.wiki .external {
653 background-position: 0% 60%;
664 background-position: 0% 60%;
654 background-repeat: no-repeat;
665 background-repeat: no-repeat;
655 padding-left: 12px;
666 padding-left: 12px;
656 background-image: url(../images/external.png);
667 background-image: url(../images/external.png);
657 }
668 }
658
669
659 div.wiki a.new {
670 div.wiki a.new {
660 color: #b73535;
671 color: #b73535;
661 }
672 }
662
673
663 div.wiki pre {
674 div.wiki pre {
664 margin: 1em 1em 1em 1.6em;
675 margin: 1em 1em 1em 1.6em;
665 padding: 2px 2px 2px 0;
676 padding: 2px 2px 2px 0;
666 background-color: #fafafa;
677 background-color: #fafafa;
667 border: 1px solid #dadada;
678 border: 1px solid #dadada;
668 width:auto;
679 width:auto;
669 overflow-x: auto;
680 overflow-x: auto;
670 }
681 }
671
682
672 div.wiki ul.toc {
683 div.wiki ul.toc {
673 background-color: #ffffdd;
684 background-color: #ffffdd;
674 border: 1px solid #e4e4e4;
685 border: 1px solid #e4e4e4;
675 padding: 4px;
686 padding: 4px;
676 line-height: 1.2em;
687 line-height: 1.2em;
677 margin-bottom: 12px;
688 margin-bottom: 12px;
678 margin-right: 12px;
689 margin-right: 12px;
679 margin-left: 0;
690 margin-left: 0;
680 display: table
691 display: table
681 }
692 }
682 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
693 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
683
694
684 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
695 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
685 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
696 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
686 div.wiki ul.toc li { list-style-type:none;}
697 div.wiki ul.toc li { list-style-type:none;}
687 div.wiki ul.toc li.heading2 { margin-left: 6px; }
698 div.wiki ul.toc li.heading2 { margin-left: 6px; }
688 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
699 div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
689
700
690 div.wiki ul.toc a {
701 div.wiki ul.toc a {
691 font-size: 0.9em;
702 font-size: 0.9em;
692 font-weight: normal;
703 font-weight: normal;
693 text-decoration: none;
704 text-decoration: none;
694 color: #606060;
705 color: #606060;
695 }
706 }
696 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
707 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
697
708
698 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
709 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
699 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
710 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
700 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
711 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
701
712
702 div.wiki img { vertical-align: middle; }
713 div.wiki img { vertical-align: middle; }
703
714
704 /***** My page layout *****/
715 /***** My page layout *****/
705 .block-receiver {
716 .block-receiver {
706 border:1px dashed #c0c0c0;
717 border:1px dashed #c0c0c0;
707 margin-bottom: 20px;
718 margin-bottom: 20px;
708 padding: 15px 0 15px 0;
719 padding: 15px 0 15px 0;
709 }
720 }
710
721
711 .mypage-box {
722 .mypage-box {
712 margin:0 0 20px 0;
723 margin:0 0 20px 0;
713 color:#505050;
724 color:#505050;
714 line-height:1.5em;
725 line-height:1.5em;
715 }
726 }
716
727
717 .handle {
728 .handle {
718 cursor: move;
729 cursor: move;
719 }
730 }
720
731
721 a.close-icon {
732 a.close-icon {
722 display:block;
733 display:block;
723 margin-top:3px;
734 margin-top:3px;
724 overflow:hidden;
735 overflow:hidden;
725 width:12px;
736 width:12px;
726 height:12px;
737 height:12px;
727 background-repeat: no-repeat;
738 background-repeat: no-repeat;
728 cursor:pointer;
739 cursor:pointer;
729 background-image:url('../images/close.png');
740 background-image:url('../images/close.png');
730 }
741 }
731
742
732 a.close-icon:hover {
743 a.close-icon:hover {
733 background-image:url('../images/close_hl.png');
744 background-image:url('../images/close_hl.png');
734 }
745 }
735
746
736 /***** Gantt chart *****/
747 /***** Gantt chart *****/
737 .gantt_hdr {
748 .gantt_hdr {
738 position:absolute;
749 position:absolute;
739 top:0;
750 top:0;
740 height:16px;
751 height:16px;
741 border-top: 1px solid #c0c0c0;
752 border-top: 1px solid #c0c0c0;
742 border-bottom: 1px solid #c0c0c0;
753 border-bottom: 1px solid #c0c0c0;
743 border-right: 1px solid #c0c0c0;
754 border-right: 1px solid #c0c0c0;
744 text-align: center;
755 text-align: center;
745 overflow: hidden;
756 overflow: hidden;
746 }
757 }
747
758
748 .task {
759 .task {
749 position: absolute;
760 position: absolute;
750 height:8px;
761 height:8px;
751 font-size:0.8em;
762 font-size:0.8em;
752 color:#888;
763 color:#888;
753 padding:0;
764 padding:0;
754 margin:0;
765 margin:0;
755 line-height:0.8em;
766 line-height:0.8em;
756 white-space:nowrap;
767 white-space:nowrap;
757 }
768 }
758
769
759 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
770 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
760 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
771 .task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
761 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
772 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
762
773
763 .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
774 .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
764 .task_late.parent, .task_done.parent { height: 3px;}
775 .task_late.parent, .task_done.parent { height: 3px;}
765 .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;}
776 .task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;}
766 .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;}
777 .task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;}
767
778
768 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
779 .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
769
780
770 /***** Icons *****/
781 /***** Icons *****/
771 .icon {
782 .icon {
772 background-position: 0% 50%;
783 background-position: 0% 50%;
773 background-repeat: no-repeat;
784 background-repeat: no-repeat;
774 padding-left: 20px;
785 padding-left: 20px;
775 padding-top: 2px;
786 padding-top: 2px;
776 padding-bottom: 3px;
787 padding-bottom: 3px;
777 }
788 }
778
789
779 .icon-add { background-image: url(../images/add.png); }
790 .icon-add { background-image: url(../images/add.png); }
780 .icon-edit { background-image: url(../images/edit.png); }
791 .icon-edit { background-image: url(../images/edit.png); }
781 .icon-copy { background-image: url(../images/copy.png); }
792 .icon-copy { background-image: url(../images/copy.png); }
782 .icon-duplicate { background-image: url(../images/duplicate.png); }
793 .icon-duplicate { background-image: url(../images/duplicate.png); }
783 .icon-del { background-image: url(../images/delete.png); }
794 .icon-del { background-image: url(../images/delete.png); }
784 .icon-move { background-image: url(../images/move.png); }
795 .icon-move { background-image: url(../images/move.png); }
785 .icon-save { background-image: url(../images/save.png); }
796 .icon-save { background-image: url(../images/save.png); }
786 .icon-cancel { background-image: url(../images/cancel.png); }
797 .icon-cancel { background-image: url(../images/cancel.png); }
787 .icon-multiple { background-image: url(../images/table_multiple.png); }
798 .icon-multiple { background-image: url(../images/table_multiple.png); }
788 .icon-folder { background-image: url(../images/folder.png); }
799 .icon-folder { background-image: url(../images/folder.png); }
789 .open .icon-folder { background-image: url(../images/folder_open.png); }
800 .open .icon-folder { background-image: url(../images/folder_open.png); }
790 .icon-package { background-image: url(../images/package.png); }
801 .icon-package { background-image: url(../images/package.png); }
791 .icon-home { background-image: url(../images/home.png); }
802 .icon-home { background-image: url(../images/home.png); }
792 .icon-user { background-image: url(../images/user.png); }
803 .icon-user { background-image: url(../images/user.png); }
793 .icon-projects { background-image: url(../images/projects.png); }
804 .icon-projects { background-image: url(../images/projects.png); }
794 .icon-help { background-image: url(../images/help.png); }
805 .icon-help { background-image: url(../images/help.png); }
795 .icon-attachment { background-image: url(../images/attachment.png); }
806 .icon-attachment { background-image: url(../images/attachment.png); }
796 .icon-history { background-image: url(../images/history.png); }
807 .icon-history { background-image: url(../images/history.png); }
797 .icon-time { background-image: url(../images/time.png); }
808 .icon-time { background-image: url(../images/time.png); }
798 .icon-time-add { background-image: url(../images/time_add.png); }
809 .icon-time-add { background-image: url(../images/time_add.png); }
799 .icon-stats { background-image: url(../images/stats.png); }
810 .icon-stats { background-image: url(../images/stats.png); }
800 .icon-warning { background-image: url(../images/warning.png); }
811 .icon-warning { background-image: url(../images/warning.png); }
801 .icon-fav { background-image: url(../images/fav.png); }
812 .icon-fav { background-image: url(../images/fav.png); }
802 .icon-fav-off { background-image: url(../images/fav_off.png); }
813 .icon-fav-off { background-image: url(../images/fav_off.png); }
803 .icon-reload { background-image: url(../images/reload.png); }
814 .icon-reload { background-image: url(../images/reload.png); }
804 .icon-lock { background-image: url(../images/locked.png); }
815 .icon-lock { background-image: url(../images/locked.png); }
805 .icon-unlock { background-image: url(../images/unlock.png); }
816 .icon-unlock { background-image: url(../images/unlock.png); }
806 .icon-checked { background-image: url(../images/true.png); }
817 .icon-checked { background-image: url(../images/true.png); }
807 .icon-details { background-image: url(../images/zoom_in.png); }
818 .icon-details { background-image: url(../images/zoom_in.png); }
808 .icon-report { background-image: url(../images/report.png); }
819 .icon-report { background-image: url(../images/report.png); }
809 .icon-comment { background-image: url(../images/comment.png); }
820 .icon-comment { background-image: url(../images/comment.png); }
810 .icon-summary { background-image: url(../images/lightning.png); }
821 .icon-summary { background-image: url(../images/lightning.png); }
811
822
812 .icon-file { background-image: url(../images/files/default.png); }
823 .icon-file { background-image: url(../images/files/default.png); }
813 .icon-file.text-plain { background-image: url(../images/files/text.png); }
824 .icon-file.text-plain { background-image: url(../images/files/text.png); }
814 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
825 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
815 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
826 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
816 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
827 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
817 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
828 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
818 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
829 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
819 .icon-file.image-gif { background-image: url(../images/files/image.png); }
830 .icon-file.image-gif { background-image: url(../images/files/image.png); }
820 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
831 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
821 .icon-file.image-png { background-image: url(../images/files/image.png); }
832 .icon-file.image-png { background-image: url(../images/files/image.png); }
822 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
833 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
823 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
834 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
824 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
835 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
825 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
836 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
826
837
827 img.gravatar {
838 img.gravatar {
828 padding: 2px;
839 padding: 2px;
829 border: solid 1px #d5d5d5;
840 border: solid 1px #d5d5d5;
830 background: #fff;
841 background: #fff;
831 }
842 }
832
843
833 div.issue img.gravatar {
844 div.issue img.gravatar {
834 float: right;
845 float: right;
835 margin: 0 0 0 1em;
846 margin: 0 0 0 1em;
836 padding: 5px;
847 padding: 5px;
837 }
848 }
838
849
839 div.issue table img.gravatar {
850 div.issue table img.gravatar {
840 height: 14px;
851 height: 14px;
841 width: 14px;
852 width: 14px;
842 padding: 2px;
853 padding: 2px;
843 float: left;
854 float: left;
844 margin: 0 0.5em 0 0;
855 margin: 0 0.5em 0 0;
845 }
856 }
846
857
847 h2 img.gravatar {
858 h2 img.gravatar {
848 padding: 3px;
859 padding: 3px;
849 margin: -2px 4px -4px 0;
860 margin: -2px 4px -4px 0;
850 vertical-align: top;
861 vertical-align: top;
851 }
862 }
852
863
853 h4 img.gravatar {
864 h4 img.gravatar {
854 padding: 3px;
865 padding: 3px;
855 margin: -6px 0 -4px 0;
866 margin: -6px 0 -4px 0;
856 vertical-align: top;
867 vertical-align: top;
857 }
868 }
858
869
859 td.username img.gravatar {
870 td.username img.gravatar {
860 float: left;
871 float: left;
861 margin: 0 1em 0 0;
872 margin: 0 1em 0 0;
862 }
873 }
863
874
864 #activity dt img.gravatar {
875 #activity dt img.gravatar {
865 float: left;
876 float: left;
866 margin: 0 1em 1em 0;
877 margin: 0 1em 1em 0;
867 }
878 }
868
879
869 #activity dt,
880 #activity dt,
870 .journal {
881 .journal {
871 clear: left;
882 clear: left;
872 }
883 }
873
884
874 h2 img { vertical-align:middle; }
885 h2 img { vertical-align:middle; }
875
886
876 .hascontextmenu { cursor: context-menu; }
887 .hascontextmenu { cursor: context-menu; }
877
888
878 /***** Media print specific styles *****/
889 /***** Media print specific styles *****/
879 @media print {
890 @media print {
880 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
891 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
881 #main { background: #fff; }
892 #main { background: #fff; }
882 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
893 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
883 #wiki_add_attachment { display:none; }
894 #wiki_add_attachment { display:none; }
884 }
895 }
General Comments 0
You need to be logged in to leave comments. Login now