├── 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 | Tracker:
7 | <%= select_tag :tracker, options_for_select(@trackers.map(&:name), :onchange => params[:tracker]) %>
8 |
9 |
10 | File:
11 | <%= file_field_tag :template %>
12 |
13 |
14 | Action:
15 | <%= select_tag :template_action, options_for_select(['Replace existing template', 'Add new template'], :onchange => params[:template_action]) %>
16 |
17 |
18 | Use for all trackers:
19 | <%= check_box_tag :use_for_all %>
20 |
21 |
22 | <%= submit_tag 'Upload' %>
23 |
24 | <% end %>
25 |
26 |
27 |
28 |
29 | Tracker
30 | Current Template
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 | <%= tracker.name %>
43 |
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 |
48 |
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------