├── Gemfile ├── assets ├── images │ └── word.png └── stylesheets │ └── word.css ├── config ├── locales │ └── en.yml └── routes.rb ├── template_example.docx ├── test └── test_helper.rb ├── init.rb ├── app ├── views │ ├── settings │ │ ├── plugin.html.erb │ │ └── _export_docx_settings.html.erb │ └── issues │ │ └── _action_menu.html.erb ├── controllers │ └── docx_controller.rb └── helpers │ └── docx_helper.rb └── README.rdoc /Gemfile: -------------------------------------------------------------------------------- 1 | gem 'docx', '~> 0.2.07', :require => ["docx"] -------------------------------------------------------------------------------- /assets/images/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masweetman/export_docx/HEAD/assets/images/word.png -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # English strings go here for Rails i18n 2 | en: 3 | # my_label: "My label" 4 | -------------------------------------------------------------------------------- /template_example.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masweetman/export_docx/HEAD/template_example.docx -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # Plugin's routes 2 | # See: http://guides.rubyonrails.org/routing.html 3 | 4 | get '/issues/:id/export_docx', :to => 'docx#issue_export_docx' 5 | post '/settings/plugin/export_docx/upload', :to => 'docx#template_upload' 6 | get '/settings/plugin/export_docx/download', :to => 'docx#template_download' -------------------------------------------------------------------------------- /assets/stylesheets/word.css: -------------------------------------------------------------------------------- 1 | .icon-word-button { 2 | background-color: Transparent!important; 3 | border: none; 4 | font: inherit; 5 | cursor: pointer; 6 | background-image: url(../images/word.png); 7 | } 8 | 9 | .icon-word { 10 | background-image: url(../images/word.png); 11 | } 12 | 13 | .fake-button { padding:0;border:none;display:inline; } -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | Redmine::Plugin.register :export_docx do 2 | name 'Export DOCX' 3 | author 'Mike Sweetman' 4 | description 'This plugin exports issues to DOCX files' 5 | version '1.3.1' 6 | url 'http://github.com/masweetman/export_docx' 7 | author_url 'http://github.com/masweetman' 8 | 9 | settings :default => {'empty' => true}, :partial => 'settings/export_docx_settings' 10 | end 11 | -------------------------------------------------------------------------------- /app/views/settings/plugin.html.erb: -------------------------------------------------------------------------------- 1 | <%= title [l(:label_plugins), {:controller => 'admin', :action => 'plugins'}], @plugin.name %> 2 | 3 | <% if @plugin.id == :export_docx %> 4 | <%= render :partial => @partial, :locals => {:settings => @settings}%> 5 | <% else %> 6 |
7 | <%= form_tag({:action => 'plugin'}) do %> 8 |
9 | <%= render :partial => @partial, :locals => {:settings => @settings}%> 10 |
11 | <%= submit_tag l(:button_apply) %> 12 | <% end %> 13 |
14 | <% end %> 15 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Export DOCX 2 | 3 | This Redmine plugin allows you to export issues to Microsoft Word files. 4 | 5 | == Installation 6 | 7 | * Clone into your plugins folder: git clone https://github.com/masweetman/export_docx.git 8 | * Run bundle update 9 | * Restart Redmine 10 | 11 | == Creating templates 12 | 13 | * Create a Micorsoft Word template. Refer to template_example.docx for details. 14 | * Go to Administration --> Plugins --> Export DOCX --> Configure 15 | * Upload your template 16 | 17 | == HTML tags 18 | 19 | This plugin is only meant for use with plain text. Markdown and Textile content will not be translated. HTML tags used with plugins such as redmine_ckeditor will not be translated. Maybe someone else can develop those features! But I have no plans to do it. -------------------------------------------------------------------------------- /app/views/settings/_export_docx_settings.html.erb: -------------------------------------------------------------------------------- 1 | <% @trackers = Tracker.all.order(:name) %> 2 | 3 |
4 | <%= form_tag({ :controller => 'docx', :action => 'template_upload' }, multipart: true) do %> 5 |

6 | 7 | <%= select_tag :tracker, options_for_select(@trackers.map(&:name), :onchange => params[:tracker]) %> 8 |

9 |

10 | 11 | <%= file_field_tag :template %> 12 |

13 |

14 | 15 | <%= select_tag :template_action, options_for_select(['Replace existing template', 'Add new template'], :onchange => params[:template_action]) %> 16 |

17 |

18 | 19 | <%= check_box_tag :use_for_all %> 20 |

21 |

22 | <%= submit_tag 'Upload' %> 23 |

24 | <% end %> 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | <% @trackers.each do |tracker| %> 34 | 35 | <% paths = [] %> 36 | <% Dir.glob('files/export_docx/templates/*.docx').each do |path| %> 37 | <% filename = path.gsub('files/export_docx/templates/', '') %> 38 | <% paths << path if filename[0..tracker.name.length - 1].downcase.gsub(' ','-') == tracker.name.downcase.gsub(' ','-') && (filename[tracker.name.length].to_i > 0 || filename[tracker.name.length] == '.') %> 39 | <% end %> 40 | 41 | "> 42 | 43 | 48 | 49 | <% end %> 50 |
TrackerCurrent Template
<%= tracker.name %> 44 | <% paths.each do |path_to_file| %> 45 |

<%= link_to path_to_file.gsub('files/export_docx/templates/', ''), { :controller => 'docx', :action => 'template_download', :path => path_to_file } %>

46 | <% end %> 47 |
-------------------------------------------------------------------------------- /app/controllers/docx_controller.rb: -------------------------------------------------------------------------------- 1 | class DocxController < ApplicationController 2 | include DocxHelper 3 | 4 | def template_upload 5 | reset_template_names #updates names for version 1.1.0 6 | 7 | tracker = Tracker.find_by_name(params[:tracker]) 8 | use_for_all = params[:use_for_all] 9 | uploaded_io = params[:template] 10 | if params[:template_action] == 'Add new template' 11 | append_template = true 12 | else 13 | append_template = false 14 | remove_templates_for(tracker) 15 | end 16 | 17 | upload_file(tracker, uploaded_io, append_template) 18 | 19 | if use_for_all == '1' 20 | source = list_templates_for(tracker).last 21 | Tracker.all.each do |t| 22 | unless t == tracker 23 | if append_template 24 | dest = 'files/export_docx/templates/' + t.name.downcase.gsub(' ','-') + (list_templates_for(t).count + 1).to_s + '.docx' 25 | else 26 | dest = 'files/export_docx/templates/' + t.name.downcase.gsub(' ','-') + '1.docx' 27 | remove_templates_for(t) 28 | end 29 | FileUtils.copy_file(source, dest) 30 | end 31 | end 32 | end 33 | 34 | redirect_to plugin_settings_path(Redmine::Plugin.find('export_docx')) 35 | end 36 | 37 | def template_download 38 | path_to_file = params[:path] 39 | if File.exist?(path_to_file) 40 | send_file(path_to_file, :disposition => 'attachment') 41 | else 42 | flash[:error] = 'There is no template for ' + issue.tracker.name + ' issues.' 43 | redirect_to plugin_settings_path(Redmine::Plugin.find('export_docx')) 44 | end 45 | end 46 | 47 | def issue_export_docx 48 | issue = Issue.find(params[:id]) 49 | if params[:file].present? 50 | template_path = 'files/export_docx/templates/' + params[:file] 51 | else 52 | template_path = list_templates_for(issue.tracker).last 53 | end 54 | issue_to_docx(issue, template_path) 55 | path_to_file = 'files/export_docx/export/export.docx' 56 | if File.exist?(path_to_file) 57 | send_file(path_to_file, :filename => "#{issue.project.identifier}-#{issue.id}.docx", :disposition => 'attachment') 58 | end 59 | end 60 | 61 | end -------------------------------------------------------------------------------- /app/views/issues/_action_menu.html.erb: -------------------------------------------------------------------------------- 1 | <% tracker = @issue.tracker %> 2 | 3 | <% files = [] %> 4 | <% Dir.glob('files/export_docx/templates/*.docx').each do |path| %> 5 | <% filename = path.gsub('files/export_docx/templates/', '') %> 6 | <% files << filename if filename[0..tracker.name.length - 1].downcase.gsub(' ', '-') == tracker.name.downcase.gsub(' ', '-') && (filename[tracker.name.length].to_i > 0 || filename[tracker.name.length] == '.') %> 7 | <% end %> 8 | 9 |
10 | 11 | <% if files.count == 1 %> 12 | <%= link_to 'Export', { :controller => 'docx', :action => 'issue_export_docx', :id => @issue.id }, :class => 'icon icon-word' %> 13 | <% end %> 14 | 15 | <% if files.count > 1 %> 16 | <%= form_tag({ :controller => 'docx', :action => 'issue_export_docx', :id => @issue.id }, :method => :get) do %> 17 | Template: 18 | <%= hidden_field_tag :id, @issue.id %> 19 | <%= select_tag(:file, options_for_select(files, params[:file]), :onchange => params[:file]) %> 20 | <%= submit_tag 'Export', :class => 'icon icon-word-button' %> 21 | <% end %> 22 | <% end %> 23 | 24 | <%= link_to l(:button_edit), edit_issue_path(@issue), :onclick => 'showAndScrollTo("update", "issue_notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit) if @issue.editable? %> 25 | 26 | <%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %> 27 | <%= watcher_link(@issue, User.current) %> 28 | <%= link_to l(:button_copy), project_copy_issue_path(@project, @issue), :class => 'icon icon-copy' if User.current.allowed_to?(:copy_issues, @project) && Issue.allowed_target_projects.any? %> 29 | 30 | <% if Redmine::VERSION.to_a[0] >= 3 && Redmine::VERSION.to_a[1] >= 3 %> 31 | <%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if @issue.deletable? %> 32 | <% else %> 33 | <%= link_to l(:button_delete), issue_path(@issue), :data => {:confirm => issues_destroy_confirmation_message(@issue)}, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %> 34 | <% end %> 35 | 36 |
37 | 38 | <% content_for :header_tags do %> 39 | <%= stylesheet_link_tag 'word', :plugin => 'export_docx' %> 40 | <% end %> -------------------------------------------------------------------------------- /app/helpers/docx_helper.rb: -------------------------------------------------------------------------------- 1 | require 'docx' 2 | 3 | module DocxHelper 4 | include IssuesHelper 5 | include CustomFieldsHelper 6 | 7 | def reset_template_names 8 | templates = list_templates 9 | templates.each do |t| 10 | unless t.gsub('.docx', '').last.to_i > 0 11 | source = t 12 | dest = t.gsub('.docx', '1.docx') 13 | FileUtils.copy_file(source, dest) 14 | FileUtils.rm source 15 | end 16 | end 17 | end 18 | 19 | def upload_file(tracker, uploaded_io, append_template) 20 | if append_template 21 | filename = tracker.name.downcase.gsub(' ', '-') + (list_templates_for(tracker).count + 1).to_s + '.docx' 22 | else 23 | filename = tracker.name.downcase.gsub(' ', '-') + '1.docx' 24 | end 25 | 26 | if File.extname(uploaded_io.original_filename) == '.docx' 27 | folder_structure 28 | File.open(Rails.root.join('files', 'export_docx', 'templates', filename), 'wb') do |file| 29 | file.write(uploaded_io.read) 30 | flash[:notice] = filename + ' uploaded successfully.' 31 | end 32 | else 33 | flash[:error] = 'Template must be a .docx file.' 34 | end 35 | end 36 | 37 | def remove_templates_for(tracker) 38 | list_templates_for(tracker).each do |t| 39 | FileUtils.rm t 40 | end 41 | end 42 | 43 | def list_templates 44 | Dir.glob 'files/export_docx/templates/*.docx' 45 | end 46 | 47 | def list_templates_for(tracker) 48 | templates = [] 49 | list_templates.each do |template| 50 | filename = template.gsub('files/export_docx/templates/', '') 51 | templates << template if filename[0..tracker.name.length - 1].downcase.gsub(' ','-') == tracker.name.downcase.gsub(' ','-') && (filename[tracker.name.length].to_i > 0 || filename[tracker.name.length] == '.') 52 | end 53 | return templates 54 | end 55 | 56 | def folder_structure 57 | FileUtils.mkdir_p 'files/export_docx/templates' 58 | FileUtils.mkdir_p 'files/export_docx/export' 59 | end 60 | 61 | def issue_to_docx(issue, template_path) 62 | path_to_file = template_path.to_s 63 | if File.exist?(path_to_file) 64 | doc = Docx::Document.open(path_to_file) 65 | doc.bookmarks.keys.each do |bookmark| 66 | # write standard issue fields 67 | case bookmark.downcase 68 | when 'project' 69 | doc.bookmarks[bookmark].insert_text_after(issue.project.name) unless issue.project.nil? 70 | when 'tracker' 71 | doc.bookmarks[bookmark].insert_text_after(issue.tracker.name) unless issue.tracker.nil? 72 | when 'id' 73 | doc.bookmarks[bookmark].insert_text_after(issue.id.to_s) unless issue.id.nil? 74 | when 'subject' 75 | doc.bookmarks[bookmark].insert_text_after(issue.subject) unless issue.subject.nil? 76 | when 'description' 77 | doc.bookmarks[bookmark].insert_multiple_lines(issue.description.lines.map(&:chomp)) unless issue.description.nil? 78 | when 'notes' 79 | lines = [] 80 | index = 1 81 | issue.journals.each.with_index do |journal, i| 82 | lines << "##{index} - #{format_time(journal.created_on)} - #{journal.user}" 83 | journal.details.each do |detail| 84 | lines << "- " + show_detail(detail, true) 85 | end 86 | lines += journal.notes.lines.map(&:chomp) if journal.notes 87 | lines << "" if i < issue.journals.size - 1 88 | index += 1 89 | end 90 | doc.bookmarks[bookmark].insert_multiple_lines(lines) 91 | when 'status' 92 | doc.bookmarks[bookmark].insert_text_after(issue.status.name) unless issue.status.nil? 93 | when 'priority' 94 | doc.bookmarks[bookmark].insert_text_after(issue.priority.name) unless issue.priority.nil? 95 | when 'author', 'added_by' 96 | doc.bookmarks[bookmark].insert_text_after(issue.author.name) unless issue.author.nil? 97 | when 'assignee', 'assigned_to' 98 | doc.bookmarks[bookmark].insert_text_after(issue.assigned_to.name) unless issue.assigned_to.nil? 99 | when 'category' 100 | doc.bookmarks[bookmark].insert_text_after(issue.category.name) unless issue.category.nil? 101 | when 'target_version', 'fixed_version' 102 | doc.bookmarks[bookmark].insert_text_after(issue.fixed_version.name) unless issue.fixed_version.nil? 103 | when 'start_date' 104 | doc.bookmarks[bookmark].insert_text_after(format_date(issue.start_date)) unless issue.start_date.nil? 105 | when 'due_date' 106 | doc.bookmarks[bookmark].insert_text_after(format_date(issue.due_date)) unless issue.due_date.nil? 107 | when 'created_on' 108 | doc.bookmarks[bookmark].insert_text_after(format_date(issue.created_on)) unless issue.created_on.nil? 109 | when 'closed_on' 110 | doc.bookmarks[bookmark].insert_text_after(format_date(issue.closed_on)) unless issue.closed_on.nil? 111 | when 'percent_done', 'done_ratio' 112 | doc.bookmarks[bookmark].insert_text_after(issue.done_ratio.to_s + '%') unless issue.done_ratio.nil? 113 | when 'estimated_time', 'estimated_hours' 114 | doc.bookmarks[bookmark].insert_text_after(issue.estimated_hours.to_s) unless issue.estimated_hours.nil? 115 | when 'spent_time', 'spent_hours' 116 | doc.bookmarks[bookmark].insert_text_after(issue.spent_hours.to_s) unless issue.spent_hours.nil? 117 | else 118 | #write custom issue fields 119 | custom_field = CustomField.where("name LIKE ?", bookmark.tr('_','%')).first 120 | unless custom_field.nil? || issue.custom_field_value(custom_field.id).nil? || issue.custom_field_value(custom_field.id).empty? || issue.custom_field_value(custom_field.id).first.empty? 121 | if custom_field.field_format == 'text' 122 | doc.bookmarks[bookmark].insert_multiple_lines(issue.custom_field_value(custom_field.id).lines.map(&:chomp)) 123 | elsif custom_field.field_format == 'list' && custom_field.multiple? 124 | doc.bookmarks[bookmark].insert_multiple_lines(issue.custom_field_value(custom_field.id)) 125 | elsif custom_field.field_format == 'user' 126 | if custom_field.multiple? 127 | users = [] 128 | users = issue.custom_field_value(custom_field.id).map{ |u| User.find(u).name } 129 | doc.bookmarks[bookmark].insert_multiple_lines(users) unless users == [] 130 | else 131 | doc.bookmarks[bookmark].insert_text_after(User.find(issue.custom_field_value(custom_field.id)).to_s) 132 | end 133 | elsif custom_field.field_format == 'version' 134 | if custom_field.multiple? 135 | versions = [] 136 | versions = issue.custom_field_value(custom_field.id).map{ |v| Version.find(v).name } 137 | doc.bookmarks[bookmark].insert_multiple_lines(versions) unless versions == [] 138 | else 139 | doc.bookmarks[bookmark].insert_text_after(Version.find(issue.custom_field_value(custom_field.id)).to_s) 140 | end 141 | elsif custom_field.field_format == 'date' 142 | doc.bookmarks[bookmark].insert_text_after(format_date(issue.custom_field_value(custom_field.id).to_date)) 143 | elsif custom_field.field_format == 'bool' 144 | if doc.bookmarks[bookmark].get_run_before.node.xpath('descendant::*').last.attributes['val'].nil? 145 | if issue.custom_field_value(custom_field.id) == '1' 146 | doc.bookmarks[bookmark].insert_text_after('Yes') 147 | else 148 | doc.bookmarks[bookmark].insert_text_after('No') 149 | end 150 | else 151 | doc.bookmarks[bookmark].get_run_before.node.xpath('descendant::*').last.attributes['val'].value = issue.custom_field_value(custom_field.id).to_s 152 | end 153 | else 154 | doc.bookmarks[bookmark].insert_text_after(issue.custom_field_value(custom_field.id).to_s) 155 | end 156 | end 157 | end 158 | end 159 | doc.save('files/export_docx/export/export.docx') 160 | else 161 | flash[:error] = 'There is no template for ' + issue.tracker.name + ' issues. Please notify your Redmine administrator.' 162 | redirect_to issue 163 | end 164 | end 165 | 166 | end 167 | --------------------------------------------------------------------------------