@@ -371,12 +371,16 module IssuesHelper | |||
|
371 | 371 | def issues_to_csv(issues, project, query, options={}) |
|
372 | 372 | decimal_separator = l(:general_csv_decimal_separator) |
|
373 | 373 | encoding = l(:general_csv_encoding) |
|
374 | columns = (options[:columns] == 'all' ? query.available_columns : query.columns) | |
|
374 | columns = (options[:columns] == 'all' ? query.available_inline_columns : query.inline_columns) | |
|
375 | if options[:description] | |
|
376 | if description = query.available_columns.detect {|q| q.name == :description} | |
|
377 | columns << description | |
|
378 | end | |
|
379 | end | |
|
375 | 380 | |
|
376 | 381 | export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| |
|
377 | 382 | # csv header fields |
|
378 |
csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } |
|
|
379 | (options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : []) | |
|
383 | csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } | |
|
380 | 384 | |
|
381 | 385 | # csv lines |
|
382 | 386 | issues.each do |issue| |
@@ -398,8 +402,7 module IssuesHelper | |||
|
398 | 402 | end |
|
399 | 403 | s.to_s |
|
400 | 404 | end |
|
401 |
csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } |
|
|
402 | (options[:description] ? [Redmine::CodesetUtil.from_utf8(issue.description, encoding)] : []) | |
|
405 | csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } | |
|
403 | 406 | end |
|
404 | 407 | end |
|
405 | 408 | export |
@@ -50,6 +50,14 module QueriesHelper | |||
|
50 | 50 | end |
|
51 | 51 | end |
|
52 | 52 | |
|
53 | def available_block_columns_tags(query) | |
|
54 | tags = ''.html_safe | |
|
55 | query.available_block_columns.each do |column| | |
|
56 | tags << content_tag('label', check_box_tag('c[]', column.name.to_s, query.has_column?(column)) + " #{column.caption}", :class => 'inline') | |
|
57 | end | |
|
58 | tags | |
|
59 | end | |
|
60 | ||
|
53 | 61 | def column_header(column) |
|
54 | 62 | column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption, |
|
55 | 63 | :default_order => column.default_order) : |
@@ -70,6 +78,8 module QueriesHelper | |||
|
70 | 78 | when 'String' |
|
71 | 79 | if column.name == :subject |
|
72 | 80 | link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) |
|
81 | elsif column.name == :description | |
|
82 | issue.description? ? content_tag('div', textilizable(issue, :description), :class => "wiki") : '' | |
|
73 | 83 | else |
|
74 | 84 | h(value) |
|
75 | 85 | end |
@@ -27,6 +27,7 class QueryColumn | |||
|
27 | 27 | self.groupable = name.to_s |
|
28 | 28 | end |
|
29 | 29 | self.default_order = options[:default_order] |
|
30 | @inline = options.key?(:inline) ? options[:inline] : true | |
|
30 | 31 | @caption_key = options[:caption] || "field_#{name}" |
|
31 | 32 | end |
|
32 | 33 | |
@@ -43,6 +44,10 class QueryColumn | |||
|
43 | 44 | @sortable.is_a?(Proc) ? @sortable.call : @sortable |
|
44 | 45 | end |
|
45 | 46 | |
|
47 | def inline? | |
|
48 | @inline | |
|
49 | end | |
|
50 | ||
|
46 | 51 | def value(issue) |
|
47 | 52 | issue.send name |
|
48 | 53 | end |
@@ -58,6 +63,7 class QueryCustomFieldColumn < QueryColumn | |||
|
58 | 63 | self.name = "cf_#{custom_field.id}".to_sym |
|
59 | 64 | self.sortable = custom_field.order_statement || false |
|
60 | 65 | self.groupable = custom_field.group_statement || false |
|
66 | @inline = true | |
|
61 | 67 | @cf = custom_field |
|
62 | 68 | end |
|
63 | 69 | |
@@ -153,7 +159,8 class Query < ActiveRecord::Base | |||
|
153 | 159 | QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), |
|
154 | 160 | QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), |
|
155 | 161 | QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'), |
|
156 | QueryColumn.new(:relations, :caption => :label_related_issues) | |
|
162 | QueryColumn.new(:relations, :caption => :label_related_issues), | |
|
163 | QueryColumn.new(:description, :inline => false) | |
|
157 | 164 | ] |
|
158 | 165 | cattr_reader :available_columns |
|
159 | 166 | |
@@ -506,6 +513,22 class Query < ActiveRecord::Base | |||
|
506 | 513 | end.compact |
|
507 | 514 | end |
|
508 | 515 | |
|
516 | def inline_columns | |
|
517 | columns.select(&:inline?) | |
|
518 | end | |
|
519 | ||
|
520 | def block_columns | |
|
521 | columns.reject(&:inline?) | |
|
522 | end | |
|
523 | ||
|
524 | def available_inline_columns | |
|
525 | available_columns.select(&:inline?) | |
|
526 | end | |
|
527 | ||
|
528 | def available_block_columns | |
|
529 | available_columns.reject(&:inline?) | |
|
530 | end | |
|
531 | ||
|
509 | 532 | def default_columns_names |
|
510 | 533 | @default_columns_names ||= begin |
|
511 | 534 | default_columns = Setting.issue_list_default_columns.map(&:to_sym) |
@@ -10,7 +10,7 | |||
|
10 | 10 | :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> |
|
11 | 11 | </th> |
|
12 | 12 | <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> |
|
13 | <% query.columns.each do |column| %> | |
|
13 | <% query.inline_columns.each do |column| %> | |
|
14 | 14 | <%= column_header(column) %> |
|
15 | 15 | <% end %> |
|
16 | 16 | </tr> |
@@ -21,7 +21,7 | |||
|
21 | 21 | <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %> |
|
22 | 22 | <% reset_cycle %> |
|
23 | 23 | <tr class="group open"> |
|
24 | <td colspan="<%= query.columns.size + 2 %>"> | |
|
24 | <td colspan="<%= query.inline_columns.size + 2 %>"> | |
|
25 | 25 | <span class="expander" onclick="toggleRowGroup(this);"> </span> |
|
26 | 26 | <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, issue) %> <span class="count"><%= @issue_count_by_group[group] %></span> |
|
27 | 27 | <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", |
@@ -33,8 +33,15 | |||
|
33 | 33 | <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>"> |
|
34 | 34 | <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td> |
|
35 | 35 | <td class="id"><%= link_to issue.id, issue_path(issue) %></td> |
|
36 | <%= raw query.columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %> | |
|
36 | <%= raw query.inline_columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %> | |
|
37 | 37 | </tr> |
|
38 | <% @query.block_columns.each do |column| | |
|
39 | if (text = column_content(column, issue)) && text.present? -%> | |
|
40 | <tr class="<%= current_cycle %>"> | |
|
41 | <td colspan="<%= @query.inline_columns.size + 2 %>" class="<%= column.css_classes %>"><%= text %></td> | |
|
42 | </tr> | |
|
43 | <% end -%> | |
|
44 | <% end -%> | |
|
38 | 45 | <% end -%> |
|
39 | 46 | </tbody> |
|
40 | 47 | </table> |
@@ -34,6 +34,10 | |||
|
34 | 34 | @query.group_by) |
|
35 | 35 | ) %></td> |
|
36 | 36 | </tr> |
|
37 | <tr> | |
|
38 | <td><%= l(:button_show) %></td> | |
|
39 | <td><%= available_block_columns_tags(@query) %></td> | |
|
40 | </tr> | |
|
37 | 41 | </table> |
|
38 | 42 | </div> |
|
39 | 43 | </fieldset> |
@@ -73,7 +77,7 | |||
|
73 | 77 | <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label> |
|
74 | 78 | </p> |
|
75 | 79 | <p> |
|
76 | <label><%= check_box_tag 'description', '1' %> <%= l(:field_description) %></label> | |
|
80 | <label><%= check_box_tag 'description', '1', @query.has_column?(:description) %> <%= l(:field_description) %></label> | |
|
77 | 81 | </p> |
|
78 | 82 | <p class="buttons"> |
|
79 | 83 | <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);" %> |
@@ -4,7 +4,7 | |||
|
4 | 4 | <%= label_tag "available_columns", l(:description_available_columns) %> |
|
5 | 5 | <br /> |
|
6 | 6 | <%= select_tag 'available_columns', |
|
7 | options_for_select((query.available_columns - query.columns).collect {|column| [column.caption, column.name]}), | |
|
7 | options_for_select((query.available_inline_columns - query.columns).collect {|column| [column.caption, column.name]}), | |
|
8 | 8 | :multiple => true, :size => 10, :style => "width:150px", |
|
9 | 9 | :ondblclick => "moveOptions(this.form.available_columns, this.form.selected_columns);" %> |
|
10 | 10 | </td> |
@@ -18,7 +18,7 | |||
|
18 | 18 | <%= label_tag "selected_columns", l(:description_selected_columns) %> |
|
19 | 19 | <br /> |
|
20 | 20 | <%= select_tag((defined?(tag_name) ? tag_name : 'c[]'), |
|
21 | options_for_select(query.columns.collect {|column| [column.caption, column.name]}), | |
|
21 | options_for_select(query.inline_columns.collect {|column| [column.caption, column.name]}), | |
|
22 | 22 | :id => 'selected_columns', :multiple => true, :size => 10, :style => "width:150px", |
|
23 | 23 | :ondblclick => "moveOptions(this.form.selected_columns, this.form.available_columns);") %> |
|
24 | 24 | </td> |
@@ -21,6 +21,9 | |||
|
21 | 21 | |
|
22 | 22 | <p><label for="query_group_by"><%= l(:field_group_by) %></label> |
|
23 | 23 | <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p> |
|
24 | ||
|
25 | <p><label><%= l(:button_show) %></label> | |
|
26 | <%= available_block_columns_tags(@query) %></p> | |
|
24 | 27 | </div> |
|
25 | 28 | |
|
26 | 29 | <fieldset id="filters"><legend><%= l(:label_filter_plural) %></legend> |
@@ -403,6 +403,9 class TCPDF | |||
|
403 | 403 | Error("Incorrect orientation: #{orientation}") |
|
404 | 404 | end |
|
405 | 405 | |
|
406 | @fw = @w_pt/@k | |
|
407 | @fh = @h_pt/@k | |
|
408 | ||
|
406 | 409 | @cur_orientation = @def_orientation |
|
407 | 410 | @w = @w_pt/@k |
|
408 | 411 | @h = @h_pt/@k |
@@ -3615,9 +3618,9 class TCPDF | |||
|
3615 | 3618 | restspace = GetPageHeight() - GetY() - GetBreakMargin(); |
|
3616 | 3619 | |
|
3617 | 3620 | writeHTML(html, true, fill); # write html text |
|
3621 | SetX(x) | |
|
3618 | 3622 | |
|
3619 | 3623 | currentY = GetY(); |
|
3620 | ||
|
3621 | 3624 | @auto_page_break = false; |
|
3622 | 3625 | # check if a new page has been created |
|
3623 | 3626 | if (@page > pagenum) |
@@ -3625,11 +3628,13 class TCPDF | |||
|
3625 | 3628 | currentpage = @page; |
|
3626 | 3629 | @page = pagenum; |
|
3627 | 3630 | SetY(GetPageHeight() - restspace - GetBreakMargin()); |
|
3631 | SetX(x) | |
|
3628 | 3632 | Cell(w, restspace - 1, "", b, 0, 'L', 0); |
|
3629 | 3633 | b = b2; |
|
3630 | 3634 | @page += 1; |
|
3631 | 3635 | while @page < currentpage |
|
3632 | 3636 | SetY(@t_margin); # put cursor at the beginning of text |
|
3637 | SetX(x) | |
|
3633 | 3638 | Cell(w, @page_break_trigger - @t_margin, "", b, 0, 'L', 0); |
|
3634 | 3639 | @page += 1; |
|
3635 | 3640 | end |
@@ -3638,10 +3643,12 class TCPDF | |||
|
3638 | 3643 | end |
|
3639 | 3644 | # design a cell around the text on last page |
|
3640 | 3645 | SetY(@t_margin); # put cursor at the beginning of text |
|
3646 | SetX(x) | |
|
3641 | 3647 | Cell(w, currentY - @t_margin, "", b, 0, 'L', 0); |
|
3642 | 3648 | else |
|
3643 | 3649 | SetY(y); # put cursor at the beginning of text |
|
3644 | 3650 | # design a cell around the text |
|
3651 | SetX(x) | |
|
3645 | 3652 | Cell(w, [h, (currentY - y)].max, "", border, 0, 'L', 0); |
|
3646 | 3653 | end |
|
3647 | 3654 | @auto_page_break = true; |
@@ -34,12 +34,12 module Redmine | |||
|
34 | 34 | include Redmine::I18n |
|
35 | 35 | attr_accessor :footer_date |
|
36 | 36 | |
|
37 | def initialize(lang) | |
|
37 | def initialize(lang, orientation='P') | |
|
38 | 38 | @@k_path_cache = Rails.root.join('tmp', 'pdf') |
|
39 | 39 | FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache) |
|
40 | 40 | set_language_if_valid lang |
|
41 | 41 | pdf_encoding = l(:general_pdf_encoding).upcase |
|
42 |
super( |
|
|
42 | super(orientation, 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding) | |
|
43 | 43 | case current_language.to_s.downcase |
|
44 | 44 | when 'vi' |
|
45 | 45 | @font_for_content = 'DejaVuSans' |
@@ -236,7 +236,7 module Redmine | |||
|
236 | 236 | |
|
237 | 237 | # fetch row values |
|
238 | 238 | def fetch_row_values(issue, query, level) |
|
239 | query.columns.collect do |column| | |
|
239 | query.inline_columns.collect do |column| | |
|
240 | 240 | s = if column.is_a?(QueryCustomFieldColumn) |
|
241 | 241 | cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id} |
|
242 | 242 | show_value(cv) |
@@ -263,10 +263,10 module Redmine | |||
|
263 | 263 | # by captions |
|
264 | 264 | pdf.SetFontStyle('B',8) |
|
265 | 265 | col_padding = pdf.GetStringWidth('OO') |
|
266 | col_width_min = query.columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding} | |
|
266 | col_width_min = query.inline_columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding} | |
|
267 | 267 | col_width_max = Array.new(col_width_min) |
|
268 | 268 | col_width_avg = Array.new(col_width_min) |
|
269 | word_width_max = query.columns.map {|c| | |
|
269 | word_width_max = query.inline_columns.map {|c| | |
|
270 | 270 | n = 10 |
|
271 | 271 | c.caption.split.each {|w| |
|
272 | 272 | x = pdf.GetStringWidth(w) + col_padding |
@@ -370,13 +370,13 module Redmine | |||
|
370 | 370 | # render it background to find the max height used |
|
371 | 371 | base_x = pdf.GetX |
|
372 | 372 | base_y = pdf.GetY |
|
373 | max_height = issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true) | |
|
373 | max_height = issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) | |
|
374 | 374 | pdf.Rect(base_x, base_y, table_width + col_id_width, max_height, 'FD'); |
|
375 | 375 | pdf.SetXY(base_x, base_y); |
|
376 | 376 | |
|
377 | 377 | # write the cells on page |
|
378 | 378 | pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1) |
|
379 | issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true) | |
|
379 | issues_to_pdf_write_cells(pdf, query.inline_columns, col_width, row_height, true) | |
|
380 | 380 | issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width) |
|
381 | 381 | pdf.SetY(base_y + max_height); |
|
382 | 382 | |
@@ -387,7 +387,7 module Redmine | |||
|
387 | 387 | |
|
388 | 388 | # Returns a PDF string of a list of issues |
|
389 | 389 | def issues_to_pdf(issues, project, query) |
|
390 | pdf = ITCPDF.new(current_language) | |
|
390 | pdf = ITCPDF.new(current_language, "L") | |
|
391 | 391 | title = query.new_record? ? l(:label_issue_plural) : query.name |
|
392 | 392 | title = "#{project} - #{title}" if project |
|
393 | 393 | pdf.SetTitle(title) |
@@ -407,11 +407,17 module Redmine | |||
|
407 | 407 | # column widths |
|
408 | 408 | table_width = page_width - right_margin - 10 # fixed left margin |
|
409 | 409 | col_width = [] |
|
410 | unless query.columns.empty? | |
|
410 | unless query.inline_columns.empty? | |
|
411 | 411 | col_width = calc_col_width(issues, query, table_width - col_id_width, pdf) |
|
412 | 412 | table_width = col_width.inject(0) {|s,v| s += v} |
|
413 | 413 | end |
|
414 | 414 | |
|
415 | # use full width if the description is displayed | |
|
416 | if table_width > 0 && query.has_column?(:description) | |
|
417 | col_width = col_width.map {|w| w = w * (page_width - right_margin - 10 - col_id_width) / table_width} | |
|
418 | table_width = col_width.inject(0) {|s,v| s += v} | |
|
419 | end | |
|
420 | ||
|
415 | 421 | # title |
|
416 | 422 | pdf.SetFontStyle('B',11) |
|
417 | 423 | pdf.RDMCell(190,10, title) |
@@ -454,6 +460,13 module Redmine | |||
|
454 | 460 | issues_to_pdf_write_cells(pdf, col_values, col_width, row_height) |
|
455 | 461 | issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width) |
|
456 | 462 | pdf.SetY(base_y + max_height); |
|
463 | ||
|
464 | if query.has_column?(:description) && issue.description? | |
|
465 | pdf.SetX(10) | |
|
466 | pdf.SetAutoPageBreak(true, 20) | |
|
467 | pdf.RDMwriteHTMLCell(0, 5, 10, 0, issue.description.to_s, issue.attachments, "LRBT") | |
|
468 | pdf.SetAutoPageBreak(false) | |
|
469 | end | |
|
457 | 470 | end |
|
458 | 471 | |
|
459 | 472 | if issues.size == Setting.issues_export_limit.to_i |
@@ -149,6 +149,8 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, t | |||
|
149 | 149 | tr.issue td.subject, tr.issue td.relations { text-align: left; } |
|
150 | 150 | tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;} |
|
151 | 151 | tr.issue td.relations span {white-space: nowrap;} |
|
152 | table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;} | |
|
153 | table.issues td.description pre {white-space:normal;} | |
|
152 | 154 | |
|
153 | 155 | tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} |
|
154 | 156 | tr.issue.idnt-1 td.subject {padding-left: 0.5em;} |
@@ -418,7 +418,7 class IssuesControllerTest < ActionController::TestCase | |||
|
418 | 418 | assert_equal 'text/csv; header=present', @response.content_type |
|
419 | 419 | assert @response.body.starts_with?("#,") |
|
420 | 420 | lines = @response.body.chomp.split("\n") |
|
421 | assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size | |
|
421 | assert_equal assigns(:query).available_inline_columns.size + 1, lines[0].split(',').size | |
|
422 | 422 | end |
|
423 | 423 | |
|
424 | 424 | def test_index_csv_with_multi_column_field |
@@ -825,6 +825,17 class IssuesControllerTest < ActionController::TestCase | |||
|
825 | 825 | assert_equal 'application/pdf', response.content_type |
|
826 | 826 | end |
|
827 | 827 | |
|
828 | def test_index_with_description_column | |
|
829 | get :index, :set_filter => 1, :c => %w(subject description) | |
|
830 | ||
|
831 | assert_select 'table.issues thead th', 3 # columns: chekbox + id + subject | |
|
832 | assert_select 'td.description[colspan=3]', :text => 'Unable to print recipes' | |
|
833 | ||
|
834 | get :index, :set_filter => 1, :c => %w(subject description), :format => 'pdf' | |
|
835 | assert_response :success | |
|
836 | assert_equal 'application/pdf', response.content_type | |
|
837 | end | |
|
838 | ||
|
828 | 839 | def test_index_send_html_if_query_is_invalid |
|
829 | 840 | get :index, :f => ['start_date'], :op => {:start_date => '='} |
|
830 | 841 | assert_equal 'text/html', @response.content_type |
@@ -737,7 +737,9 class QueryTest < ActiveSupport::TestCase | |||
|
737 | 737 | |
|
738 | 738 | def test_default_columns |
|
739 | 739 | q = Query.new |
|
740 |
assert |
|
|
740 | assert q.columns.any? | |
|
741 | assert q.inline_columns.any? | |
|
742 | assert q.block_columns.empty? | |
|
741 | 743 | end |
|
742 | 744 | |
|
743 | 745 | def test_set_column_names |
@@ -748,6 +750,21 class QueryTest < ActiveSupport::TestCase | |||
|
748 | 750 | assert q.has_column?(c) |
|
749 | 751 | end |
|
750 | 752 | |
|
753 | def test_inline_and_block_columns | |
|
754 | q = Query.new | |
|
755 | q.column_names = ['subject', 'description', 'tracker'] | |
|
756 | ||
|
757 | assert_equal [:subject, :tracker], q.inline_columns.map(&:name) | |
|
758 | assert_equal [:description], q.block_columns.map(&:name) | |
|
759 | end | |
|
760 | ||
|
761 | def test_custom_field_columns_should_be_inline | |
|
762 | q = Query.new | |
|
763 | columns = q.available_columns.select {|column| column.is_a? QueryCustomFieldColumn} | |
|
764 | assert columns.any? | |
|
765 | assert_nil columns.detect {|column| !column.inline?} | |
|
766 | end | |
|
767 | ||
|
751 | 768 | def test_query_should_preload_spent_hours |
|
752 | 769 | q = Query.new(:name => '_', :column_names => [:subject, :spent_hours]) |
|
753 | 770 | assert q.has_column?(:spent_hours) |
General Comments 0
You need to be logged in to leave comments.
Login now