##// END OF EJS Templates
Gantt code cleanup....
Jean-Philippe Lang -
r4408:98c7c179ca3f
parent child
Show More
@@ -1,1038 +1,993
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 303 output = ''
304 304 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
305 305
306 306 start_date = project.start_date
307 307 start_date ||= self.date_from
308 308 start_left = ((start_date - self.date_from)*options[:zoom]).floor
309 309
310 310 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
311 311 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
312 312 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
313 313 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
314 314
315 315 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
316 316 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
317 317
318 318 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
319 319 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
320 320 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
321 321
322 322 # Bar graphic
323 323
324 324 # Make sure that negative i_left and i_width don't
325 325 # overflow the subject
326 326 if i_end > 0 && i_left <= options[:g_width]
327 327 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
328 328 end
329 329
330 330 if l_width > 0 && i_left <= options[:g_width]
331 331 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
332 332 end
333 333 if d_width > 0 && i_left <= options[:g_width]
334 334 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
335 335 end
336 336
337 337
338 338 # Starting diamond
339 339 if start_left <= options[:g_width] && start_left > 0
340 340 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
341 341 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
342 342 output << "</div>"
343 343 end
344 344
345 345 # Ending diamond
346 346 # Don't show items too far ahead
347 347 if i_end <= options[:g_width] && i_end > 0
348 348 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
349 349 end
350 350
351 351 # DIsplay the Project name and %
352 352 if i_end <= options[:g_width]
353 353 # Display the status even if it's floated off to the left
354 354 status_px = i_end + 12 # 12px for the diamond
355 355 status_px = 0 if status_px <= 0
356 356
357 357 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
358 358 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
359 359 output << "</div>"
360 360 end
361 361 @lines << output
362 362 output
363 363 when :image
364 364 options[:image].stroke('transparent')
365 365 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
366 366
367 367 # Make sure negative i_left doesn't overflow the subject
368 368 if i_left > options[:subject_width]
369 369 options[:image].fill('blue')
370 370 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
371 371 options[:image].fill('black')
372 372 options[:image].text(i_left + 11, options[:top] + 1, project.name)
373 373 end
374 374 when :pdf
375 375 options[:pdf].SetY(options[:top]+1.5)
376 376 i_left = ((project.due_date - @date_from)*options[:zoom])
377 377
378 378 # Make sure negative i_left doesn't overflow the subject
379 379 if i_left > 0
380 380 options[:pdf].SetX(options[:subject_width] + i_left)
381 381 options[:pdf].SetFillColor(50,50,200)
382 382 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
383 383
384 384 options[:pdf].SetY(options[:top]+1.5)
385 385 options[:pdf].SetX(options[:subject_width] + i_left + 3)
386 386 options[:pdf].Cell(30, 2, "#{project.name}")
387 387 end
388 388 end
389 389 else
390 390 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
391 391 ''
392 392 end
393 393 end
394 394
395 395 def subject_for_version(version, options)
396 396 case options[:format]
397 397 when :html
398 398 output = ''
399 399 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 400 if version.is_a? Version
401 401 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
402 402 output << view.link_to_version(version)
403 403 output << '</span>'
404 404 else
405 405 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
406 406 ''
407 407 end
408 408 output << "</small></div>"
409 409 @subjects << output
410 410 output
411 411 when :image
412 412 options[:image].fill('black')
413 413 options[:image].stroke('transparent')
414 414 options[:image].stroke_width(1)
415 415 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project)
416 416 when :pdf
417 417 pdf_new_page?(options)
418 418 options[:pdf].SetY(options[:top])
419 419 options[:pdf].SetX(15)
420 420
421 421 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
422 422 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
423 423
424 424 options[:pdf].SetY(options[:top])
425 425 options[:pdf].SetX(options[:subject_width])
426 426 options[:pdf].Cell(options[:g_width], 5, "", "LR")
427 427 end
428 428 end
429 429
430 430 def line_for_version(version, options)
431 431 # Skip versions that don't have a start_date
432 432 if version.is_a?(Version) && version.start_date && version.due_date
433 433 options[:zoom] ||= 1
434 434 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
435 435
436 436 case options[:format]
437 437 when :html
438 output = ''
439 i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
440 # TODO: or version.fixed_issues.collect(&:start_date).min
441 start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
442 start_date ||= self.date_from
443 start_left = ((start_date - self.date_from)*options[:zoom]).floor
444
445 i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
446 i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
447 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
448 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
438 coords = coordinates(version.fixed_issues.minimum('start_date'), version.due_date, version.completed_pourcent, options[:zoom])
439 label = "#{h version } #{h version.completed_pourcent.to_i.to_s}%"
440 label = h("#{version.project} -") + label unless @project && @project == version.project
441 output = html_task(options[:top], coords, :css => "version task", :label => label, :markers => true)
449 442
450 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
451
452 i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
453 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
454 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
455
456 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
457
458 # Bar graphic
459
460 # Make sure that negative i_left and i_width don't
461 # overflow the subject
462 if i_width > 0 && i_left <= options[:g_width]
463 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
464 end
465 if l_width > 0 && i_left <= options[:g_width]
466 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
467 end
468 if d_width > 0 && i_left <= options[:g_width]
469 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
470 end
471
472
473 # Starting diamond
474 if start_left <= options[:g_width] && start_left > 0
475 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
476 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
477 output << "</div>"
478 end
479
480 # Ending diamond
481 # Don't show items too far ahead
482 if i_left <= options[:g_width] && i_end > 0
483 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
484 end
485
486 # Display the Version name and %
487 if i_end <= options[:g_width]
488 # Display the status even if it's floated off to the left
489 status_px = i_end + 12 # 12px for the diamond
490 status_px = 0 if status_px <= 0
491
492 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
493 output << h("#{version.project} -") unless @project && @project == version.project
494 output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
495 output << "</div>"
496 end
497 443 @lines << output
498 444 output
499 445 when :image
500 446 options[:image].stroke('transparent')
501 447 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
502 448
503 449 # Make sure negative i_left doesn't overflow the subject
504 450 if i_left > options[:subject_width]
505 451 options[:image].fill('green')
506 452 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
507 453 options[:image].fill('black')
508 454 options[:image].text(i_left + 11, options[:top] + 1, version.name)
509 455 end
510 456 when :pdf
511 457 options[:pdf].SetY(options[:top]+1.5)
512 458 i_left = ((version.start_date - @date_from)*options[:zoom])
513 459
514 460 # Make sure negative i_left doesn't overflow the subject
515 461 if i_left > 0
516 462 options[:pdf].SetX(options[:subject_width] + i_left)
517 463 options[:pdf].SetFillColor(50,200,50)
518 464 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
519 465
520 466 options[:pdf].SetY(options[:top]+1.5)
521 467 options[:pdf].SetX(options[:subject_width] + i_left + 3)
522 468 options[:pdf].Cell(30, 2, "#{version.name}")
523 469 end
524 470 end
525 471 else
526 472 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
527 473 ''
528 474 end
529 475 end
530 476
531 477 def subject_for_issue(issue, options)
532 478 case options[:format]
533 479 when :html
534 480 output = ''
535 481 output << "<div class='tooltip'>"
536 482 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> "
537 483 if issue.is_a? Issue
538 484 css_classes = []
539 485 css_classes << 'issue-overdue' if issue.overdue?
540 486 css_classes << 'issue-behind-schedule' if issue.behind_schedule?
541 487 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
542 488
543 489 if issue.assigned_to.present?
544 490 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
545 491 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
546 492 end
547 493 output << "<span class='#{css_classes.join(' ')}'>"
548 494 output << view.link_to_issue(issue)
549 495 output << '</span>'
550 496 else
551 497 ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
552 498 ''
553 499 end
554 500 output << "</small></div>"
555 501
556 502 # Tooltip
557 503 if issue.is_a? Issue
558 504 output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
559 505 output << view.render_issue_tooltip(issue)
560 506 output << "</span>"
561 507 end
562 508
563 509 output << "</div>"
564 510 @subjects << output
565 511 output
566 512 when :image
567 513 options[:image].fill('black')
568 514 options[:image].stroke('transparent')
569 515 options[:image].stroke_width(1)
570 516 options[:image].text(options[:indent], options[:top] + 2, issue.subject)
571 517 when :pdf
572 518 pdf_new_page?(options)
573 519 options[:pdf].SetY(options[:top])
574 520 options[:pdf].SetX(15)
575 521
576 522 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
577 523 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
578 524
579 525 options[:pdf].SetY(options[:top])
580 526 options[:pdf].SetX(options[:subject_width])
581 527 options[:pdf].Cell(options[:g_width], 5, "", "LR")
582 528 end
583 529 end
584 530
585 531 def line_for_issue(issue, options)
586 532 # Skip issues that don't have a due_before (due_date or version's due_date)
587 533 if issue.is_a?(Issue) && issue.due_before
588 534 case options[:format]
589 535 when :html
590 536 coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom])
591 537 css = "task " + (issue.leaf? ? 'leaf' : 'parent')
592 538 output = html_task(options[:top], coords, :css => css, :label => "#{ issue.status.name } #{ issue.done_ratio }%", :issue => issue)
593 539
594 540 @lines << output
595 541 output
596 542
597 543 when :image
598 544 # Handle nil start_dates, rare but can happen.
599 545 i_start_date = if issue.start_date && issue.start_date >= @date_from
600 546 issue.start_date
601 547 else
602 548 @date_from
603 549 end
604 550
605 551 i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
606 552 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
607 553 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
608 554 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
609 555 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
610 556
611 557 i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
612 558 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
613 559 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
614 560 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
615 561
616 562
617 563 # Make sure that negative i_left and i_width don't
618 564 # overflow the subject
619 565 if i_width > 0
620 566 options[:image].fill('grey')
621 567 options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
622 568 options[:image].fill('red')
623 569 options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
624 570 options[:image].fill('blue')
625 571 options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
626 572 end
627 573
628 574 # Show the status and % done next to the subject if it overflows
629 575 options[:image].fill('black')
630 576 if i_width > 0
631 577 options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
632 578 else
633 579 options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
634 580 end
635 581
636 582 when :pdf
637 583 options[:pdf].SetY(options[:top]+1.5)
638 584 # Handle nil start_dates, rare but can happen.
639 585 i_start_date = if issue.start_date && issue.start_date >= @date_from
640 586 issue.start_date
641 587 else
642 588 @date_from
643 589 end
644 590
645 591 i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
646 592
647 593 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
648 594 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
649 595 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
650 596
651 597 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
652 598
653 599 i_left = ((i_start_date - @date_from)*options[:zoom])
654 600 i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
655 601 d_width = ((i_done_date - i_start_date)*options[:zoom])
656 602 l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
657 603 l_width ||= 0
658 604
659 605 # Make sure that negative i_left and i_width don't
660 606 # overflow the subject
661 607 if i_width > 0
662 608 options[:pdf].SetX(options[:subject_width] + i_left)
663 609 options[:pdf].SetFillColor(200,200,200)
664 610 options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
665 611 end
666 612
667 613 if l_width > 0
668 614 options[:pdf].SetY(options[:top]+1.5)
669 615 options[:pdf].SetX(options[:subject_width] + i_left)
670 616 options[:pdf].SetFillColor(255,100,100)
671 617 options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
672 618 end
673 619 if d_width > 0
674 620 options[:pdf].SetY(options[:top]+1.5)
675 621 options[:pdf].SetX(options[:subject_width] + i_left)
676 622 options[:pdf].SetFillColor(100,100,255)
677 623 options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
678 624 end
679 625
680 626 options[:pdf].SetY(options[:top]+1.5)
681 627
682 628 # Make sure that negative i_left and i_width don't
683 629 # overflow the subject
684 630 if (i_left + i_width) >= 0
685 631 options[:pdf].SetX(options[:subject_width] + i_left + i_width)
686 632 else
687 633 options[:pdf].SetX(options[:subject_width])
688 634 end
689 635 options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
690 636 end
691 637 else
692 638 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
693 639 ''
694 640 end
695 641 end
696 642
697 643 # Generates a gantt image
698 644 # Only defined if RMagick is avalaible
699 645 def to_image(format='PNG')
700 646 date_to = (@date_from >> @months)-1
701 647 show_weeks = @zoom > 1
702 648 show_days = @zoom > 2
703 649
704 650 subject_width = 400
705 651 header_heigth = 18
706 652 # width of one day in pixels
707 653 zoom = @zoom*2
708 654 g_width = (@date_to - @date_from + 1)*zoom
709 655 g_height = 20 * number_of_rows + 30
710 656 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
711 657 height = g_height + headers_heigth
712 658
713 659 imgl = Magick::ImageList.new
714 660 imgl.new_image(subject_width+g_width+1, height)
715 661 gc = Magick::Draw.new
716 662
717 663 # Subjects
718 664 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
719 665
720 666 # Months headers
721 667 month_f = @date_from
722 668 left = subject_width
723 669 @months.times do
724 670 width = ((month_f >> 1) - month_f) * zoom
725 671 gc.fill('white')
726 672 gc.stroke('grey')
727 673 gc.stroke_width(1)
728 674 gc.rectangle(left, 0, left + width, height)
729 675 gc.fill('black')
730 676 gc.stroke('transparent')
731 677 gc.stroke_width(1)
732 678 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
733 679 left = left + width
734 680 month_f = month_f >> 1
735 681 end
736 682
737 683 # Weeks headers
738 684 if show_weeks
739 685 left = subject_width
740 686 height = header_heigth
741 687 if @date_from.cwday == 1
742 688 # date_from is monday
743 689 week_f = date_from
744 690 else
745 691 # find next monday after date_from
746 692 week_f = @date_from + (7 - @date_from.cwday + 1)
747 693 width = (7 - @date_from.cwday + 1) * zoom
748 694 gc.fill('white')
749 695 gc.stroke('grey')
750 696 gc.stroke_width(1)
751 697 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
752 698 left = left + width
753 699 end
754 700 while week_f <= date_to
755 701 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
756 702 gc.fill('white')
757 703 gc.stroke('grey')
758 704 gc.stroke_width(1)
759 705 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
760 706 gc.fill('black')
761 707 gc.stroke('transparent')
762 708 gc.stroke_width(1)
763 709 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
764 710 left = left + width
765 711 week_f = week_f+7
766 712 end
767 713 end
768 714
769 715 # Days details (week-end in grey)
770 716 if show_days
771 717 left = subject_width
772 718 height = g_height + header_heigth - 1
773 719 wday = @date_from.cwday
774 720 (date_to - @date_from + 1).to_i.times do
775 721 width = zoom
776 722 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
777 723 gc.stroke('grey')
778 724 gc.stroke_width(1)
779 725 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
780 726 left = left + width
781 727 wday = wday + 1
782 728 wday = 1 if wday > 7
783 729 end
784 730 end
785 731
786 732 # border
787 733 gc.fill('transparent')
788 734 gc.stroke('grey')
789 735 gc.stroke_width(1)
790 736 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
791 737 gc.stroke('black')
792 738 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
793 739
794 740 # content
795 741 top = headers_heigth + 20
796 742
797 743 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
798 744
799 745 # today red line
800 746 if Date.today >= @date_from and Date.today <= date_to
801 747 gc.stroke('red')
802 748 x = (Date.today-@date_from+1)*zoom + subject_width
803 749 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
804 750 end
805 751
806 752 gc.draw(imgl)
807 753 imgl.format = format
808 754 imgl.to_blob
809 755 end if Object.const_defined?(:Magick)
810 756
811 757 def to_pdf
812 758 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
813 759 pdf.SetTitle("#{l(:label_gantt)} #{project}")
814 760 pdf.AliasNbPages
815 761 pdf.footer_date = format_date(Date.today)
816 762 pdf.AddPage("L")
817 763 pdf.SetFontStyle('B',12)
818 764 pdf.SetX(15)
819 765 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
820 766 pdf.Ln
821 767 pdf.SetFontStyle('B',9)
822 768
823 769 subject_width = PDF::LeftPaneWidth
824 770 header_heigth = 5
825 771
826 772 headers_heigth = header_heigth
827 773 show_weeks = false
828 774 show_days = false
829 775
830 776 if self.months < 7
831 777 show_weeks = true
832 778 headers_heigth = 2*header_heigth
833 779 if self.months < 3
834 780 show_days = true
835 781 headers_heigth = 3*header_heigth
836 782 end
837 783 end
838 784
839 785 g_width = PDF.right_pane_width
840 786 zoom = (g_width) / (self.date_to - self.date_from + 1)
841 787 g_height = 120
842 788 t_height = g_height + headers_heigth
843 789
844 790 y_start = pdf.GetY
845 791
846 792 # Months headers
847 793 month_f = self.date_from
848 794 left = subject_width
849 795 height = header_heigth
850 796 self.months.times do
851 797 width = ((month_f >> 1) - month_f) * zoom
852 798 pdf.SetY(y_start)
853 799 pdf.SetX(left)
854 800 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
855 801 left = left + width
856 802 month_f = month_f >> 1
857 803 end
858 804
859 805 # Weeks headers
860 806 if show_weeks
861 807 left = subject_width
862 808 height = header_heigth
863 809 if self.date_from.cwday == 1
864 810 # self.date_from is monday
865 811 week_f = self.date_from
866 812 else
867 813 # find next monday after self.date_from
868 814 week_f = self.date_from + (7 - self.date_from.cwday + 1)
869 815 width = (7 - self.date_from.cwday + 1) * zoom-1
870 816 pdf.SetY(y_start + header_heigth)
871 817 pdf.SetX(left)
872 818 pdf.Cell(width + 1, height, "", "LTR")
873 819 left = left + width+1
874 820 end
875 821 while week_f <= self.date_to
876 822 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
877 823 pdf.SetY(y_start + header_heigth)
878 824 pdf.SetX(left)
879 825 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
880 826 left = left + width
881 827 week_f = week_f+7
882 828 end
883 829 end
884 830
885 831 # Days headers
886 832 if show_days
887 833 left = subject_width
888 834 height = header_heigth
889 835 wday = self.date_from.cwday
890 836 pdf.SetFontStyle('B',7)
891 837 (self.date_to - self.date_from + 1).to_i.times do
892 838 width = zoom
893 839 pdf.SetY(y_start + 2 * header_heigth)
894 840 pdf.SetX(left)
895 841 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
896 842 left = left + width
897 843 wday = wday + 1
898 844 wday = 1 if wday > 7
899 845 end
900 846 end
901 847
902 848 pdf.SetY(y_start)
903 849 pdf.SetX(15)
904 850 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
905 851
906 852 # Tasks
907 853 top = headers_heigth + y_start
908 854 options = {
909 855 :top => top,
910 856 :zoom => zoom,
911 857 :subject_width => subject_width,
912 858 :g_width => g_width,
913 859 :indent => 0,
914 860 :indent_increment => 5,
915 861 :top_increment => 5,
916 862 :format => :pdf,
917 863 :pdf => pdf
918 864 }
919 865 render(options)
920 866 pdf.Output
921 867 end
922 868
923 869 private
924 870
925 871 def coordinates(start_date, end_date, progress, zoom=nil)
926 872 zoom ||= @zoom
927 873
928 874 coords = {}
929 875 if start_date && end_date && start_date < self.date_to && end_date > self.date_from
930 876 if start_date > self.date_from
931 877 coords[:start] = start_date - self.date_from
932 878 coords[:bar_start] = start_date - self.date_from
933 879 else
934 880 coords[:bar_start] = 0
935 881 end
936 882 if end_date < self.date_to
937 883 coords[:end] = end_date - self.date_from
938 884 coords[:bar_end] = end_date - self.date_from + 1
939 885 else
940 886 coords[:bar_end] = self.date_to - self.date_from + 1
941 887 end
942 888
943 889 if progress
944 890 progress_date = start_date + (end_date - start_date) * (progress / 100.0)
945 891 if progress_date > self.date_from && progress_date > start_date
946 892 if progress_date < self.date_to
947 893 coords[:bar_progress_end] = progress_date - self.date_from + 1
948 894 else
949 895 coords[:bar_progress_end] = self.date_to - self.date_from + 1
950 896 end
951 897 end
952 898
953 899 if progress_date < Date.today
954 900 late_date = [Date.today, end_date].min
955 901 if late_date > self.date_from && late_date > start_date
956 902 if late_date < self.date_to
957 903 coords[:bar_late_end] = late_date - self.date_from + 1
958 904 else
959 905 coords[:bar_late_end] = self.date_to - self.date_from + 1
960 906 end
961 907 end
962 908 end
963 909 end
964 910 end
965 911
966 912 # Transforms dates into pixels witdh
967 913 coords.keys.each do |key|
968 914 coords[key] = (coords[key] * zoom).floor
969 915 end
970 916 coords
971 917 end
972 918
973 919 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
974 920 def sort_issues!(issues)
975 921 issues.sort! do |a, b|
976 922 cmp = 0
977 923 cmp = (a.start_date <=> b.start_date) if a.start_date? && b.start_date?
978 924 cmp = (a.due_date <=> b.due_date) if cmp == 0 && a.due_date? && b.due_date?
979 925 cmp = (a.id <=> b.id) if cmp == 0
980 926 cmp
981 927 end
982 928 end
983 929
984 930 def current_limit
985 931 if @max_rows
986 932 @max_rows - @number_of_rows
987 933 else
988 934 nil
989 935 end
990 936 end
991 937
992 938 def abort?
993 939 if @max_rows && @number_of_rows >= @max_rows
994 940 @truncated = true
995 941 end
996 942 end
997 943
998 944 def pdf_new_page?(options)
999 945 if options[:top] > 180
1000 946 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
1001 947 options[:pdf].AddPage("L")
1002 948 options[:top] = 15
1003 949 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
1004 950 end
1005 951 end
1006 952
1007 953 def html_task(top, coords, options={})
1008 954 output = ''
1009 955 # Renders the task bar, with progress and late
1010 956 if coords[:bar_start] && coords[:bar_end]
1011 957 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>"
1012 958
1013 959 if coords[:bar_late_end]
1014 960 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>"
1015 961 end
1016 962 if coords[:bar_progress_end]
1017 963 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>"
1018 964 end
1019 965 end
966 # Renders the markers
967 if options[:markers]
968 if coords[:start]
969 output << "<div style='top:#{ top }px;left:#{ coords[:start] }px;width:15px;' class='#{options[:css]} marker starting'>&nbsp;</div>"
970 end
971 if coords[:end]
972 output << "<div style='top:#{ top }px;left:#{ coords[:end] }px;width:15px;' class='#{options[:css]} marker ending'>&nbsp;</div>"
973 end
974 end
1020 975 # Renders the label on the right
1021 976 if options[:label]
1022 977 output << "<div style='top:#{ top }px;left:#{ (coords[:bar_end] || 0) + 5 }px;' class='#{options[:css]} label'>"
1023 978 output << options[:label]
1024 979 output << "</div>"
1025 980 end
1026 981 # Renders the tooltip
1027 982 if options[:issue] && coords[:bar_start] && coords[:bar_end]
1028 983 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;'>"
1029 984 output << '<span class="tip">'
1030 985 output << view.render_issue_tooltip(options[:issue])
1031 986 output << "</span></div>"
1032 987 end
1033 988
1034 989 output
1035 990 end
1036 991 end
1037 992 end
1038 993 end
@@ -1,945 +1,946
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 794
795 795 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
796 796 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
797 797 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
798 798
799 799 .task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
800 800 .task_late.parent, .task_done.parent { height: 3px;}
801 801 .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 802 .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 803
804 .milestone { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; }
805 .milestone_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
806 .milestone_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
807 .milestone_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
804 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
805 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
806 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
807 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; }
808
808 809 .project-line { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; }
809 810 .project_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
810 811 .project_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
811 812 .project_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
812 813
813 814 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
814 815 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
815 816
816 817 /***** Icons *****/
817 818 .icon {
818 819 background-position: 0% 50%;
819 820 background-repeat: no-repeat;
820 821 padding-left: 20px;
821 822 padding-top: 2px;
822 823 padding-bottom: 3px;
823 824 }
824 825
825 826 .icon-add { background-image: url(../images/add.png); }
826 827 .icon-edit { background-image: url(../images/edit.png); }
827 828 .icon-copy { background-image: url(../images/copy.png); }
828 829 .icon-duplicate { background-image: url(../images/duplicate.png); }
829 830 .icon-del { background-image: url(../images/delete.png); }
830 831 .icon-move { background-image: url(../images/move.png); }
831 832 .icon-save { background-image: url(../images/save.png); }
832 833 .icon-cancel { background-image: url(../images/cancel.png); }
833 834 .icon-multiple { background-image: url(../images/table_multiple.png); }
834 835 .icon-folder { background-image: url(../images/folder.png); }
835 836 .open .icon-folder { background-image: url(../images/folder_open.png); }
836 837 .icon-package { background-image: url(../images/package.png); }
837 838 .icon-home { background-image: url(../images/home.png); }
838 839 .icon-user { background-image: url(../images/user.png); }
839 840 .icon-projects { background-image: url(../images/projects.png); }
840 841 .icon-help { background-image: url(../images/help.png); }
841 842 .icon-attachment { background-image: url(../images/attachment.png); }
842 843 .icon-history { background-image: url(../images/history.png); }
843 844 .icon-time { background-image: url(../images/time.png); }
844 845 .icon-time-add { background-image: url(../images/time_add.png); }
845 846 .icon-stats { background-image: url(../images/stats.png); }
846 847 .icon-warning { background-image: url(../images/warning.png); }
847 848 .icon-fav { background-image: url(../images/fav.png); }
848 849 .icon-fav-off { background-image: url(../images/fav_off.png); }
849 850 .icon-reload { background-image: url(../images/reload.png); }
850 851 .icon-lock { background-image: url(../images/locked.png); }
851 852 .icon-unlock { background-image: url(../images/unlock.png); }
852 853 .icon-checked { background-image: url(../images/true.png); }
853 854 .icon-details { background-image: url(../images/zoom_in.png); }
854 855 .icon-report { background-image: url(../images/report.png); }
855 856 .icon-comment { background-image: url(../images/comment.png); }
856 857 .icon-summary { background-image: url(../images/lightning.png); }
857 858 .icon-server-authentication { background-image: url(../images/server_key.png); }
858 859 .icon-issue { background-image: url(../images/ticket.png); }
859 860 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
860 861 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
861 862
862 863 .icon-file { background-image: url(../images/files/default.png); }
863 864 .icon-file.text-plain { background-image: url(../images/files/text.png); }
864 865 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
865 866 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
866 867 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
867 868 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
868 869 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
869 870 .icon-file.image-gif { background-image: url(../images/files/image.png); }
870 871 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
871 872 .icon-file.image-png { background-image: url(../images/files/image.png); }
872 873 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
873 874 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
874 875 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
875 876 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
876 877
877 878 img.gravatar {
878 879 padding: 2px;
879 880 border: solid 1px #d5d5d5;
880 881 background: #fff;
881 882 }
882 883
883 884 div.issue img.gravatar {
884 885 float: right;
885 886 margin: 0 0 0 1em;
886 887 padding: 5px;
887 888 }
888 889
889 890 div.issue table img.gravatar {
890 891 height: 14px;
891 892 width: 14px;
892 893 padding: 2px;
893 894 float: left;
894 895 margin: 0 0.5em 0 0;
895 896 }
896 897
897 898 h2 img.gravatar {
898 899 padding: 3px;
899 900 margin: -2px 4px -4px 0;
900 901 vertical-align: top;
901 902 }
902 903
903 904 h4 img.gravatar {
904 905 padding: 3px;
905 906 margin: -6px 0 -4px 0;
906 907 vertical-align: top;
907 908 }
908 909
909 910 td.username img.gravatar {
910 911 float: left;
911 912 margin: 0 1em 0 0;
912 913 }
913 914
914 915 #activity dt img.gravatar {
915 916 float: left;
916 917 margin: 0 1em 1em 0;
917 918 }
918 919
919 920 /* Used on 12px Gravatar img tags without the icon background */
920 921 .icon-gravatar {
921 922 float: left;
922 923 margin-right: 4px;
923 924 }
924 925
925 926 #activity dt,
926 927 .journal {
927 928 clear: left;
928 929 }
929 930
930 931 .journal-link {
931 932 float: right;
932 933 }
933 934
934 935 h2 img { vertical-align:middle; }
935 936
936 937 .hascontextmenu { cursor: context-menu; }
937 938
938 939 /***** Media print specific styles *****/
939 940 @media print {
940 941 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
941 942 #main { background: #fff; }
942 943 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
943 944 #wiki_add_attachment { display:none; }
944 945 .hide-when-print { display: none; }
945 946 }
@@ -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 219 assert_select "div.project_todo"
220 220 assert_select "div.project-line.starting"
221 221 assert_select "div.project-line.ending"
222 222 assert_select "div.label.project-name", /#{@project.name}/
223 223 end
224 224 end
225 225
226 226 context "version" do
227 227 should "be rendered" do
228 assert_select "div.milestone_todo"
229 assert_select "div.milestone.starting"
230 assert_select "div.milestone.ending"
231 assert_select "div.label.version-name", /#{@version.name}/
228 assert_select "div.version.task_todo"
229 assert_select "div.version.starting"
230 assert_select "div.version.ending"
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 313 :start_date => Date.yesterday,
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 321 assert_select "div.project_todo[style*=left:52px]"
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 326 assert_select "div.project_todo[style*=width:31px]"
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 334 assert_select "div.project_late[style*=left:52px]"
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 339 assert_select "div.project_late[style*=width:6px]"
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 346 assert_select "div.project_done[style*=left:52px]"
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 351 assert_select "div.project_done[style*=left:52px]"
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 361 assert_select "div.project-line.starting", false
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 366 assert_select "div.project-line.starting[style*=left:52px]"
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 376 assert_select "div.project-line.ending", false
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 382 assert_select "div.project-line.ending[style*=left:84px]"
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 391 assert_select "div.project-name", /#{@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 396 assert_select "div.project-name", /#{@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 401 assert_select "div.project-name", /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 :start_date => Date.yesterday,
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 assert_select "div.milestone_todo[style*=left:52px]"
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 assert_select "div.milestone_todo[style*=width:31px]"
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 assert_select "div.milestone_late[style*=left:52px]"
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 assert_select "div.milestone_late[style*=width:6px]"
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 assert_select "div.milestone_done[style*=left:52px]"
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 assert_select "div.milestone_done[style*=left:52px]"
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 assert_select "div.milestone.starting", false
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 assert_select "div.milestone.starting[style*=left:52px]"
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 assert_select "div.milestone.ending", false
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 assert_select "div.milestone.ending[style*=left:84px]"
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 assert_select "div.version-name", /#{@version.name}/
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 assert_select "div.version-name", /#{@version.name}/
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 assert_select "div.version-name", /30%/
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