diff --git a/lib/open_project/xls_export/filename_helper.rb b/lib/open_project/xls_export/filename_helper.rb index c806109d29..64b1351b6c 100644 --- a/lib/open_project/xls_export/filename_helper.rb +++ b/lib/open_project/xls_export/filename_helper.rb @@ -1,7 +1,9 @@ -class FilenameHelper - # Remove characters that could cause problems on popular OSses - # => A string that does not start with a space or dot and does not contain any of \/:*?"<>| - def self.sane_filename(str) - str.gsub(/^[ \.]/,"").gsub(/[\\\/:\*\?"<>|"]/, "_") +module OpenProject::XlsExport + class FilenameHelper + # Remove characters that could cause problems on popular OSses + # => A string that does not start with a space or dot and does not contain any of \/:*?"<>| + def self.sane_filename(str) + str.gsub(/^[ \.]/,"").gsub(/[\\\/:\*\?"<>|"]/, "_") + end end end \ No newline at end of file diff --git a/lib/open_project/xls_export/filter_settings_helper.rb b/lib/open_project/xls_export/filter_settings_helper.rb index d4b8951fd3..6d67de1838 100644 --- a/lib/open_project/xls_export/filter_settings_helper.rb +++ b/lib/open_project/xls_export/filter_settings_helper.rb @@ -1,27 +1,29 @@ -class FilterSettingsHelper - class << self - def group_by_setting(query) - I18n.t("field_#{query.group_by.to_s.gsub(/\_id$/, "")}") if query.group_by - end - - def filter_settings(query) - filters = query.available_filters - filters.sort{|a,b| a[1][:order] <=> b[1][:order]}.collect do |field, options| - if query.has_filter? field - o = query.filters[field.to_s][:operator] - ((options[:name] || I18n.t(("field_#{field.to_s.gsub(/\_id$/, "")}"))) + " " + - I18n.t(Query.operators[o], :default => o.to_s) + " " + - query.values_for(field).collect do |v| - if options[:values] - options[:values].detect do |o| - o[1] == v +module OpenProject::XlsExport + class FilterSettingsHelper + class << self + def group_by_setting(query) + I18n.t("field_#{query.group_by.to_s.gsub(/\_id$/, "")}") if query.group_by + end + + def filter_settings(query) + filters = query.available_filters + filters.sort{|a,b| a[1][:order] <=> b[1][:order]}.collect do |field, options| + if query.has_filter? field + o = query.filters[field.to_s][:operator] + ((options[:name] || I18n.t(("field_#{field.to_s.gsub(/\_id$/, "")}"))) + " " + + I18n.t(Query.operators[o], :default => o.to_s) + " " + + query.values_for(field).collect do |v| + if options[:values] + options[:values].detect do |o| + o[1] == v + end + else + [v] end - else - [v] - end - end.compact.collect(&:first).join(" #{I18n.t(:sentence_separator_or)} ")) - end - end.compact + end.compact.collect(&:first).join(" #{I18n.t(:sentence_separator_or)} ")) + end + end.compact + end end end -end \ No newline at end of file +end diff --git a/lib/open_project/xls_export/patches/issues_controller_patch.rb b/lib/open_project/xls_export/patches/issues_controller_patch.rb index 34943cf17f..b5f5609c30 100644 --- a/lib/open_project/xls_export/patches/issues_controller_patch.rb +++ b/lib/open_project/xls_export/patches/issues_controller_patch.rb @@ -2,79 +2,82 @@ #require_dependency 'xls_report/spreadsheet_builder' #require_dependency 'additional_formats/filter_settings_helper' -module OpenProject::XlsExport::Patches - module IssuesControllerPatch - def self.included(base) # :nodoc: - base.send(:include, InstanceMethods) +module OpenProject::XlsExport + module Patches + module IssuesControllerPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) - base.class_eval do - unloadable + base.class_eval do + unloadable + end end - end - module InstanceMethods + module InstanceMethods - # If the index action is called, hook the xls format into the issues controller - def respond_to(&block) - if ((params["action"].to_sym == :index or params[:action] == "all") && params["format"].to_s.downcase == "xls") - super do |format| - yield format - format.xls do - @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version], - :order => sort_clause) - unless @issues.empty? - send_data(issues_to_xls, :type => "application/vnd.ms-excel", - :filename => FilenameHelper.sane_filename("(#{I18n.l(DateTime.now)}) Issue Report#{" " + @project.name if @project}.xls")) + # If the index action is called, hook the xls format into the issues controller + def respond_to(&block) + if ((params["action"].to_sym == :index or params[:action] == "all") && params["format"].to_s.downcase == "xls") + super do |format| + yield format + format.xls do + @issues = @query.issues(:include => [:assigned_to, :type, :priority, :category, :fixed_version], + :order => sort_clause) + unless @issues.empty? + send_data(issues_to_xls, :type => "application/vnd.ms-excel", + :filename => FilenameHelper.sane_filename("(#{I18n.l(DateTime.now)}) Issue Report#{" " + @project.name if @project}.xls")) + end end end + else + super(&block) end - else - super(&block) end - end - # Convert an issues query with associated issues to xls using the queries columns as headers - def build_spreadsheet(project, issues, query) - columns = query.columns - sb = SpreadsheetBuilder.new - project_name = (project.name if project) || "All Projects" - sb.add_title("#{project_name} >> #{l(:label_issue_plural)} (#{format_date(Date.today)})") + # Convert an issues query with associated issues to xls using the queries columns as headers + def build_spreadsheet(project, issues, query) + columns = query.columns - filters = FilterSettingsHelper.filter_settings(query) - sb.add_headers l(:label_filter_plural) - sb.add_row(filters) - group_by_settings = FilterSettingsHelper.group_by_setting(query) - if group_by_settings - sb.add_headers l(:field_group_by) - sb.add_row group_by_settings - end - sb.add_empty_row + sb = SpreadsheetBuilder.new + project_name = (project.name if project) || "All Projects" + sb.add_title("#{project_name} >> #{l(:label_issue_plural)} (#{format_date(Date.today)})") - headers = (columns.collect(&:caption) << l(:field_description)).unshift("#") - sb.add_headers headers + filters = FilterSettingsHelper.filter_settings(query) + sb.add_headers l(:label_filter_plural) + sb.add_row(filters) + group_by_settings = FilterSettingsHelper.group_by_setting(query) + if group_by_settings + sb.add_headers l(:field_group_by) + sb.add_row group_by_settings + end + sb.add_empty_row - issues.each do |issue| - sb.add_row((columns.collect do |column| - cv = column.value(issue) - (cv.respond_to? :name) ? cv.name : cv - end << issue.description).unshift(issue.id)) - end + headers = (columns.collect(&:caption) << l(:field_description)).unshift("#") + sb.add_headers headers - headers.each_with_index do |h,idx| - h = h.to_s.downcase - if (h =~ /.*hours.*/ or h == "spent_time") - sb.add_format_option_to_column idx, :number_format => "0.0 h" - elsif (h =~ /.*cost.*/) - sb.add_format_option_to_column idx, :number_format => number_to_currency(0.00) + issues.each do |issue| + sb.add_row((columns.collect do |column| + cv = column.value(issue) + (cv.respond_to? :name) ? cv.name : cv + end << issue.description).unshift(issue.id)) end - end - sb - end + headers.each_with_index do |h,idx| + h = h.to_s.downcase + if (h =~ /.*hours.*/ or h == "spent_time") + sb.add_format_option_to_column idx, :number_format => "0.0 h" + elsif (h =~ /.*cost.*/) + sb.add_format_option_to_column idx, :number_format => number_to_currency(0.00) + end + end - # Return an xls file from a spreadsheet builder - def issues_to_xls - build_spreadsheet(@project, @issues, @query).xls + sb + end + + # Return an xls file from a spreadsheet builder + def issues_to_xls + build_spreadsheet(@project, @issues, @query).xls + end end end end diff --git a/lib/open_project/xls_export/spreadsheet_builder.rb b/lib/open_project/xls_export/spreadsheet_builder.rb index f15fb75e1a..9a498aaaa3 100644 --- a/lib/open_project/xls_export/spreadsheet_builder.rb +++ b/lib/open_project/xls_export/spreadsheet_builder.rb @@ -11,165 +11,168 @@ require 'spreadsheet' # If a worksheet with an index larger than the number of worksheets is requested, # a new worksheet is created. # -class SpreadsheetBuilder - Worksheet = Struct.new(:sheet, :column_widths) unless defined? Worksheet +module OpenProject::XlsExport + class SpreadsheetBuilder - def initialize(name = nil) - Spreadsheet.client_encoding = 'UTF-8' - @xls = Spreadsheet::Workbook.new - @worksheets = [] - worksheet(0, name) - end + Worksheet = Struct.new(:sheet, :column_widths) unless defined? Worksheet - # Retrieve or create the worksheet at index x - def worksheet(idx, name = nil) - name ||= "Worksheet #{@worksheets.length + 1}" - if @worksheets[idx].nil? - @worksheets[idx] = Worksheet.new.tap do |wb| - wb.sheet = @xls.create_worksheet(:name => name) - wb.column_widths = [] - end + def initialize(name = nil) + Spreadsheet.client_encoding = 'UTF-8' + @xls = Spreadsheet::Workbook.new + @worksheets = [] + worksheet(0, name) end - @sheet = @worksheets[idx].sheet - @column_widths = @worksheets[idx].column_widths - end + # Retrieve or create the worksheet at index x + def worksheet(idx, name = nil) + name ||= "Worksheet #{@worksheets.length + 1}" + if @worksheets[idx].nil? + @worksheets[idx] = Worksheet.new.tap do |wb| + wb.sheet = @xls.create_worksheet(:name => name) + wb.column_widths = [] + end + end - # Update column widths and wrap text if neccessary - def update_sheet_widths - @column_widths.count.times do |idx| - if @column_widths[idx] > 60 - @sheet.column(idx).width = 60 - @sheet.rows.each do |row| - fmt = row.formats[idx] || @sheet.column(idx).default_format - fmt.text_wrap = true - row.set_format(idx, fmt) + @sheet = @worksheets[idx].sheet + @column_widths = @worksheets[idx].column_widths + end + + # Update column widths and wrap text if neccessary + def update_sheet_widths + @column_widths.count.times do |idx| + if @column_widths[idx] > 60 + @sheet.column(idx).width = 60 + @sheet.rows.each do |row| + fmt = row.formats[idx] || @sheet.column(idx).default_format + fmt.text_wrap = true + row.set_format(idx, fmt) + end + else + @sheet.column(idx).width = @column_widths[idx] end - else - @sheet.column(idx).width = @column_widths[idx] end end - end - # Get the approximate width of a value as seen in the excel sheet - def get_value_width(value) - if ['Time', 'Date'].include?(value.class.name) - return 18 unless value.to_s.length < 18 + # Get the approximate width of a value as seen in the excel sheet + def get_value_width(value) + if ['Time', 'Date'].include?(value.class.name) + return 18 unless value.to_s.length < 18 + end + + tot_w = [Float(0)] + idx=0 + value.to_s.each_char do |c| + case c + when '0'..'9' + tot_w[idx] += 1.2 + when '.', ';', ':', ',', ' ', 'i', 'I', 'j', 'J', '(', ')', '[', ']', '!', '-', 't', 'l' + tot_w[idx] += 0.7 + when 'W', 'M', 'D' + tot_w[idx] += 1.2 + when "\n" + idx = idx + 1 + tot_w << Float(0) + else + tot_w[idx] += 1.05 + end + end + + wdth=0 + tot_w.each do |w| + wdth = w unless w :bold, :size => 18) + @sheet.row(0).set_format(0, title_format) end - wdth=0 - tot_w.each do |w| - wdth = w unless w :bold) + add_row(arr, idx) + idx ||= @sheet.last_row_index + (arr.size + 1).times { |i| @sheet.row(idx).set_format(i, header_format) } end - title_format = Spreadsheet::Format.new(:weight => :bold, :size => 18) - @sheet.row(0).set_format(0, title_format) - end - - # Add an empty row in the next sequential position. Convenience method - # for calling add_row([""]) - def add_empty_row - add_row([""]) - end - - # Add headers. This is usually used for adding a table header to the - # second row in the document, but the row can be set using the second - # optional parameter. The format is automatically set to bold font - def add_headers(arr, idx = nil) - header_format = Spreadsheet::Format.new(:weight => :bold) - add_row(arr, idx) - idx ||= @sheet.last_row_index - (arr.size + 1).times { |i| @sheet.row(idx).set_format(i, header_format) } - end - # Add a simple row. This will default to the next row in the sequence. - # Fixnums, Dates and Times are preserved, all other types are converted - # to String as the spreadsheet gem cannot do more formats - def add_row(arr, idx = nil) - idx ||= [@sheet.last_row_index + 1, 1].max - column_array = [] - arr.each_with_index do |c,i| - value = if ['Time', 'Date', 'Fixnum', 'Float', 'Integer'].include?(c.class.name) - c - elsif c.class == BigDecimal - c.to_f - else - c.to_s.gsub('_', ' ').gsub("\r\n", "\n").gsub("\r", "\n") + # Add a simple row. This will default to the next row in the sequence. + # Fixnums, Dates and Times are preserved, all other types are converted + # to String as the spreadsheet gem cannot do more formats + def add_row(arr, idx = nil) + idx ||= [@sheet.last_row_index + 1, 1].max + column_array = [] + arr.each_with_index do |c,i| + value = if ['Time', 'Date', 'Fixnum', 'Float', 'Integer'].include?(c.class.name) + c + elsif c.class == BigDecimal + c.to_f + else + c.to_s.gsub('_', ' ').gsub("\r\n", "\n").gsub("\r", "\n") + end + column_array << value + @column_widths[i] = 0 if @column_widths[i].nil? + value_width = get_value_width(value) + @column_widths[i] = value_width if @column_widths[i] < value_width end - column_array << value - @column_widths[i] = 0 if @column_widths[i].nil? - value_width = get_value_width(value) - @column_widths[i] = value_width if @column_widths[i] < value_width + @sheet.row(idx).concat column_array end - @sheet.row(idx).concat column_array - end - # Add a default format to the column at index - def add_format_option_to_column(index, opt) - unless opt.empty? - fmt = @sheet.column(index).default_format - opt.each do |k,v| - fmt.send(:"#{k.to_sym}=", v) if fmt.respond_to? :"#{k.to_sym}=" + # Add a default format to the column at index + def add_format_option_to_column(index, opt) + unless opt.empty? + fmt = @sheet.column(index).default_format + opt.each do |k,v| + fmt.send(:"#{k.to_sym}=", v) if fmt.respond_to? :"#{k.to_sym}=" + end + @sheet.column(index).default_format = fmt end - @sheet.column(index).default_format = fmt end - end - # Return the next free row we would write to in natural indexing (Starting at 1) - def current_row - @sheet.row_count - end + # Return the next free row we would write to in natural indexing (Starting at 1) + def current_row + @sheet.row_count + end - # Return the xls file as a string - def xls - @worksheets.length.times do |i| - worksheet(i) - update_sheet_widths + # Return the xls file as a string + def xls + @worksheets.length.times do |i| + worksheet(i) + update_sheet_widths + end + io = StringIO.new + @xls.write(io) + io.rewind + io.read end - io = StringIO.new - @xls.write(io) - io.rewind - io.read - end -private - def raw_xls - @xls - end + private + def raw_xls + @xls + end - def raw_sheet - @sheet + def raw_sheet + @sheet + end end end