This commit is contained in:
Karel Pičman 2022-05-19 09:53:39 +02:00
parent ac5b10f4fb
commit 0892e536e1
32 changed files with 257 additions and 25 deletions

View File

@ -1,6 +1,11 @@
Changelog for Redmine DMSF
==========================
3.0.1 *????-??-??*
-------------------
Office documents displayed inline (preview)
3.0.0 *2022-04-28*
-------------------

View File

@ -20,6 +20,6 @@
FROM debian:latest
RUN apt-get update
RUN apt-get -qq install mariadb-server postgresql sqlite3 libsqlite3-dev ruby ruby-dev build-essential libmariadb-dev libpq-dev subversion git litmus
RUN apt-get -qq install mariadb-server postgresql sqlite3 libsqlite3-dev ruby ruby-dev build-essential libmariadb-dev libpq-dev subversion git litmus libreoffice
COPY . /app
WORKDIR /app

View File

@ -35,9 +35,11 @@ Features
* Wiki macros for quick content linking
* Full read/write webdav functionality
* Optional document content full-text search
* Documents and folders symbolic links
* Documents and folders' symbolic links
* Trash bin
* Documents attachable to issues
* Office documents are displayed inline
* Editing of office documents
* Compatible with Redmine 5.0.x
Dependencies
@ -112,6 +114,16 @@ sudo yum install xapian-core xapian-bindings-ruby libxapian-dev poppler-utils an
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz libemail-outlook-message-perl
```
### Inline displaying of office documents (optional)
If LibreOffice binary `libreoffice` is present in the server, office documents (.odt, .ods,...) are displayed inline.
`libreoffice` package is available in the most of Linux distributions, e.g. on Debain based systems:
```
sudo apt install libreoffice
```
Usage
-----

View File

@ -21,12 +21,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'redmine'
require 'zip'
# All files in lib sub-folder are already loaded by Zeitwerk
if RedmineApp::Application.config.autoloader != :zeitwerk
require File.dirname(__FILE__) + '/lib/redmine_dmsf'
end
require File.dirname(__FILE__) + '/lib/redmine_dmsf'
def dmsf_init
# Administration menu extension

View File

@ -69,10 +69,15 @@ class DmsfFilesController < ApplicationController
member = Member.find_by(user_id: User.current.id, project_id: @file.project.id)
# IE has got a tendency to cache files
expires_in 0.year, 'must-revalidate' => true
send_file @revision.disk_file,
filename: filename_for_content_disposition(@revision.formatted_name(member)),
type: @revision.detect_content_type,
pdf_preview = @file.pdf_preview
filename = filename_for_content_disposition(@revision.formatted_name(member))
if pdf_preview.present?
basename = File.basename(filename, '.*')
send_file pdf_preview, filename: "#{basename}.pdf", type: 'application/pdf', disposition: 'inline'
else
send_file @revision.disk_file, filename: filename, type: @revision.detect_content_type,
disposition: params[:disposition].present? ? params[:disposition] : @revision.dmsf_file.disposition
end
rescue RedmineDmsf::Errors::DmsfAccessError => e
Rails.logger.error e.message
render_403

View File

@ -78,6 +78,9 @@ class DmsfFile < ActiveRecord::Base
project_key: 'project_id',
date_column: "#{table_name}.updated_at"
cattr_accessor :previews_storage_path
@@previews_storage_path = File.join(Rails.root, 'tmp', 'dmsf_previews')
before_create :default_values
def default_values
@ -465,23 +468,34 @@ class DmsfFile < ActiveRecord::Base
end
def text?
last_revision && Redmine::MimeType.is_type?('text', last_revision.disk_filename)
Redmine::MimeType.is_type?('text', last_revision&.disk_filename)
end
def image?
last_revision && Redmine::MimeType.is_type?('image', last_revision.disk_filename)
Redmine::MimeType.is_type?('image', last_revision&.disk_filename)
end
def pdf?
last_revision && (Redmine::MimeType.of(last_revision.disk_filename) == 'application/pdf')
Redmine::MimeType.of(last_revision&.disk_filename) == 'application/pdf'
end
def video?
last_revision && Redmine::MimeType.is_type?('video', last_revision.disk_filename)
Redmine::MimeType.is_type?('video', last_revision&.disk_filename)
end
def html?
last_revision && (Redmine::MimeType.of(last_revision.disk_filename) == 'text/html')
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/html'
end
def office_doc?
case File.extname(last_revision&.disk_filename)
when '.odt', '.ods', '.odp', '.odg', # LibreOffice
'.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office
'.rtf' # Universal
true
else
false
end
end
def disposition
@ -492,7 +506,30 @@ class DmsfFile < ActiveRecord::Base
image? && Redmine::Thumbnail.convert_available?
end
def preview(limit)
def previewable?
office_doc? && RedmineDmsf::Preview.office_available?
end
# Deletes all previews
def self.clear_previews
Dir.glob(File.join(DmsfFile.previews_storage_path, '*.pdf')).each do |file|
File.delete file
end
end
def pdf_preview
return '' unless previewable?
target = File.join(DmsfFile.previews_storage_path, "#{id}_#{last_revision.digest}.pdf")
begin
RedmineDmsf::Preview.generate last_revision.disk_file.to_s, target
rescue => e
Rails.logger.error "An error occured while generating preview for #{last_revision.disk_file} to #{target}\nException was: #{e.message}"
nil
end
end
def text_preview(limit)
result = +'No preview available'
if text?
begin

View File

@ -106,10 +106,18 @@
<% rescue %>
<p class="warning"><%= l(:error_tmpfile_can_not_be_created) %></p>
<% ensure %>
<% FileUtils.rm_f(testfilename) %>
<% FileUtils.rm_f testfilename %>
<% end %>
<% end %>
<p>
<%= content_tag :label, l(:label_dmsf_office_bin) %>
<%= text_field_tag 'settings[office_bin]', @settings['office_bin'], size: 10 %>
<em class="info">
<%= l(:note_dmsf_office_bin) %> <%= l(:label_default) %>: 'libreoffice'
</em>
</p>
<p>
<%= content_tag :label, l(:label_physical_file_delete) %>
<%= check_box_tag 'settings[dmsf_really_delete_files]', true, @settings['dmsf_really_delete_files'] %>

View File

@ -445,6 +445,9 @@ cs:
label_dmsf_max_notification_receivers_info: Maximální počet zobrazených příjemců notifikace
note_dmsf_max_notification_receivers_info: Omezí maximální počet zobrazených příjemců e-mailové notifikace
label_dmsf_office_bin: Binárka Libreofficu
note_dmsf_office_bin: Binárka ke konverzi kancelářských dokumentů do formátu PDF k poskytnutí jejich náhledu. Jestliže
náhledy nechcete, vložte se například 'xxx'.
easy_pages:
modules:

View File

@ -441,6 +441,9 @@ de:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ en:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ es:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ fr:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -444,6 +444,9 @@ hu:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ it: # Italian strings thx 2 Matteo Arceci!
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -446,6 +446,9 @@ ja:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ ko:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ nl:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ pl:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ pt-BR:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ sl:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -444,6 +444,9 @@ zh-TW:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -445,6 +445,9 @@ zh:
label_dmsf_max_notification_receivers_info: Maximum notification receivers info
note_dmsf_max_notification_receivers_info: Limits maximum number of displayed email notification receivers.
label_dmsf_office_bin: Libreoffice binary
note_dmsf_office_bin: A binary to convert office documents to PDF format and provide their preview. If you want
to prevent previews of office documents, put here 'xxx' for example.
easy_pages:
modules:

View File

@ -33,7 +33,7 @@ Redmine::Plugin.register :redmine_dmsf do
end
author 'Vít Jonáš / Daniel Munn / Karel Pičman'
description 'Document Management System Features'
version '3.0.0'
version '3.0.1 devel'
requires_redmine version_or_higher: '4.2.0'
@ -62,7 +62,8 @@ Redmine::Plugin.register :redmine_dmsf do
'dmsf_webdav_ignore_1b_file_for_authentication' => '1',
'dmsf_projects_as_subfolders' => nil,
'only_approval_zero_minor_version' => '0',
'dmsf_max_notification_receivers_info' => 10
'dmsf_max_notification_receivers_info' => 10,
'office_bin' => 'libreoffice'
}
end

View File

@ -144,7 +144,7 @@ module RedmineDmsf
raise ArgumentError if args.length < 2 # Requires file id and lines number
file = DmsfFile.visible.find args[0]
if User.current&.allowed_to?(:view_dmsf_files, file.project)
file.preview(args[1]).gsub("\n", '<br/>').html_safe
file.text_preview(args[1]).gsub("\n", '<br/>').html_safe
else
raise l(:notice_not_authorized)
end

View File

@ -0,0 +1,65 @@
# encoding: utf-8
# frozen_string_literal: true
#
# Redmine plugin for Document Management System "Features"
#
# Copyright © 2011 Vít Jonáš <vit.jonas@gmail.com>
# Copyright © 2011-22 Karel Pičman <karel.picman@kontron.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module RedmineDmsf
module Preview
extend Redmine::Utils::Shell
OFFICE_BIN = (Setting.plugin_redmine_dmsf['office_bin'] || 'libreoffice').freeze
def self.office_available?
if defined?(@office_available)
return @office_available
end
begin
`#{shell_quote OFFICE_BIN} --version`
@office_available = $?.success?
rescue
@office_available = false
end
unless @office_available
logger.warn "LibreOffice's command line binary (#{OFFICE_BIN}) not available"
end
@office_available
end
def self.generate(source, target)
if File.exist?(target)
target
else
dir = File.dirname(target)
cmd = "#{shell_quote(OFFICE_BIN)} --convert-to pdf --headless --outdir #{shell_quote(dir)} #{shell_quote(source)}"
if system(cmd)
source_filename = File.basename(source, '.*')
target_filename = File.basename(target)
FileUtils.mv File.join(dir, "#{source_filename}.pdf"), File.join(dir, target_filename)
target
else
logger.error "Creating preview failed (#{$?}):\nCommand: #{cmd}"
''
end
end
end
end
end

View File

@ -256,3 +256,25 @@ dmsf_file_revisions_012:
dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2022-02-03 13:39:27 +02:00
dmsf_file_revisions_013:
id: 13
dmsf_file_id: 13
source_dmsf_file_revision_id: NULL
name: 'test.odt'
disk_filename: 'test.odt'
size: 4
mime_type: 'application/vnd.oasis.opendocument.text'
title: 'Office document'
description: 'LibreOffice text'
workflow: 0
minor_version: 0
major_version: 1
comment: NULL
deleted: 0
deleted_by_user_id: NULL
user_id: 1
dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL
digest: '89a6d0ea9aafc21a978152f3e4977812d5d7d623505749471f256a90fc7c5f72'
created_at: 2017-04-01 08:54:00 +02:00

View File

@ -108,3 +108,11 @@ dmsf_files_012:
notification: false
deleted: 0
deleted_by_user_id: NULL
dmsf_files_013:
id: 13
project_id: 1
dmsf_folder_id: NULL
name: 'test.odt'
deleted: 0
deleted_by_user_id: NULL

View File

@ -30,6 +30,11 @@ class DmsfFilesControllerTest < RedmineDmsf::Test::TestCase
@request.session[:user_id] = @jsmith.id
end
def teardown
super
DmsfFile.clear_previews
end
def test_show_file_ok
# Permissions OK
get :show, params: { id: @file1.id }
@ -72,6 +77,13 @@ class DmsfFilesControllerTest < RedmineDmsf::Test::TestCase
assert_response :forbidden
end
def test_view_preview
get :view, params: { id: @file13.id }
assert_response :success
assert_equal 'application/pdf', @response.media_type
assert @response.body.starts_with?('%PDF')
end
def delete_forbidden
# Missing permissions
@role_manager.remove_permission! :file_manipulation

View File

@ -67,6 +67,7 @@ module RedmineDmsf
@file9 = DmsfFile.find 9
@file10 = DmsfFile.find 10
@file12 = DmsfFile.find 12
@file13 = DmsfFile.find 13
@folder1 = DmsfFolder.find 1
@folder2 = DmsfFolder.find 2
@folder3 = DmsfFolder.find 3

View File

@ -280,4 +280,17 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest
assert @file1.watched_by?(@jsmith)
end
def test_office_doc
assert @file13.office_doc?
end
def test_previewable
assert @file13.previewable?
end
def test_pdf_preview
assert_not_empty @file13.pdf_preview
assert_empty @file1.pdf_preview
end
end

View File

@ -202,19 +202,19 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest
# {{dmsft(document_id)}}
def test_macro_dmsft
text = textilizable("{{dmsft(#{@file1.id}, 1)}}")
assert_equal "<p>#{@file1.preview(1)}</p>", text
assert_equal "<p>#{@file1.text_preview(1)}</p>", text
end
def test_macro_dmsft_no_permissions
@manager_role.remove_permission! :view_dmsf_files
text = textilizable("{{dmsft(#{@file1.id}, 1)}}")
assert_not_equal "<p>#{@file1.preview(1)}</p>", text
assert_not_equal "<p>#{@file1.text_preview(1)}</p>", text
end
def test_macro_dmsft_dmsf_off
@project1.disable_module! :dmsf
text = textilizable("{{dmsft(#{@file1.id}, 1)}}")
assert_not_equal "<p>#{@file1.preview(1)}</p>", text
assert_not_equal "<p>#{@file1.text_preview(1)}</p>", text
end
# {{dmsf_image(file_id)}}

View File

@ -53,6 +53,7 @@ module RedmineDmsf
@file5 = DmsfFile.find 5
@file7 = DmsfFile.find 7
@file8 = DmsfFile.find 8
@file13 = DmsfFile.find 13
@folder1 = DmsfFolder.find 1
@folder2 = DmsfFolder.find 2
@folder6 = DmsfFolder.find 6