##// END OF EJS Templates
Gantt code cleanup....
Jean-Philippe Lang -
r4409:edc35d4d5b04
parent child
Show More
@@ -1,993 +1,939
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 module Redmine
19 19 module Helpers
20 20 # Simple class to handle gantt chart data
21 21 class Gantt
22 22 include ERB::Util
23 23 include Redmine::I18n
24 24
25 25 # :nodoc:
26 26 # Some utility methods for the PDF export
27 27 class PDF
28 28 MaxCharactorsForSubject = 45
29 29 TotalWidth = 280
30 30 LeftPaneWidth = 100
31 31
32 32 def self.right_pane_width
33 33 TotalWidth - LeftPaneWidth
34 34 end
35 35 end
36 36
37 37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows
38 38 attr_accessor :query
39 39 attr_accessor :project
40 40 attr_accessor :view
41 41
42 42 def initialize(options={})
43 43 options = options.dup
44 44
45 45 if options[:year] && options[:year].to_i >0
46 46 @year_from = options[:year].to_i
47 47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
48 48 @month_from = options[:month].to_i
49 49 else
50 50 @month_from = 1
51 51 end
52 52 else
53 53 @month_from ||= Date.today.month
54 54 @year_from ||= Date.today.year
55 55 end
56 56
57 57 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
58 58 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
59 59 months = (options[:months] || User.current.pref[:gantt_months]).to_i
60 60 @months = (months > 0 && months < 25) ? months : 6
61 61
62 62 # Save gantt parameters as user preference (zoom and months count)
63 63 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
64 64 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
65 65 User.current.preference.save
66 66 end
67 67
68 68 @date_from = Date.civil(@year_from, @month_from, 1)
69 69 @date_to = (@date_from >> @months) - 1
70 70
71 71 @subjects = ''
72 72 @lines = ''
73 73 @number_of_rows = nil
74 74
75 75 @truncated = false
76 76 if options.has_key?(:max_rows)
77 77 @max_rows = options[:max_rows]
78 78 else
79 79 @max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i
80 80 end
81 81 end
82 82
83 83 def common_params
84 84 { :controller => 'gantts', :action => 'show', :project_id => @project }
85 85 end
86 86
87 87 def params
88 88 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
89 89 end
90 90
91 91 def params_previous
92 92 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
93 93 end
94 94
95 95 def params_next
96 96 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
97 97 end
98 98
99 99 ### Extracted from the HTML view/helpers
100 100 # Returns the number of rows that will be rendered on the Gantt chart
101 101 def number_of_rows
102 102 return @number_of_rows if @number_of_rows
103 103
104 104 rows = if @project
105 105 number_of_rows_on_project(@project)
106 106 else
107 107 Project.roots.visible.has_module('issue_tracking').inject(0) do |total, project|
108 108 total += number_of_rows_on_project(project)
109 109 end
110 110 end
111 111
112 112 rows > @max_rows ? @max_rows : rows
113 113 end
114 114
115 115 # Returns the number of rows that will be used to list a project on
116 116 # the Gantt chart. This will recurse for each subproject.
117 117 def number_of_rows_on_project(project)
118 118 # Remove the project requirement for Versions because it will
119 119 # restrict issues to only be on the current project. This
120 120 # ends up missing issues which are assigned to shared versions.
121 121 @query.project = nil if @query.project
122 122
123 123 # One Root project
124 124 count = 1
125 125 # Issues without a Version
126 126 count += project.issues.for_gantt.without_version.with_query(@query).count
127 127
128 128 # Versions
129 129 count += project.versions.count
130 130
131 131 # Issues on the Versions
132 132 project.versions.each do |version|
133 133 count += version.fixed_issues.for_gantt.with_query(@query).count
134 134 end
135 135
136 136 # Subprojects
137 137 project.children.visible.has_module('issue_tracking').each do |subproject|
138 138 count += number_of_rows_on_project(subproject)
139 139 end
140 140
141 141 count
142 142 end
143 143
144 144 # Renders the subjects of the Gantt chart, the left side.
145 145 def subjects(options={})
146 146 render(options.merge(:only => :subjects)) unless @subjects_rendered
147 147 @subjects
148 148 end
149 149
150 150 # Renders the lines of the Gantt chart, the right side
151 151 def lines(options={})
152 152 render(options.merge(:only => :lines)) unless @lines_rendered
153 153 @lines
154 154 end
155 155
156 156 def render(options={})
157 157 options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
158 158
159 159 @subjects = '' unless options[:only] == :lines
160 160 @lines = '' unless options[:only] == :subjects
161 161 @number_of_rows = 0
162 162
163 163 if @project
164 164 render_project(@project, options)
165 165 else
166 166 Project.roots.visible.has_module('issue_tracking').each do |project|
167 167 render_project(project, options)
168 168 break if abort?
169 169 end
170 170 end
171 171
172 172 @subjects_rendered = true unless options[:only] == :lines
173 173 @lines_rendered = true unless options[:only] == :subjects
174 174
175 175 render_end(options)
176 176 end
177 177
178 178 def render_project(project, options={})
179 179 options[:top] = 0 unless options.key? :top
180 180 options[:indent_increment] = 20 unless options.key? :indent_increment
181 181 options[:top_increment] = 20 unless options.key? :top_increment
182 182
183 183 subject_for_project(project, options) unless options[:only] == :lines
184 184 line_for_project(project, options) unless options[:only] == :subjects
185 185
186 186 options[:top] += options[:top_increment]
187 187 options[:indent] += options[:indent_increment]
188 188 @number_of_rows += 1
189 189 return if abort?
190 190
191 191 # Second, Issues without a version
192 192 issues = project.issues.for_gantt.without_version.with_query(@query).all(:limit => current_limit)
193 193 sort_issues!(issues)
194 194 if issues
195 195 render_issues(issues, options)
196 196 return if abort?
197 197 end
198 198
199 199 # Third, Versions
200 200 project.versions.sort.each do |version|
201 201 render_version(version, options)
202 202 return if abort?
203 203 end
204 204
205 205 # Fourth, subprojects
206 206 project.children.visible.has_module('issue_tracking').each do |project|
207 207 render_project(project, options)
208 208 return if abort?
209 209 end unless project.leaf?
210 210
211 211 # Remove indent to hit the next sibling
212 212 options[:indent] -= options[:indent_increment]
213 213 end
214 214
215 215 def render_issues(issues, options={})
216 216 issues.each do |i|
217 217 subject_for_issue(i, options) unless options[:only] == :lines
218 218 line_for_issue(i, options) unless options[:only] == :subjects
219 219
220 220 options[:top] += options[:top_increment]
221 221 @number_of_rows += 1
222 222 return if abort?
223 223 end
224 224 end
225 225
226 226 def render_version(version, options={})
227 227 # Version header
228 228 subject_for_version(version, options) unless options[:only] == :lines
229 229 line_for_version(version, options) unless options[:only] == :subjects
230 230
231 231 options[:top] += options[:top_increment]
232 232 @number_of_rows += 1
233 233 return if abort?
234 234
235 235 # Remove the project requirement for Versions because it will
236 236 # restrict issues to only be on the current project. This
237 237 # ends up missing issues which are assigned to shared versions.
238 238 @query.project = nil if @query.project
239 239
240 240 issues = version.fixed_issues.for_gantt.with_query(@query).all(:limit => current_limit)
241 241 if issues
242 242 sort_issues!(issues)
243 243 # Indent issues
244 244 options[:indent] += options[:indent_increment]
245 245 render_issues(issues, options)
246 246 options[:indent] -= options[:indent_increment]
247 247 end
248 248 end
249 249
250 250 def render_end(options={})
251 251 case options[:format]
252 252 when :pdf
253 253 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
254 254 end
255 255 end
256 256
257 257 def subject_for_project(project, options)
258 258 case options[:format]
259 259 when :html
260 260 output = ''
261 261
262 262 output << "<div class='project-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
263 263 if project.is_a? Project
264 264 output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
265 265 output << view.link_to_project(project)
266 266 output << '</span>'
267 267 else
268 268 ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
269 269 ''
270 270 end
271 271 output << "</small></div>"
272 272 @subjects << output
273 273 output
274 274 when :image
275 275
276 276 options[:image].fill('black')
277 277 options[:image].stroke('transparent')
278 278 options[:image].stroke_width(1)
279 279 options[:image].text(options[:indent], options[:top] + 2, project.name)
280 280 when :pdf
281 281 pdf_new_page?(options)
282 282 options[:pdf].SetY(options[:top])
283 283 options[:pdf].SetX(15)
284 284
285 285 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
286 286 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
287 287
288 288 options[:pdf].SetY(options[:top])
289 289 options[:pdf].SetX(options[:subject_width])
290 290 options[:pdf].Cell(options[:g_width], 5, "", "LR")
291 291 end
292 292 end
293 293
294 294 def line_for_project(project, options)
295 295 # Skip versions that don't have a start_date or due date
296 296 if project.is_a?(Project) && project.start_date && project.due_date
297 297 options[:zoom] ||= 1
298 298 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
299 299
300 300
301 301 case options[:format]
302 302 when :html
303 output = ''
304 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
305
306 start_date = project.start_date
307 start_date ||= self.date_from
308 start_left = ((start_date - self.date_from)*options[:zoom]).floor
309
310 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
311 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
312 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
313 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
314
315 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
316 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
317
318 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
319 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
320 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
321
322 # Bar graphic
323
324 # Make sure that negative i_left and i_width don't
325 # overflow the subject
326 if i_end > 0 && i_left <= options[:g_width]
327 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
328 end
303 coords = coordinates(project.start_date, project.due_date, project.completed_percent(:include_subprojects => true), options[:zoom])
304 label = "#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%"
305 output = html_task(options[:top], coords, :css => "project task", :label => label, :markers => true)
329 306
330 if l_width > 0 && i_left <= options[:g_width]
331 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
332 end
333 if d_width > 0 && i_left <= options[:g_width]
334 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
335 end
336
337
338 # Starting diamond
339 if start_left <= options[:g_width] && start_left > 0
340 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
341 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
342 output << "</div>"
343 end
344
345 # Ending diamond
346 # Don't show items too far ahead
347 if i_end <= options[:g_width] && i_end > 0
348 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
349 end
350
351 # DIsplay the Project name and %
352 if i_end <= options[:g_width]
353 # Display the status even if it's floated off to the left
354 status_px = i_end + 12 # 12px for the diamond
355 status_px = 0 if status_px <= 0
356
357 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
358 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
359 output << "</div>"
360 end
361 307 @lines << output
362 308 output
363 309 when :image
364 310 options[:image].stroke('transparent')
365 311 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
366 312
367 313 # Make sure negative i_left doesn't overflow the subject
368 314 if i_left > options[:subject_width]
369 315 options[:image].fill('blue')
370 316 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
371 317 options[:image].fill('black')
372 318 options[:image].text(i_left + 11, options[:top] + 1, project.name)
373 319 end
374 320 when :pdf
375 321 options[:pdf].SetY(options[:top]+1.5)
376 322 i_left = ((project.due_date - @date_from)*options[:zoom])
377 323
378 324 # Make sure negative i_left doesn't overflow the subject
379 325 if i_left > 0
380 326 options[:pdf].SetX(options[:subject_width] + i_left)
381 327 options[:pdf].SetFillColor(50,50,200)
382 328 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
383 329
384 330 options[:pdf].SetY(options[:top]+1.5)
385 331 options[:pdf].SetX(options[:subject_width] + i_left + 3)
386 332 options[:pdf].Cell(30, 2, "#{project.name}")
387 333 end
388 334 end
389 335 else
390 336 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
391 337 ''
392 338 end
393 339 end
394 340
395 341 def subject_for_version(version, options)
396 342 case options[:format]
397 343 when :html
398 344 output = ''
399 345 output << "<div class='version-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
400 346 if version.is_a? Version
401 347 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
402 348 output << view.link_to_version(version)
403 349 output << '</span>'
404 350 else
405 351 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
406 352 ''
407 353 end
408 354 output << "</small></div>"
409 355 @subjects << output
410 356 output
411 357 when :image
412 358 options[:image].fill('black')
413 359 options[:image].stroke('transparent')
414 360 options[:image].stroke_width(1)
415 361 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project)
416 362 when :pdf
417 363 pdf_new_page?(options)
418 364 options[:pdf].SetY(options[:top])
419 365 options[:pdf].SetX(15)
420 366
421 367 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
422 368 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
423 369
424 370 options[:pdf].SetY(options[:top])
425 371 options[:pdf].SetX(options[:subject_width])
426 372 options[:pdf].Cell(options[:g_width], 5, "", "LR")
427 373 end
428 374 end
429 375
430 376 def line_for_version(version, options)
431 377 # Skip versions that don't have a start_date
432 378 if version.is_a?(Version) && version.start_date && version.due_date
433 379 options[:zoom] ||= 1
434 380 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
435 381
436 382 case options[:format]
437 383 when :html
438 384 coords = coordinates(version.fixed_issues.minimum('start_date'), version.due_date, version.completed_pourcent, options[:zoom])
439 385 label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
440 386 label = h("#{version.project} -") + label unless @project && @project == version.project
441 387 output = html_task(options[:top], coords, :css => "version task", :label => label, :markers => true)
442 388
443 389 @lines << output
444 390 output
445 391 when :image
446 392 options[:image].stroke('transparent')
447 393 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
448 394
449 395 # Make sure negative i_left doesn't overflow the subject
450 396 if i_left > options[:subject_width]
451 397 options[:image].fill('green')
452 398 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
453 399 options[:image].fill('black')
454 400 options[:image].text(i_left + 11, options[:top] + 1, version.name)
455 401 end
456 402 when :pdf
457 403 options[:pdf].SetY(options[:top]+1.5)
458 404 i_left = ((version.start_date - @date_from)*options[:zoom])
459 405
460 406 # Make sure negative i_left doesn't overflow the subject
461 407 if i_left > 0
462 408 options[:pdf].SetX(options[:subject_width] + i_left)
463 409 options[:pdf].SetFillColor(50,200,50)
464 410 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
465 411
466 412 options[:pdf].SetY(options[:top]+1.5)
467 413 options[:pdf].SetX(options[:subject_width] + i_left + 3)
468 414 options[:pdf].Cell(30, 2, "#{version.name}")
469 415 end
470 416 end
471 417 else
472 418 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
473 419 ''
474 420 end
475 421 end
476 422
477 423 def subject_for_issue(issue, options)
478 424 case options[:format]
479 425 when :html
480 426 output = ''
481 427 output << "<div class='tooltip'>"
482 428 output << "<div class='issue-subject' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
483 429 if issue.is_a? Issue
484 430 css_classes = []
485 431 css_classes << 'issue-overdue' if issue.overdue?
486 432 css_classes << 'issue-behind-schedule' if issue.behind_schedule?
487 433 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
488 434
489 435 if issue.assigned_to.present?
490 436 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
491 437 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
492 438 end
493 439 output << "<span class='#{css_classes.join(' ')}'>"
494 440 output << view.link_to_issue(issue)
495 441 output << '</span>'
496 442 else
497 443 ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
498 444 ''
499 445 end
500 446 output << "</small></div>"
501 447
502 448 # Tooltip
503 449 if issue.is_a? Issue
504 450 output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
505 451 output << view.render_issue_tooltip(issue)
506 452 output << "</span>"
507 453 end
508 454
509 455 output << "</div>"
510 456 @subjects << output
511 457 output
512 458 when :image
513 459 options[:image].fill('black')
514 460 options[:image].stroke('transparent')
515 461 options[:image].stroke_width(1)
516 462 options[:image].text(options[:indent], options[:top] + 2, issue.subject)
517 463 when :pdf
518 464 pdf_new_page?(options)
519 465 options[:pdf].SetY(options[:top])
520 466 options[:pdf].SetX(15)
521 467
522 468 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
523 469 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
524 470
525 471 options[:pdf].SetY(options[:top])
526 472 options[:pdf].SetX(options[:subject_width])
527 473 options[:pdf].Cell(options[:g_width], 5, "", "LR")
528 474 end
529 475 end
530 476
531 477 def line_for_issue(issue, options)
532 478 # Skip issues that don't have a due_before (due_date or version's due_date)
533 479 if issue.is_a?(Issue) && issue.due_before
534 480 case options[:format]
535 481 when :html
536 482 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
537 483 css = "task " + (issue.leaf? ? 'leaf' : 'parent')
538 484 output = html_task(options[:top], coords, :css => css, :label => "#{ issue.status.name } #{ issue.done_ratio }%", :issue => issue)
539 485
540 486 @lines << output
541 487 output
542 488
543 489 when :image
544 490 # Handle nil start_dates, rare but can happen.
545 491 i_start_date = if issue.start_date && issue.start_date >= @date_from
546 492 issue.start_date
547 493 else
548 494 @date_from
549 495 end
550 496
551 497 i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
552 498 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
553 499 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
554 500 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
555 501 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
556 502
557 503 i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
558 504 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
559 505 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
560 506 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
561 507
562 508
563 509 # Make sure that negative i_left and i_width don't
564 510 # overflow the subject
565 511 if i_width > 0
566 512 options[:image].fill('grey')
567 513 options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
568 514 options[:image].fill('red')
569 515 options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
570 516 options[:image].fill('blue')
571 517 options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
572 518 end
573 519
574 520 # Show the status and % done next to the subject if it overflows
575 521 options[:image].fill('black')
576 522 if i_width > 0
577 523 options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
578 524 else
579 525 options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
580 526 end
581 527
582 528 when :pdf
583 529 options[:pdf].SetY(options[:top]+1.5)
584 530 # Handle nil start_dates, rare but can happen.
585 531 i_start_date = if issue.start_date && issue.start_date >= @date_from
586 532 issue.start_date
587 533 else
588 534 @date_from
589 535 end
590 536
591 537 i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
592 538
593 539 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
594 540 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
595 541 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
596 542
597 543 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
598 544
599 545 i_left = ((i_start_date - @date_from)*options[:zoom])
600 546 i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
601 547 d_width = ((i_done_date - i_start_date)*options[:zoom])
602 548 l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
603 549 l_width ||= 0
604 550
605 551 # Make sure that negative i_left and i_width don't
606 552 # overflow the subject
607 553 if i_width > 0
608 554 options[:pdf].SetX(options[:subject_width] + i_left)
609 555 options[:pdf].SetFillColor(200,200,200)
610 556 options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
611 557 end
612 558
613 559 if l_width > 0
614 560 options[:pdf].SetY(options[:top]+1.5)
615 561 options[:pdf].SetX(options[:subject_width] + i_left)
616 562 options[:pdf].SetFillColor(255,100,100)
617 563 options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
618 564 end
619 565 if d_width > 0
620 566 options[:pdf].SetY(options[:top]+1.5)
621 567 options[:pdf].SetX(options[:subject_width] + i_left)
622 568 options[:pdf].SetFillColor(100,100,255)
623 569 options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
624 570 end
625 571
626 572 options[:pdf].SetY(options[:top]+1.5)
627 573
628 574 # Make sure that negative i_left and i_width don't
629 575 # overflow the subject
630 576 if (i_left + i_width) >= 0
631 577 options[:pdf].SetX(options[:subject_width] + i_left + i_width)
632 578 else
633 579 options[:pdf].SetX(options[:subject_width])
634 580 end
635 581 options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
636 582 end
637 583 else
638 584 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
639 585 ''
640 586 end
641 587 end
642 588
643 589 # Generates a gantt image
644 590 # Only defined if RMagick is avalaible
645 591 def to_image(format='PNG')
646 592 date_to = (@date_from >> @months)-1
647 593 show_weeks = @zoom > 1
648 594 show_days = @zoom > 2
649 595
650 596 subject_width = 400
651 597 header_heigth = 18
652 598 # width of one day in pixels
653 599 zoom = @zoom*2
654 600 g_width = (@date_to - @date_from + 1)*zoom
655 601 g_height = 20 * number_of_rows + 30
656 602 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
657 603 height = g_height + headers_heigth
658 604
659 605 imgl = Magick::ImageList.new
660 606 imgl.new_image(subject_width+g_width+1, height)
661 607 gc = Magick::Draw.new
662 608
663 609 # Subjects
664 610 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
665 611
666 612 # Months headers
667 613 month_f = @date_from
668 614 left = subject_width
669 615 @months.times do
670 616 width = ((month_f >> 1) - month_f) * zoom
671 617 gc.fill('white')
672 618 gc.stroke('grey')
673 619 gc.stroke_width(1)
674 620 gc.rectangle(left, 0, left + width, height)
675 621 gc.fill('black')
676 622 gc.stroke('transparent')
677 623 gc.stroke_width(1)
678 624 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
679 625 left = left + width
680 626 month_f = month_f >> 1
681 627 end
682 628
683 629 # Weeks headers
684 630 if show_weeks
685 631 left = subject_width
686 632 height = header_heigth
687 633 if @date_from.cwday == 1
688 634 # date_from is monday
689 635 week_f = date_from
690 636 else
691 637 # find next monday after date_from
692 638 week_f = @date_from + (7 - @date_from.cwday + 1)
693 639 width = (7 - @date_from.cwday + 1) * zoom
694 640 gc.fill('white')
695 641 gc.stroke('grey')
696 642 gc.stroke_width(1)
697 643 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
698 644 left = left + width
699 645 end
700 646 while week_f <= date_to
701 647 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
702 648 gc.fill('white')
703 649 gc.stroke('grey')
704 650 gc.stroke_width(1)
705 651 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
706 652 gc.fill('black')
707 653 gc.stroke('transparent')
708 654 gc.stroke_width(1)
709 655 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
710 656 left = left + width
711 657 week_f = week_f+7
712 658 end
713 659 end
714 660
715 661 # Days details (week-end in grey)
716 662 if show_days
717 663 left = subject_width
718 664 height = g_height + header_heigth - 1
719 665 wday = @date_from.cwday
720 666 (date_to - @date_from + 1).to_i.times do
721 667 width = zoom
722 668 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
723 669 gc.stroke('grey')
724 670 gc.stroke_width(1)
725 671 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
726 672 left = left + width
727 673 wday = wday + 1
728 674 wday = 1 if wday > 7
729 675 end
730 676 end
731 677
732 678 # border
733 679 gc.fill('transparent')
734 680 gc.stroke('grey')
735 681 gc.stroke_width(1)
736 682 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
737 683 gc.stroke('black')
738 684 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
739 685
740 686 # content
741 687 top = headers_heigth + 20
742 688
743 689 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
744 690
745 691 # today red line
746 692 if Date.today >= @date_from and Date.today <= date_to
747 693 gc.stroke('red')
748 694 x = (Date.today-@date_from+1)*zoom + subject_width
749 695 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
750 696 end
751 697
752 698 gc.draw(imgl)
753 699 imgl.format = format
754 700 imgl.to_blob
755 701 end if Object.const_defined?(:Magick)
756 702
757 703 def to_pdf
758 704 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
759 705 pdf.SetTitle("#{l(:label_gantt)} #{project}")
760 706 pdf.AliasNbPages
761 707 pdf.footer_date = format_date(Date.today)
762 708 pdf.AddPage("L")
763 709 pdf.SetFontStyle('B',12)
764 710 pdf.SetX(15)
765 711 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
766 712 pdf.Ln
767 713 pdf.SetFontStyle('B',9)
768 714
769 715 subject_width = PDF::LeftPaneWidth
770 716 header_heigth = 5
771 717
772 718 headers_heigth = header_heigth
773 719 show_weeks = false
774 720 show_days = false
775 721
776 722 if self.months < 7
777 723 show_weeks = true
778 724 headers_heigth = 2*header_heigth
779 725 if self.months < 3
780 726 show_days = true
781 727 headers_heigth = 3*header_heigth
782 728 end
783 729 end
784 730
785 731 g_width = PDF.right_pane_width
786 732 zoom = (g_width) / (self.date_to - self.date_from + 1)
787 733 g_height = 120
788 734 t_height = g_height + headers_heigth
789 735
790 736 y_start = pdf.GetY
791 737
792 738 # Months headers
793 739 month_f = self.date_from
794 740 left = subject_width
795 741 height = header_heigth
796 742 self.months.times do
797 743 width = ((month_f >> 1) - month_f) * zoom
798 744 pdf.SetY(y_start)
799 745 pdf.SetX(left)
800 746 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
801 747 left = left + width
802 748 month_f = month_f >> 1
803 749 end
804 750
805 751 # Weeks headers
806 752 if show_weeks
807 753 left = subject_width
808 754 height = header_heigth
809 755 if self.date_from.cwday == 1
810 756 # self.date_from is monday
811 757 week_f = self.date_from
812 758 else
813 759 # find next monday after self.date_from
814 760 week_f = self.date_from + (7 - self.date_from.cwday + 1)
815 761 width = (7 - self.date_from.cwday + 1) * zoom-1
816 762 pdf.SetY(y_start + header_heigth)
817 763 pdf.SetX(left)
818 764 pdf.Cell(width + 1, height, "", "LTR")
819 765 left = left + width+1
820 766 end
821 767 while week_f <= self.date_to
822 768 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
823 769 pdf.SetY(y_start + header_heigth)
824 770 pdf.SetX(left)
825 771 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
826 772 left = left + width
827 773 week_f = week_f+7
828 774 end
829 775 end
830 776
831 777 # Days headers
832 778 if show_days
833 779 left = subject_width
834 780 height = header_heigth
835 781 wday = self.date_from.cwday
836 782 pdf.SetFontStyle('B',7)
837 783 (self.date_to - self.date_from + 1).to_i.times do
838 784 width = zoom
839 785 pdf.SetY(y_start + 2 * header_heigth)
840 786 pdf.SetX(left)
841 787 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
842 788 left = left + width
843 789 wday = wday + 1
844 790 wday = 1 if wday > 7
845 791 end
846 792 end
847 793
848 794 pdf.SetY(y_start)
849 795 pdf.SetX(15)
850 796 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
851 797
852 798 # Tasks
853 799 top = headers_heigth + y_start
854 800 options = {
855 801 :top => top,
856 802 :zoom => zoom,
857 803 :subject_width => subject_width,
858 804 :g_width => g_width,
859 805 :indent => 0,
860 806 :indent_increment => 5,
861 807 :top_increment => 5,
862 808 :format => :pdf,
863 809 :pdf => pdf
864 810 }
865 811 render(options)
866 812 pdf.Output
867 813 end
868 814
869 815 private
870 816
871 817 def coordinates(start_date, end_date, progress, zoom=nil)
872 818 zoom ||= @zoom
873 819
874 820 coords = {}
875 821 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
876 822 if start_date > self.date_from
877 823 coords[:start] = start_date - self.date_from
878 824 coords[:bar_start] = start_date - self.date_from
879 825 else
880 826 coords[:bar_start] = 0
881 827 end
882 828 if end_date < self.date_to
883 829 coords[:end] = end_date - self.date_from
884 830 coords[:bar_end] = end_date - self.date_from + 1
885 831 else
886 832 coords[:bar_end] = self.date_to - self.date_from + 1
887 833 end
888 834
889 835 if progress
890 836 progress_date = start_date + (end_date - start_date) * (progress / 100.0)
891 837 if progress_date > self.date_from && progress_date > start_date
892 838 if progress_date < self.date_to
893 839 coords[:bar_progress_end] = progress_date - self.date_from + 1
894 840 else
895 841 coords[:bar_progress_end] = self.date_to - self.date_from + 1
896 842 end
897 843 end
898 844
899 845 if progress_date < Date.today
900 846 late_date = [Date.today, end_date].min
901 847 if late_date > self.date_from && late_date > start_date
902 848 if late_date < self.date_to
903 849 coords[:bar_late_end] = late_date - self.date_from + 1
904 850 else
905 851 coords[:bar_late_end] = self.date_to - self.date_from + 1
906 852 end
907 853 end
908 854 end
909 855 end
910 856 end
911 857
912 858 # Transforms dates into pixels witdh
913 859 coords.keys.each do |key|
914 860 coords[key] = (coords[key] * zoom).floor
915 861 end
916 862 coords
917 863 end
918 864
919 865 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
920 866 def sort_issues!(issues)
921 867 issues.sort! do |a, b|
922 868 cmp = 0
923 869 cmp = (a.start_date <=> b.start_date) if a.start_date? && b.start_date?
924 870 cmp = (a.due_date <=> b.due_date) if cmp == 0 && a.due_date? && b.due_date?
925 871 cmp = (a.id <=> b.id) if cmp == 0
926 872 cmp
927 873 end
928 874 end
929 875
930 876 def current_limit
931 877 if @max_rows
932 878 @max_rows - @number_of_rows
933 879 else
934 880 nil
935 881 end
936 882 end
937 883
938 884 def abort?
939 885 if @max_rows && @number_of_rows >= @max_rows
940 886 @truncated = true
941 887 end
942 888 end
943 889
944 890 def pdf_new_page?(options)
945 891 if options[:top] > 180
946 892 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
947 893 options[:pdf].AddPage("L")
948 894 options[:top] = 15
949 895 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
950 896 end
951 897 end
952 898
953 899 def html_task(top, coords, options={})
954 900 output = ''
955 901 # Renders the task bar, with progress and late
956 902 if coords[:bar_start] && coords[:bar_end]
957 903 output << "<div style='top:#{ top }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_todo'>&nbsp;</div>"
958 904
959 905 if coords[:bar_late_end]
960 906 output << "<div style='top:#{ top }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_late_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_late'>&nbsp;</div>"
961 907 end
962 908 if coords[:bar_progress_end]
963 909 output << "<div style='top:#{ top }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_progress_end] - coords[:bar_start] - 2}px;' class='#{options[:css]} task_done'>&nbsp;</div>"
964 910 end
965 911 end
966 912 # Renders the markers
967 913 if options[:markers]
968 914 if coords[:start]
969 915 output << "<div style='top:#{ top }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>"
970 916 end
971 917 if coords[:end]
972 918 output << "<div style='top:#{ top }px;left:#{ coords[:end] }px;width:15px;' class='#{options[:css]} marker ending'>&nbsp;</div>"
973 919 end
974 920 end
975 921 # Renders the label on the right
976 922 if options[:label]
977 923 output << "<div style='top:#{ top }px;left:#{ (coords[:bar_end] || 0) + 5 }px;' class='#{options[:css]} label'>"
978 924 output << options[:label]
979 925 output << "</div>"
980 926 end
981 927 # Renders the tooltip
982 928 if options[:issue] && coords[:bar_start] && coords[:bar_end]
983 929 output << "<div class='tooltip' style='position: absolute;top:#{ top }px;left:#{ coords[:bar_start] }px;width:#{ coords[:bar_end] - coords[:bar_start] }px;height:12px;'>"
984 930 output << '<span class="tip">'
985 931 output << view.render_issue_tooltip(options[:issue])
986 932 output << "</span></div>"
987 933 end
988 934
989 935 output
990 936 end
991 937 end
992 938 end
993 939 end
@@ -1,946 +1,947
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 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
68 68
69 69 #main {background-color:#EEEEEE;}
70 70
71 71 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
72 72 * html #sidebar{ width: 22%; }
73 73 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
74 74 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
75 75 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
76 76 #sidebar .contextual { margin-right: 1em; }
77 77
78 78 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
79 79 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
80 80 html>body #content { min-height: 600px; }
81 81 * html body #content { height: 600px; } /* IE */
82 82
83 83 #main.nosidebar #sidebar{ display: none; }
84 84 #main.nosidebar #content{ width: auto; border-right: 0; }
85 85
86 86 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
87 87
88 88 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
89 89 #login-form table td {padding: 6px;}
90 90 #login-form label {font-weight: bold;}
91 91 #login-form input#username, #login-form input#password { width: 300px; }
92 92
93 93 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
94 94
95 95 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
96 96
97 97 /***** Links *****/
98 98 a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
99 99 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
100 100 a img{ border: 0; }
101 101
102 102 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
103 103
104 104 /***** Tables *****/
105 105 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
106 106 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
107 107 table.list td { vertical-align: top; }
108 108 table.list td.id { width: 2%; text-align: center;}
109 109 table.list td.checkbox { width: 15px; padding: 0px;}
110 110 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
111 111 table.list td.buttons a { padding-right: 0.6em; }
112 112 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
113 113
114 114 tr.project td.name a { white-space:nowrap; }
115 115
116 116 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
117 117 tr.project.idnt-1 td.name {padding-left: 0.5em;}
118 118 tr.project.idnt-2 td.name {padding-left: 2em;}
119 119 tr.project.idnt-3 td.name {padding-left: 3.5em;}
120 120 tr.project.idnt-4 td.name {padding-left: 5em;}
121 121 tr.project.idnt-5 td.name {padding-left: 6.5em;}
122 122 tr.project.idnt-6 td.name {padding-left: 8em;}
123 123 tr.project.idnt-7 td.name {padding-left: 9.5em;}
124 124 tr.project.idnt-8 td.name {padding-left: 11em;}
125 125 tr.project.idnt-9 td.name {padding-left: 12.5em;}
126 126
127 127 tr.issue { text-align: center; white-space: nowrap; }
128 128 tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
129 129 tr.issue td.subject { text-align: left; }
130 130 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
131 131
132 132 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
133 133 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
134 134 tr.issue.idnt-2 td.subject {padding-left: 2em;}
135 135 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
136 136 tr.issue.idnt-4 td.subject {padding-left: 5em;}
137 137 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
138 138 tr.issue.idnt-6 td.subject {padding-left: 8em;}
139 139 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
140 140 tr.issue.idnt-8 td.subject {padding-left: 11em;}
141 141 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
142 142
143 143 tr.entry { border: 1px solid #f8f8f8; }
144 144 tr.entry td { white-space: nowrap; }
145 145 tr.entry td.filename { width: 30%; }
146 146 tr.entry td.size { text-align: right; font-size: 90%; }
147 147 tr.entry td.revision, tr.entry td.author { text-align: center; }
148 148 tr.entry td.age { text-align: right; }
149 149 tr.entry.file td.filename a { margin-left: 16px; }
150 150
151 151 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
152 152 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
153 153
154 154 tr.changeset td.author { text-align: center; width: 15%; }
155 155 tr.changeset td.committed_on { text-align: center; width: 15%; }
156 156
157 157 table.files tr.file td { text-align: center; }
158 158 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
159 159 table.files tr.file td.digest { font-size: 80%; }
160 160
161 161 table.members td.roles, table.memberships td.roles { width: 45%; }
162 162
163 163 tr.message { height: 2.6em; }
164 164 tr.message td.subject { padding-left: 20px; }
165 165 tr.message td.created_on { white-space: nowrap; }
166 166 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
167 167 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
168 168 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
169 169
170 170 tr.version.closed, tr.version.closed a { color: #999; }
171 171 tr.version td.name { padding-left: 20px; }
172 172 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
173 173 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; }
174 174
175 175 tr.user td { width:13%; }
176 176 tr.user td.email { width:18%; }
177 177 tr.user td { white-space: nowrap; }
178 178 tr.user.locked, tr.user.registered { color: #aaa; }
179 179 tr.user.locked a, tr.user.registered a { color: #aaa; }
180 180
181 181 tr.time-entry { text-align: center; white-space: nowrap; }
182 182 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
183 183 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
184 184 td.hours .hours-dec { font-size: 0.9em; }
185 185
186 186 table.plugins td { vertical-align: middle; }
187 187 table.plugins td.configure { text-align: right; padding-right: 1em; }
188 188 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
189 189 table.plugins span.description { display: block; font-size: 0.9em; }
190 190 table.plugins span.url { display: block; font-size: 0.9em; }
191 191
192 192 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
193 193 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
194 194
195 195 table.list tbody tr:hover { background-color:#ffffdd; }
196 196 table.list tbody tr.group:hover { background-color:inherit; }
197 197 table td {padding:2px;}
198 198 table p {margin:0;}
199 199 .odd {background-color:#f6f7f8;}
200 200 .even {background-color: #fff;}
201 201
202 202 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
203 203 a.sort.asc { background-image: url(../images/sort_asc.png); }
204 204 a.sort.desc { background-image: url(../images/sort_desc.png); }
205 205
206 206 table.attributes { width: 100% }
207 207 table.attributes th { vertical-align: top; text-align: left; }
208 208 table.attributes td { vertical-align: top; }
209 209
210 210 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
211 211
212 212 td.center {text-align:center;}
213 213
214 214 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
215 215
216 216 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
217 217 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
218 218 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
219 219 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
220 220
221 221 #watchers ul {margin: 0; padding: 0;}
222 222 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
223 223 #watchers select {width: 95%; display: block;}
224 224 #watchers a.delete {opacity: 0.4;}
225 225 #watchers a.delete:hover {opacity: 1;}
226 226 #watchers img.gravatar {vertical-align: middle;margin: 0 4px 2px 0;}
227 227
228 228 .highlight { background-color: #FCFD8D;}
229 229 .highlight.token-1 { background-color: #faa;}
230 230 .highlight.token-2 { background-color: #afa;}
231 231 .highlight.token-3 { background-color: #aaf;}
232 232
233 233 .box{
234 234 padding:6px;
235 235 margin-bottom: 10px;
236 236 background-color:#f6f6f6;
237 237 color:#505050;
238 238 line-height:1.5em;
239 239 border: 1px solid #e4e4e4;
240 240 }
241 241
242 242 div.square {
243 243 border: 1px solid #999;
244 244 float: left;
245 245 margin: .3em .4em 0 .4em;
246 246 overflow: hidden;
247 247 width: .6em; height: .6em;
248 248 }
249 249 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
250 250 .contextual input, .contextual select {font-size:0.9em;}
251 251 .message .contextual { margin-top: 0; }
252 252
253 253 .splitcontentleft{float:left; width:49%;}
254 254 .splitcontentright{float:right; width:49%;}
255 255 form {display: inline;}
256 256 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
257 257 fieldset {border: 1px solid #e4e4e4; margin:0;}
258 258 legend {color: #484848;}
259 259 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
260 260 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
261 261 blockquote blockquote { margin-left: 0;}
262 262 acronym { border-bottom: 1px dotted; cursor: help; }
263 263 textarea.wiki-edit { width: 99%; }
264 264 li p {margin-top: 0;}
265 265 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
266 266 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
267 267 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
268 268 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
269 269
270 270 div.issue div.subject div div { padding-left: 16px; }
271 271 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
272 272 div.issue div.subject>div>p { margin-top: 0.5em; }
273 273 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
274 274
275 275 #issue_tree table.issues { border: 0; }
276 276 #issue_tree td.checkbox {display:none;}
277 277
278 278 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
279 279 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
280 280 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
281 281
282 282 fieldset#date-range p { margin: 2px 0 2px 0; }
283 283 fieldset#filters table { border-collapse: collapse; }
284 284 fieldset#filters table td { padding: 0; vertical-align: middle; }
285 285 fieldset#filters tr.filter { height: 2em; }
286 286 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
287 287 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
288 288
289 289 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
290 290 div#issue-changesets div.changeset { padding: 4px;}
291 291 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
292 292 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
293 293
294 294 div#activity dl, #search-results { margin-left: 2em; }
295 295 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
296 296 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
297 297 div#activity dt.me .time { border-bottom: 1px solid #999; }
298 298 div#activity dt .time { color: #777; font-size: 80%; }
299 299 div#activity dd .description, #search-results dd .description { font-style: italic; }
300 300 div#activity span.project:after, #search-results span.project:after { content: " -"; }
301 301 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
302 302
303 303 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
304 304
305 305 div#search-results-counts {float:right;}
306 306 div#search-results-counts ul { margin-top: 0.5em; }
307 307 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
308 308
309 309 dt.issue { background-image: url(../images/ticket.png); }
310 310 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
311 311 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
312 312 dt.issue-note { background-image: url(../images/ticket_note.png); }
313 313 dt.changeset { background-image: url(../images/changeset.png); }
314 314 dt.news { background-image: url(../images/news.png); }
315 315 dt.message { background-image: url(../images/message.png); }
316 316 dt.reply { background-image: url(../images/comments.png); }
317 317 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
318 318 dt.attachment { background-image: url(../images/attachment.png); }
319 319 dt.document { background-image: url(../images/document.png); }
320 320 dt.project { background-image: url(../images/projects.png); }
321 321 dt.time-entry { background-image: url(../images/time.png); }
322 322
323 323 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
324 324
325 325 div#roadmap .related-issues { margin-bottom: 1em; }
326 326 div#roadmap .related-issues td.checkbox { display: none; }
327 327 div#roadmap .wiki h1:first-child { display: none; }
328 328 div#roadmap .wiki h1 { font-size: 120%; }
329 329 div#roadmap .wiki h2 { font-size: 110%; }
330 330
331 331 div#version-summary { float:right; width:380px; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
332 332 div#version-summary fieldset { margin-bottom: 1em; }
333 333 div#version-summary .total-hours { text-align: right; }
334 334
335 335 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
336 336 table#time-report tbody tr { font-style: italic; color: #777; }
337 337 table#time-report tbody tr.last-level { font-style: normal; color: #555; }
338 338 table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
339 339 table#time-report .hours-dec { font-size: 0.9em; }
340 340
341 341 form .attributes { margin-bottom: 8px; }
342 342 form .attributes p { padding-top: 1px; padding-bottom: 2px; }
343 343 form .attributes select { min-width: 50%; }
344 344
345 345 ul.projects { margin: 0; padding-left: 1em; }
346 346 ul.projects.root { margin: 0; padding: 0; }
347 347 ul.projects ul.projects { border-left: 3px solid #e0e0e0; }
348 348 ul.projects li.root { list-style-type:none; margin-bottom: 1em; }
349 349 ul.projects li.child { list-style-type:none; margin-top: 1em;}
350 350 ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
351 351 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
352 352
353 353 #tracker_project_ids ul { margin: 0; padding-left: 1em; }
354 354 #tracker_project_ids li { list-style-type:none; }
355 355
356 356 ul.properties {padding:0; font-size: 0.9em; color: #777;}
357 357 ul.properties li {list-style-type:none;}
358 358 ul.properties li span {font-style:italic;}
359 359
360 360 .total-hours { font-size: 110%; font-weight: bold; }
361 361 .total-hours span.hours-int { font-size: 120%; }
362 362
363 363 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
364 364 #user_firstname, #user_lastname, #user_mail, #my_account_form select { width: 90%; }
365 365
366 366 #workflow_copy_form select { width: 200px; }
367 367
368 368 .pagination {font-size: 90%}
369 369 p.pagination {margin-top:8px;}
370 370
371 371 /***** Tabular forms ******/
372 372 .tabular p{
373 373 margin: 0;
374 374 padding: 5px 0 8px 0;
375 375 padding-left: 180px; /*width of left column containing the label elements*/
376 376 height: 1%;
377 377 clear:left;
378 378 }
379 379
380 380 html>body .tabular p {overflow:hidden;}
381 381
382 382 .tabular label{
383 383 font-weight: bold;
384 384 float: left;
385 385 text-align: right;
386 386 margin-left: -180px; /*width of left column*/
387 387 width: 175px; /*width of labels. Should be smaller than left column to create some right
388 388 margin*/
389 389 }
390 390
391 391 .tabular label.floating{
392 392 font-weight: normal;
393 393 margin-left: 0px;
394 394 text-align: left;
395 395 width: 270px;
396 396 }
397 397
398 398 .tabular label.block{
399 399 font-weight: normal;
400 400 margin-left: 0px !important;
401 401 text-align: left;
402 402 float: none;
403 403 display: block;
404 404 width: auto;
405 405 }
406 406
407 407 .tabular label.inline{
408 408 float:none;
409 409 margin-left: 5px !important;
410 410 width: auto;
411 411 }
412 412
413 413 input#time_entry_comments { width: 90%;}
414 414
415 415 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
416 416
417 417 .tabular.settings p{ padding-left: 300px; }
418 418 .tabular.settings label{ margin-left: -300px; width: 295px; }
419 419 .tabular.settings textarea { width: 99%; }
420 420
421 421 fieldset.settings label { display: block; }
422 422 .parent { padding-left: 20px; }
423 423
424 424 .required {color: #bb0000;}
425 425 .summary {font-style: italic;}
426 426
427 427 #attachments_fields input[type=text] {margin-left: 8px; }
428 428
429 429 div.attachments { margin-top: 12px; }
430 430 div.attachments p { margin:4px 0 2px 0; }
431 431 div.attachments img { vertical-align: middle; }
432 432 div.attachments span.author { font-size: 0.9em; color: #888; }
433 433
434 434 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
435 435 .other-formats span + span:before { content: "| "; }
436 436
437 437 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
438 438
439 439 /* Project members tab */
440 440 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
441 441 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
442 442 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
443 443 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
444 444 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
445 445 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
446 446
447 447 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
448 448
449 449 input#principal_search, input#user_search {width:100%}
450 450
451 451 * html div#tab-content-members fieldset div { height: 450px; }
452 452
453 453 /***** Flash & error messages ****/
454 454 #errorExplanation, div.flash, .nodata, .warning {
455 455 padding: 4px 4px 4px 30px;
456 456 margin-bottom: 12px;
457 457 font-size: 1.1em;
458 458 border: 2px solid;
459 459 }
460 460
461 461 div.flash {margin-top: 8px;}
462 462
463 463 div.flash.error, #errorExplanation {
464 464 background: url(../images/exclamation.png) 8px 50% no-repeat;
465 465 background-color: #ffe3e3;
466 466 border-color: #dd0000;
467 467 color: #880000;
468 468 }
469 469
470 470 div.flash.notice {
471 471 background: url(../images/true.png) 8px 5px no-repeat;
472 472 background-color: #dfffdf;
473 473 border-color: #9fcf9f;
474 474 color: #005f00;
475 475 }
476 476
477 477 div.flash.warning {
478 478 background: url(../images/warning.png) 8px 5px no-repeat;
479 479 background-color: #FFEBC1;
480 480 border-color: #FDBF3B;
481 481 color: #A6750C;
482 482 text-align: left;
483 483 }
484 484
485 485 .nodata, .warning {
486 486 text-align: center;
487 487 background-color: #FFEBC1;
488 488 border-color: #FDBF3B;
489 489 color: #A6750C;
490 490 }
491 491
492 492 #errorExplanation ul { font-size: 0.9em;}
493 493 #errorExplanation h2, #errorExplanation p { display: none; }
494 494
495 495 /***** Ajax indicator ******/
496 496 #ajax-indicator {
497 497 position: absolute; /* fixed not supported by IE */
498 498 background-color:#eee;
499 499 border: 1px solid #bbb;
500 500 top:35%;
501 501 left:40%;
502 502 width:20%;
503 503 font-weight:bold;
504 504 text-align:center;
505 505 padding:0.6em;
506 506 z-index:100;
507 507 filter:alpha(opacity=50);
508 508 opacity: 0.5;
509 509 }
510 510
511 511 html>body #ajax-indicator { position: fixed; }
512 512
513 513 #ajax-indicator span {
514 514 background-position: 0% 40%;
515 515 background-repeat: no-repeat;
516 516 background-image: url(../images/loading.gif);
517 517 padding-left: 26px;
518 518 vertical-align: bottom;
519 519 }
520 520
521 521 /***** Calendar *****/
522 522 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
523 523 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
524 524 table.cal thead th.week-number {width: auto;}
525 525 table.cal tbody tr {height: 100px;}
526 526 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
527 527 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
528 528 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
529 529 table.cal td.odd p.day-num {color: #bbb;}
530 530 table.cal td.today {background:#ffffdd;}
531 531 table.cal td.today p.day-num {font-weight: bold;}
532 532 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
533 533 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
534 534 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
535 535 p.cal.legend span {display:block;}
536 536
537 537 /***** Tooltips ******/
538 538 .tooltip{position:relative;z-index:24;}
539 539 .tooltip:hover{z-index:25;color:#000;}
540 540 .tooltip span.tip{display: none; text-align:left;}
541 541
542 542 div.tooltip:hover span.tip{
543 543 display:block;
544 544 position:absolute;
545 545 top:12px; left:24px; width:270px;
546 546 border:1px solid #555;
547 547 background-color:#fff;
548 548 padding: 4px;
549 549 font-size: 0.8em;
550 550 color:#505050;
551 551 }
552 552
553 553 /***** Progress bar *****/
554 554 table.progress {
555 555 border: 1px solid #D7D7D7;
556 556 border-collapse: collapse;
557 557 border-spacing: 0pt;
558 558 empty-cells: show;
559 559 text-align: center;
560 560 float:left;
561 561 margin: 1px 6px 1px 0px;
562 562 }
563 563
564 564 table.progress td { height: 0.9em; }
565 565 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
566 566 table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
567 567 table.progress td.open { background: #FFF none repeat scroll 0%; }
568 568 p.pourcent {font-size: 80%;}
569 569 p.progress-info {clear: left; font-style: italic; font-size: 80%;}
570 570
571 571 /***** Tabs *****/
572 572 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
573 573 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:1em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
574 574 #content .tabs ul li {
575 575 float:left;
576 576 list-style-type:none;
577 577 white-space:nowrap;
578 578 margin-right:8px;
579 579 background:#fff;
580 580 position:relative;
581 581 margin-bottom:-1px;
582 582 }
583 583 #content .tabs ul li a{
584 584 display:block;
585 585 font-size: 0.9em;
586 586 text-decoration:none;
587 587 line-height:1.3em;
588 588 padding:4px 6px 4px 6px;
589 589 border: 1px solid #ccc;
590 590 border-bottom: 1px solid #bbbbbb;
591 591 background-color: #eeeeee;
592 592 color:#777;
593 593 font-weight:bold;
594 594 }
595 595
596 596 #content .tabs ul li a:hover {
597 597 background-color: #ffffdd;
598 598 text-decoration:none;
599 599 }
600 600
601 601 #content .tabs ul li a.selected {
602 602 background-color: #fff;
603 603 border: 1px solid #bbbbbb;
604 604 border-bottom: 1px solid #fff;
605 605 }
606 606
607 607 #content .tabs ul li a.selected:hover {
608 608 background-color: #fff;
609 609 }
610 610
611 611 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
612 612
613 613 button.tab-left, button.tab-right {
614 614 font-size: 0.9em;
615 615 cursor: pointer;
616 616 height:24px;
617 617 border: 1px solid #ccc;
618 618 border-bottom: 1px solid #bbbbbb;
619 619 position:absolute;
620 620 padding:4px;
621 621 width: 20px;
622 622 bottom: -1px;
623 623 }
624 624
625 625 button.tab-left {
626 626 right: 20px;
627 627 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
628 628 }
629 629
630 630 button.tab-right {
631 631 right: 0;
632 632 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
633 633 }
634 634
635 635 /***** Auto-complete *****/
636 636 div.autocomplete {
637 637 position:absolute;
638 638 width:400px;
639 639 margin:0;
640 640 padding:0;
641 641 }
642 642 div.autocomplete ul {
643 643 list-style-type:none;
644 644 margin:0;
645 645 padding:0;
646 646 }
647 647 div.autocomplete ul li {
648 648 list-style-type:none;
649 649 display:block;
650 650 margin:-1px 0 0 0;
651 651 padding:2px;
652 652 cursor:pointer;
653 653 font-size: 90%;
654 654 border: 1px solid #ccc;
655 655 border-left: 1px solid #ccc;
656 656 border-right: 1px solid #ccc;
657 657 background-color:white;
658 658 }
659 659 div.autocomplete ul li.selected { background-color: #ffb;}
660 660 div.autocomplete ul li span.informal {
661 661 font-size: 80%;
662 662 color: #aaa;
663 663 }
664 664
665 665 #parent_issue_candidates ul li {width: 500px;}
666 666 #related_issue_candidates ul li {width: 500px;}
667 667
668 668 /***** Diff *****/
669 669 .diff_out { background: #fcc; }
670 670 .diff_in { background: #cfc; }
671 671
672 672 /***** Wiki *****/
673 673 div.wiki table {
674 674 border: 1px solid #505050;
675 675 border-collapse: collapse;
676 676 margin-bottom: 1em;
677 677 }
678 678
679 679 div.wiki table, div.wiki td, div.wiki th {
680 680 border: 1px solid #bbb;
681 681 padding: 4px;
682 682 }
683 683
684 684 div.wiki .external {
685 685 background-position: 0% 60%;
686 686 background-repeat: no-repeat;
687 687 padding-left: 12px;
688 688 background-image: url(../images/external.png);
689 689 }
690 690
691 691 div.wiki a.new {
692 692 color: #b73535;
693 693 }
694 694
695 695 div.wiki pre {
696 696 margin: 1em 1em 1em 1.6em;
697 697 padding: 2px 2px 2px 0;
698 698 background-color: #fafafa;
699 699 border: 1px solid #dadada;
700 700 width:auto;
701 701 overflow-x: auto;
702 702 overflow-y: hidden;
703 703 }
704 704
705 705 div.wiki ul.toc {
706 706 background-color: #ffffdd;
707 707 border: 1px solid #e4e4e4;
708 708 padding: 4px;
709 709 line-height: 1.2em;
710 710 margin-bottom: 12px;
711 711 margin-right: 12px;
712 712 margin-left: 0;
713 713 display: table
714 714 }
715 715 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
716 716
717 717 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
718 718 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
719 719 div.wiki ul.toc ul { margin: 0; padding: 0; }
720 720 div.wiki ul.toc li { list-style-type:none; margin: 0;}
721 721 div.wiki ul.toc li li { margin-left: 1.5em; }
722 722 div.wiki ul.toc li li li { font-size: 0.8em; }
723 723
724 724 div.wiki ul.toc a {
725 725 font-size: 0.9em;
726 726 font-weight: normal;
727 727 text-decoration: none;
728 728 color: #606060;
729 729 }
730 730 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
731 731
732 732 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
733 733 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
734 734 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
735 735
736 736 div.wiki img { vertical-align: middle; }
737 737
738 738 /***** My page layout *****/
739 739 .block-receiver {
740 740 border:1px dashed #c0c0c0;
741 741 margin-bottom: 20px;
742 742 padding: 15px 0 15px 0;
743 743 }
744 744
745 745 .mypage-box {
746 746 margin:0 0 20px 0;
747 747 color:#505050;
748 748 line-height:1.5em;
749 749 }
750 750
751 751 .handle {
752 752 cursor: move;
753 753 }
754 754
755 755 a.close-icon {
756 756 display:block;
757 757 margin-top:3px;
758 758 overflow:hidden;
759 759 width:12px;
760 760 height:12px;
761 761 background-repeat: no-repeat;
762 762 cursor:pointer;
763 763 background-image:url('../images/close.png');
764 764 }
765 765
766 766 a.close-icon:hover {
767 767 background-image:url('../images/close_hl.png');
768 768 }
769 769
770 770 /***** Gantt chart *****/
771 771 .gantt_hdr {
772 772 position:absolute;
773 773 top:0;
774 774 height:16px;
775 775 border-top: 1px solid #c0c0c0;
776 776 border-bottom: 1px solid #c0c0c0;
777 777 border-right: 1px solid #c0c0c0;
778 778 text-align: center;
779 779 overflow: hidden;
780 780 }
781 781
782 782 .task {
783 783 position: absolute;
784 784 height:8px;
785 785 font-size:0.8em;
786 786 color:#888;
787 787 padding:0;
788 788 margin:0;
789 789 line-height:0.8em;
790 790 white-space:nowrap;
791 791 }
792 792
793 793 .task.label {width:100%;}
794 .task.label.project, .task.label.version { font-weight: bold; }
794 795
795 796 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
796 797 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
797 798 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
798 799
799 800 .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
800 801 .task_late.parent, .task_done.parent { height: 3px;}
801 802 .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;}
802 803 .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;}
803 804
804 805 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
805 806 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
806 807 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
807 808 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; }
808 809
809 .project-line { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; }
810 .project_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
811 .project_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
812 .project_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
810 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
811 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
812 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
813 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; }
813 814
814 815 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
815 816 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
816 817
817 818 /***** Icons *****/
818 819 .icon {
819 820 background-position: 0% 50%;
820 821 background-repeat: no-repeat;
821 822 padding-left: 20px;
822 823 padding-top: 2px;
823 824 padding-bottom: 3px;
824 825 }
825 826
826 827 .icon-add { background-image: url(../images/add.png); }
827 828 .icon-edit { background-image: url(../images/edit.png); }
828 829 .icon-copy { background-image: url(../images/copy.png); }
829 830 .icon-duplicate { background-image: url(../images/duplicate.png); }
830 831 .icon-del { background-image: url(../images/delete.png); }
831 832 .icon-move { background-image: url(../images/move.png); }
832 833 .icon-save { background-image: url(../images/save.png); }
833 834 .icon-cancel { background-image: url(../images/cancel.png); }
834 835 .icon-multiple { background-image: url(../images/table_multiple.png); }
835 836 .icon-folder { background-image: url(../images/folder.png); }
836 837 .open .icon-folder { background-image: url(../images/folder_open.png); }
837 838 .icon-package { background-image: url(../images/package.png); }
838 839 .icon-home { background-image: url(../images/home.png); }
839 840 .icon-user { background-image: url(../images/user.png); }
840 841 .icon-projects { background-image: url(../images/projects.png); }
841 842 .icon-help { background-image: url(../images/help.png); }
842 843 .icon-attachment { background-image: url(../images/attachment.png); }
843 844 .icon-history { background-image: url(../images/history.png); }
844 845 .icon-time { background-image: url(../images/time.png); }
845 846 .icon-time-add { background-image: url(../images/time_add.png); }
846 847 .icon-stats { background-image: url(../images/stats.png); }
847 848 .icon-warning { background-image: url(../images/warning.png); }
848 849 .icon-fav { background-image: url(../images/fav.png); }
849 850 .icon-fav-off { background-image: url(../images/fav_off.png); }
850 851 .icon-reload { background-image: url(../images/reload.png); }
851 852 .icon-lock { background-image: url(../images/locked.png); }
852 853 .icon-unlock { background-image: url(../images/unlock.png); }
853 854 .icon-checked { background-image: url(../images/true.png); }
854 855 .icon-details { background-image: url(../images/zoom_in.png); }
855 856 .icon-report { background-image: url(../images/report.png); }
856 857 .icon-comment { background-image: url(../images/comment.png); }
857 858 .icon-summary { background-image: url(../images/lightning.png); }
858 859 .icon-server-authentication { background-image: url(../images/server_key.png); }
859 860 .icon-issue { background-image: url(../images/ticket.png); }
860 861 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
861 862 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
862 863
863 864 .icon-file { background-image: url(../images/files/default.png); }
864 865 .icon-file.text-plain { background-image: url(../images/files/text.png); }
865 866 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
866 867 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
867 868 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
868 869 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
869 870 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
870 871 .icon-file.image-gif { background-image: url(../images/files/image.png); }
871 872 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
872 873 .icon-file.image-png { background-image: url(../images/files/image.png); }
873 874 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
874 875 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
875 876 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
876 877 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
877 878
878 879 img.gravatar {
879 880 padding: 2px;
880 881 border: solid 1px #d5d5d5;
881 882 background: #fff;
882 883 }
883 884
884 885 div.issue img.gravatar {
885 886 float: right;
886 887 margin: 0 0 0 1em;
887 888 padding: 5px;
888 889 }
889 890
890 891 div.issue table img.gravatar {
891 892 height: 14px;
892 893 width: 14px;
893 894 padding: 2px;
894 895 float: left;
895 896 margin: 0 0.5em 0 0;
896 897 }
897 898
898 899 h2 img.gravatar {
899 900 padding: 3px;
900 901 margin: -2px 4px -4px 0;
901 902 vertical-align: top;
902 903 }
903 904
904 905 h4 img.gravatar {
905 906 padding: 3px;
906 907 margin: -6px 0 -4px 0;
907 908 vertical-align: top;
908 909 }
909 910
910 911 td.username img.gravatar {
911 912 float: left;
912 913 margin: 0 1em 0 0;
913 914 }
914 915
915 916 #activity dt img.gravatar {
916 917 float: left;
917 918 margin: 0 1em 1em 0;
918 919 }
919 920
920 921 /* Used on 12px Gravatar img tags without the icon background */
921 922 .icon-gravatar {
922 923 float: left;
923 924 margin-right: 4px;
924 925 }
925 926
926 927 #activity dt,
927 928 .journal {
928 929 clear: left;
929 930 }
930 931
931 932 .journal-link {
932 933 float: right;
933 934 }
934 935
935 936 h2 img { vertical-align:middle; }
936 937
937 938 .hascontextmenu { cursor: context-menu; }
938 939
939 940 /***** Media print specific styles *****/
940 941 @media print {
941 942 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
942 943 #main { background: #fff; }
943 944 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
944 945 #wiki_add_attachment { display:none; }
945 946 .hide-when-print { display: none; }
946 947 }
@@ -1,727 +1,727
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 require File.expand_path('../../../../../test_helper', __FILE__)
19 19
20 20 class Redmine::Helpers::GanttTest < ActiveSupport::TestCase
21 21 # Utility methods and classes so assert_select can be used.
22 22 class GanttViewTest < ActionView::Base
23 23 include ActionView::Helpers::UrlHelper
24 24 include ActionView::Helpers::TextHelper
25 25 include ActionController::UrlWriter
26 26 include ApplicationHelper
27 27 include ProjectsHelper
28 28 include IssuesHelper
29 29
30 30 def self.default_url_options
31 31 {:only_path => true }
32 32 end
33 33
34 34 end
35 35
36 36 include ActionController::Assertions::SelectorAssertions
37 37
38 38 def setup
39 39 @response = ActionController::TestResponse.new
40 40 # Fixtures
41 41 ProjectCustomField.delete_all
42 42 Project.destroy_all
43 43
44 44 User.current = User.find(1)
45 45 end
46 46
47 47 def build_view
48 48 @view = GanttViewTest.new
49 49 end
50 50
51 51 def html_document
52 52 HTML::Document.new(@response.body)
53 53 end
54 54
55 55 # Creates a Gantt chart for a 4 week span
56 56 def create_gantt(project=Project.generate!, options={})
57 57 @project = project
58 58 @gantt = Redmine::Helpers::Gantt.new(options)
59 59 @gantt.project = @project
60 60 @gantt.query = Query.generate_default!(:project => @project)
61 61 @gantt.view = build_view
62 62 @gantt.instance_variable_set('@date_from', options[:date_from] || 2.weeks.ago.to_date)
63 63 @gantt.instance_variable_set('@date_to', options[:date_to] || 2.weeks.from_now.to_date)
64 64 end
65 65
66 66 context "#number_of_rows" do
67 67
68 68 context "with one project" do
69 69 should "return the number of rows just for that project"
70 70 end
71 71
72 72 context "with no project" do
73 73 should "return the total number of rows for all the projects, resursively"
74 74 end
75 75
76 76 should "not exceed max_rows option" do
77 77 p = Project.generate!
78 78 5.times do
79 79 Issue.generate_for_project!(p)
80 80 end
81 81
82 82 create_gantt(p)
83 83 @gantt.render
84 84 assert_equal 6, @gantt.number_of_rows
85 85 assert !@gantt.truncated
86 86
87 87 create_gantt(p, :max_rows => 3)
88 88 @gantt.render
89 89 assert_equal 3, @gantt.number_of_rows
90 90 assert @gantt.truncated
91 91 end
92 92 end
93 93
94 94 context "#number_of_rows_on_project" do
95 95 setup do
96 96 create_gantt
97 97 end
98 98
99 99 should "clear the @query.project so cross-project issues and versions can be counted" do
100 100 assert @gantt.query.project
101 101 @gantt.number_of_rows_on_project(@project)
102 102 assert_nil @gantt.query.project
103 103 end
104 104
105 105 should "count 1 for the project itself" do
106 106 assert_equal 1, @gantt.number_of_rows_on_project(@project)
107 107 end
108 108
109 109 should "count the number of issues without a version" do
110 110 @project.issues << Issue.generate_for_project!(@project, :fixed_version => nil)
111 111 assert_equal 2, @gantt.number_of_rows_on_project(@project)
112 112 end
113 113
114 114 should "count the number of versions" do
115 115 @project.versions << Version.generate!
116 116 @project.versions << Version.generate!
117 117 assert_equal 3, @gantt.number_of_rows_on_project(@project)
118 118 end
119 119
120 120 should "count the number of issues on versions, including cross-project" do
121 121 version = Version.generate!
122 122 @project.versions << version
123 123 @project.issues << Issue.generate_for_project!(@project, :fixed_version => version)
124 124
125 125 assert_equal 3, @gantt.number_of_rows_on_project(@project)
126 126 end
127 127
128 128 should "recursive and count the number of rows on each subproject" do
129 129 @project.versions << Version.generate! # +1
130 130
131 131 @subproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1
132 132 @subproject.set_parent!(@project)
133 133 @subproject.issues << Issue.generate_for_project!(@subproject) # +1
134 134 @subproject.issues << Issue.generate_for_project!(@subproject) # +1
135 135
136 136 @subsubproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1
137 137 @subsubproject.set_parent!(@subproject)
138 138 @subsubproject.issues << Issue.generate_for_project!(@subsubproject) # +1
139 139
140 140 assert_equal 7, @gantt.number_of_rows_on_project(@project) # +1 for self
141 141 end
142 142 end
143 143
144 144 # TODO: more of an integration test
145 145 context "#subjects" do
146 146 setup do
147 147 create_gantt
148 148 @project.enabled_module_names = [:issue_tracking]
149 149 @tracker = Tracker.generate!
150 150 @project.trackers << @tracker
151 151 @version = Version.generate!(:effective_date => 1.week.from_now.to_date, :sharing => 'none')
152 152 @project.versions << @version
153 153
154 154 @issue = Issue.generate!(:fixed_version => @version,
155 155 :subject => "gantt#line_for_project",
156 156 :tracker => @tracker,
157 157 :project => @project,
158 158 :done_ratio => 30,
159 159 :start_date => Date.yesterday,
160 160 :due_date => 1.week.from_now.to_date)
161 161 @project.issues << @issue
162 162
163 163 @response.body = @gantt.subjects
164 164 end
165 165
166 166 context "project" do
167 167 should "be rendered" do
168 168 assert_select "div.project-name a", /#{@project.name}/
169 169 end
170 170
171 171 should "have an indent of 4" do
172 172 assert_select "div.project-name[style*=left:4px]"
173 173 end
174 174 end
175 175
176 176 context "version" do
177 177 should "be rendered" do
178 178 assert_select "div.version-name a", /#{@version.name}/
179 179 end
180 180
181 181 should "be indented 24 (one level)" do
182 182 assert_select "div.version-name[style*=left:24px]"
183 183 end
184 184 end
185 185
186 186 context "issue" do
187 187 should "be rendered" do
188 188 assert_select "div.issue-subject", /#{@issue.subject}/
189 189 end
190 190
191 191 should "be indented 44 (two levels)" do
192 192 assert_select "div.issue-subject[style*=left:44px]"
193 193 end
194 194 end
195 195 end
196 196
197 197 context "#lines" do
198 198 setup do
199 199 create_gantt
200 200 @project.enabled_module_names = [:issue_tracking]
201 201 @tracker = Tracker.generate!
202 202 @project.trackers << @tracker
203 203 @version = Version.generate!(:effective_date => 1.week.from_now.to_date)
204 204 @project.versions << @version
205 205 @issue = Issue.generate!(:fixed_version => @version,
206 206 :subject => "gantt#line_for_project",
207 207 :tracker => @tracker,
208 208 :project => @project,
209 209 :done_ratio => 30,
210 210 :start_date => Date.yesterday,
211 211 :due_date => 1.week.from_now.to_date)
212 212 @project.issues << @issue
213 213
214 214 @response.body = @gantt.lines
215 215 end
216 216
217 217 context "project" do
218 218 should "be rendered" do
219 assert_select "div.project_todo"
220 assert_select "div.project-line.starting"
221 assert_select "div.project-line.ending"
222 assert_select "div.label.project-name", /#{@project.name}/
219 assert_select "div.project.task_todo"
220 assert_select "div.project.starting"
221 assert_select "div.project.ending"
222 assert_select "div.label.project", /#{@project.name}/
223 223 end
224 224 end
225 225
226 226 context "version" do
227 227 should "be rendered" do
228 228 assert_select "div.version.task_todo"
229 229 assert_select "div.version.starting"
230 230 assert_select "div.version.ending"
231 231 assert_select "div.label.version", /#{@version.name}/
232 232 end
233 233 end
234 234
235 235 context "issue" do
236 236 should "be rendered" do
237 237 assert_select "div.task_todo"
238 238 assert_select "div.task.label", /#{@issue.done_ratio}/
239 239 assert_select "div.tooltip", /#{@issue.subject}/
240 240 end
241 241 end
242 242 end
243 243
244 244 context "#render_project" do
245 245 should "be tested"
246 246 end
247 247
248 248 context "#render_issues" do
249 249 should "be tested"
250 250 end
251 251
252 252 context "#render_version" do
253 253 should "be tested"
254 254 end
255 255
256 256 context "#subject_for_project" do
257 257 setup do
258 258 create_gantt
259 259 end
260 260
261 261 context ":html format" do
262 262 should "add an absolute positioned div" do
263 263 @response.body = @gantt.subject_for_project(@project, {:format => :html})
264 264 assert_select "div[style*=absolute]"
265 265 end
266 266
267 267 should "use the indent option to move the div to the right" do
268 268 @response.body = @gantt.subject_for_project(@project, {:format => :html, :indent => 40})
269 269 assert_select "div[style*=left:40]"
270 270 end
271 271
272 272 should "include the project name" do
273 273 @response.body = @gantt.subject_for_project(@project, {:format => :html})
274 274 assert_select 'div', :text => /#{@project.name}/
275 275 end
276 276
277 277 should "include a link to the project" do
278 278 @response.body = @gantt.subject_for_project(@project, {:format => :html})
279 279 assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/
280 280 end
281 281
282 282 should "style overdue projects" do
283 283 @project.enabled_module_names = [:issue_tracking]
284 284 @project.versions << Version.generate!(:effective_date => Date.yesterday)
285 285
286 286 assert @project.overdue?, "Need an overdue project for this test"
287 287 @response.body = @gantt.subject_for_project(@project, {:format => :html})
288 288
289 289 assert_select 'div span.project-overdue'
290 290 end
291 291
292 292
293 293 end
294 294
295 295 should "test the PNG format"
296 296 should "test the PDF format"
297 297 end
298 298
299 299 context "#line_for_project" do
300 300 setup do
301 301 create_gantt
302 302 @project.enabled_module_names = [:issue_tracking]
303 303 @tracker = Tracker.generate!
304 304 @project.trackers << @tracker
305 305 @version = Version.generate!(:effective_date => Date.yesterday)
306 306 @project.versions << @version
307 307
308 308 @project.issues << Issue.generate!(:fixed_version => @version,
309 309 :subject => "gantt#line_for_project",
310 310 :tracker => @tracker,
311 311 :project => @project,
312 312 :done_ratio => 30,
313 :start_date => Date.yesterday,
313 :start_date => 1.week.ago.to_date,
314 314 :due_date => 1.week.from_now.to_date)
315 315 end
316 316
317 317 context ":html format" do
318 318 context "todo line" do
319 319 should "start from the starting point on the left" do
320 320 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
321 assert_select "div.project_todo[style*=left:52px]"
321 assert_select "div.project.task_todo[style*=left:28px]", true, @response.body
322 322 end
323 323
324 324 should "be the total width of the project" do
325 325 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
326 assert_select "div.project_todo[style*=width:31px]"
326 assert_select "div.project.task_todo[style*=width:58px]", true, @response.body
327 327 end
328 328
329 329 end
330 330
331 331 context "late line" do
332 332 should "start from the starting point on the left" do
333 333 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
334 assert_select "div.project_late[style*=left:52px]"
334 assert_select "div.project.task_late[style*=left:28px]", true, @response.body
335 335 end
336 336
337 337 should "be the total delayed width of the project" do
338 338 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
339 assert_select "div.project_late[style*=width:6px]"
339 assert_select "div.project.task_late[style*=width:30px]", true, @response.body
340 340 end
341 341 end
342 342
343 343 context "done line" do
344 344 should "start from the starting point on the left" do
345 345 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
346 assert_select "div.project_done[style*=left:52px]"
346 assert_select "div.project.task_done[style*=left:28px]", true, @response.body
347 347 end
348 348
349 349 should "Be the total done width of the project" do
350 350 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
351 assert_select "div.project_done[style*=left:52px]"
351 assert_select "div.project.task_done[style*=width:18px]", true, @response.body
352 352 end
353 353 end
354 354
355 355 context "starting marker" do
356 356 should "not appear if the starting point is off the gantt chart" do
357 357 # Shift the date range of the chart
358 358 @gantt.instance_variable_set('@date_from', Date.today)
359 359
360 360 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
361 assert_select "div.project-line.starting", false
361 assert_select "div.project.starting", false, @response.body
362 362 end
363 363
364 364 should "appear at the starting point" do
365 365 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
366 assert_select "div.project-line.starting[style*=left:52px]"
366 assert_select "div.project.starting[style*=left:28px]", true, @response.body
367 367 end
368 368 end
369 369
370 370 context "ending marker" do
371 371 should "not appear if the starting point is off the gantt chart" do
372 372 # Shift the date range of the chart
373 373 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
374 374
375 375 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
376 assert_select "div.project-line.ending", false
376 assert_select "div.project.ending", false, @response.body
377 377
378 378 end
379 379
380 380 should "appear at the end of the date range" do
381 381 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
382 assert_select "div.project-line.ending[style*=left:84px]"
382 assert_select "div.project.ending[style*=left:84px]", true, @response.body
383 383 end
384 384 end
385 385
386 386 context "status content" do
387 387 should "appear at the far left, even if it's far in the past" do
388 388 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
389 389
390 390 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
391 assert_select "div.project-name", /#{@project.name}/
391 assert_select "div.project.label", /#{@project.name}/
392 392 end
393 393
394 394 should "show the project name" do
395 395 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
396 assert_select "div.project-name", /#{@project.name}/
396 assert_select "div.project.label", /#{@project.name}/
397 397 end
398 398
399 399 should "show the percent complete" do
400 400 @response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
401 assert_select "div.project-name", /0%/
401 assert_select "div.project.label", /0%/
402 402 end
403 403 end
404 404 end
405 405
406 406 should "test the PNG format"
407 407 should "test the PDF format"
408 408 end
409 409
410 410 context "#subject_for_version" do
411 411 setup do
412 412 create_gantt
413 413 @project.enabled_module_names = [:issue_tracking]
414 414 @tracker = Tracker.generate!
415 415 @project.trackers << @tracker
416 416 @version = Version.generate!(:effective_date => Date.yesterday)
417 417 @project.versions << @version
418 418
419 419 @project.issues << Issue.generate!(:fixed_version => @version,
420 420 :subject => "gantt#subject_for_version",
421 421 :tracker => @tracker,
422 422 :project => @project,
423 423 :start_date => Date.today)
424 424
425 425 end
426 426
427 427 context ":html format" do
428 428 should "add an absolute positioned div" do
429 429 @response.body = @gantt.subject_for_version(@version, {:format => :html})
430 430 assert_select "div[style*=absolute]"
431 431 end
432 432
433 433 should "use the indent option to move the div to the right" do
434 434 @response.body = @gantt.subject_for_version(@version, {:format => :html, :indent => 40})
435 435 assert_select "div[style*=left:40]"
436 436 end
437 437
438 438 should "include the version name" do
439 439 @response.body = @gantt.subject_for_version(@version, {:format => :html})
440 440 assert_select 'div', :text => /#{@version.name}/
441 441 end
442 442
443 443 should "include a link to the version" do
444 444 @response.body = @gantt.subject_for_version(@version, {:format => :html})
445 445 assert_select 'a[href=?]', Regexp.escape("/versions/show/#{@version.to_param}"), :text => /#{@version.name}/
446 446 end
447 447
448 448 should "style late versions" do
449 449 assert @version.overdue?, "Need an overdue version for this test"
450 450 @response.body = @gantt.subject_for_version(@version, {:format => :html})
451 451
452 452 assert_select 'div span.version-behind-schedule'
453 453 end
454 454
455 455 should "style behind schedule versions" do
456 456 assert @version.behind_schedule?, "Need a behind schedule version for this test"
457 457 @response.body = @gantt.subject_for_version(@version, {:format => :html})
458 458
459 459 assert_select 'div span.version-behind-schedule'
460 460 end
461 461 end
462 462 should "test the PNG format"
463 463 should "test the PDF format"
464 464 end
465 465
466 466 context "#line_for_version" do
467 467 setup do
468 468 create_gantt
469 469 @project.enabled_module_names = [:issue_tracking]
470 470 @tracker = Tracker.generate!
471 471 @project.trackers << @tracker
472 472 @version = Version.generate!(:effective_date => 1.week.from_now.to_date)
473 473 @project.versions << @version
474 474
475 475 @project.issues << Issue.generate!(:fixed_version => @version,
476 476 :subject => "gantt#line_for_project",
477 477 :tracker => @tracker,
478 478 :project => @project,
479 479 :done_ratio => 30,
480 480 :start_date => 1.week.ago.to_date,
481 481 :due_date => 1.week.from_now.to_date)
482 482 end
483 483
484 484 context ":html format" do
485 485 context "todo line" do
486 486 should "start from the starting point on the left" do
487 487 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
488 488 assert_select "div.version.task_todo[style*=left:28px]", true, @response.body
489 489 end
490 490
491 491 should "be the total width of the version" do
492 492 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
493 493 assert_select "div.version.task_todo[style*=width:58px]", true, @response.body
494 494 end
495 495
496 496 end
497 497
498 498 context "late line" do
499 499 should "start from the starting point on the left" do
500 500 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
501 501 assert_select "div.version.task_late[style*=left:28px]", true, @response.body
502 502 end
503 503
504 504 should "be the total delayed width of the version" do
505 505 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
506 506 assert_select "div.version.task_late[style*=width:30px]", true, @response.body
507 507 end
508 508 end
509 509
510 510 context "done line" do
511 511 should "start from the starting point on the left" do
512 512 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
513 513 assert_select "div.version.task_done[style*=left:28px]", true, @response.body
514 514 end
515 515
516 516 should "Be the total done width of the version" do
517 517 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
518 518 assert_select "div.version.task_done[style*=width:18px]", true, @response.body
519 519 end
520 520 end
521 521
522 522 context "starting marker" do
523 523 should "not appear if the starting point is off the gantt chart" do
524 524 # Shift the date range of the chart
525 525 @gantt.instance_variable_set('@date_from', Date.today)
526 526
527 527 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
528 528 assert_select "div.version.starting", false
529 529 end
530 530
531 531 should "appear at the starting point" do
532 532 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
533 533 assert_select "div.version.starting[style*=left:28px]", true, @response.body
534 534 end
535 535 end
536 536
537 537 context "ending marker" do
538 538 should "not appear if the starting point is off the gantt chart" do
539 539 # Shift the date range of the chart
540 540 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
541 541
542 542 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
543 543 assert_select "div.version.ending", false
544 544
545 545 end
546 546
547 547 should "appear at the end of the date range" do
548 548 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
549 549 assert_select "div.version.ending[style*=left:84px]", true, @response.body
550 550 end
551 551 end
552 552
553 553 context "status content" do
554 554 should "appear at the far left, even if it's far in the past" do
555 555 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
556 556
557 557 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
558 558 assert_select "div.version.label", /#{@version.name}/
559 559 end
560 560
561 561 should "show the version name" do
562 562 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
563 563 assert_select "div.version.label", /#{@version.name}/
564 564 end
565 565
566 566 should "show the percent complete" do
567 567 @response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
568 568 assert_select "div.version.label", /30%/
569 569 end
570 570 end
571 571 end
572 572
573 573 should "test the PNG format"
574 574 should "test the PDF format"
575 575 end
576 576
577 577 context "#subject_for_issue" do
578 578 setup do
579 579 create_gantt
580 580 @project.enabled_module_names = [:issue_tracking]
581 581 @tracker = Tracker.generate!
582 582 @project.trackers << @tracker
583 583
584 584 @issue = Issue.generate!(:subject => "gantt#subject_for_issue",
585 585 :tracker => @tracker,
586 586 :project => @project,
587 587 :start_date => 3.days.ago.to_date,
588 588 :due_date => Date.yesterday)
589 589 @project.issues << @issue
590 590
591 591 end
592 592
593 593 context ":html format" do
594 594 should "add an absolute positioned div" do
595 595 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
596 596 assert_select "div[style*=absolute]"
597 597 end
598 598
599 599 should "use the indent option to move the div to the right" do
600 600 @response.body = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40})
601 601 assert_select "div[style*=left:40]"
602 602 end
603 603
604 604 should "include the issue subject" do
605 605 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
606 606 assert_select 'div', :text => /#{@issue.subject}/
607 607 end
608 608
609 609 should "include a link to the issue" do
610 610 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
611 611 assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/
612 612 end
613 613
614 614 should "style overdue issues" do
615 615 assert @issue.overdue?, "Need an overdue issue for this test"
616 616 @response.body = @gantt.subject_for_issue(@issue, {:format => :html})
617 617
618 618 assert_select 'div span.issue-overdue'
619 619 end
620 620
621 621 end
622 622 should "test the PNG format"
623 623 should "test the PDF format"
624 624 end
625 625
626 626 context "#line_for_issue" do
627 627 setup do
628 628 create_gantt
629 629 @project.enabled_module_names = [:issue_tracking]
630 630 @tracker = Tracker.generate!
631 631 @project.trackers << @tracker
632 632 @version = Version.generate!(:effective_date => 1.week.from_now.to_date)
633 633 @project.versions << @version
634 634 @issue = Issue.generate!(:fixed_version => @version,
635 635 :subject => "gantt#line_for_project",
636 636 :tracker => @tracker,
637 637 :project => @project,
638 638 :done_ratio => 30,
639 639 :start_date => 1.week.ago.to_date,
640 640 :due_date => 1.week.from_now.to_date)
641 641 @project.issues << @issue
642 642 end
643 643
644 644 context ":html format" do
645 645 context "todo line" do
646 646 should "start from the starting point on the left" do
647 647 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
648 648 assert_select "div.task_todo[style*=left:28px]", true, @response.body
649 649 end
650 650
651 651 should "be the total width of the issue" do
652 652 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
653 653 assert_select "div.task_todo[style*=width:58px]", true, @response.body
654 654 end
655 655
656 656 end
657 657
658 658 context "late line" do
659 659 should "start from the starting point on the left" do
660 660 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
661 661 assert_select "div.task_late[style*=left:28px]", true, @response.body
662 662 end
663 663
664 664 should "be the total delayed width of the issue" do
665 665 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
666 666 assert_select "div.task_late[style*=width:30px]", true, @response.body
667 667 end
668 668 end
669 669
670 670 context "done line" do
671 671 should "start from the starting point on the left" do
672 672 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
673 673 assert_select "div.task_done[style*=left:28px]", true, @response.body
674 674 end
675 675
676 676 should "Be the total done width of the issue" do
677 677 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
678 678 assert_select "div.task_done[style*=width:18px]", true, @response.body
679 679 end
680 680
681 681 should "not be the total done width if the chart starts after issue start date" do
682 682 create_gantt(@project, :date_from => 5.days.ago.to_date)
683 683
684 684 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
685 685 assert_select "div.task_done[style*=left:0px]", true, @response.body
686 686 assert_select "div.task_done[style*=width:10px]", true, @response.body
687 687 end
688 688 end
689 689
690 690 context "status content" do
691 691 should "appear at the far left, even if it's far in the past" do
692 692 @gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
693 693
694 694 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
695 695 assert_select "div.task.label", true, @response.body
696 696 end
697 697
698 698 should "show the issue status" do
699 699 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
700 700 assert_select "div.task.label", /#{@issue.status.name}/
701 701 end
702 702
703 703 should "show the percent complete" do
704 704 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
705 705 assert_select "div.task.label", /30%/
706 706 end
707 707 end
708 708 end
709 709
710 710 should "have an issue tooltip" do
711 711 @response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
712 712 assert_select "div.tooltip", /#{@issue.subject}/
713 713 end
714 714
715 715 should "test the PNG format"
716 716 should "test the PDF format"
717 717 end
718 718
719 719 context "#to_image" do
720 720 should "be tested"
721 721 end
722 722
723 723 context "#to_pdf" do
724 724 should "be tested"
725 725 end
726 726
727 727 end
General Comments 0
You need to be logged in to leave comments. Login now