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