Revision 1298:4f746d8966dd .svn/pristine/02
| .svn/pristine/02/025612e4a47b7cb1c36f52b9661b0cc1f94b2a45.svn-base | ||
|---|---|---|
| 1 |
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html |
|
| 2 |
one: |
|
| 3 |
id: 1 |
|
| 4 |
<% for attribute in attributes -%> |
|
| 5 |
<%= attribute.name %>: <%= attribute.default %> |
|
| 6 |
<% end -%> |
|
| 7 |
two: |
|
| 8 |
id: 2 |
|
| 9 |
<% for attribute in attributes -%> |
|
| 10 |
<%= attribute.name %>: <%= attribute.default %> |
|
| 11 |
<% end -%> |
|
| .svn/pristine/02/026bb2d46eaa121e79862de3515bb99ff0827ca3.svn-base | ||
|---|---|---|
| 1 |
# Redmine - project management software |
|
| 2 |
# Copyright (C) 2006-2013 Jean-Philippe Lang |
|
| 3 |
# |
|
| 4 |
# This program is free software; you can redistribute it and/or |
|
| 5 |
# modify it under the terms of the GNU General Public License |
|
| 6 |
# as published by the Free Software Foundation; either version 2 |
|
| 7 |
# of the License, or (at your option) any later version. |
|
| 8 |
# |
|
| 9 |
# This program is distributed in the hope that it will be useful, |
|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 12 |
# GNU General Public License for more details. |
|
| 13 |
# |
|
| 14 |
# You should have received a copy of the GNU General Public License |
|
| 15 |
# along with this program; if not, write to the Free Software |
|
| 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 17 |
|
|
| 18 |
module Redmine |
|
| 19 |
module Helpers |
|
| 20 |
# Simple class to handle gantt chart data |
|
| 21 |
class Gantt |
|
| 22 |
include ERB::Util |
|
| 23 |
include Redmine::I18n |
|
| 24 |
include Redmine::Utils::DateCalculation |
|
| 25 |
|
|
| 26 |
# Relation types that are rendered |
|
| 27 |
DRAW_TYPES = {
|
|
| 28 |
IssueRelation::TYPE_BLOCKS => { :landscape_margin => 16, :color => '#F34F4F' },
|
|
| 29 |
IssueRelation::TYPE_PRECEDES => { :landscape_margin => 20, :color => '#628FEA' }
|
|
| 30 |
}.freeze |
|
| 31 |
|
|
| 32 |
# :nodoc: |
|
| 33 |
# Some utility methods for the PDF export |
|
| 34 |
class PDF |
|
| 35 |
MaxCharactorsForSubject = 45 |
|
| 36 |
TotalWidth = 280 |
|
| 37 |
LeftPaneWidth = 100 |
|
| 38 |
|
|
| 39 |
def self.right_pane_width |
|
| 40 |
TotalWidth - LeftPaneWidth |
|
| 41 |
end |
|
| 42 |
end |
|
| 43 |
|
|
| 44 |
attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :truncated, :max_rows |
|
| 45 |
attr_accessor :query |
|
| 46 |
attr_accessor :project |
|
| 47 |
attr_accessor :view |
|
| 48 |
|
|
| 49 |
def initialize(options={})
|
|
| 50 |
options = options.dup |
|
| 51 |
if options[:year] && options[:year].to_i >0 |
|
| 52 |
@year_from = options[:year].to_i |
|
| 53 |
if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12 |
|
| 54 |
@month_from = options[:month].to_i |
|
| 55 |
else |
|
| 56 |
@month_from = 1 |
|
| 57 |
end |
|
| 58 |
else |
|
| 59 |
@month_from ||= Date.today.month |
|
| 60 |
@year_from ||= Date.today.year |
|
| 61 |
end |
|
| 62 |
zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i |
|
| 63 |
@zoom = (zoom > 0 && zoom < 5) ? zoom : 2 |
|
| 64 |
months = (options[:months] || User.current.pref[:gantt_months]).to_i |
|
| 65 |
@months = (months > 0 && months < 25) ? months : 6 |
|
| 66 |
# Save gantt parameters as user preference (zoom and months count) |
|
| 67 |
if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || |
|
| 68 |
@months != User.current.pref[:gantt_months])) |
|
| 69 |
User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months |
|
| 70 |
User.current.preference.save |
|
| 71 |
end |
|
| 72 |
@date_from = Date.civil(@year_from, @month_from, 1) |
|
| 73 |
@date_to = (@date_from >> @months) - 1 |
|
| 74 |
@subjects = '' |
|
| 75 |
@lines = '' |
|
| 76 |
@number_of_rows = nil |
|
| 77 |
@issue_ancestors = [] |
|
| 78 |
@truncated = false |
|
| 79 |
if options.has_key?(:max_rows) |
|
| 80 |
@max_rows = options[:max_rows] |
|
| 81 |
else |
|
| 82 |
@max_rows = Setting.gantt_items_limit.blank? ? nil : Setting.gantt_items_limit.to_i |
|
| 83 |
end |
|
| 84 |
end |
|
| 85 |
|
|
| 86 |
def common_params |
|
| 87 |
{ :controller => 'gantts', :action => 'show', :project_id => @project }
|
|
| 88 |
end |
|
| 89 |
|
|
| 90 |
def params |
|
| 91 |
common_params.merge({:zoom => zoom, :year => year_from,
|
|
| 92 |
:month => month_from, :months => months}) |
|
| 93 |
end |
|
| 94 |
|
|
| 95 |
def params_previous |
|
| 96 |
common_params.merge({:year => (date_from << months).year,
|
|
| 97 |
:month => (date_from << months).month, |
|
| 98 |
:zoom => zoom, :months => months}) |
|
| 99 |
end |
|
| 100 |
|
|
| 101 |
def params_next |
|
| 102 |
common_params.merge({:year => (date_from >> months).year,
|
|
| 103 |
:month => (date_from >> months).month, |
|
| 104 |
:zoom => zoom, :months => months}) |
|
| 105 |
end |
|
| 106 |
|
|
| 107 |
# Returns the number of rows that will be rendered on the Gantt chart |
|
| 108 |
def number_of_rows |
|
| 109 |
return @number_of_rows if @number_of_rows |
|
| 110 |
rows = projects.inject(0) {|total, p| total += number_of_rows_on_project(p)}
|
|
| 111 |
rows > @max_rows ? @max_rows : rows |
|
| 112 |
end |
|
| 113 |
|
|
| 114 |
# Returns the number of rows that will be used to list a project on |
|
| 115 |
# the Gantt chart. This will recurse for each subproject. |
|
| 116 |
def number_of_rows_on_project(project) |
|
| 117 |
return 0 unless projects.include?(project) |
|
| 118 |
count = 1 |
|
| 119 |
count += project_issues(project).size |
|
| 120 |
count += project_versions(project).size |
|
| 121 |
count |
|
| 122 |
end |
|
| 123 |
|
|
| 124 |
# Renders the subjects of the Gantt chart, the left side. |
|
| 125 |
def subjects(options={})
|
|
| 126 |
render(options.merge(:only => :subjects)) unless @subjects_rendered |
|
| 127 |
@subjects |
|
| 128 |
end |
|
| 129 |
|
|
| 130 |
# Renders the lines of the Gantt chart, the right side |
|
| 131 |
def lines(options={})
|
|
| 132 |
render(options.merge(:only => :lines)) unless @lines_rendered |
|
| 133 |
@lines |
|
| 134 |
end |
|
| 135 |
|
|
| 136 |
# Returns issues that will be rendered |
|
| 137 |
def issues |
|
| 138 |
@issues ||= @query.issues( |
|
| 139 |
:include => [:assigned_to, :tracker, :priority, :category, :fixed_version], |
|
| 140 |
:order => "#{Project.table_name}.lft ASC, #{Issue.table_name}.id ASC",
|
|
| 141 |
:limit => @max_rows |
|
| 142 |
) |
|
| 143 |
end |
|
| 144 |
|
|
| 145 |
# Returns a hash of the relations between the issues that are present on the gantt |
|
| 146 |
# and that should be displayed, grouped by issue ids. |
|
| 147 |
def relations |
|
| 148 |
return @relations if @relations |
|
| 149 |
if issues.any? |
|
| 150 |
issue_ids = issues.map(&:id) |
|
| 151 |
@relations = IssueRelation. |
|
| 152 |
where(:issue_from_id => issue_ids, :issue_to_id => issue_ids, :relation_type => DRAW_TYPES.keys). |
|
| 153 |
group_by(&:issue_from_id) |
|
| 154 |
else |
|
| 155 |
@relations = {}
|
|
| 156 |
end |
|
| 157 |
end |
|
| 158 |
|
|
| 159 |
# Return all the project nodes that will be displayed |
|
| 160 |
def projects |
|
| 161 |
return @projects if @projects |
|
| 162 |
ids = issues.collect(&:project).uniq.collect(&:id) |
|
| 163 |
if ids.any? |
|
| 164 |
# All issues projects and their visible ancestors |
|
| 165 |
@projects = Project.visible.all( |
|
| 166 |
:joins => "LEFT JOIN #{Project.table_name} child ON #{Project.table_name}.lft <= child.lft AND #{Project.table_name}.rgt >= child.rgt",
|
|
| 167 |
:conditions => ["child.id IN (?)", ids], |
|
| 168 |
:order => "#{Project.table_name}.lft ASC"
|
|
| 169 |
).uniq |
|
| 170 |
else |
|
| 171 |
@projects = [] |
|
| 172 |
end |
|
| 173 |
end |
|
| 174 |
|
|
| 175 |
# Returns the issues that belong to +project+ |
|
| 176 |
def project_issues(project) |
|
| 177 |
@issues_by_project ||= issues.group_by(&:project) |
|
| 178 |
@issues_by_project[project] || [] |
|
| 179 |
end |
|
| 180 |
|
|
| 181 |
# Returns the distinct versions of the issues that belong to +project+ |
|
| 182 |
def project_versions(project) |
|
| 183 |
project_issues(project).collect(&:fixed_version).compact.uniq |
|
| 184 |
end |
|
| 185 |
|
|
| 186 |
# Returns the issues that belong to +project+ and are assigned to +version+ |
|
| 187 |
def version_issues(project, version) |
|
| 188 |
project_issues(project).select {|issue| issue.fixed_version == version}
|
|
| 189 |
end |
|
| 190 |
|
|
| 191 |
def render(options={})
|
|
| 192 |
options = {:top => 0, :top_increment => 20,
|
|
| 193 |
:indent_increment => 20, :render => :subject, |
|
| 194 |
:format => :html}.merge(options) |
|
| 195 |
indent = options[:indent] || 4 |
|
| 196 |
@subjects = '' unless options[:only] == :lines |
|
| 197 |
@lines = '' unless options[:only] == :subjects |
|
| 198 |
@number_of_rows = 0 |
|
| 199 |
Project.project_tree(projects) do |project, level| |
|
| 200 |
options[:indent] = indent + level * options[:indent_increment] |
|
| 201 |
render_project(project, options) |
|
| 202 |
break if abort? |
|
| 203 |
end |
|
| 204 |
@subjects_rendered = true unless options[:only] == :lines |
|
| 205 |
@lines_rendered = true unless options[:only] == :subjects |
|
| 206 |
render_end(options) |
|
| 207 |
end |
|
| 208 |
|
|
| 209 |
def render_project(project, options={})
|
|
| 210 |
subject_for_project(project, options) unless options[:only] == :lines |
|
| 211 |
line_for_project(project, options) unless options[:only] == :subjects |
|
| 212 |
options[:top] += options[:top_increment] |
|
| 213 |
options[:indent] += options[:indent_increment] |
|
| 214 |
@number_of_rows += 1 |
|
| 215 |
return if abort? |
|
| 216 |
issues = project_issues(project).select {|i| i.fixed_version.nil?}
|
|
| 217 |
sort_issues!(issues) |
|
| 218 |
if issues |
|
| 219 |
render_issues(issues, options) |
|
| 220 |
return if abort? |
|
| 221 |
end |
|
| 222 |
versions = project_versions(project) |
|
| 223 |
versions.each do |version| |
|
| 224 |
render_version(project, version, options) |
|
| 225 |
end |
|
| 226 |
# Remove indent to hit the next sibling |
|
| 227 |
options[:indent] -= options[:indent_increment] |
|
| 228 |
end |
|
| 229 |
|
|
| 230 |
def render_issues(issues, options={})
|
|
| 231 |
@issue_ancestors = [] |
|
| 232 |
issues.each do |i| |
|
| 233 |
subject_for_issue(i, options) unless options[:only] == :lines |
|
| 234 |
line_for_issue(i, options) unless options[:only] == :subjects |
|
| 235 |
options[:top] += options[:top_increment] |
|
| 236 |
@number_of_rows += 1 |
|
| 237 |
break if abort? |
|
| 238 |
end |
|
| 239 |
options[:indent] -= (options[:indent_increment] * @issue_ancestors.size) |
|
| 240 |
end |
|
| 241 |
|
|
| 242 |
def render_version(project, version, options={})
|
|
| 243 |
# Version header |
|
| 244 |
subject_for_version(version, options) unless options[:only] == :lines |
|
| 245 |
line_for_version(version, options) unless options[:only] == :subjects |
|
| 246 |
options[:top] += options[:top_increment] |
|
| 247 |
@number_of_rows += 1 |
|
| 248 |
return if abort? |
|
| 249 |
issues = version_issues(project, version) |
|
| 250 |
if issues |
|
| 251 |
sort_issues!(issues) |
|
| 252 |
# Indent issues |
|
| 253 |
options[:indent] += options[:indent_increment] |
|
| 254 |
render_issues(issues, options) |
|
| 255 |
options[:indent] -= options[:indent_increment] |
|
| 256 |
end |
|
| 257 |
end |
|
| 258 |
|
|
| 259 |
def render_end(options={})
|
|
| 260 |
case options[:format] |
|
| 261 |
when :pdf |
|
| 262 |
options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) |
|
| 263 |
end |
|
| 264 |
end |
|
| 265 |
|
|
| 266 |
def subject_for_project(project, options) |
|
| 267 |
case options[:format] |
|
| 268 |
when :html |
|
| 269 |
html_class = "" |
|
| 270 |
html_class << 'icon icon-projects ' |
|
| 271 |
html_class << (project.overdue? ? 'project-overdue' : '') |
|
| 272 |
s = view.link_to_project(project).html_safe |
|
| 273 |
subject = view.content_tag(:span, s, |
|
| 274 |
:class => html_class).html_safe |
|
| 275 |
html_subject(options, subject, :css => "project-name") |
|
| 276 |
when :image |
|
| 277 |
image_subject(options, project.name) |
|
| 278 |
when :pdf |
|
| 279 |
pdf_new_page?(options) |
|
| 280 |
pdf_subject(options, project.name) |
|
| 281 |
end |
|
| 282 |
end |
|
| 283 |
|
|
| 284 |
def line_for_project(project, options) |
|
| 285 |
# Skip versions that don't have a start_date or due date |
|
| 286 |
if project.is_a?(Project) && project.start_date && project.due_date |
|
| 287 |
options[:zoom] ||= 1 |
|
| 288 |
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] |
|
| 289 |
coords = coordinates(project.start_date, project.due_date, nil, options[:zoom]) |
|
| 290 |
label = h(project) |
|
| 291 |
case options[:format] |
|
| 292 |
when :html |
|
| 293 |
html_task(options, coords, :css => "project task", :label => label, :markers => true) |
|
| 294 |
when :image |
|
| 295 |
image_task(options, coords, :label => label, :markers => true, :height => 3) |
|
| 296 |
when :pdf |
|
| 297 |
pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) |
|
| 298 |
end |
|
| 299 |
else |
|
| 300 |
'' |
|
| 301 |
end |
|
| 302 |
end |
|
| 303 |
|
|
| 304 |
def subject_for_version(version, options) |
|
| 305 |
case options[:format] |
|
| 306 |
when :html |
|
| 307 |
html_class = "" |
|
| 308 |
html_class << 'icon icon-package ' |
|
| 309 |
html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " |
|
| 310 |
html_class << (version.overdue? ? 'version-overdue' : '') |
|
| 311 |
html_class << ' version-closed' unless version.open? |
|
| 312 |
if version.start_date && version.due_date && version.completed_pourcent |
|
| 313 |
progress_date = calc_progress_date(version.start_date, |
|
| 314 |
version.due_date, version.completed_pourcent) |
|
| 315 |
html_class << ' behind-start-date' if progress_date < self.date_from |
|
| 316 |
html_class << ' over-end-date' if progress_date > self.date_to |
|
| 317 |
end |
|
| 318 |
s = view.link_to_version(version).html_safe |
|
| 319 |
subject = view.content_tag(:span, s, |
|
| 320 |
:class => html_class).html_safe |
|
| 321 |
html_subject(options, subject, :css => "version-name", |
|
| 322 |
:id => "version-#{version.id}")
|
|
| 323 |
when :image |
|
| 324 |
image_subject(options, version.to_s_with_project) |
|
| 325 |
when :pdf |
|
| 326 |
pdf_new_page?(options) |
|
| 327 |
pdf_subject(options, version.to_s_with_project) |
|
| 328 |
end |
|
| 329 |
end |
|
| 330 |
|
|
| 331 |
def line_for_version(version, options) |
|
| 332 |
# Skip versions that don't have a start_date |
|
| 333 |
if version.is_a?(Version) && version.due_date && version.start_date |
|
| 334 |
options[:zoom] ||= 1 |
|
| 335 |
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom] |
|
| 336 |
coords = coordinates(version.start_date, |
|
| 337 |
version.due_date, version.completed_percent, |
|
| 338 |
options[:zoom]) |
|
| 339 |
label = "#{h version} #{h version.completed_percent.to_i.to_s}%"
|
|
| 340 |
label = h("#{version.project} -") + label unless @project && @project == version.project
|
|
| 341 |
case options[:format] |
|
| 342 |
when :html |
|
| 343 |
html_task(options, coords, :css => "version task", |
|
| 344 |
:label => label, :markers => true, :version => version) |
|
| 345 |
when :image |
|
| 346 |
image_task(options, coords, :label => label, :markers => true, :height => 3) |
|
| 347 |
when :pdf |
|
| 348 |
pdf_task(options, coords, :label => label, :markers => true, :height => 0.8) |
|
| 349 |
end |
|
| 350 |
else |
|
| 351 |
'' |
|
| 352 |
end |
|
| 353 |
end |
|
| 354 |
|
|
| 355 |
def subject_for_issue(issue, options) |
|
| 356 |
while @issue_ancestors.any? && !issue.is_descendant_of?(@issue_ancestors.last) |
|
| 357 |
@issue_ancestors.pop |
|
| 358 |
options[:indent] -= options[:indent_increment] |
|
| 359 |
end |
|
| 360 |
output = case options[:format] |
|
| 361 |
when :html |
|
| 362 |
css_classes = '' |
|
| 363 |
css_classes << ' issue-overdue' if issue.overdue? |
|
| 364 |
css_classes << ' issue-behind-schedule' if issue.behind_schedule? |
|
| 365 |
css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to |
|
| 366 |
css_classes << ' issue-closed' if issue.closed? |
|
| 367 |
if issue.start_date && issue.due_before && issue.done_ratio |
|
| 368 |
progress_date = calc_progress_date(issue.start_date, |
|
| 369 |
issue.due_before, issue.done_ratio) |
|
| 370 |
css_classes << ' behind-start-date' if progress_date < self.date_from |
|
| 371 |
css_classes << ' over-end-date' if progress_date > self.date_to |
|
| 372 |
end |
|
| 373 |
s = "".html_safe |
|
| 374 |
if issue.assigned_to.present? |
|
| 375 |
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name |
|
| 376 |
s << view.avatar(issue.assigned_to, |
|
| 377 |
:class => 'gravatar icon-gravatar', |
|
| 378 |
:size => 10, |
|
| 379 |
:title => assigned_string).to_s.html_safe |
|
| 380 |
end |
|
| 381 |
s << view.link_to_issue(issue).html_safe |
|
| 382 |
subject = view.content_tag(:span, s, :class => css_classes).html_safe |
|
| 383 |
html_subject(options, subject, :css => "issue-subject", |
|
| 384 |
:title => issue.subject, :id => "issue-#{issue.id}") + "\n"
|
|
| 385 |
when :image |
|
| 386 |
image_subject(options, issue.subject) |
|
| 387 |
when :pdf |
|
| 388 |
pdf_new_page?(options) |
|
| 389 |
pdf_subject(options, issue.subject) |
|
| 390 |
end |
|
| 391 |
unless issue.leaf? |
|
| 392 |
@issue_ancestors << issue |
|
| 393 |
options[:indent] += options[:indent_increment] |
|
| 394 |
end |
|
| 395 |
output |
|
| 396 |
end |
|
| 397 |
|
|
| 398 |
def line_for_issue(issue, options) |
|
| 399 |
# Skip issues that don't have a due_before (due_date or version's due_date) |
|
| 400 |
if issue.is_a?(Issue) && issue.due_before |
|
| 401 |
coords = coordinates(issue.start_date, issue.due_before, issue.done_ratio, options[:zoom]) |
|
| 402 |
label = "#{issue.status.name} #{issue.done_ratio}%"
|
|
| 403 |
case options[:format] |
|
| 404 |
when :html |
|
| 405 |
html_task(options, coords, |
|
| 406 |
:css => "task " + (issue.leaf? ? 'leaf' : 'parent'), |
|
| 407 |
:label => label, :issue => issue, |
|
| 408 |
:markers => !issue.leaf?) |
|
| 409 |
when :image |
|
| 410 |
image_task(options, coords, :label => label) |
|
| 411 |
when :pdf |
|
| 412 |
pdf_task(options, coords, :label => label) |
|
| 413 |
end |
|
| 414 |
else |
|
| 415 |
'' |
|
| 416 |
end |
|
| 417 |
end |
|
| 418 |
|
|
| 419 |
# Generates a gantt image |
|
| 420 |
# Only defined if RMagick is avalaible |
|
| 421 |
def to_image(format='PNG') |
|
| 422 |
date_to = (@date_from >> @months) - 1 |
|
| 423 |
show_weeks = @zoom > 1 |
|
| 424 |
show_days = @zoom > 2 |
|
| 425 |
subject_width = 400 |
|
| 426 |
header_height = 18 |
|
| 427 |
# width of one day in pixels |
|
| 428 |
zoom = @zoom * 2 |
|
| 429 |
g_width = (@date_to - @date_from + 1) * zoom |
|
| 430 |
g_height = 20 * number_of_rows + 30 |
|
| 431 |
headers_height = (show_weeks ? 2 * header_height : header_height) |
|
| 432 |
height = g_height + headers_height |
|
| 433 |
imgl = Magick::ImageList.new |
|
| 434 |
imgl.new_image(subject_width + g_width + 1, height) |
|
| 435 |
gc = Magick::Draw.new |
|
| 436 |
gc.font = Redmine::Configuration['rmagick_font_path'] || "" |
|
| 437 |
# Subjects |
|
| 438 |
gc.stroke('transparent')
|
|
| 439 |
subjects(:image => gc, :top => (headers_height + 20), :indent => 4, :format => :image) |
|
| 440 |
# Months headers |
|
| 441 |
month_f = @date_from |
|
| 442 |
left = subject_width |
|
| 443 |
@months.times do |
|
| 444 |
width = ((month_f >> 1) - month_f) * zoom |
|
| 445 |
gc.fill('white')
|
|
| 446 |
gc.stroke('grey')
|
|
| 447 |
gc.stroke_width(1) |
|
| 448 |
gc.rectangle(left, 0, left + width, height) |
|
| 449 |
gc.fill('black')
|
|
| 450 |
gc.stroke('transparent')
|
|
| 451 |
gc.stroke_width(1) |
|
| 452 |
gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
|
|
| 453 |
left = left + width |
|
| 454 |
month_f = month_f >> 1 |
|
| 455 |
end |
|
| 456 |
# Weeks headers |
|
| 457 |
if show_weeks |
|
| 458 |
left = subject_width |
|
| 459 |
height = header_height |
|
| 460 |
if @date_from.cwday == 1 |
|
| 461 |
# date_from is monday |
|
| 462 |
week_f = date_from |
|
| 463 |
else |
|
| 464 |
# find next monday after date_from |
|
| 465 |
week_f = @date_from + (7 - @date_from.cwday + 1) |
|
| 466 |
width = (7 - @date_from.cwday + 1) * zoom |
|
| 467 |
gc.fill('white')
|
|
| 468 |
gc.stroke('grey')
|
|
| 469 |
gc.stroke_width(1) |
|
| 470 |
gc.rectangle(left, header_height, left + width, 2 * header_height + g_height - 1) |
|
| 471 |
left = left + width |
|
| 472 |
end |
|
| 473 |
while week_f <= date_to |
|
| 474 |
width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom |
|
| 475 |
gc.fill('white')
|
|
| 476 |
gc.stroke('grey')
|
|
| 477 |
gc.stroke_width(1) |
|
| 478 |
gc.rectangle(left.round, header_height, left.round + width, 2 * header_height + g_height - 1) |
|
| 479 |
gc.fill('black')
|
|
| 480 |
gc.stroke('transparent')
|
|
| 481 |
gc.stroke_width(1) |
|
| 482 |
gc.text(left.round + 2, header_height + 14, week_f.cweek.to_s) |
|
| 483 |
left = left + width |
|
| 484 |
week_f = week_f + 7 |
|
| 485 |
end |
|
| 486 |
end |
|
| 487 |
# Days details (week-end in grey) |
|
| 488 |
if show_days |
|
| 489 |
left = subject_width |
|
| 490 |
height = g_height + header_height - 1 |
|
| 491 |
wday = @date_from.cwday |
|
| 492 |
(date_to - @date_from + 1).to_i.times do |
|
| 493 |
width = zoom |
|
| 494 |
gc.fill(non_working_week_days.include?(wday) ? '#eee' : 'white') |
|
| 495 |
gc.stroke('#ddd')
|
|
| 496 |
gc.stroke_width(1) |
|
| 497 |
gc.rectangle(left, 2 * header_height, left + width, 2 * header_height + g_height - 1) |
|
| 498 |
left = left + width |
|
| 499 |
wday = wday + 1 |
|
| 500 |
wday = 1 if wday > 7 |
|
| 501 |
end |
|
| 502 |
end |
|
| 503 |
# border |
|
| 504 |
gc.fill('transparent')
|
|
| 505 |
gc.stroke('grey')
|
|
| 506 |
gc.stroke_width(1) |
|
| 507 |
gc.rectangle(0, 0, subject_width + g_width, headers_height) |
|
| 508 |
gc.stroke('black')
|
|
| 509 |
gc.rectangle(0, 0, subject_width + g_width, g_height + headers_height - 1) |
|
| 510 |
# content |
|
| 511 |
top = headers_height + 20 |
|
| 512 |
gc.stroke('transparent')
|
|
| 513 |
lines(:image => gc, :top => top, :zoom => zoom, |
|
| 514 |
:subject_width => subject_width, :format => :image) |
|
| 515 |
# today red line |
|
| 516 |
if Date.today >= @date_from and Date.today <= date_to |
|
| 517 |
gc.stroke('red')
|
|
| 518 |
x = (Date.today - @date_from + 1) * zoom + subject_width |
|
| 519 |
gc.line(x, headers_height, x, headers_height + g_height - 1) |
|
| 520 |
end |
|
| 521 |
gc.draw(imgl) |
|
| 522 |
imgl.format = format |
|
| 523 |
imgl.to_blob |
|
| 524 |
end if Object.const_defined?(:Magick) |
|
| 525 |
|
|
| 526 |
def to_pdf |
|
| 527 |
pdf = ::Redmine::Export::PDF::ITCPDF.new(current_language) |
|
| 528 |
pdf.SetTitle("#{l(:label_gantt)} #{project}")
|
|
| 529 |
pdf.alias_nb_pages |
|
| 530 |
pdf.footer_date = format_date(Date.today) |
|
| 531 |
pdf.AddPage("L")
|
|
| 532 |
pdf.SetFontStyle('B', 12)
|
|
| 533 |
pdf.SetX(15) |
|
| 534 |
pdf.RDMCell(PDF::LeftPaneWidth, 20, project.to_s) |
|
| 535 |
pdf.Ln |
|
| 536 |
pdf.SetFontStyle('B', 9)
|
|
| 537 |
subject_width = PDF::LeftPaneWidth |
|
| 538 |
header_height = 5 |
|
| 539 |
headers_height = header_height |
|
| 540 |
show_weeks = false |
|
| 541 |
show_days = false |
|
| 542 |
if self.months < 7 |
|
| 543 |
show_weeks = true |
|
| 544 |
headers_height = 2 * header_height |
|
| 545 |
if self.months < 3 |
|
| 546 |
show_days = true |
|
| 547 |
headers_height = 3 * header_height |
|
| 548 |
end |
|
| 549 |
end |
|
| 550 |
g_width = PDF.right_pane_width |
|
| 551 |
zoom = (g_width) / (self.date_to - self.date_from + 1) |
|
| 552 |
g_height = 120 |
|
| 553 |
t_height = g_height + headers_height |
|
| 554 |
y_start = pdf.GetY |
|
| 555 |
# Months headers |
|
| 556 |
month_f = self.date_from |
|
| 557 |
left = subject_width |
|
| 558 |
height = header_height |
|
| 559 |
self.months.times do |
|
| 560 |
width = ((month_f >> 1) - month_f) * zoom |
|
| 561 |
pdf.SetY(y_start) |
|
| 562 |
pdf.SetX(left) |
|
| 563 |
pdf.RDMCell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
|
|
| 564 |
left = left + width |
|
| 565 |
month_f = month_f >> 1 |
|
| 566 |
end |
|
| 567 |
# Weeks headers |
|
| 568 |
if show_weeks |
|
| 569 |
left = subject_width |
|
| 570 |
height = header_height |
|
| 571 |
if self.date_from.cwday == 1 |
|
| 572 |
# self.date_from is monday |
|
| 573 |
week_f = self.date_from |
|
| 574 |
else |
|
| 575 |
# find next monday after self.date_from |
|
| 576 |
week_f = self.date_from + (7 - self.date_from.cwday + 1) |
|
| 577 |
width = (7 - self.date_from.cwday + 1) * zoom-1 |
|
| 578 |
pdf.SetY(y_start + header_height) |
|
| 579 |
pdf.SetX(left) |
|
| 580 |
pdf.RDMCell(width + 1, height, "", "LTR") |
|
| 581 |
left = left + width + 1 |
|
| 582 |
end |
|
| 583 |
while week_f <= self.date_to |
|
| 584 |
width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom |
|
| 585 |
pdf.SetY(y_start + header_height) |
|
| 586 |
pdf.SetX(left) |
|
| 587 |
pdf.RDMCell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C") |
|
| 588 |
left = left + width |
|
| 589 |
week_f = week_f + 7 |
|
| 590 |
end |
|
| 591 |
end |
|
| 592 |
# Days headers |
|
| 593 |
if show_days |
|
| 594 |
left = subject_width |
|
| 595 |
height = header_height |
|
| 596 |
wday = self.date_from.cwday |
|
| 597 |
pdf.SetFontStyle('B', 7)
|
|
| 598 |
(self.date_to - self.date_from + 1).to_i.times do |
|
| 599 |
width = zoom |
|
| 600 |
pdf.SetY(y_start + 2 * header_height) |
|
| 601 |
pdf.SetX(left) |
|
| 602 |
pdf.RDMCell(width, height, day_name(wday).first, "LTR", 0, "C") |
|
| 603 |
left = left + width |
|
| 604 |
wday = wday + 1 |
|
| 605 |
wday = 1 if wday > 7 |
|
| 606 |
end |
|
| 607 |
end |
|
| 608 |
pdf.SetY(y_start) |
|
| 609 |
pdf.SetX(15) |
|
| 610 |
pdf.RDMCell(subject_width + g_width - 15, headers_height, "", 1) |
|
| 611 |
# Tasks |
|
| 612 |
top = headers_height + y_start |
|
| 613 |
options = {
|
|
| 614 |
:top => top, |
|
| 615 |
:zoom => zoom, |
|
| 616 |
:subject_width => subject_width, |
|
| 617 |
:g_width => g_width, |
|
| 618 |
:indent => 0, |
|
| 619 |
:indent_increment => 5, |
|
| 620 |
:top_increment => 5, |
|
| 621 |
:format => :pdf, |
|
| 622 |
:pdf => pdf |
|
| 623 |
} |
|
| 624 |
render(options) |
|
| 625 |
pdf.Output |
|
| 626 |
end |
|
| 627 |
|
|
| 628 |
private |
|
| 629 |
|
|
| 630 |
def coordinates(start_date, end_date, progress, zoom=nil) |
|
| 631 |
zoom ||= @zoom |
|
| 632 |
coords = {}
|
|
| 633 |
if start_date && end_date && start_date < self.date_to && end_date > self.date_from |
|
| 634 |
if start_date > self.date_from |
|
| 635 |
coords[:start] = start_date - self.date_from |
|
| 636 |
coords[:bar_start] = start_date - self.date_from |
|
| 637 |
else |
|
| 638 |
coords[:bar_start] = 0 |
|
| 639 |
end |
|
| 640 |
if end_date < self.date_to |
|
| 641 |
coords[:end] = end_date - self.date_from |
|
| 642 |
coords[:bar_end] = end_date - self.date_from + 1 |
|
| 643 |
else |
|
| 644 |
coords[:bar_end] = self.date_to - self.date_from + 1 |
|
| 645 |
end |
|
| 646 |
if progress |
|
| 647 |
progress_date = calc_progress_date(start_date, end_date, progress) |
|
| 648 |
if progress_date > self.date_from && progress_date > start_date |
|
| 649 |
if progress_date < self.date_to |
|
| 650 |
coords[:bar_progress_end] = progress_date - self.date_from |
|
| 651 |
else |
|
| 652 |
coords[:bar_progress_end] = self.date_to - self.date_from + 1 |
|
| 653 |
end |
|
| 654 |
end |
|
| 655 |
if progress_date < Date.today |
|
| 656 |
late_date = [Date.today, end_date].min |
|
| 657 |
if late_date > self.date_from && late_date > start_date |
|
| 658 |
if late_date < self.date_to |
|
| 659 |
coords[:bar_late_end] = late_date - self.date_from + 1 |
|
| 660 |
else |
|
| 661 |
coords[:bar_late_end] = self.date_to - self.date_from + 1 |
|
| 662 |
end |
|
| 663 |
end |
|
| 664 |
end |
|
| 665 |
end |
|
| 666 |
end |
|
| 667 |
# Transforms dates into pixels witdh |
|
| 668 |
coords.keys.each do |key| |
|
| 669 |
coords[key] = (coords[key] * zoom).floor |
|
| 670 |
end |
|
| 671 |
coords |
|
| 672 |
end |
|
| 673 |
|
|
| 674 |
def calc_progress_date(start_date, end_date, progress) |
|
| 675 |
start_date + (end_date - start_date + 1) * (progress / 100.0) |
|
| 676 |
end |
|
| 677 |
|
|
| 678 |
# TODO: Sorts a collection of issues by start_date, due_date, id for gantt rendering |
|
| 679 |
def sort_issues!(issues) |
|
| 680 |
issues.sort! { |a, b| gantt_issue_compare(a, b) }
|
|
| 681 |
end |
|
| 682 |
|
|
| 683 |
# TODO: top level issues should be sorted by start date |
|
| 684 |
def gantt_issue_compare(x, y) |
|
| 685 |
if x.root_id == y.root_id |
|
| 686 |
x.lft <=> y.lft |
|
| 687 |
else |
|
| 688 |
x.root_id <=> y.root_id |
|
| 689 |
end |
|
| 690 |
end |
|
| 691 |
|
|
| 692 |
def current_limit |
|
| 693 |
if @max_rows |
|
| 694 |
@max_rows - @number_of_rows |
|
| 695 |
else |
|
| 696 |
nil |
|
| 697 |
end |
|
| 698 |
end |
|
| 699 |
|
|
| 700 |
def abort? |
|
| 701 |
if @max_rows && @number_of_rows >= @max_rows |
|
| 702 |
@truncated = true |
|
| 703 |
end |
|
| 704 |
end |
|
| 705 |
|
|
| 706 |
def pdf_new_page?(options) |
|
| 707 |
if options[:top] > 180 |
|
| 708 |
options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top]) |
|
| 709 |
options[:pdf].AddPage("L")
|
|
| 710 |
options[:top] = 15 |
|
| 711 |
options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1) |
|
| 712 |
end |
|
| 713 |
end |
|
| 714 |
|
|
| 715 |
def html_subject(params, subject, options={})
|
|
| 716 |
style = "position: absolute;top:#{params[:top]}px;left:#{params[:indent]}px;"
|
|
| 717 |
style << "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
|
|
| 718 |
output = view.content_tag(:div, subject, |
|
| 719 |
:class => options[:css], :style => style, |
|
| 720 |
:title => options[:title], |
|
| 721 |
:id => options[:id]) |
|
| 722 |
@subjects << output |
|
| 723 |
output |
|
| 724 |
end |
|
| 725 |
|
|
| 726 |
def pdf_subject(params, subject, options={})
|
|
| 727 |
params[:pdf].SetY(params[:top]) |
|
| 728 |
params[:pdf].SetX(15) |
|
| 729 |
char_limit = PDF::MaxCharactorsForSubject - params[:indent] |
|
| 730 |
params[:pdf].RDMCell(params[:subject_width] - 15, 5, |
|
| 731 |
(" " * params[:indent]) +
|
|
| 732 |
subject.to_s.sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'),
|
|
| 733 |
"LR") |
|
| 734 |
params[:pdf].SetY(params[:top]) |
|
| 735 |
params[:pdf].SetX(params[:subject_width]) |
|
| 736 |
params[:pdf].RDMCell(params[:g_width], 5, "", "LR") |
|
| 737 |
end |
|
| 738 |
|
|
| 739 |
def image_subject(params, subject, options={})
|
|
| 740 |
params[:image].fill('black')
|
|
| 741 |
params[:image].stroke('transparent')
|
|
| 742 |
params[:image].stroke_width(1) |
|
| 743 |
params[:image].text(params[:indent], params[:top] + 2, subject) |
|
| 744 |
end |
|
| 745 |
|
|
| 746 |
def issue_relations(issue) |
|
| 747 |
rels = {}
|
|
| 748 |
if relations[issue.id] |
|
| 749 |
relations[issue.id].each do |relation| |
|
| 750 |
(rels[relation.relation_type] ||= []) << relation.issue_to_id |
|
| 751 |
end |
|
| 752 |
end |
|
| 753 |
rels |
|
| 754 |
end |
|
| 755 |
|
|
| 756 |
def html_task(params, coords, options={})
|
|
| 757 |
output = '' |
|
| 758 |
# Renders the task bar, with progress and late |
|
| 759 |
if coords[:bar_start] && coords[:bar_end] |
|
| 760 |
width = coords[:bar_end] - coords[:bar_start] - 2 |
|
| 761 |
style = "" |
|
| 762 |
style << "top:#{params[:top]}px;"
|
|
| 763 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 764 |
style << "width:#{width}px;"
|
|
| 765 |
html_id = "task-todo-issue-#{options[:issue].id}" if options[:issue]
|
|
| 766 |
html_id = "task-todo-version-#{options[:version].id}" if options[:version]
|
|
| 767 |
content_opt = {:style => style,
|
|
| 768 |
:class => "#{options[:css]} task_todo",
|
|
| 769 |
:id => html_id} |
|
| 770 |
if options[:issue] |
|
| 771 |
rels = issue_relations(options[:issue]) |
|
| 772 |
if rels.present? |
|
| 773 |
content_opt[:data] = {"rels" => rels.to_json}
|
|
| 774 |
end |
|
| 775 |
end |
|
| 776 |
output << view.content_tag(:div, ' '.html_safe, content_opt) |
|
| 777 |
if coords[:bar_late_end] |
|
| 778 |
width = coords[:bar_late_end] - coords[:bar_start] - 2 |
|
| 779 |
style = "" |
|
| 780 |
style << "top:#{params[:top]}px;"
|
|
| 781 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 782 |
style << "width:#{width}px;"
|
|
| 783 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 784 |
:style => style, |
|
| 785 |
:class => "#{options[:css]} task_late")
|
|
| 786 |
end |
|
| 787 |
if coords[:bar_progress_end] |
|
| 788 |
width = coords[:bar_progress_end] - coords[:bar_start] - 2 |
|
| 789 |
style = "" |
|
| 790 |
style << "top:#{params[:top]}px;"
|
|
| 791 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 792 |
style << "width:#{width}px;"
|
|
| 793 |
html_id = "task-done-issue-#{options[:issue].id}" if options[:issue]
|
|
| 794 |
html_id = "task-done-version-#{options[:version].id}" if options[:version]
|
|
| 795 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 796 |
:style => style, |
|
| 797 |
:class => "#{options[:css]} task_done",
|
|
| 798 |
:id => html_id) |
|
| 799 |
end |
|
| 800 |
end |
|
| 801 |
# Renders the markers |
|
| 802 |
if options[:markers] |
|
| 803 |
if coords[:start] |
|
| 804 |
style = "" |
|
| 805 |
style << "top:#{params[:top]}px;"
|
|
| 806 |
style << "left:#{coords[:start]}px;"
|
|
| 807 |
style << "width:15px;" |
|
| 808 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 809 |
:style => style, |
|
| 810 |
:class => "#{options[:css]} marker starting")
|
|
| 811 |
end |
|
| 812 |
if coords[:end] |
|
| 813 |
style = "" |
|
| 814 |
style << "top:#{params[:top]}px;"
|
|
| 815 |
style << "left:#{coords[:end] + params[:zoom]}px;"
|
|
| 816 |
style << "width:15px;" |
|
| 817 |
output << view.content_tag(:div, ' '.html_safe, |
|
| 818 |
:style => style, |
|
| 819 |
:class => "#{options[:css]} marker ending")
|
|
| 820 |
end |
|
| 821 |
end |
|
| 822 |
# Renders the label on the right |
|
| 823 |
if options[:label] |
|
| 824 |
style = "" |
|
| 825 |
style << "top:#{params[:top]}px;"
|
|
| 826 |
style << "left:#{(coords[:bar_end] || 0) + 8}px;"
|
|
| 827 |
style << "width:15px;" |
|
| 828 |
output << view.content_tag(:div, options[:label], |
|
| 829 |
:style => style, |
|
| 830 |
:class => "#{options[:css]} label")
|
|
| 831 |
end |
|
| 832 |
# Renders the tooltip |
|
| 833 |
if options[:issue] && coords[:bar_start] && coords[:bar_end] |
|
| 834 |
s = view.content_tag(:span, |
|
| 835 |
view.render_issue_tooltip(options[:issue]).html_safe, |
|
| 836 |
:class => "tip") |
|
| 837 |
style = "" |
|
| 838 |
style << "position: absolute;" |
|
| 839 |
style << "top:#{params[:top]}px;"
|
|
| 840 |
style << "left:#{coords[:bar_start]}px;"
|
|
| 841 |
style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
|
|
| 842 |
style << "height:12px;" |
|
| 843 |
output << view.content_tag(:div, s.html_safe, |
|
| 844 |
:style => style, |
|
| 845 |
:class => "tooltip") |
|
| 846 |
end |
|
| 847 |
@lines << output |
|
| 848 |
output |
|
| 849 |
end |
|
| 850 |
|
|
| 851 |
def pdf_task(params, coords, options={})
|
|
| 852 |
height = options[:height] || 2 |
|
| 853 |
# Renders the task bar, with progress and late |
|
| 854 |
if coords[:bar_start] && coords[:bar_end] |
|
| 855 |
params[:pdf].SetY(params[:top] + 1.5) |
|
| 856 |
params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) |
|
| 857 |
params[:pdf].SetFillColor(200, 200, 200) |
|
| 858 |
params[:pdf].RDMCell(coords[:bar_end] - coords[:bar_start], height, "", 0, 0, "", 1) |
|
| 859 |
if coords[:bar_late_end] |
|
| 860 |
params[:pdf].SetY(params[:top] + 1.5) |
|
| 861 |
params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) |
|
| 862 |
params[:pdf].SetFillColor(255, 100, 100) |
|
| 863 |
params[:pdf].RDMCell(coords[:bar_late_end] - coords[:bar_start], height, "", 0, 0, "", 1) |
|
| 864 |
end |
|
| 865 |
if coords[:bar_progress_end] |
|
| 866 |
params[:pdf].SetY(params[:top] + 1.5) |
|
| 867 |
params[:pdf].SetX(params[:subject_width] + coords[:bar_start]) |
|
| 868 |
params[:pdf].SetFillColor(90, 200, 90) |
|
| 869 |
params[:pdf].RDMCell(coords[:bar_progress_end] - coords[:bar_start], height, "", 0, 0, "", 1) |
|
| 870 |
end |
|
| 871 |
end |
|
| 872 |
# Renders the markers |
|
| 873 |
if options[:markers] |
|
| 874 |
if coords[:start] |
|
| 875 |
params[:pdf].SetY(params[:top] + 1) |
|
| 876 |
params[:pdf].SetX(params[:subject_width] + coords[:start] - 1) |
|
| 877 |
params[:pdf].SetFillColor(50, 50, 200) |
|
| 878 |
params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) |
|
| 879 |
end |
|
| 880 |
if coords[:end] |
|
| 881 |
params[:pdf].SetY(params[:top] + 1) |
|
| 882 |
params[:pdf].SetX(params[:subject_width] + coords[:end] - 1) |
|
| 883 |
params[:pdf].SetFillColor(50, 50, 200) |
|
| 884 |
params[:pdf].RDMCell(2, 2, "", 0, 0, "", 1) |
|
| 885 |
end |
|
| 886 |
end |
|
| 887 |
# Renders the label on the right |
|
| 888 |
if options[:label] |
|
| 889 |
params[:pdf].SetX(params[:subject_width] + (coords[:bar_end] || 0) + 5) |
|
| 890 |
params[:pdf].RDMCell(30, 2, options[:label]) |
|
| 891 |
end |
|
| 892 |
end |
|
| 893 |
|
|
| 894 |
def image_task(params, coords, options={})
|
|
| 895 |
height = options[:height] || 6 |
|
| 896 |
# Renders the task bar, with progress and late |
|
| 897 |
if coords[:bar_start] && coords[:bar_end] |
|
| 898 |
params[:image].fill('#aaa')
|
|
| 899 |
params[:image].rectangle(params[:subject_width] + coords[:bar_start], |
|
| 900 |
params[:top], |
|
| 901 |
params[:subject_width] + coords[:bar_end], |
|
| 902 |
params[:top] - height) |
|
| 903 |
if coords[:bar_late_end] |
|
| 904 |
params[:image].fill('#f66')
|
|
| 905 |
params[:image].rectangle(params[:subject_width] + coords[:bar_start], |
|
| 906 |
params[:top], |
|
| 907 |
params[:subject_width] + coords[:bar_late_end], |
|
| 908 |
params[:top] - height) |
|
| 909 |
end |
|
| 910 |
if coords[:bar_progress_end] |
|
| 911 |
params[:image].fill('#00c600')
|
|
| 912 |
params[:image].rectangle(params[:subject_width] + coords[:bar_start], |
|
| 913 |
params[:top], |
|
| 914 |
params[:subject_width] + coords[:bar_progress_end], |
|
| 915 |
params[:top] - height) |
|
| 916 |
end |
|
| 917 |
end |
|
| 918 |
# Renders the markers |
|
| 919 |
if options[:markers] |
|
| 920 |
if coords[:start] |
|
| 921 |
x = params[:subject_width] + coords[:start] |
|
| 922 |
y = params[:top] - height / 2 |
|
| 923 |
params[:image].fill('blue')
|
|
| 924 |
params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) |
|
| 925 |
end |
|
| 926 |
if coords[:end] |
|
| 927 |
x = params[:subject_width] + coords[:end] + params[:zoom] |
|
| 928 |
y = params[:top] - height / 2 |
|
| 929 |
params[:image].fill('blue')
|
|
| 930 |
params[:image].polygon(x - 4, y, x, y - 4, x + 4, y, x, y + 4) |
|
| 931 |
end |
|
| 932 |
end |
|
| 933 |
# Renders the label on the right |
|
| 934 |
if options[:label] |
|
| 935 |
params[:image].fill('black')
|
|
| 936 |
params[:image].text(params[:subject_width] + (coords[:bar_end] || 0) + 5, |
|
| 937 |
params[:top] + 1, |
|
| 938 |
options[:label]) |
|
| 939 |
end |
|
| 940 |
end |
|
| 941 |
end |
|
| 942 |
end |
|
| 943 |
end |
|
| .svn/pristine/02/02c68441083bdea630158440f0e7d9d62ff8f790.svn-base | ||
|---|---|---|
| 1 |
# encoding: utf-8 |
|
| 2 |
# |
|
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006-2013 Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
| 19 |
|
|
| 20 |
require File.expand_path('../../test_helper', __FILE__)
|
|
| 21 |
|
|
| 22 |
class MailHandlerTest < ActiveSupport::TestCase |
|
| 23 |
fixtures :users, :projects, :enabled_modules, :roles, |
|
| 24 |
:members, :member_roles, :users, |
|
| 25 |
:issues, :issue_statuses, |
|
| 26 |
:workflows, :trackers, :projects_trackers, |
|
| 27 |
:versions, :enumerations, :issue_categories, |
|
| 28 |
:custom_fields, :custom_fields_trackers, :custom_fields_projects, |
|
| 29 |
:boards, :messages |
|
| 30 |
|
|
| 31 |
FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' |
|
| 32 |
|
|
| 33 |
def setup |
|
| 34 |
ActionMailer::Base.deliveries.clear |
|
| 35 |
Setting.notified_events = Redmine::Notifiable.all.collect(&:name) |
|
| 36 |
end |
|
| 37 |
|
|
| 38 |
def teardown |
|
| 39 |
Setting.clear_cache |
|
| 40 |
end |
|
| 41 |
|
|
| 42 |
def test_add_issue |
|
| 43 |
ActionMailer::Base.deliveries.clear |
|
| 44 |
# This email contains: 'Project: onlinestore' |
|
| 45 |
issue = submit_email('ticket_on_given_project.eml')
|
|
| 46 |
assert issue.is_a?(Issue) |
|
| 47 |
assert !issue.new_record? |
|
| 48 |
issue.reload |
|
| 49 |
assert_equal Project.find(2), issue.project |
|
| 50 |
assert_equal issue.project.trackers.first, issue.tracker |
|
| 51 |
assert_equal 'New ticket on a given project', issue.subject |
|
| 52 |
assert_equal User.find_by_login('jsmith'), issue.author
|
|
| 53 |
assert_equal IssueStatus.find_by_name('Resolved'), issue.status
|
|
| 54 |
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
|
|
| 55 |
assert_equal '2010-01-01', issue.start_date.to_s |
|
| 56 |
assert_equal '2010-12-31', issue.due_date.to_s |
|
| 57 |
assert_equal User.find_by_login('jsmith'), issue.assigned_to
|
|
| 58 |
assert_equal Version.find_by_name('Alpha'), issue.fixed_version
|
|
| 59 |
assert_equal 2.5, issue.estimated_hours |
|
| 60 |
assert_equal 30, issue.done_ratio |
|
| 61 |
assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] |
|
| 62 |
# keywords should be removed from the email body |
|
| 63 |
assert !issue.description.match(/^Project:/i) |
|
| 64 |
assert !issue.description.match(/^Status:/i) |
|
| 65 |
assert !issue.description.match(/^Start Date:/i) |
|
| 66 |
# Email notification should be sent |
|
| 67 |
mail = ActionMailer::Base.deliveries.last |
|
| 68 |
assert_not_nil mail |
|
| 69 |
assert mail.subject.include?('New ticket on a given project')
|
|
| 70 |
end |
|
| 71 |
|
|
| 72 |
def test_add_issue_with_default_tracker |
|
| 73 |
# This email contains: 'Project: onlinestore' |
|
| 74 |
issue = submit_email( |
|
| 75 |
'ticket_on_given_project.eml', |
|
| 76 |
:issue => {:tracker => 'Support request'}
|
|
| 77 |
) |
|
| 78 |
assert issue.is_a?(Issue) |
|
| 79 |
assert !issue.new_record? |
|
| 80 |
issue.reload |
|
| 81 |
assert_equal 'Support request', issue.tracker.name |
|
| 82 |
end |
|
| 83 |
|
|
| 84 |
def test_add_issue_with_status |
|
| 85 |
# This email contains: 'Project: onlinestore' and 'Status: Resolved' |
|
| 86 |
issue = submit_email('ticket_on_given_project.eml')
|
|
| 87 |
assert issue.is_a?(Issue) |
|
| 88 |
assert !issue.new_record? |
|
| 89 |
issue.reload |
|
| 90 |
assert_equal Project.find(2), issue.project |
|
| 91 |
assert_equal IssueStatus.find_by_name("Resolved"), issue.status
|
|
| 92 |
end |
|
| 93 |
|
|
| 94 |
def test_add_issue_with_attributes_override |
|
| 95 |
issue = submit_email( |
|
| 96 |
'ticket_with_attributes.eml', |
|
| 97 |
:allow_override => 'tracker,category,priority' |
|
| 98 |
) |
|
| 99 |
assert issue.is_a?(Issue) |
|
| 100 |
assert !issue.new_record? |
|
| 101 |
issue.reload |
|
| 102 |
assert_equal 'New ticket on a given project', issue.subject |
|
| 103 |
assert_equal User.find_by_login('jsmith'), issue.author
|
|
| 104 |
assert_equal Project.find(2), issue.project |
|
| 105 |
assert_equal 'Feature request', issue.tracker.to_s |
|
| 106 |
assert_equal 'Stock management', issue.category.to_s |
|
| 107 |
assert_equal 'Urgent', issue.priority.to_s |
|
| 108 |
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
|
|
| 109 |
end |
|
| 110 |
|
|
| 111 |
def test_add_issue_with_group_assignment |
|
| 112 |
with_settings :issue_group_assignment => '1' do |
|
| 113 |
issue = submit_email('ticket_on_given_project.eml') do |email|
|
|
| 114 |
email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
|
|
| 115 |
end |
|
| 116 |
assert issue.is_a?(Issue) |
|
| 117 |
assert !issue.new_record? |
|
| 118 |
issue.reload |
|
| 119 |
assert_equal Group.find(11), issue.assigned_to |
|
| 120 |
end |
|
| 121 |
end |
|
| 122 |
|
|
| 123 |
def test_add_issue_with_partial_attributes_override |
|
| 124 |
issue = submit_email( |
|
| 125 |
'ticket_with_attributes.eml', |
|
| 126 |
:issue => {:priority => 'High'},
|
|
| 127 |
:allow_override => ['tracker'] |
|
| 128 |
) |
|
| 129 |
assert issue.is_a?(Issue) |
|
| 130 |
assert !issue.new_record? |
|
| 131 |
issue.reload |
|
| 132 |
assert_equal 'New ticket on a given project', issue.subject |
|
| 133 |
assert_equal User.find_by_login('jsmith'), issue.author
|
|
| 134 |
assert_equal Project.find(2), issue.project |
|
| 135 |
assert_equal 'Feature request', issue.tracker.to_s |
|
| 136 |
assert_nil issue.category |
|
| 137 |
assert_equal 'High', issue.priority.to_s |
|
| 138 |
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
|
|
| 139 |
end |
|
| 140 |
|
|
| 141 |
def test_add_issue_with_spaces_between_attribute_and_separator |
|
| 142 |
issue = submit_email( |
|
| 143 |
'ticket_with_spaces_between_attribute_and_separator.eml', |
|
| 144 |
:allow_override => 'tracker,category,priority' |
|
| 145 |
) |
|
| 146 |
assert issue.is_a?(Issue) |
|
| 147 |
assert !issue.new_record? |
|
| 148 |
issue.reload |
|
| 149 |
assert_equal 'New ticket on a given project', issue.subject |
|
| 150 |
assert_equal User.find_by_login('jsmith'), issue.author
|
|
| 151 |
assert_equal Project.find(2), issue.project |
|
| 152 |
assert_equal 'Feature request', issue.tracker.to_s |
|
| 153 |
assert_equal 'Stock management', issue.category.to_s |
|
| 154 |
assert_equal 'Urgent', issue.priority.to_s |
|
| 155 |
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
|
|
| 156 |
end |
|
| 157 |
|
|
| 158 |
def test_add_issue_with_attachment_to_specific_project |
|
| 159 |
issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
|
|
| 160 |
assert issue.is_a?(Issue) |
|
| 161 |
assert !issue.new_record? |
|
| 162 |
issue.reload |
|
| 163 |
assert_equal 'Ticket created by email with attachment', issue.subject |
|
| 164 |
assert_equal User.find_by_login('jsmith'), issue.author
|
|
| 165 |
assert_equal Project.find(2), issue.project |
|
| 166 |
assert_equal 'This is a new ticket with attachments', issue.description |
|
| 167 |
# Attachment properties |
|
| 168 |
assert_equal 1, issue.attachments.size |
|
| 169 |
assert_equal 'Paella.jpg', issue.attachments.first.filename |
|
| 170 |
assert_equal 'image/jpeg', issue.attachments.first.content_type |
|
| 171 |
assert_equal 10790, issue.attachments.first.filesize |
|
| 172 |
end |
|
| 173 |
|
|
| 174 |
def test_add_issue_with_custom_fields |
|
| 175 |
issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
|
|
| 176 |
assert issue.is_a?(Issue) |
|
| 177 |
assert !issue.new_record? |
|
| 178 |
issue.reload |
|
| 179 |
assert_equal 'New ticket with custom field values', issue.subject |
|
| 180 |
assert_equal 'PostgreSQL', issue.custom_field_value(1) |
|
| 181 |
assert_equal 'Value for a custom field', issue.custom_field_value(2) |
|
| 182 |
assert !issue.description.match(/^searchable field:/i) |
|
| 183 |
end |
|
| 184 |
|
|
| 185 |
def test_add_issue_with_version_custom_fields |
|
| 186 |
field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3]) |
|
| 187 |
|
|
| 188 |
issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
|
|
| 189 |
email << "Affected version: 1.0\n" |
|
| 190 |
end |
|
| 191 |
assert issue.is_a?(Issue) |
|
| 192 |
assert !issue.new_record? |
|
| 193 |
issue.reload |
|
| 194 |
assert_equal '2', issue.custom_field_value(field) |
|
| 195 |
end |
|
| 196 |
|
|
| 197 |
def test_add_issue_should_match_assignee_on_display_name |
|
| 198 |
user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz') |
|
| 199 |
User.add_to_project(user, Project.find(2)) |
|
| 200 |
issue = submit_email('ticket_on_given_project.eml') do |email|
|
|
| 201 |
email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz') |
|
| 202 |
end |
|
| 203 |
assert issue.is_a?(Issue) |
|
| 204 |
assert_equal user, issue.assigned_to |
|
| 205 |
end |
|
| 206 |
|
|
| 207 |
def test_add_issue_with_cc |
|
| 208 |
issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
|
|
| 209 |
assert issue.is_a?(Issue) |
|
| 210 |
assert !issue.new_record? |
|
| 211 |
issue.reload |
|
| 212 |
assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
|
|
| 213 |
assert_equal 1, issue.watcher_user_ids.size |
|
| 214 |
end |
|
| 215 |
|
|
| 216 |
def test_add_issue_by_unknown_user |
|
| 217 |
assert_no_difference 'User.count' do |
|
| 218 |
assert_equal false, |
|
| 219 |
submit_email( |
|
| 220 |
'ticket_by_unknown_user.eml', |
|
| 221 |
:issue => {:project => 'ecookbook'}
|
|
| 222 |
) |
|
| 223 |
end |
|
| 224 |
end |
|
| 225 |
|
|
| 226 |
def test_add_issue_by_anonymous_user |
|
| 227 |
Role.anonymous.add_permission!(:add_issues) |
|
| 228 |
assert_no_difference 'User.count' do |
|
| 229 |
issue = submit_email( |
|
| 230 |
'ticket_by_unknown_user.eml', |
|
| 231 |
:issue => {:project => 'ecookbook'},
|
|
| 232 |
:unknown_user => 'accept' |
|
| 233 |
) |
|
| 234 |
assert issue.is_a?(Issue) |
|
| 235 |
assert issue.author.anonymous? |
|
| 236 |
end |
|
| 237 |
end |
|
| 238 |
|
|
| 239 |
def test_add_issue_by_anonymous_user_with_no_from_address |
|
| 240 |
Role.anonymous.add_permission!(:add_issues) |
|
| 241 |
assert_no_difference 'User.count' do |
|
| 242 |
issue = submit_email( |
|
| 243 |
'ticket_by_empty_user.eml', |
|
| 244 |
:issue => {:project => 'ecookbook'},
|
|
| 245 |
:unknown_user => 'accept' |
|
| 246 |
) |
|
| 247 |
assert issue.is_a?(Issue) |
|
| 248 |
assert issue.author.anonymous? |
|
| 249 |
end |
|
| 250 |
end |
|
| 251 |
|
|
| 252 |
def test_add_issue_by_anonymous_user_on_private_project |
|
| 253 |
Role.anonymous.add_permission!(:add_issues) |
|
| 254 |
assert_no_difference 'User.count' do |
|
| 255 |
assert_no_difference 'Issue.count' do |
|
| 256 |
assert_equal false, |
|
| 257 |
submit_email( |
|
| 258 |
'ticket_by_unknown_user.eml', |
|
| 259 |
:issue => {:project => 'onlinestore'},
|
|
| 260 |
:unknown_user => 'accept' |
|
| 261 |
) |
|
| 262 |
end |
|
| 263 |
end |
|
| 264 |
end |
|
| 265 |
|
|
| 266 |
def test_add_issue_by_anonymous_user_on_private_project_without_permission_check |
|
| 267 |
assert_no_difference 'User.count' do |
|
| 268 |
assert_difference 'Issue.count' do |
|
| 269 |
issue = submit_email( |
|
| 270 |
'ticket_by_unknown_user.eml', |
|
| 271 |
:issue => {:project => 'onlinestore'},
|
|
| 272 |
:no_permission_check => '1', |
|
| 273 |
:unknown_user => 'accept' |
|
| 274 |
) |
|
| 275 |
assert issue.is_a?(Issue) |
|
| 276 |
assert issue.author.anonymous? |
|
| 277 |
assert !issue.project.is_public? |
|
| 278 |
assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt] |
|
| 279 |
end |
|
| 280 |
end |
|
| 281 |
end |
|
| 282 |
|
|
| 283 |
def test_add_issue_by_created_user |
|
| 284 |
Setting.default_language = 'en' |
|
| 285 |
assert_difference 'User.count' do |
|
| 286 |
issue = submit_email( |
|
| 287 |
'ticket_by_unknown_user.eml', |
|
| 288 |
:issue => {:project => 'ecookbook'},
|
|
| 289 |
:unknown_user => 'create' |
|
| 290 |
) |
|
| 291 |
assert issue.is_a?(Issue) |
|
| 292 |
assert issue.author.active? |
|
| 293 |
assert_equal 'john.doe@somenet.foo', issue.author.mail |
|
| 294 |
assert_equal 'John', issue.author.firstname |
|
| 295 |
assert_equal 'Doe', issue.author.lastname |
|
| 296 |
|
|
| 297 |
# account information |
|
| 298 |
email = ActionMailer::Base.deliveries.first |
|
| 299 |
assert_not_nil email |
|
| 300 |
assert email.subject.include?('account activation')
|
|
| 301 |
login = mail_body(email).match(/\* Login: (.*)$/)[1].strip |
|
| 302 |
password = mail_body(email).match(/\* Password: (.*)$/)[1].strip |
|
| 303 |
assert_equal issue.author, User.try_to_login(login, password) |
|
| 304 |
end |
|
| 305 |
end |
|
| 306 |
|
|
| 307 |
def test_created_user_should_be_added_to_groups |
|
| 308 |
group1 = Group.generate! |
|
| 309 |
group2 = Group.generate! |
|
| 310 |
|
|
| 311 |
assert_difference 'User.count' do |
|
| 312 |
submit_email( |
|
| 313 |
'ticket_by_unknown_user.eml', |
|
| 314 |
:issue => {:project => 'ecookbook'},
|
|
| 315 |
:unknown_user => 'create', |
|
| 316 |
:default_group => "#{group1.name},#{group2.name}"
|
|
| 317 |
) |
|
| 318 |
end |
|
| 319 |
user = User.order('id DESC').first
|
|
| 320 |
assert_same_elements [group1, group2], user.groups |
|
| 321 |
end |
|
| 322 |
|
|
| 323 |
def test_created_user_should_not_receive_account_information_with_no_account_info_option |
|
| 324 |
assert_difference 'User.count' do |
|
| 325 |
submit_email( |
|
| 326 |
'ticket_by_unknown_user.eml', |
|
| 327 |
:issue => {:project => 'ecookbook'},
|
|
| 328 |
:unknown_user => 'create', |
|
| 329 |
:no_account_notice => '1' |
|
| 330 |
) |
|
| 331 |
end |
|
| 332 |
|
|
| 333 |
# only 1 email for the new issue notification |
|
| 334 |
assert_equal 1, ActionMailer::Base.deliveries.size |
|
| 335 |
email = ActionMailer::Base.deliveries.first |
|
| 336 |
assert_include 'Ticket by unknown user', email.subject |
|
| 337 |
end |
|
| 338 |
|
|
| 339 |
def test_created_user_should_have_mail_notification_to_none_with_no_notification_option |
|
| 340 |
assert_difference 'User.count' do |
|
| 341 |
submit_email( |
|
| 342 |
'ticket_by_unknown_user.eml', |
|
| 343 |
:issue => {:project => 'ecookbook'},
|
|
| 344 |
:unknown_user => 'create', |
|
| 345 |
:no_notification => '1' |
|
| 346 |
) |
|
| 347 |
end |
|
| 348 |
user = User.order('id DESC').first
|
|
| 349 |
assert_equal 'none', user.mail_notification |
|
| 350 |
end |
|
| 351 |
|
|
| 352 |
def test_add_issue_without_from_header |
|
| 353 |
Role.anonymous.add_permission!(:add_issues) |
|
| 354 |
assert_equal false, submit_email('ticket_without_from_header.eml')
|
|
| 355 |
end |
|
| 356 |
|
|
| 357 |
def test_add_issue_with_invalid_attributes |
|
| 358 |
issue = submit_email( |
|
| 359 |
'ticket_with_invalid_attributes.eml', |
|
| 360 |
:allow_override => 'tracker,category,priority' |
|
| 361 |
) |
|
| 362 |
assert issue.is_a?(Issue) |
|
| 363 |
assert !issue.new_record? |
|
| 364 |
issue.reload |
|
| 365 |
assert_nil issue.assigned_to |
|
| 366 |
assert_nil issue.start_date |
|
| 367 |
assert_nil issue.due_date |
|
| 368 |
assert_equal 0, issue.done_ratio |
|
| 369 |
assert_equal 'Normal', issue.priority.to_s |
|
| 370 |
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
|
|
| 371 |
end |
|
| 372 |
|
|
| 373 |
def test_add_issue_with_localized_attributes |
|
| 374 |
User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
|
|
| 375 |
issue = submit_email( |
|
| 376 |
'ticket_with_localized_attributes.eml', |
|
| 377 |
:allow_override => 'tracker,category,priority' |
|
| 378 |
) |
|
| 379 |
assert issue.is_a?(Issue) |
|
| 380 |
assert !issue.new_record? |
|
| 381 |
issue.reload |
|
| 382 |
assert_equal 'New ticket on a given project', issue.subject |
|
| 383 |
assert_equal User.find_by_login('jsmith'), issue.author
|
|
| 384 |
assert_equal Project.find(2), issue.project |
|
| 385 |
assert_equal 'Feature request', issue.tracker.to_s |
|
| 386 |
assert_equal 'Stock management', issue.category.to_s |
|
| 387 |
assert_equal 'Urgent', issue.priority.to_s |
|
| 388 |
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
|
|
| 389 |
end |
|
| 390 |
|
|
| 391 |
def test_add_issue_with_japanese_keywords |
|
| 392 |
ja_dev = "\xe9\x96\x8b\xe7\x99\xba" |
|
| 393 |
ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
|
|
| 394 |
tracker = Tracker.create!(:name => ja_dev) |
|
| 395 |
Project.find(1).trackers << tracker |
|
| 396 |
issue = submit_email( |
|
| 397 |
'japanese_keywords_iso_2022_jp.eml', |
|
| 398 |
:issue => {:project => 'ecookbook'},
|
|
| 399 |
:allow_override => 'tracker' |
|
| 400 |
) |
|
| 401 |
assert_kind_of Issue, issue |
|
| 402 |
assert_equal tracker, issue.tracker |
|
| 403 |
end |
|
| 404 |
|
|
| 405 |
def test_add_issue_from_apple_mail |
|
| 406 |
issue = submit_email( |
|
| 407 |
'apple_mail_with_attachment.eml', |
|
| 408 |
:issue => {:project => 'ecookbook'}
|
|
| 409 |
) |
|
| 410 |
assert_kind_of Issue, issue |
|
| 411 |
assert_equal 1, issue.attachments.size |
|
| 412 |
|
|
| 413 |
attachment = issue.attachments.first |
|
| 414 |
assert_equal 'paella.jpg', attachment.filename |
|
| 415 |
assert_equal 10790, attachment.filesize |
|
| 416 |
assert File.exist?(attachment.diskfile) |
|
| 417 |
assert_equal 10790, File.size(attachment.diskfile) |
|
| 418 |
assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest |
|
| 419 |
end |
|
| 420 |
|
|
| 421 |
def test_thunderbird_with_attachment_ja |
|
| 422 |
issue = submit_email( |
|
| 423 |
'thunderbird_with_attachment_ja.eml', |
|
| 424 |
:issue => {:project => 'ecookbook'}
|
|
| 425 |
) |
|
| 426 |
assert_kind_of Issue, issue |
|
| 427 |
assert_equal 1, issue.attachments.size |
|
| 428 |
ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" |
|
| 429 |
ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
|
|
| 430 |
attachment = issue.attachments.first |
|
| 431 |
assert_equal ja, attachment.filename |
|
| 432 |
assert_equal 5, attachment.filesize |
|
| 433 |
assert File.exist?(attachment.diskfile) |
|
| 434 |
assert_equal 5, File.size(attachment.diskfile) |
|
| 435 |
assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
| 436 |
end |
|
| 437 |
|
|
| 438 |
def test_gmail_with_attachment_ja |
|
| 439 |
issue = submit_email( |
|
| 440 |
'gmail_with_attachment_ja.eml', |
|
| 441 |
:issue => {:project => 'ecookbook'}
|
|
| 442 |
) |
|
| 443 |
assert_kind_of Issue, issue |
|
| 444 |
assert_equal 1, issue.attachments.size |
|
| 445 |
ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt" |
|
| 446 |
ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
|
|
| 447 |
attachment = issue.attachments.first |
|
| 448 |
assert_equal ja, attachment.filename |
|
| 449 |
assert_equal 5, attachment.filesize |
|
| 450 |
assert File.exist?(attachment.diskfile) |
|
| 451 |
assert_equal 5, File.size(attachment.diskfile) |
|
| 452 |
assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
| 453 |
end |
|
| 454 |
|
|
| 455 |
def test_thunderbird_with_attachment_latin1 |
|
| 456 |
issue = submit_email( |
|
| 457 |
'thunderbird_with_attachment_iso-8859-1.eml', |
|
| 458 |
:issue => {:project => 'ecookbook'}
|
|
| 459 |
) |
|
| 460 |
assert_kind_of Issue, issue |
|
| 461 |
assert_equal 1, issue.attachments.size |
|
| 462 |
u = "" |
|
| 463 |
u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
|
|
| 464 |
u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" |
|
| 465 |
u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
|
|
| 466 |
11.times { u << u1 }
|
|
| 467 |
attachment = issue.attachments.first |
|
| 468 |
assert_equal "#{u}.png", attachment.filename
|
|
| 469 |
assert_equal 130, attachment.filesize |
|
| 470 |
assert File.exist?(attachment.diskfile) |
|
| 471 |
assert_equal 130, File.size(attachment.diskfile) |
|
| 472 |
assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest |
|
| 473 |
end |
|
| 474 |
|
|
| 475 |
def test_gmail_with_attachment_latin1 |
|
| 476 |
issue = submit_email( |
|
| 477 |
'gmail_with_attachment_iso-8859-1.eml', |
|
| 478 |
:issue => {:project => 'ecookbook'}
|
|
| 479 |
) |
|
| 480 |
assert_kind_of Issue, issue |
|
| 481 |
assert_equal 1, issue.attachments.size |
|
| 482 |
u = "" |
|
| 483 |
u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
|
|
| 484 |
u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc" |
|
| 485 |
u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
|
|
| 486 |
11.times { u << u1 }
|
|
| 487 |
attachment = issue.attachments.first |
|
| 488 |
assert_equal "#{u}.txt", attachment.filename
|
|
| 489 |
assert_equal 5, attachment.filesize |
|
| 490 |
assert File.exist?(attachment.diskfile) |
|
| 491 |
assert_equal 5, File.size(attachment.diskfile) |
|
| 492 |
assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
| 493 |
end |
|
| 494 |
|
|
| 495 |
def test_add_issue_with_iso_8859_1_subject |
|
| 496 |
issue = submit_email( |
|
| 497 |
'subject_as_iso-8859-1.eml', |
|
| 498 |
:issue => {:project => 'ecookbook'}
|
|
| 499 |
) |
|
| 500 |
str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..." |
|
| 501 |
str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
|
|
| 502 |
assert_kind_of Issue, issue |
|
| 503 |
assert_equal str, issue.subject |
|
| 504 |
end |
|
| 505 |
|
|
| 506 |
def test_add_issue_with_japanese_subject |
|
| 507 |
issue = submit_email( |
|
| 508 |
'subject_japanese_1.eml', |
|
| 509 |
:issue => {:project => 'ecookbook'}
|
|
| 510 |
) |
|
| 511 |
assert_kind_of Issue, issue |
|
| 512 |
ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" |
|
| 513 |
ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
|
|
| 514 |
assert_equal ja, issue.subject |
|
| 515 |
end |
|
| 516 |
|
|
| 517 |
def test_add_issue_with_no_subject_header |
|
| 518 |
issue = submit_email( |
|
| 519 |
'no_subject_header.eml', |
|
| 520 |
:issue => {:project => 'ecookbook'}
|
|
| 521 |
) |
|
| 522 |
assert_kind_of Issue, issue |
|
| 523 |
assert_equal '(no subject)', issue.subject |
|
| 524 |
end |
|
| 525 |
|
|
| 526 |
def test_add_issue_with_mixed_japanese_subject |
|
| 527 |
issue = submit_email( |
|
| 528 |
'subject_japanese_2.eml', |
|
| 529 |
:issue => {:project => 'ecookbook'}
|
|
| 530 |
) |
|
| 531 |
assert_kind_of Issue, issue |
|
| 532 |
ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" |
|
| 533 |
ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
|
|
| 534 |
assert_equal ja, issue.subject |
|
| 535 |
end |
|
Also available in: Unified diff