Compare commits

...

No commits in common. "main" and "active_storage" have entirely different histories.

116 changed files with 1927 additions and 1044 deletions

195
.github/workflows/rubyonrails.yml vendored Normal file
View File

@ -0,0 +1,195 @@
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# GitHub CI script
name: "GitHub CI"
on:
push:
branches: ["active_storage"]
pull_request:
branches: ["active_storage"]
jobs:
plugin_tests:
strategy:
fail-fast: false
matrix:
engine: [mysql, postgresql, sqlite]
include:
- engine: mysql
# Database configuration for Redmine
database_configuration: >
test:
adapter: mysql2
database: test
username: redmine
password: redmine
encoding: utf8mb4
collation: utf8mb4_unicode_ci
# SQL commands to create a database for Redmine
sql1: CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8mb4;
sql2: CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'redmine';
sql3: GRANT ALL PRIVILEGES ON test.* TO 'redmine'@'localhost';
# SQL client command
database_command: mysql -uroot -proot -e
# SQL service
database_service: mysql
- engine: postgresql
# Database configuration for Redmine
database_configuration: >
test:
adapter: postgresql
database: test
username: redmine
password: redmine
host: localhost
# SQL commands to create a database for Redmine
sql1: CREATE ROLE redmine LOGIN ENCRYPTED PASSWORD 'redmine' NOINHERIT VALID UNTIL 'infinity';
sql2: CREATE DATABASE test WITH ENCODING='UTF8' OWNER=redmine;
sql3: ALTER USER redmine CREATEDB;ALTER ROLE redmine WITH SUPERUSER;
# SQL client command
database_command: sudo -u postgres psql -c
# SQL service
database_service: postgresql
- engine: sqlite
# Database configuration for Redmine
database_configuration: >
test:
adapter: sqlite3
database: db/redmine.sqlite3
# No database needed here. It's just a file.
runs-on: ubuntu-latest
env:
RAILS_ENV: test
NAME: redmine_dmsf
steps:
- name: Install dependencies
# Install necessary packages
run: |
sudo apt-get update
sudo apt-get install -y litmus libreoffice subversion xapian-omega
- name: Clone Redmine
# Get the latest stable Redmine
run: svn export https://svn.redmine.org/redmine/branches/6.1-stable/ /opt/redmine
- name: Checkout code
uses: actions/checkout@v3
- name: Link the plugin
# Link the plugin to the redmine folder
run: |
ln -s $(pwd) /opt/redmine/plugins/redmine_dmsf
- name: Install Ruby and gems
uses: ruby/setup-ruby@v1 # The latest major version
with:
bundler-cache: true
ruby-version: '3.2'
- name: Setup database
# Create the database
run: |
echo "${{matrix.database_configuration}}" > /opt/redmine/config/database.yml
if [[ "${{matrix.database_service}}" ]]; then
sudo systemctl start ${{matrix.engine}}
fi
if [[ "${{matrix.database_command}}" ]]; then
${{matrix.database_command}} "${{matrix.sql1}}"
${{matrix.database_command}} "${{matrix.sql2}}"
${{matrix.database_command}} "${{matrix.sql3}}"
fi
- name: Configure WebDAV
# Add configuration for WebDAV to work
run: |
cp /opt/redmine/config/additional_environment.rb.example /opt/redmine/config/additional_environment.rb
echo "# WebDAV" >> /opt/redmine/config/additional_environment.rb
echo "config.log_level = :info" >> /opt/redmine/config/additional_environment.rb
echo "# Redmine DMSF's WebDAV" >> /opt/redmine/config/additional_environment.rb
echo "require \"#{Rails.root}/plugins/redmine_dmsf/lib/redmine_dmsf/webdav/custom_middleware\"" >> /opt/redmine/config/additional_environment.rb
echo "config.middleware.insert_before ActionDispatch::Cookies, RedmineDmsf::Webdav::CustomMiddleware" >> /opt/redmine/config/additional_environment.rb
- name: Configure Active Storage
# Add configuration for Active Storage to work
run: |
echo "# Active Storage" >> /opt/redmine/config/additional_environment.rb
echo "require 'active_storage/engine'" >> /opt/redmine/config/additional_environment.rb
echo "require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'xapian_analyzer').to_s" >> /opt/redmine/config/additional_environment.rb
echo "config.active_storage.service = :test" >> /opt/redmine/config/additional_environment.rb
echo "config.active_storage.analyzers.append RedmineDmsf::XapianAnalyzer" >> /opt/redmine/config/additional_environment.rb
echo "# Active Storage" > /opt/redmine/config/storage.yml
echo "test:" >> /opt/redmine/config/storage.yml
echo " service: Disk" >> /opt/redmine/config/storage.yml
echo " root: <%= Rails.root.join('files', 'dmsf') %>" >> /opt/redmine/config/storage.yml
- name: Install Redmine
# Install Redmine
run: |
cd /opt/redmine
bundle config set --local without 'development'
bundle install
bundle exec rake generate_secret_token
bin/rails active_storage:install
bundle exec rake db:migrate
bundle exec rake redmine:plugins:migrate
bundle exec rake redmine:load_default_data
bundle exec rake assets:precompile
env:
REDMINE_LANG: en
- name: Standard tests
# Run the tests
run: |
cd /opt/redmine
bundle exec rake redmine:plugins:test:units
bundle exec rake redmine:plugins:test:functionals
bundle exec rake redmine:plugins:test:integration
- name: Helpers tests
run: |
cd /opt/redmine
ruby plugins/redmine_dmsf/test/helpers/dmsf_files_helper_test.rb
ruby plugins/redmine_dmsf/test/helpers/dmsf_helper_test.rb
ruby plugins/redmine_dmsf/test/helpers/dmsf_links_helper_test.rb
ruby plugins/redmine_dmsf/test/helpers/dmsf_queries_helper_test.rb
- name: Rubocop
# Run the Rubocop tests
run: |
cd /opt/redmine
bundle exec rubocop -c plugins/redmine_dmsf/.rubocop.yml plugins/redmine_dmsf/
- name: Litmus
Prepare Redmine's environment for WebDAV testing
Run Puma server
Run Litmus tests (Omit 'http' tests due to 'timeout waiting for interim response' and locks due to complex bogus conditional)
Shutdown Puma
Clean up Redmine's environment from WebDAV testing
run: |
cd /opt/redmine
bundle exec rake redmine:dmsf_webdav_test_on
bundle exec rails server -u Puma -e test -d
sleep 5
litmus http://localhost:3000/dmsf/webdav/dmsf_test_project admin admin
kill $(pgrep -f puma)
bundle exec rake redmine:dmsf_webdav_test_off
env:
TESTS: "basic copymove props"
- name: Cleanup
# Rollback plugin's changes to the database
# Stop the database engine
run: |
cd /opt/redmine
bundle exec rake redmine:plugins:migrate VERSION=0
if [[ "${{matrix.database_service}}" ]]; then
sudo systemctl stop ${{matrix.engine}}
fi
- name: Archive test.log
if: always()
uses: actions/upload-artifact@v4
with:
name: "test_${{matrix.engine}}.log"
path: /opt/redmine/log/test.log

View File

@ -118,6 +118,8 @@ Rails/SkipsModelValidations:
- db/migrate/20140519133201_trash_bin.rb - db/migrate/20140519133201_trash_bin.rb
- db/migrate/07_dmsf_1_4_4.rb - db/migrate/07_dmsf_1_4_4.rb
- db/migrate/20240829093801_rename_dmsf_digest_token.rb - db/migrate/20240829093801_rename_dmsf_digest_token.rb
- db/migrate/20251015130601_active_storage_migration.rb
- db/migrate/20251017101001_restore_updated_at.rb
Rails/ThreeStateBooleanColumn: Rails/ThreeStateBooleanColumn:
Exclude: Exclude:
@ -130,6 +132,10 @@ Rails/UniqueValidationWithoutIndex:
- app/models/dmsf_file.rb - app/models/dmsf_file.rb
- app/models/dmsf_workflow_step.rb # Impossible due to steps sorting - app/models/dmsf_workflow_step.rb # Impossible due to steps sorting
Style/ClassVars:
Exclude:
- lib/redmine_dmsf.rb # @@xapian_available
Style/ExpandPathArguments: Style/ExpandPathArguments:
Enabled: false Enabled: false

View File

@ -1,6 +1,9 @@
Changelog for Redmine DMSF Changelog for Redmine DMSF
========================== ==========================
4.2.4 *????-??-??*
------------------
4.2.3 *2025-10-06* 4.2.3 *2025-10-06*
------------------ ------------------

View File

@ -18,14 +18,14 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
source 'https://rubygems.org' do source 'https://rubygems.org' do
gem 'active_record_union'
gem 'activestorage'
gem 'image_processing', '~> 1.2'
gem 'ox' # Dav4Rack gem 'ox' # Dav4Rack
gem 'rake' unless Dir.exist?(File.expand_path('../../redmine_dashboard', __FILE__)) gem 'rake' unless Dir.exist?(File.expand_path('../../redmine_dashboard', __FILE__))
gem 'simple_enum'
gem 'uuidtools' gem 'uuidtools'
gem 'zip-zip' unless Dir.exist?(File.expand_path('../../vault', __FILE__)) gem 'zip-zip' unless Dir.exist?(File.expand_path('../../vault', __FILE__))
# Redmine extensions
gem 'active_record_union'
gem 'simple_enum'
group :xapian do group :xapian do
gem 'xapian-ruby' gem 'xapian-ruby'
end end

377
README.md Normal file
View File

@ -0,0 +1,377 @@
# Redmine DMSF Plugin 5.0.0 devel
[![GitHub CI](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml/badge.svg?branch=devel)](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml)
[![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](https://github.com/support-ukraine/support-ukraine)
Redmine DMSF is Document Management System Features plugin for Redmine issue tracking system; It is aimed to replace current Redmine's Documents module.
Redmine DMSF now comes bundled with WebDAV functionality: if switched on within plugin settings this will be accessible from _/dmsf/webdav_.
WebDAV functionality is provided through Dav4Rack library.
The development has been supported by [Kontron](https://www.kontron.com) and has been released as open source thanks to their generosity.
Project home: <https://github.com/picman/redmine_dmsf>
Redmine Document Management System "Features" plugin is distributed under GNU General Public License v3 (GPL).
Redmine is a flexible project management web application, released under the terms of the GNU General Public License v2 (GPL) at <https://www.redmine.org/>
Further information about the GPL license can be found at
<https://www.gnu.org/licenses/gpl-3.0.html>
## Features
* Directory structure
* Document versioning / revision history
* Document locking
* Multi (drag/drop depending on browser) upload/download
* Direct document or document link sending via email
* Configurable document approval workflow
* Document access auditing
* Integration with Redmine's activity feed
* Wiki macros for a quick content linking
* Full read/write WebDAV functionality
* Optional document content full-text search
* Documents and folders' symbolic links
* Trash bin
* Documents attachable to issues
* Office documents are displayed inline
* Editing of office documents
* REST API
* DMS Document revision as a custom field type
* Compatible with Redmine 6
## Dependencies
* Redmine 6.1 or higher
### Full-text search (optional)
#### Indexing
If you want to use full-text search features, you must setup file content indexing.
It is necessary to index DMSF files with omindex before searching attempts to receive some output:
1. Change the configuration part of redmine_dmsf/extra/xapian_indexer.rb file according to your environment.
(The path to the index database set in xapian_indexer.rb must corresponds to the path set in the plugin's settings.)
2. Run `ruby redmine_dmsf/extra/xapian_indexer.rb -v`
This command should be run on regular basis (e.g. from cron)
Example of cron job (once per hour at 8th minute):
8 * * * * root /usr/bin/ruby redmine_dmsf/extra/xapian_indexer.rb
See redmine_dmsf/extra/xapian_indexer.rb for help.
#### Searching
If you want to use fulltext search abilities, install xapian packages. In case of using of Bitnami
stack or Ruby installed via RVM it might be necessary to install Xapian bindings from sources. See https://xapian.org
for details.
To index some files with omega you may have to install some other packages like
xpdf, antiword, ...
From Omega documentation:
* HTML (.html, .htm, .shtml, .shtm, .xhtml, .xhtm)
* PHP (.php) - our HTML parser knows to ignore PHP code
* text files (.txt, .text)
* SVG (.svg)
* CSV (Comma-Separated Values) files (.csv)
* PDF (.pdf) if pdftotext is available (comes with poppler or xpdf)
* PostScript (.ps, .eps, .ai) if ps2pdf (from ghostscript) and pdftotext (comes with poppler or xpdf) are available
* OpenOffice/StarOffice documents (.sxc, .stc, .sxd, .std, .sxi, .sti, .sxm, .sxw, .sxg, .stw) if unzip is available
* OpenDocument format documents (.odt, .ods, .odp, .odg, .odc, .odf, .odb, .odi, .odm, .ott, .ots, .otp, .otg, .otc, .otf, .oti, .oth) if unzip is available
* MS Word documents (.dot) if antiword is available (.doc files are left to libmagic, as they may actually be RTF (AbiWord saves RTF when asked to save as .doc, and Microsoft Word quietly loads RTF files with a .doc extension), or plain-text).
* MS Excel documents (.xls, .xlb, .xlt, .xlr, .xla) if xls2csv is available (comes with catdoc)
* MS Powerpoint documents (.ppt, .pps) if catppt is available (comes with catdoc)
* MS Office 2007 documents (.docx, .docm, .dotx, .dotm, .xlsx, .xlsm, .xltx, .xltm, .pptx, .pptm, .potx, .potm, .ppsx, .ppsm) if unzip is available
* Wordperfect documents (.wpd) if wpd2text is available (comes with libwpd)
* MS Works documents (.wps, .wpt) if wps2text is available (comes with libwps)
* MS Outlook message (.msg) if perl with Email::Outlook::Message and HTML::Parser modules is available
* MS Publisher documents (.pub) if pub2xhtml is available (comes with libmspub)
* AbiWord documents (.abw)
* Compressed AbiWord documents (.zabw)
* Rich Text Format documents (.rtf) if unrtf is available
* Perl POD documentation (.pl, .pm, .pod) if pod2text is available
* reStructured text (.rst, .rest) if rst2html is available (comes with docutils)
* Markdown (.md, .markdown) if markdown is available
* TeX DVI files (.dvi) if catdvi is available
* DjVu files (.djv, .djvu) if djvutxt is available
* XPS files (.xps) if unzip is available
* Debian packages (.deb, .udeb) if dpkg-deb is available
* RPM packages (.rpm) if rpm is available
* Atom feeds (.atom)
* MAFF (.maff) if unzip is available
* MHTML (.mhtml, .mht) if perl with MIME::Tools is available
* MIME email messages (.eml) and USENET articles if perl with MIME::Tools and HTML::Parser is available
* vCard files (.vcf, .vcard) if perl with Text::vCard is available
You can use following commands to install some of the required indexing tools:
On Debian use:
```
sudo apt-get install xapian-omega ruby-xapian libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz-utils libemail-outlook-message-perl
```
On Ubuntu use:
```
sudo apt-get install xapian-omega ruby-xapian libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
libwps-tools gzip unrtf catdvi djview djview3 uuid uuid-dev xz-utils libemail-outlook-message-perl
```
On CentOS use:
```
sudo yum install xapian-core xapian-bindings-ruby libxapian-dev poppler-utils antiword unzip catdoc libwpd-tools \
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.
The command must be runable by the web app's user. Test it in advance, e.g:
`sudo -u www-data libreoffice --convert-to pdf my_document.odt`
`libreoffice` package is available in the most of Linux distributions, e.g. on Debain based systems:
```
sudo apt install libreoffice liblibreoffice-java
```
## Usage
DMSF is designed to act as project module, so it must be checked as an enabled module within the project settings.
Search will now automatically search DMSF content when a Redmine search is performed, additionally a "Documents" and "Folders" check box will be visible, allowing you to search DMSF content exclusively.
## Linking DMSF object from Wiki entries (macros)
You can link DMSF object from Wikis using a macro tag `{{ }}`. List of available macros with their description is
available from the wiki's toolbar.
## Hooks
You can implement these hooks in your plugin and extend DMSF functionality in certain events.
E.g.
class DmsfUploadControllerHooks < Redmine::Hook::Listener
def dmsf_upload_controller_after_commit(context={})
context[:controller].flash[:info] = 'Okay'
end
end
**dmsf_upload_controller_after_commit**
Called after all uploaded files are committed.
parameters: *files*
**dmsf_helper_upload_after_commit**
Called after an individual file is committed. The controller is not available.
Parameters: *file*
**dmsf_workflow_controller_before_approval**
Called before an approval. If the hook returns false, the approval is not recorded.
parameters: *revision*, *step_action*
**dmsf_files_controller_before_view**
Allows a preview of the file by an external plugin. If the hook returns true, the file is not sent by DMSF. It is
expected that the file is sent by the hook.
parameters: *file*
## Setup / Upgrade
You can either clone the master branch or download the latest zipped version. Before installing ensure that the Redmine
instance is stopped.
git clone git@github.com:picman/redmine_dmsf.git
wget https://github.com/picman/redmine_dmsf/archive/master.zip
1. In case of upgrade **BACKUP YOUR DATABASE, ORIGINAL PLUGIN AND THE FOLDER WITH DOCUMENTS** first!!!
2. Put redmine_dmsf plugin directory into plugins. The plugins sub-directory must be named just **redmine_dmsf**. In case
of need rename _redmine_dmsf-x.y.z_ to *redmine_dmsf*.
3. **Go to the redmine directory**
`cd redmine`
4. Install dependencies:
`bundle install`
4.1 In production environment
bundle config set --local without 'development test'
bundle install
4.2 Without Xapian fulltext search (on Windows)
bundle config set --local without 'xapian'
bundle install
5. Install Active Storage => [Active Storage](#active-storage)
6. Enable WebDAV => [WebDAV](#webdav)
7. Initialize/Update database
`RAILS_ENV=production bundle exec rake redmine:plugins:migrate NAME=redmine_dmsf`
8. Install assets
`RAILS_ENV="production" bundle exec rake assets:precompile`
9. The access rights must be set for web server, e.g.:
`chown -R www-data:www-data plugins/redmine_dmsf`
10. Restart the web server, e.g.:
`systemctl restart apache2`
11. You should configure the plugin via Redmine interface: Administration -> Plugins -> DMSF -> Configure. (You should check and then save the plugin's configuration after each upgrade.)
12. Don't forget to grant permissions for DMSF in Administration -> Roles and permissions
13. Assign DMSF permissions to appropriate roles.
14. There are a few handy rake tasks:
I) To convert documents from the standard Redmine document module
Available options:
* project - id or identifier of a project (default to all projects)
* dry_run - perform just a check without any conversion
* issues - Convert also files attached to issues
Example:
rake redmine:dmsf_convert_documents project=test RAILS_ENV="production"
(If you don't run the rake task as the web server user, don't forget to change the ownership of the imported files, e.g.
chown -R www-data:www-data /redmine/files/dmsf
afterwards)
II) To alert all users who are expected to do an approval in the current approval steps
Example:
rake redmine:dmsf_alert_approvals RAILS_ENV="production"
III) To create missing checksums for all document revisions
Available options:
* dry_run - test, no changes to the database
* forceSHA256 - replace old MD5 with SHA256
Example:
bundle exec rake redmine:dmsf_create_digests RAILS_ENV="production"
bundle exec rake redmine:dmsf_create_digests forceSHA256=1 RAILS_ENV="production"
bundle exec rake redmine:dmsf_create_digests dry_run=1 RAILS_ENV="production"
IV) To maintain DMSF
* Remove all files with no database record from the document directory
* Remove all links project_id = -1 (added links to an issue which hasn't been created)
Available options:
* dry_run - No physical deletion but to list of all unused files only
Example:
rake redmine:dmsf_maintenance RAILS_ENV="production"
rake redmine:dmsf_maintenance dry_run=1 RAILS_ENV="production"
### WebDAV
In order to enable WebDAV module, it is necessary to put the following code into your
`config/additional_environment.rb`:
```ruby
# Redmine DMSF's WebDAV
require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'webdav', 'custom_middleware').to_s
config.middleware.insert_before ActionDispatch::Cookies, RedmineDmsf::Webdav::CustomMiddleware
```
### Active Storage
Documents are stored using Active Storage. It requires the following lines to be added into
`config/additional_environment.rb`:
```ruby
# Active storage
require 'active_storage/engine'
require Rails.root.join('plugins', 'redmine_dmsf', 'lib', 'redmine_dmsf', 'xapian_analyzer').to_s
config.active_storage.service = :local # Store files locally
#config.active_storage.service = :amazon # Store files on Amazon S3
config.active_storage.analyzers.append RedmineDmsf::XapianAnalyzer # Index uploaded files for Xapian full-text search
```
Then install Active Storage with the following commands:
```shell
bin/rails active_storage:install RAILS_ENV=production
bin/rails db:migrate RAILS_ENV=production
```
Configure your DMS files storage in config/storage.yml:
```yml
local:
service: Disk
root: <%= Rails.root.join('dmsf_as') %>
## Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
#amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# bucket: your_own_bucket-<%= Rails.env %>
# region: "" # e.g. 'us-east-1'
```
### Installation in a sub-uri
In order to documents and folders are available via WebDAV in case that the Redmine is configured to be run in a sub-uri
it's necessary to add the following configuration option into your `config/additional_environment.rb`:
```ruby
config.relative_url_root = '/redmine'
```
## Uninstalling DMSF
Before uninstalling the DMSF plugin, please ensure that the Redmine instance is stopped.
1. `cd [redmine-install-dir]`
2. `rake redmine:plugins:migrate NAME=redmine_dmsf VERSION=0 RAILS_ENV=production`
3. `rm plugins/redmine_dmsf -Rf`
After these steps re-start your instance of Redmine.
## Contributing
If you've added something, why not share it. Fork the repository (github.com/picman/redmine_dmsf),
make the changes and send a pull request to the maintainers.
Changes with tests, and full documentation are preferred.
## Additional Documentation
[CHANGELOG.md](CHANGELOG.md) - Project changelog
---
Special thanks to <a href="https://jetbrains.com"><img src="jetbrains-variant-3.svg" alt="JetBrains logo" width="59" height="68"></a> for providing an excellent IDE.

View File

@ -510,7 +510,7 @@ class DmsfController < ApplicationController
raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project) raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project)
zip = Zip.new zip = Zip.new
zip_entries(zip, selected_folders, selected_files) zip_entries zip, selected_folders, selected_files
zipped_content = zip.finish zipped_content = zip.finish
max_filesize = RedmineDmsf.dmsf_max_email_filesize max_filesize = RedmineDmsf.dmsf_max_email_filesize
@ -548,7 +548,7 @@ class DmsfController < ApplicationController
def download_entries(selected_folders, selected_files) def download_entries(selected_folders, selected_files)
zip = Zip.new zip = Zip.new
zip_entries(zip, selected_folders, selected_files) zip_entries zip, selected_folders, selected_files
zip.dmsf_files.each do |f| zip.dmsf_files.each do |f|
# Action # Action
audit = DmsfFileRevisionAccess.new audit = DmsfFileRevisionAccess.new
@ -583,7 +583,7 @@ class DmsfController < ApplicationController
end end
selected_files.each do |selected_file_id| selected_files.each do |selected_file_id|
file = DmsfFile.visible.find_by(id: selected_file_id) file = DmsfFile.visible.find_by(id: selected_file_id)
raise DmsfFileNotFoundError unless file&.last_revision && File.exist?(file.last_revision&.disk_file) raise DmsfFileNotFoundError unless file.last_revision&.file&.attached?
unless (file.project == @project) || User.current.allowed_to?(:view_dmsf_files, file.project) unless (file.project == @project) || User.current.allowed_to?(:view_dmsf_files, file.project)
raise DmsfAccessError raise DmsfAccessError

View File

@ -52,7 +52,7 @@ class DmsfFilesController < ApplicationController
end end
check_project @revision.dmsf_file check_project @revision.dmsf_file
raise ActionController::MissingFile if @file.deleted? raise ActionController::MissingFile if @file.deleted? || !@revision.file.attached?
# Action # Action
access = DmsfFileRevisionAccess.new access = DmsfFileRevisionAccess.new
@ -67,7 +67,7 @@ class DmsfFilesController < ApplicationController
Rails.logger.error "Could not send email notifications: #{e.message}" Rails.logger.error "Could not send email notifications: #{e.message}"
end end
# Allow a preview of the file by an external plugin # Allow a preview of the file by an external plugin
results = call_hook(:dmsf_files_controller_before_view, { file: @revision.disk_file }) results = call_hook(:dmsf_files_controller_before_view, { file: @revision.file.download })
return if results.first == true return if results.first == true
member = Member.find_by(user_id: User.current.id, project_id: @file.project.id) member = Member.find_by(user_id: User.current.id, project_id: @file.project.id)
@ -82,14 +82,14 @@ class DmsfFilesController < ApplicationController
# Text preview # Text preview
elsif !api_request? && params[:download].blank? && (@file.size <= Setting.file_max_size_displayed.to_i.kilobyte) && elsif !api_request? && params[:download].blank? && (@file.size <= Setting.file_max_size_displayed.to_i.kilobyte) &&
(@file.text? || @file.markdown? || @file.textile?) && !@file.html? && formats.include?(:html) (@file.text? || @file.markdown? || @file.textile?) && !@file.html? && formats.include?(:html)
@content = File.read(@revision.disk_file, mode: 'rb') @content = @revision.file.download
render action: 'document' render action: 'document'
# Offer the file for download # Offer the file for download
else else
params[:disposition] = 'attachment' if params[:filename].present? params[:disposition] = 'attachment' if params[:filename].present?
send_file @revision.disk_file, send_data @revision.file.download,
filename: filename, filename: filename,
type: @revision.detect_content_type, type: @revision.content_type,
disposition: params[:disposition].presence || @revision.dmsf_file.disposition disposition: params[:disposition].presence || @revision.dmsf_file.disposition
end end
rescue DmsfAccessError => e rescue DmsfAccessError => e
@ -131,41 +131,30 @@ class DmsfFilesController < ApplicationController
revision.minor_version = DmsfUploadHelper.db_version(params[:version_minor]) revision.minor_version = DmsfUploadHelper.db_version(params[:version_minor])
revision.patch_version = DmsfUploadHelper.db_version(params[:version_patch]) revision.patch_version = DmsfUploadHelper.db_version(params[:version_patch])
# New content
if params[:dmsf_attachments].present? if params[:dmsf_attachments].present?
keys = params[:dmsf_attachments].keys keys = params[:dmsf_attachments].keys
file_upload = params[:dmsf_attachments][keys.first] if keys&.first file_upload = params[:dmsf_attachments][keys.first] if keys&.first
end a = Attachment.find_by_token(file_upload[:token]) if file_upload
if file_upload if a
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, file_upload) revision.size = a.filesize
if upload revision.shared_file.attach(
revision.size = upload.size io: File.open(a.diskfile),
revision.disk_filename = revision.new_storage_filename filename: a.filename,
revision.mime_type = upload.mime_type content_type: a.content_type.presence || 'application/octet-stream',
revision.digest = upload.digest identify: false
)
end end
else else
revision.size = last_revision.size revision.size = last_revision.size
revision.disk_filename = last_revision.disk_filename
revision.mime_type = last_revision.mime_type
revision.digest = last_revision.digest
end end
# Custom fields # Custom fields
revision.copy_custom_field_values(params[:dmsf_file_revision][:custom_field_values], last_revision) revision.copy_custom_field_values(params[:dmsf_file_revision][:custom_field_values], last_revision)
@file.name = revision.name
ok = true ok = true
if revision.save if revision.save
revision.assign_workflow params[:dmsf_workflow_id] revision.assign_workflow params[:dmsf_workflow_id]
if upload if @file.locked? && !@file.locks.empty?
begin
FileUtils.mv upload.tempfile_path, revision.disk_file(search_if_not_exists: false)
rescue StandardError => e
Rails.logger.error e.message
flash[:error] = e.message
revision.destroy
ok = false
end
end
if ok && @file.locked? && !@file.locks.empty?
begin begin
@file.unlock! @file.unlock!
flash[:notice] = "#{l(:notice_file_unlocked)}, " flash[:notice] = "#{l(:notice_file_unlocked)}, "
@ -333,20 +322,6 @@ class DmsfFilesController < ApplicationController
redirect_to trash_dmsf_path(@project) redirect_to trash_dmsf_path(@project)
end end
def thumbnail
tbnail = @file.thumbnail(size: params[:size])
if tbnail
if stale?(etag: tbnail)
send_file tbnail,
filename: filename_for_content_disposition(@file.last_revision.disk_file),
type: @file.last_revision.detect_content_type,
disposition: 'inline'
end
else
head :not_found
end
end
private private
def find_file def find_file

View File

@ -29,10 +29,10 @@ class DmsfPublicUrlsController < ApplicationController
revision = dmsf_public_url.dmsf_file.last_revision revision = dmsf_public_url.dmsf_file.last_revision
begin begin
# IE has got a tendency to cache files # IE has got a tendency to cache files
expires_in(0.years, 'must-revalidate' => true) expires_in 0.years, 'must-revalidate' => true
send_file(revision.disk_file, send_data(revision.file.download,
filename: filename_for_content_disposition(revision.name), filename: filename_for_content_disposition(revision.name),
type: revision.detect_content_type, type: revision.content_type,
disposition: dmsf_public_url.dmsf_file.disposition) disposition: dmsf_public_url.dmsf_file.disposition)
rescue StandardError => e rescue StandardError => e
Rails.logger.error e.message Rails.logger.error e.message

View File

@ -49,7 +49,6 @@ class DmsfUploadController < ApplicationController
@uploads.push upload @uploads.push upload
params[:committed_files][key][:disk_filename] = upload.disk_filename params[:committed_files][key][:disk_filename] = upload.disk_filename
params[:committed_files][key][:digest] = upload.digest
params[:committed_files][key][:tempfile_path] = upload.tempfile_path params[:committed_files][key][:tempfile_path] = upload.tempfile_path
end end
commit_files if params[:committed_files].present? commit_files if params[:committed_files].present?
@ -66,11 +65,6 @@ class DmsfUploadController < ApplicationController
# REST API and Redmine attachment form # REST API and Redmine attachment form
def upload def upload
unless request.media_type == 'application/octet-stream'
head :not_acceptable
return
end
@attachment = Attachment.new(file: request.body) @attachment = Attachment.new(file: request.body)
@attachment.author = User.current @attachment.author = User.current
@attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16) @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
@ -114,7 +108,6 @@ class DmsfUploadController < ApplicationController
uploaded_file[:disk_filename] = upload.disk_filename uploaded_file[:disk_filename] = upload.disk_filename
uploaded_file[:tempfile_path] = upload.tempfile_path uploaded_file[:tempfile_path] = upload.tempfile_path
uploaded_file[:size] = upload.size uploaded_file[:size] = upload.size
uploaded_file[:digest] = upload.digest
uploaded_file[:mime_type] = upload.mime_type uploaded_file[:mime_type] = upload.mime_type
end end
commit_files_internal uploaded_files commit_files_internal uploaded_files

View File

@ -34,7 +34,7 @@ module DmsfHelper
def self.sanitize_filename(filename) def self.sanitize_filename(filename)
# Get only the filename, not the whole path # Get only the filename, not the whole path
just_filename = File.basename(filename.gsub('\\\\', '/')) just_filename = File.basename(filename.gsub('\\\\', '/'))
# Replace all non alphanumeric, hyphens or periods with underscore # Replace all non-alphanumeric, hyphens or periods with underscore
just_filename.gsub!(/[^\w.\-]/, '_') just_filename.gsub!(/[^\w.\-]/, '_')
# Keep the extension if any # Keep the extension if any
if !/^[a-zA-Z0-9_.\-]*$/.match?(just_filename) && just_filename =~ /(.[a-zA-Z0-9]+)$/ if !/^[a-zA-Z0-9_.\-]*$/.match?(just_filename) && just_filename =~ /(.[a-zA-Z0-9]+)$/

View File

@ -42,7 +42,6 @@ module DmsfUploadHelper
else else
file = DmsfFile.new file = DmsfFile.new
file.project_id = project.id file.project_id = project.id
file.name = name
file.dmsf_folder = folder file.dmsf_folder = folder
file.notification = RedmineDmsf.dmsf_default_notifications? file.notification = RedmineDmsf.dmsf_default_notifications?
end end
@ -68,9 +67,7 @@ module DmsfUploadHelper
new_revision.patch_version = if committed_file[:version_patch].present? new_revision.patch_version = if committed_file[:version_patch].present?
DmsfUploadHelper.db_version committed_file[:version_patch] DmsfUploadHelper.db_version committed_file[:version_patch]
end end
new_revision.mime_type = committed_file[:mime_type]
new_revision.size = committed_file[:size] new_revision.size = committed_file[:size]
new_revision.digest = committed_file[:digest]
# Custom fields # Custom fields
new_revision.copy_custom_field_values(committed_file[:custom_field_values]) new_revision.copy_custom_field_values(committed_file[:custom_field_values])
# Need to save file first to generate id for it in case of creation. # Need to save file first to generate id for it in case of creation.
@ -86,8 +83,6 @@ module DmsfUploadHelper
next next
end end
new_revision.disk_filename = new_revision.new_storage_filename
if new_revision.save if new_revision.save
new_revision.assign_workflow committed_file[:dmsf_workflow_id] new_revision.assign_workflow committed_file[:dmsf_workflow_id]
begin begin
@ -96,8 +91,12 @@ module DmsfUploadHelper
a = Attachment.find_by_token(committed_file[:token]) a = Attachment.find_by_token(committed_file[:token])
committed_file[:tempfile_path] = a.diskfile if a committed_file[:tempfile_path] = a.diskfile if a
end end
FileUtils.mv committed_file[:tempfile_path], new_revision.disk_file(search_if_not_exists: false) new_revision.shared_file.attach(
FileUtils.chmod 'u=wr,g=r', new_revision.disk_file(search_if_not_exists: false) io: File.open(committed_file[:tempfile_path]),
filename: new_revision.name,
content_type: committed_file[:mime_type],
identify: false
)
file.last_revision = new_revision file.last_revision = new_revision
files.push file files.push file
container.dmsf_file_added file if container && !new_object container.dmsf_file_added file if container && !new_object

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# An asynchronous job to remove file from Xapian full-text search index
class RemoveFromIndexJob < ApplicationJob
def self.schedule(key)
perform_later key
end
def perform(key)
url = File.join(key[0..1], key[2..3], key)
stem_lang = RedmineDmsf.dmsf_stemming_lang
db_path = File.join RedmineDmsf.dmsf_index_database, stem_lang
db = Xapian::WritableDatabase.new(db_path, Xapian::DB_OPEN)
found = false
db.postlist('').each do |it|
doc = db.document(it.docid)
dochash = Hash[*doc.data.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten]
next unless url == dochash['url']
db.delete_document it.docid
found = true
break
end
Rails.logger.warn "Document's URL '#{url}' not found in the index" unless found
rescue StandardError => e
Rails.logger.error e.message
ensure
db&.close
end
end

View File

@ -42,15 +42,6 @@ class DmsfFile < ApplicationRecord
scope :visible, -> { where(deleted: STATUS_ACTIVE) } scope :visible, -> { where(deleted: STATUS_ACTIVE) }
scope :deleted, -> { where(deleted: STATUS_DELETED) } scope :deleted, -> { where(deleted: STATUS_DELETED) }
validates :name, dmsf_file_name: true
validates :name, length: { maximum: 255 }
validates :name,
uniqueness: {
scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) },
case_sensitive: true
}
acts_as_event( acts_as_event(
title: proc { |o| title: proc { |o|
@searched_revision = nil @searched_revision = nil
@ -81,7 +72,7 @@ class DmsfFile < ApplicationRecord
url: proc { |o| url: proc { |o|
if @searched_revision if @searched_revision
{ controller: 'dmsf_files', action: 'view', id: o.id, download: @searched_revision.id, { controller: 'dmsf_files', action: 'view', id: o.id, download: @searched_revision.id,
filename: o.name } filename: @searched_revision.name }
else else
{ controller: 'dmsf_files', action: 'view', id: o.id, filename: o.name } { controller: 'dmsf_files', action: 'view', id: o.id, filename: o.name }
end end
@ -104,7 +95,7 @@ class DmsfFile < ApplicationRecord
acts_as_watchable acts_as_watchable
acts_as_searchable( acts_as_searchable(
columns: [ columns: [
"#{table_name}.name", "#{DmsfFileRevision.table_name}.name",
"#{DmsfFileRevision.table_name}.title", "#{DmsfFileRevision.table_name}.title",
"#{DmsfFileRevision.table_name}.description", "#{DmsfFileRevision.table_name}.description",
"#{DmsfFileRevision.table_name}.comment" "#{DmsfFileRevision.table_name}.comment"
@ -143,11 +134,19 @@ class DmsfFile < ApplicationRecord
end end
def self.find_file_by_name(project, folder, name) def self.find_file_by_name(project, folder, name)
findn_file_by_name project&.id, folder, name dmsf_files = visible.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: folder&.id })
dmsf_files.each do |file|
return file if file.name == name
end
nil
end end
def self.findn_file_by_name(project_id, folder, name) def self.find_file_by_title(project, folder, name)
visible.find_by project_id: project_id, dmsf_folder_id: folder&.id, name: name dmsf_files = visible.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: folder&.id })
dmsf_files.each do |file|
return file if file.title == name
end
nil
end end
def approval_allowed_zero_minor def approval_allowed_zero_minor
@ -155,10 +154,7 @@ class DmsfFile < ApplicationRecord
end end
def last_revision def last_revision
unless defined?(@last_revision) @last_revision ||= deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first
@last_revision = deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first
end
@last_revision
end end
def deleted? def deleted?
@ -177,21 +173,21 @@ class DmsfFile < ApplicationRecord
if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project) if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project)
Rails.logger.info l(:error_file_is_locked) Rails.logger.info l(:error_file_is_locked)
if lock.reverse[0].user if lock.reverse[0].user
errors.add(:base, l(:title_locked_by_user, user: lock.reverse[0].user)) errors.add :base, l(:title_locked_by_user, user: lock.reverse[0].user)
else else
errors.add(:base, l(:error_file_is_locked)) errors.add :base, l(:error_file_is_locked)
end end
return false return false
end end
begin begin
# Revisions and links of a deleted file SHOULD be deleted too
dmsf_file_revisions.each { |r| r.delete(commit: commit, force: true) }
if commit if commit
destroy destroy
else else
self.deleted = STATUS_DELETED self.deleted = STATUS_DELETED
self.deleted_by_user = User.current self.deleted_by_user = User.current
save save
# Associated revisions should be marked as deleted too
dmsf_file_revisions.each { |r| r.delete(commit: commit, force: true) }
end end
rescue StandardError => e rescue StandardError => e
Rails.logger.error e.message Rails.logger.error e.message
@ -211,16 +207,20 @@ class DmsfFile < ApplicationRecord
save save
end end
def name
last_revision&.name.to_s
end
def title def title
last_revision ? last_revision.title : name last_revision&.title.to_s
end end
def description def description
last_revision ? last_revision.description : '' last_revision&.description.to_s
end end
def version def version
last_revision ? last_revision.version : '0' last_revision&.version.to_s
end end
def workflow def workflow
@ -228,7 +228,7 @@ class DmsfFile < ApplicationRecord
end end
def size def size
last_revision ? last_revision.size : 0 last_revision&.size.to_i
end end
def dmsf_path def dmsf_path
@ -313,22 +313,13 @@ class DmsfFile < ApplicationRecord
file = DmsfFile.new file = DmsfFile.new
file.dmsf_folder_id = folder.id if folder file.dmsf_folder_id = folder.id if folder
file.project_id = project.id file.project_id = project.id
if DmsfFile.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: filename)
1.step do |i|
gen_filename = " #{filename} #{l(:dmsf_copy, n: i)}"
unless DmsfFile.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: gen_filename)
filename = gen_filename
break
end
end
end
file.name = filename
file.notification = RedmineDmsf.dmsf_default_notifications? file.notification = RedmineDmsf.dmsf_default_notifications?
if file.save && last_revision if file.save && last_revision
new_revision = last_revision.clone new_revision = last_revision.clone
new_revision.name = filename
new_revision.title = File.basename(filename, '.*')
new_revision.dmsf_file = file new_revision.dmsf_file = file
new_revision.disk_filename = new_revision.new_storage_filename # Assign the same workflow if it's a global one, or we are in the same project
# Assign the same workflow if it's a global one or we are in the same project
new_revision.workflow = nil new_revision.workflow = nil
new_revision.dmsf_workflow_id = nil new_revision.dmsf_workflow_id = nil
new_revision.dmsf_workflow_assigned_by_user_id = nil new_revision.dmsf_workflow_assigned_by_user_id = nil
@ -340,8 +331,17 @@ class DmsfFile < ApplicationRecord
new_revision.set_workflow wf.id, nil new_revision.set_workflow wf.id, nil
new_revision.assign_workflow wf.id new_revision.assign_workflow wf.id
end end
if File.exist? last_revision.disk_file if last_revision.file.attached?
FileUtils.cp last_revision.disk_file, new_revision.disk_file(search_if_not_exists: false) begin
new_revision.shared_file.attach(
io: StringIO.new(last_revision.file.download),
filename: filename,
content_type: new_revision.file.content_type,
identify: false
)
rescue ActiveStorage::FileNotFoundError => e
Rails.logger.error e
end
end end
new_revision.comment = l(:comment_copied_from, source: "#{self.project.identifier}:#{dmsf_path_str}") new_revision.comment = l(:comment_copied_from, source: "#{self.project.identifier}:#{dmsf_path_str}")
new_revision.custom_values = [] new_revision.custom_values = []
@ -351,10 +351,18 @@ class DmsfFile < ApplicationRecord
v.value = cv.value v.value = cv.value
new_revision.custom_values << v new_revision.custom_values << v
end end
# Check the name and title
basename = File.basename(filename, '.*')
extname = File.extname(filename)
i = 1
while new_revision.invalid? && i < 1_000
new_revision.title = "#{basename} (#{i})"
new_revision.name = "#{new_revision.title}#{extname}"
i += 1
end
if new_revision.save if new_revision.save
file.last_revision = new_revision file.last_revision = new_revision
else else
errors.add :base, new_revision.errors.full_messages.to_sentence
Rails.logger.error new_revision.errors.full_messages.to_sentence Rails.logger.error new_revision.errors.full_messages.to_sentence
file.delete commit: true file.delete commit: true
file = nil file = nil
@ -397,7 +405,7 @@ class DmsfFile < ApplicationRecord
results = scope.where(find_options).uniq.to_a results = scope.where(find_options).uniq.to_a
results.delete_if { |x| !DmsfFolder.permissions?(x.dmsf_folder) } results.delete_if { |x| !DmsfFolder.permissions?(x.dmsf_folder) }
if !options[:titles_only] && RedmineDmsf::Plugin.lib_available?('xapian') if !options[:titles_only] && RedmineDmsf.xapian_available
database = nil database = nil
begin begin
lang = RedmineDmsf.dmsf_stemming_lang lang = RedmineDmsf.dmsf_stemming_lang
@ -444,25 +452,24 @@ class DmsfFile < ApplicationRecord
matchset = enquire.mset(0, 1000) matchset = enquire.mset(0, 1000)
matchset&.matches&.each do |m| matchset&.matches&.each do |m|
docdata = m.document.data { url } docdata = m.document.data
dochash = Hash[*docdata.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten] dochash = Hash[*docdata.scan(%r{(url|sample|modtime|author|type|size)=/?([^\n\]]+)}).flatten]
filename = dochash['url'] next unless dochash['url'] =~ %r{^\w{2}/\w{2}/(\w+)$} # /76/df/76dfsp2ubbgq4yvq90zrfoyxt012
next unless filename
dmsf_attrs = filename.scan(%r{^\d{4}/\d{2}/(\d{12}_(\d+)_.*)$}) key = Regexp.last_match(1)
id_attribute = 0 blob = ActiveStorage::Blob.find_by(key: key)
id_attribute = dmsf_attrs[0][1] if dmsf_attrs.length.positive? attachment = blob&.attachments&.first
next if dmsf_attrs.empty? || id_attribute.to_i.zero? dmsf_file_revision = attachment&.record
dmsf_file = DmsfFile.visible.where(limit_options).find_by(id: id_attribute) next unless dmsf_file_revision
dmsf_file = dmsf_file_revision.dmsf_file
next unless dmsf_file && DmsfFolder.permissions?(dmsf_file.dmsf_folder) && next unless dmsf_file && DmsfFolder.permissions?(dmsf_file.dmsf_folder) &&
user.allowed_to?(:view_dmsf_files, dmsf_file.project) && user.allowed_to?(:view_dmsf_files, dmsf_file.project) &&
(project_ids.blank? || project_ids.include?(dmsf_file.project_id)) (project_ids.blank? || project_ids.include?(dmsf_file.project_id))
rev_id = DmsfFileRevision.where(dmsf_file_id: dmsf_file.id, disk_filename: dmsf_attrs[0][0])
.pick(:id)
if dochash['sample'] if dochash['sample']
Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}-#{rev_id}", Redmine::Search.cache_store.write("DmsfFile-#{dmsf_file.id}-#{dmsf_file_revision.id}",
dochash['sample'].force_encoding('UTF-8')) dochash['sample'].force_encoding('UTF-8'))
end end
break if options[:limit].present? && results.count >= options[:limit] break if options[:limit].present? && results.count >= options[:limit]
@ -489,29 +496,34 @@ class DmsfFile < ApplicationRecord
end end
def text? def text?
filename = last_revision&.disk_filename return false unless last_revision
Redmine::MimeType.is_type?('text', filename) ||
Redmine::SyntaxHighlighting.filename_supported?(filename) filename = last_revision.file&.blob&.filename.to_s
last_revision.file&.blob&.text? || Redmine::SyntaxHighlighting.filename_supported?(filename)
end end
def image? def image?
Redmine::MimeType.is_type?('image', last_revision&.disk_filename) last_revision && last_revision.file&.blob&.image?
end end
def pdf? def pdf?
Redmine::MimeType.of(last_revision&.disk_filename) == 'application/pdf' last_revision&.content_type == 'application/pdf'
end end
def video? def video?
Redmine::MimeType.is_type?('video', last_revision&.disk_filename) return false unless last_revision
Redmine::MimeType.is_type?('video', last_revision.file.blob&.filename&.to_s)
end end
def html? def html?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/html' last_revision&.content_type == 'text/html'
end end
def office_doc? def office_doc?
case File.extname(last_revision&.disk_filename) return false unless last_revision
case File.extname(last_revision.file.blob&.filename&.to_s)
when '.odt', '.ods', '.odp', '.odg', # LibreOffice when '.odt', '.ods', '.odp', '.odg', # LibreOffice
'.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office '.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office
'.rtf' # Universal '.rtf' # Universal
@ -522,11 +534,11 @@ class DmsfFile < ApplicationRecord
end end
def markdown? def markdown?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/markdown' last_revision&.content_type == 'text/markdown'
end end
def textile? def textile?
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/x-textile' last_revision&.content_type == 'text/textile'
end end
def disposition def disposition
@ -534,7 +546,7 @@ class DmsfFile < ApplicationRecord
end end
def thumbnailable? def thumbnailable?
Redmine::Thumbnail.convert_available? && (image? || (pdf? && Redmine::Thumbnail.gs_available?)) last_revision.file&.variable?
end end
def previewable? def previewable?
@ -551,13 +563,14 @@ class DmsfFile < ApplicationRecord
def pdf_preview def pdf_preview
return '' unless previewable? return '' unless previewable?
target = File.join(DmsfFile.previews_storage_path, "#{File.basename(last_revision&.disk_file.to_s, '.*')}.pdf") target = File.join(DmsfFile.previews_storage_path, "#{last_revision.file.blob.key}.pdf")
begin begin
RedmineDmsf::Preview.generate last_revision&.disk_file.to_s, target last_revision.file.open do |f|
RedmineDmsf::Preview.generate f.path, target
end
rescue StandardError => e rescue StandardError => e
Rails.logger.error do Rails.logger.error do
%(An error occurred while generating preview for #{last_revision&.disk_file} to #{target}\n %(An error occurred while generating preview for #{name} to #{target}\nException was: #{e.message})
Exception was: #{e.message})
end end
'' ''
end end
@ -567,15 +580,16 @@ class DmsfFile < ApplicationRecord
result = +'No preview available' result = +'No preview available'
if text? if text?
begin begin
f = File.new(last_revision.disk_file) last_revision.file.open do |f|
f.each_line do |line| f.each_line do |line|
case f.lineno case f.lineno
when 1 when 1
result = line result = line
when limit.to_i + 1 when limit.to_i + 1
break break
else else
result << line result << line
end
end end
end end
rescue StandardError => e rescue StandardError => e
@ -586,11 +600,7 @@ class DmsfFile < ApplicationRecord
end end
def formatted_name(member) def formatted_name(member)
if last_revision last_revision&.formatted_name(member)
last_revision.formatted_name(member)
else
name
end
end end
def owner?(user) def owner?(user)
@ -622,33 +632,6 @@ class DmsfFile < ApplicationRecord
nil nil
end end
def extension
File.extname(last_revision.disk_filename).strip.downcase[1..] if last_revision
end
def thumbnail(options = {})
size = options[:size].to_i
if size.positive?
# Limit the number of thumbnails per image
size = (size / 50) * 50
# Maximum thumbnail size
size = 800 if size > 800
else
size = Setting.thumbnails_size.to_i
end
size = 100 unless size.positive?
target = File.join(Attachment.thumbnails_storage_path, "#{id}_#{last_revision.digest}_#{size}.thumb")
begin
Redmine::Thumbnail.generate last_revision.disk_file.to_s, target, size, pdf?
rescue StandardError => e
Rails.logger.error do
%(An error occured while generating thumbnail for #{last_revision.disk_file} to #{target}\n
Exception was: #{e.message})
end
nil
end
end
def locked_title def locked_title
if locked_for_user? if locked_for_user?
return l(:title_locked_by_user, user: lock.reverse[0].user) if lock.reverse[0].user return l(:title_locked_by_user, user: lock.reverse[0].user) if lock.reverse[0].user

View File

@ -30,6 +30,8 @@ class DmsfFileRevision < ApplicationRecord
belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User' belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User'
belongs_to :dmsf_workflow belongs_to :dmsf_workflow
has_one_attached :shared_file
has_many :dmsf_file_revision_access, dependent: :destroy has_many :dmsf_file_revision_access, dependent: :destroy
has_many :dmsf_workflow_step_assignment, dependent: :destroy has_many :dmsf_workflow_step_assignment, dependent: :destroy
@ -88,16 +90,42 @@ class DmsfFileRevision < ApplicationRecord
} }
) )
validates :title, presence: true validates :title, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :title, length: { maximum: 255 }
validates :major_version, presence: true validates :major_version, presence: true
validates :name, dmsf_file_name: true validates :name,
validates :name, length: { maximum: 255 } presence: true,
validates :disk_filename, length: { maximum: 255 } dmsf_file_name: true,
validates :name, dmsf_file_extension: true length: { maximum: 255 },
dmsf_file_extension: true,
dmsf_file_revision_name: true
validates :description, length: { maximum: 1.kilobyte } validates :description, length: { maximum: 1.kilobyte }
validates :size, dmsf_max_file_size: true validates :size, dmsf_max_file_size: true
def file
unless shared_file.attached?
# If no file is attached, look at the source revision
# This way we prevent the same file from being attached to multiple revisions
sr = source_revision
while sr
return sr.shared_file if sr.shared_file.attached?
sr = sr.source_revision
end
end
shared_file
end
def checksum
file.blob&.checksum
end
def content_type
res = file.blob&.content_type
res = Redmine::MimeType.of(file.blob&.filename) if res.blank?
res = 'application/octet-stream' if res.blank?
res
end
def visible?(_user = nil) def visible?(_user = nil)
deleted == STATUS_ACTIVE deleted == STATUS_ACTIVE
end end
@ -110,14 +138,6 @@ class DmsfFileRevision < ApplicationRecord
dmsf_file&.dmsf_folder dmsf_file&.dmsf_folder
end end
def self.remove_extension(filename)
filename[0, (filename.length - File.extname(filename).length)]
end
def self.filename_to_title(filename)
remove_extension(filename).gsub(/_+/, ' ')
end
def delete(commit: false, force: true) def delete(commit: false, force: true)
if dmsf_file.locked_for_user? if dmsf_file.locked_for_user?
errors.add :base, l(:error_file_is_locked) errors.add :base, l(:error_file_is_locked)
@ -167,51 +187,10 @@ class DmsfFileRevision < ApplicationRecord
ver ver
end end
def storage_base_path
time = created_at || DateTime.current
DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m')
end
def disk_file(search_if_not_exists: true)
path = storage_base_path
begin
FileUtils.mkdir_p(path)
rescue StandardError => e
Rails.logger.error e.message
end
filename = path.join(disk_filename)
if search_if_not_exists && !File.exist?(filename)
# Let's search for the physical file in source revisions
dmsf_file.dmsf_file_revisions.where(created_at: ...created_at).order(created_at: :desc).each do |rev|
filename = rev.disk_file
break if File.exist?(filename)
end
end
filename.to_s
end
def new_storage_filename
raise DmsfAccessError, 'File id is not set' unless dmsf_file&.id
filename = DmsfHelper.sanitize_filename(name)
timestamp = DateTime.current.strftime('%y%m%d%H%M%S')
timestamp.succ! while File.exist? storage_base_path.join("#{timestamp}_#{dmsf_file.id}_#{filename}")
"#{timestamp}_#{dmsf_file.id}_#{filename}"
end
def detect_content_type
content_type = mime_type
content_type = Redmine::MimeType.of(disk_filename) if content_type.blank?
content_type = 'application/octet-stream' if content_type.blank?
content_type
end
def clone def clone
new_revision = DmsfFileRevision.new new_revision = DmsfFileRevision.new
new_revision.dmsf_file = dmsf_file new_revision.dmsf_file = dmsf_file
new_revision.disk_filename = disk_filename
new_revision.size = size new_revision.size = size
new_revision.mime_type = mime_type
new_revision.title = title new_revision.title = title
new_revision.description = description new_revision.description = description
new_revision.workflow = workflow new_revision.workflow = workflow
@ -221,7 +200,6 @@ class DmsfFileRevision < ApplicationRecord
new_revision.source_revision = self new_revision.source_revision = self
new_revision.user = User.current new_revision.user = User.current
new_revision.name = name new_revision.name = name
new_revision.digest = digest
new_revision new_revision
end end
@ -300,19 +278,7 @@ class DmsfFileRevision < ApplicationRecord
end end
def copy_file_content(open_file) def copy_file_content(open_file)
sha = Digest::SHA256.new shared_file.attach io: open_file, filename: dmsf_file.name
File.open(disk_file(search_if_not_exists: false), 'wb') do |f|
if open_file.respond_to?(:read)
while (buffer = open_file.read(8192))
f.write buffer
sha.update buffer
end
else
f.write open_file
sha.update open_file
end
end
self.digest = sha.hexdigest
end end
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
@ -350,20 +316,6 @@ class DmsfFileRevision < ApplicationRecord
format2 format2
end end
def create_digest
self.digest = Digest::SHA256.file(path).hexdigest
rescue StandardError => e
Rails.logger.error e.message
self.digest = 0
end
# Returns either MD5 or SHA256 depending on the way self.digest was computed
def digest_type
return nil if digest.blank?
digest.size < 64 ? 'MD5' : 'SHA256'
end
def tooltip def tooltip
text = description.presence || +'' text = description.presence || +''
if comment.present? if comment.present?
@ -395,19 +347,32 @@ class DmsfFileRevision < ApplicationRecord
end end
def protocol def protocol
@protocol ||= PROTOCOLS[mime_type.downcase] if mime_type @protocol ||= PROTOCOLS[content_type.downcase] if content_type.present?
@protocol @protocol
end end
def delete_source_revision def delete_source_revision
derived_revisions = []
DmsfFileRevision.where(source_dmsf_file_revision_id: id).find_each do |d| DmsfFileRevision.where(source_dmsf_file_revision_id: id).find_each do |d|
# Derived revision without its own file
derived_revisions << d unless d.shared_file.attached?
# Replace the source revision
d.source_revision = source_revision d.source_revision = source_revision
d.save! d.save!
end end
return unless RedmineDmsf.physical_file_delete? return unless shared_file.attached?
dependencies = DmsfFileRevision.where(disk_filename: disk_filename).all.size if derived_revisions.empty?
FileUtils.rm_f(disk_file) if dependencies <= 1 # Remove the file from Xapian index
RemoveFromIndexJob.schedule file.blob.key if RedmineDmsf.xapian_available
# Remove the file
shared_file.purge_later if RedmineDmsf.physical_file_delete?
else
# Move the shared file to an unattached derived revision
d = derived_revisions.first
d.shared_file.attach shared_file.blob
shared_file.detach
end
end end
def copy_custom_field_values(values, source_revision = nil) def copy_custom_field_values(values, source_revision = nil)

View File

@ -28,9 +28,9 @@ class DmsfFolder < ApplicationRecord
belongs_to :deleted_by_user, class_name: 'User' belongs_to :deleted_by_user, class_name: 'User'
belongs_to :user belongs_to :user
has_many :dmsf_folders, -> { order :title }, dependent: :destroy, inverse_of: :dmsf_folder has_many :dmsf_folders, dependent: :destroy, inverse_of: :dmsf_folder
has_many :dmsf_files, dependent: :destroy has_many :dmsf_files, dependent: :destroy
has_many :folder_links, -> { where(target_type: 'DmsfFolder').order(:name) }, has_many :folder_links, -> { where(target_type: 'DmsfFolder') },
class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder
has_many :file_links, -> { where(target_type: 'DmsfFile') }, has_many :file_links, -> { where(target_type: 'DmsfFile') },
class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder
@ -91,7 +91,7 @@ class DmsfFolder < ApplicationRecord
datetime: proc { |o| o.updated_at }, datetime: proc { |o| o.updated_at },
author: proc { |o| o.user } author: proc { |o| o.user }
validates :title, presence: true, dmsf_file_name: true validates :title, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :title, uniqueness: { scope: %i[dmsf_folder_id project_id deleted], validates :title, uniqueness: { scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true } conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true }
validates :description, length: { maximum: 65_535 } validates :description, length: { maximum: 65_535 }

View File

@ -26,7 +26,9 @@ class DmsfLink < ApplicationRecord
belongs_to :deleted_by_user, class_name: 'User' belongs_to :deleted_by_user, class_name: 'User'
belongs_to :user belongs_to :user
validates :name, presence: true, length: { maximum: 255 } validates :name, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :name, uniqueness: { scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true }
# There can be project_id = -1 when attaching links to an issue. The project_id is assigned later when saving the # There can be project_id = -1 when attaching links to an issue. The project_id is assigned later when saving the
# issue. # issue.
validates :external_url, length: { maximum: 255 } validates :external_url, length: { maximum: 255 }

View File

@ -20,7 +20,7 @@
# Upload # Upload
class DmsfUpload class DmsfUpload
attr_accessor :name, :disk_filename, :mime_type, :title, :description, :comment, :major_version, :minor_version, attr_accessor :name, :disk_filename, :mime_type, :title, :description, :comment, :major_version, :minor_version,
:patch_version, :locked, :workflow, :custom_values, :tempfile_path, :digest, :token :patch_version, :locked, :workflow, :custom_values, :tempfile_path, :token
attr_reader :size attr_reader :size
def disk_file def disk_file
@ -32,12 +32,11 @@ class DmsfUpload
if a if a
uploaded = { uploaded = {
disk_filename: DmsfHelper.temp_filename(a.filename), disk_filename: DmsfHelper.temp_filename(a.filename),
content_type: a.content_type, content_type: a.content_type.presence || 'application/octet-stream',
original_filename: a.filename, original_filename: a.filename,
comment: uploaded_file[:description], comment: uploaded_file[:description],
tempfile_path: a.diskfile, tempfile_path: a.diskfile,
token: uploaded_file[:token], token: uploaded_file[:token]
digest: a.digest
} }
DmsfUpload.new project, folder, uploaded DmsfUpload.new project, folder, uploaded
else else
@ -54,7 +53,6 @@ class DmsfUpload
@size = 0 @size = 0
@tempfile_path = '' @tempfile_path = ''
@token = '' @token = ''
@digest = ''
if RedmineDmsf.empty_minor_version_by_default? if RedmineDmsf.empty_minor_version_by_default?
@major_version = 1 @major_version = 1
@minor_version = nil @minor_version = nil
@ -86,10 +84,9 @@ class DmsfUpload
end end
@tempfile_path = uploaded[:tempfile_path] @tempfile_path = uploaded[:tempfile_path]
@token = uploaded[:token] @token = uploaded[:token]
@digest = uploaded[:digest]
if file.nil? || file.last_revision.nil? if file.nil? || file.last_revision.nil?
@title = DmsfFileRevision.filename_to_title(@name) @title = File.basename(@name, '.*')
@description = uploaded[:comment] @description = uploaded[:comment]
if RedmineDmsf.empty_minor_version_by_default? if RedmineDmsf.empty_minor_version_by_default?
@major_version = 1 @major_version = 1

View File

@ -22,11 +22,10 @@ class DmsfFileExtensionValidator < ActiveModel::EachValidator
include Redmine::I18n include Redmine::I18n
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
return unless attribute.to_s == 'name'
extension = File.extname(value) extension = File.extname(value)
return if Attachment.valid_extension?(extension) return if Attachment.valid_extension?(extension)
record.errors.add(:base, l(:error_attachment_extension_not_allowed, extension: extension)) record.errors.add attribute, l(:error_attachment_extension_not_allowed, extension: extension)
end end
end end

View File

@ -22,6 +22,7 @@ class DmsfFileNameValidator < ActiveModel::EachValidator
ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/ ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
# Check invalid characters
record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value) record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value)
end end
end end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# File name validator
class DmsfFileRevisionNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Check name/title uniqueness
DmsfFile
.visible
.where(project_id: record.dmsf_file.project_id, dmsf_folder_id: record.dmsf_file.dmsf_folder_id)
.where.not(id: record.dmsf_file_id)
.find_each do |dmsf_file|
if dmsf_file.name == value
record.errors.add attribute, :taken
break
end
end
end
end

View File

@ -23,12 +23,11 @@
<% unless @locked || @system_folder %> <% unless @locked || @system_folder %>
<% if @file_manipulation_allowed %> <% if @file_manipulation_allowed %>
<%= link_to sprite_icon('add', l(:label_document_new)), <%= link_to sprite_icon('add', l(:label_document_new)),
multi_dmsf_upload_path(id: @project, folder_id: @folder), class: 'icon icon-add', multi_dmsf_upload_path(id: @project, folder_id: @folder), class: 'icon icon-add' %>
data: { cy: 'button__new-file--dmsf' } %>
<% end %> <% end %>
<% if @folder_manipulation_allowed %> <% if @folder_manipulation_allowed %>
<%= link_to sprite_icon('add', l(:link_create_folder)), new_dmsf_path(id: @project, parent_id: @folder), <%= link_to sprite_icon('add', l(:link_create_folder)), new_dmsf_path(id: @project, parent_id: @folder),
class: 'icon icon-add', data: { cy: 'button__create-folder--dmsf' } %> class: 'icon icon-add' %>
<% end %> <% end %>
<% end %> <% end %>
<%= actions_dropdown do %> <%= actions_dropdown do %>

View File

@ -46,11 +46,11 @@
</p> </p>
</div> </div>
<p> <p>
<%= submit_tag l(:button_copy), id: 'copy_button', data: { cy: "button__copy--dmsf" } %> <%= submit_tag l(:button_copy), id: 'copy_button' %>
<%# TODO: Lock and proper permissions %> <%# TODO: Lock and proper permissions %>
<% if User.current.allowed_to?(:folder_manipulation, @project) && <% if User.current.allowed_to?(:folder_manipulation, @project) &&
User.current.allowed_to?(:file_manipulation, @project) %> User.current.allowed_to?(:file_manipulation, @project) %>
<%= submit_tag l(:button_move), id: 'move_button', data: { cy: "button__move--dmsf" } %> <%= submit_tag l(:button_move), id: 'move_button' %>
<% end %> <% end %>
</p> </p>
<% end %> <% end %>

View File

@ -120,8 +120,7 @@
<% end %> <% end %>
</div> </div>
<div class="form-actions"> <div class="form-actions">
<%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive', <%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive' %>
data: { cy: "button__submit--dmsf_folder" } %>
</div> </div>
<% end %> <% end %>

View File

@ -19,32 +19,28 @@
<li> <li>
<%= context_menu_link sprite_icon('edit', l(:button_edit)), dmsf_file_path(id: dmsf_file, back_url: back_url), <%= context_menu_link sprite_icon('edit', l(:button_edit)), dmsf_file_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-edit', data: { cy: "icon__edit--dmsf_file_#{dmsf_file.id}" }, class: 'icon icon-edit', disabled: !allowed || (locked && !unlockable) %>
disabled: !allowed || (locked && !unlockable) %>
</li> </li>
<% unless dmsf_link %> <% unless dmsf_link %>
<li> <li>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"), <%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: folder, ids: ["file-#{dmsf_file.id}"], copymove_entries_path(id: project, folder_id: folder, ids: ["file-#{dmsf_file.id}"],
back_url: back_url), title: l(:title_copy), class: 'icon icon-copy', back_url: back_url), title: l(:title_copy), class: 'icon icon-copy' %>
data: { cy: "icon__copy--dmsf_file_#{dmsf_file.id}" } %>
</li> </li>
<li> <li>
<%= link_to sprite_icon('link', l(:label_link_to)), <%= link_to sprite_icon('link', l(:label_link_to)),
new_dmsf_link_path(project_id: dmsf_file.project.id, dmsf_folder_id: dmsf_file.dmsf_folder, new_dmsf_link_path(project_id: dmsf_file.project.id, dmsf_folder_id: dmsf_file.dmsf_folder,
dmsf_file_id: dmsf_file.id, type: 'link_to', back_url: back_url), dmsf_file_id: dmsf_file.id, type: 'link_to', back_url: back_url),
title: l(:title_create_link), class: 'icon dmsf-icon-link', title: l(:title_create_link), class: 'icon dmsf-icon-link' %>
data: { cy: "icon__link_to--dmsf_file_#{dmsf_file.id}" } %>
</li> </li>
<% end %> <% end %>
<li> <li>
<% if locked %> <% if locked %>
<%= context_menu_link sprite_icon('unlock', l(:button_unlock)), unlock_dmsf_files_path(id: dmsf_file, back_url: back_url), <%= context_menu_link sprite_icon('unlock', l(:button_unlock)), unlock_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-unlock', data: { cy: "icon__unlock--dmsf_file_#{dmsf_file.id}" }, class: 'icon icon-unlock', title: l(:title_locked_by_user, user: dmsf_file.locked_by), disabled: !unlockable %>
title: l(:title_locked_by_user, user: dmsf_file.locked_by), disabled: !unlockable %>
<% else %> <% else %>
<%= context_menu_link sprite_icon('lock', l(:button_lock)), lock_dmsf_files_path(id: dmsf_file, back_url: back_url), <%= context_menu_link sprite_icon('lock', l(:button_lock)), lock_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-lock', data: { cy: "icon__lock--dmsf_file_#{dmsf_file.id}" }, disabled: !allowed %> class: 'icon icon-lock', disabled: !allowed %>
<% end %> <% end %>
</li> </li>
<% if notifications %> <% if notifications %>
@ -52,14 +48,11 @@
<% if dmsf_file.notification %> <% if dmsf_file.notification %>
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)), <%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_files_path(id: dmsf_file, back_url: back_url), notify_deactivate_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-email', data: { cy: "icon__email--dmsf_file_#{dmsf_file.id}" }, class: 'icon icon-email', disabled: !allowed || locked %>
disabled: !allowed || locked %>
<% else %> <% else %>
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)), <%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_files_path(id: dmsf_file, back_url: back_url), notify_activate_dmsf_files_path(id: dmsf_file, back_url: back_url),
class: 'icon icon-email-add', class: 'icon icon-email-add', disabled: !allowed || locked %>
data: { cy: "icon__email_add--dmsf_file_#{dmsf_file.id}" },
disabled: !allowed || locked %>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
@ -71,16 +64,14 @@
<% member = Member.find_by(user_id: User.current.id, project_id: dmsf_file.project.id) %> <% member = Member.find_by(user_id: User.current.id, project_id: dmsf_file.project.id) %>
<% filename = dmsf_file.last_revision&.formatted_name(member) %> <% filename = dmsf_file.last_revision&.formatted_name(member) %>
<%= context_menu_link sprite_icon('download', l(:button_download)), <%= context_menu_link sprite_icon('download', l(:button_download)),
static_dmsf_file_path(dmsf_file, filename: filename), static_dmsf_file_path(dmsf_file, filename: filename, download: dmsf_file.last_revision&.id),
class: 'icon icon-download', data: { cy: "icon__download--dmsf_file_#{dmsf_file.id}" }, class: 'icon icon-download', disabled: false %>
disabled: false %>
</li> </li>
<li> <li>
<%= context_menu_link sprite_icon('email', l(:field_mail)), <%= context_menu_link sprite_icon('email', l(:field_mail)),
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids], entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
email_entries: true, back_url: back_url), email_entries: true, back_url: back_url),
method: :post, class: 'icon icon-email', method: :post, class: 'icon icon-email', disabled: !email_allowed %>
data: { cy: "icon__email--dmsf_file_#{dmsf_file.id}" }, disabled: !email_allowed %>
</li> </li>
<% if RedmineDmsf.dmsf_webdav? %> <% if RedmineDmsf.dmsf_webdav? %>
<li> <li>

View File

@ -21,23 +21,19 @@
<li> <li>
<%= context_menu_link sprite_icon('edit', l(:button_edit)), <%= context_menu_link sprite_icon('edit', l(:button_edit)),
edit_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url), edit_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
class: 'icon icon-edit', data: { cy: "icon__edit--dmsf_folder_#{dmsf_folder.id}" }, class: 'icon icon-edit', disabled: !allowed || locked %>
disabled: !allowed || locked %>
</li> </li>
<% end %> <% end %>
<% unless dmsf_link %> <% unless dmsf_link %>
<li> <li>
<%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"), <%= context_menu_link sprite_icon('copy', "#{l(:button_copy)}/#{l(:button_move)}"),
copymove_entries_path(id: project, folder_id: folder, ids: ["folder-#{dmsf_folder.id}"], copymove_entries_path(id: project, folder_id: folder, ids: ["folder-#{dmsf_folder.id}"],
back_url: back_url), class: 'icon icon-copy', back_url: back_url), class: 'icon icon-copy', disabled: !allowed || locked %>
data: { cy: "icon__copy--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked %>
</li> </li>
<li> <li>
<%= context_menu_link sprite_icon('link', l(:label_link_to)), <%= context_menu_link sprite_icon('link', l(:label_link_to)),
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: dmsf_folder.id, type: 'link_to', new_dmsf_link_path(project_id: project.id, dmsf_folder_id: dmsf_folder.id, type: 'link_to',
back_url: back_url), class: 'icon dmsf-icon-link', back_url: back_url), class: 'icon dmsf-icon-link' %>
data: { cy: "icon__link_to--dmsf_folder_#{dmsf_folder.id}" } %>
</li> </li>
<% end %> <% end %>
<% unless edit %> <% unless edit %>
@ -46,13 +42,11 @@
<%= context_menu_link sprite_icon('unlock', l(:button_unlock)), <%= context_menu_link sprite_icon('unlock', l(:button_unlock)),
unlock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url), unlock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
title: l(:title_locked_by_user, user: dmsf_folder.locked_by), class: 'icon icon-unlock', title: l(:title_locked_by_user, user: dmsf_folder.locked_by), class: 'icon icon-unlock',
data: { cy: "icon__unlock--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || !unlockable %> disabled: !allowed || !unlockable %>
<% else %> <% else %>
<%= context_menu_link sprite_icon('lock', l(:button_lock)), <%= context_menu_link sprite_icon('lock', l(:button_lock)),
lock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url), lock_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, back_url: back_url),
class: 'icon icon-lock', data: { cy: "icon__lock--dmsf_folder_#{dmsf_folder.id}" }, class: 'icon icon-lock', disabled: !allowed %>
disabled: !allowed %>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
@ -62,13 +56,11 @@
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)), <%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, notify_deactivate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
back_url: back_url), class: 'icon icon-email', back_url: back_url), class: 'icon icon-email',
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked || !dmsf_folder.notification? %> disabled: !allowed || locked || !dmsf_folder.notification? %>
<% else %> <% else %>
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)), <%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder, notify_activate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
back_url: back_url), class: 'icon icon-email-add', back_url: back_url), class: 'icon icon-email-add',
data: { cy: "icon__email_add--dmsf_folder_#{dmsf_folder.id}" },
disabled: !allowed || locked || dmsf_folder.notification? %> disabled: !allowed || locked || dmsf_folder.notification? %>
<% end %> <% end %>
</li> </li>
@ -79,7 +71,6 @@
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids], entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
download_entries: true, back_url: back_url), download_entries: true, back_url: back_url),
method: :post, class: 'icon icon-download', method: :post, class: 'icon icon-download',
data: { cy: "icon__download--dmsf_folder_#{dmsf_folder.id}" },
id: 'dmsf-cm-download', disabled: false %> id: 'dmsf-cm-download', disabled: false %>
</li> </li>
<li> <li>
@ -87,7 +78,6 @@
entries_operations_dmsf_path(id: dmsf_folder.project, folder_id: folder, ids: params[:ids], entries_operations_dmsf_path(id: dmsf_folder.project, folder_id: folder, ids: params[:ids],
email_entries: true, back_url: back_url), email_entries: true, back_url: back_url),
method: :post, class: 'icon icon-email', method: :post, class: 'icon icon-email',
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
disabled: !email_allowed %> disabled: !email_allowed %>
</li> </li>
<% end %> <% end %>

View File

@ -22,14 +22,12 @@
<%= link_to sprite_icon('edit', l(:button_edit)), <%= link_to sprite_icon('edit', l(:button_edit)),
edit_root_dmsf_path(id: project), edit_root_dmsf_path(id: project),
title: l(:link_edit, title: l(:link_documents)), title: l(:link_edit, title: l(:link_documents)),
class: 'icon icon-edit', class: 'icon icon-edit' %>
data: { cy: 'button__edit--dmsf' } %>
<% elsif !locked %> <% elsif !locked %>
<%= link_to sprite_icon('edit', l(:button_edit)), <%= link_to sprite_icon('edit', l(:button_edit)),
edit_dmsf_path(id: project, folder_id: folder, redirect_to_folder_id: folder.id), edit_dmsf_path(id: project, folder_id: folder, redirect_to_folder_id: folder.id),
title: l(:link_edit, title: h(folder.title)), title: l(:link_edit, title: h(folder.title)),
class: 'icon icon-edit', class: 'icon icon-edit' %>
data: { cy: 'button__edit--dmsf' } %>
<% end %> <% end %>
<% if folder && (!locked || User.current.allowed_to?(:force_file_unlock, project)) %> <% if folder && (!locked || User.current.allowed_to?(:force_file_unlock, project)) %>
<% if folder.locked? %> <% if folder.locked? %>
@ -37,13 +35,11 @@
sprite_icon('unlock', l(:button_unlock)), sprite_icon('unlock', l(:button_unlock)),
unlock_dmsf_path(id: project, folder_id: folder, current: request.url), unlock_dmsf_path(id: project, folder_id: folder, current: request.url),
title: l(:title_unlock_folder), title: l(:title_unlock_folder),
class: 'icon icon-unlock', class: 'icon icon-unlock' %>
data: { cy: 'button__unlock--dmsf' } %>
<% else %> <% else %>
<%= link_to sprite_icon('lock', l(:button_lock)), <%= link_to sprite_icon('lock', l(:button_lock)),
lock_dmsf_path(id: project, folder_id: folder, current: request.url), lock_dmsf_path(id: project, folder_id: folder, current: request.url),
title: l(:title_lock_folder), class: 'icon icon-lock', title: l(:title_lock_folder), class: 'icon icon-lock' %>
data: { cy: 'button__lock--dmsf' } %>
<% end %> <% end %>
<% end %> <% end %>
<% if notifications && !locked %> <% if notifications && !locked %>
@ -51,14 +47,12 @@
<%= link_to sprite_icon('email', l(:label_notifications_off)), <%= link_to sprite_icon('email', l(:label_notifications_off)),
notify_deactivate_dmsf_path(id: project, folder_id: folder), notify_deactivate_dmsf_path(id: project, folder_id: folder),
title: l(:title_notifications_active_deactivate), title: l(:title_notifications_active_deactivate),
class: 'icon icon-email', class: 'icon icon-email' %>
data: { cy: 'button__notifications-off--dmsf' } %>
<% else %> <% else %>
<%= link_to sprite_icon('email-disabled', l(:label_notifications_on)), <%= link_to sprite_icon('email-disabled', l(:label_notifications_on)),
notify_activate_dmsf_path(id: project, folder_id: folder), notify_activate_dmsf_path(id: project, folder_id: folder),
title: l(:title_notifications_not_active_activate), title: l(:title_notifications_not_active_activate),
class: 'icon icon-email-add', class: 'icon icon-email-add' %>
data: { cy: 'button__notifications-on--dmsf' } %>
<% end %> <% end %>
<% end %> <% end %>
<% if file_manipulation_allowed && !locked %> <% if file_manipulation_allowed && !locked %>
@ -66,8 +60,7 @@
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: folder ? folder.id : folder, new_dmsf_link_path(project_id: project.id, dmsf_folder_id: folder ? folder.id : folder,
type: 'link_from'), type: 'link_from'),
title: l(:title_create_link), title: l(:title_create_link),
class: 'icon dmsf-icon-link', class: 'icon dmsf-icon-link' %>
data: { cy: 'button__create-link--dmsf' } %>
<% end %> <% end %>
<% end %> <% end %>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: folder ? folder : project } %> <%= render partial: 'dmsf_context_menus/watch', locals: { object: folder ? folder : project } %>
@ -75,8 +68,7 @@
<%= link_to sprite_icon('del', l(:link_trash_bin)), <%= link_to sprite_icon('del', l(:link_trash_bin)),
trash_dmsf_path(project), trash_dmsf_path(project),
title: l(:link_trash_bin), title: l(:link_trash_bin),
class: 'icon icon-del', class: 'icon icon-del' %>
data: { cy: 'button__trash--dmsf' } %>
<% else %> <% else %>
<span class="icon icon-del"> <span class="icon icon-del">
<%= sprite_icon('del', l(:link_trash_bin)) %> <%= sprite_icon('del', l(:link_trash_bin)) %>

View File

@ -40,7 +40,8 @@
<% member = Member.find_by(user_id: User.current.id, project_id: file.project.id) %> <% member = Member.find_by(user_id: User.current.id, project_id: file.project.id) %>
<% filename = file.last_revision&.formatted_name(member) %> <% filename = file.last_revision&.formatted_name(member) %>
<%= link_to sprite_icon('download', l(:button_download)), <%= link_to sprite_icon('download', l(:button_download)),
static_dmsf_file_path(file, filename: filename), class: 'icon icon-download', disabled: false %> static_dmsf_file_path(file, filename: filename, download: file.last_revision&.id),
class: 'icon icon-download', disabled: false %>
<%= render partial: 'dmsf_context_menus/watch', locals: { object: file } %> <%= render partial: 'dmsf_context_menus/watch', locals: { object: file } %>
<%= delete_link(dmsf_file_path(id: file, details: true), <%= delete_link(dmsf_file_path(id: file, details: true),
back_url: dmsf_folder_path(id: file.project, folder_id: file.dmsf_folder)) if file_delete_allowed %> back_url: dmsf_folder_path(id: file.project, folder_id: file.dmsf_folder)) if file_delete_allowed %>

View File

@ -74,7 +74,7 @@
<%= f.text_area :comment, rows: 2, label: l(:label_comment), class: 'wiki-edit dmsf-description' %> <%= f.text_area :comment, rows: 2, label: l(:label_comment), class: 'wiki-edit dmsf-description' %>
</p> </p>
<div class="form-actions"> <div class="form-actions">
<%= f.submit l(:button_create), class: 'button-positive', data: { cy: "button__submit--file_dmsf"} %> <%= f.submit l(:button_create), class: 'button-positive' %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>

View File

@ -27,7 +27,7 @@
target: '_blank', target: '_blank',
rel: 'noopener', rel: 'noopener',
title: h(dmsf_file.last_revision.try(:tooltip)), title: h(dmsf_file.last_revision.try(:tooltip)),
'data-downloadurl' => "#{dmsf_file.last_revision.detect_content_type}:#{h(dmsf_file.name)}:#{file_view_url}" %> 'data-downloadurl' => "#{dmsf_file.last_revision.content_type}:#{h(dmsf_file.name)}:#{file_view_url}" %>
</td> </td>
<td class="<%= cls %>"> <td class="<%= cls %>">
<span class="size">(<%= number_to_human_size dmsf_file.last_revision.size %>)</span> <span class="size">(<%= number_to_human_size dmsf_file.last_revision.size %>)</span>

View File

@ -25,12 +25,20 @@
<div class="thumbnails"> <div class="thumbnails">
<% end %> <% end %>
<% images.each do |file| %> <% images.each do |file| %>
<div> <div class="thumbnail" title="<%= file.name %>">
<% if link_to # Redmine classic %> <% if link_to # Redmine classic %>
<%= link_to image_tag(dmsf_thumbnail_path(file), alt: file.title), view_dmsf_file_url(file) %> <% size = Setting.thumbnails_size.to_i %>
<%= link_to image_tag(file.last_revision&.file&.variant(resize_to_limit: [size, size]),
alt: file.title,
style: "max-width: #{size}px; max-height: #{size}px;",
loading: 'lazy'),
view_dmsf_file_url(file) %>
<% else # jQuery gallery %> <% else # jQuery gallery %>
<%= image_tag(dmsf_thumbnail_path(file), <%= image_tag(file.last_revision&.file&.variant(resize_to_limit: [size, size]),
{ :'data-fullsrc' => view_dmsf_file_url(file), alt: file.title }) %> :'data-fullsrc' => view_dmsf_file_url(file),
alt: file.title,
style: "max-width: #{size}px; max-height: #{size}px;",
loading: 'lazy') %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>

View File

@ -14,7 +14,7 @@ api.dmsf_file do
api.dmsf_string "{{dmsf(#{@file.id},#{@file.name},#{r.id})}}" api.dmsf_string "{{dmsf(#{@file.id},#{@file.name},#{r.id})}}"
api.content_url view_dmsf_file_url(@file, download: r) api.content_url view_dmsf_file_url(@file, download: r)
api.size r.size api.size r.size
api.mime_type r.mime_type api.mime_type r.content_type
api.title r.title api.title r.title
api.description r.description api.description r.description
api.workflow r.workflow api.workflow r.workflow
@ -35,7 +35,7 @@ api.dmsf_file do
api.dmsf_workflow_started_by_user_id r.dmsf_workflow_started_by_user_id api.dmsf_workflow_started_by_user_id r.dmsf_workflow_started_by_user_id
api.dmsf_workflow_started_at r.dmsf_workflow_started_at api.dmsf_workflow_started_at r.dmsf_workflow_started_at
api.dmsf_worklfow_state r.workflow_str(false) api.dmsf_worklfow_state r.workflow_str(false)
api.digest r.digest api.digest r.checksum
render_api_custom_values r.visible_custom_field_values, api render_api_custom_values r.visible_custom_field_values, api
end end
end end

View File

@ -133,12 +133,12 @@
</div> </div>
<div class="status attribute"> <div class="status attribute">
<%= content_tag :div, l(:label_mime), class: 'label' %> <%= content_tag :div, l(:label_mime), class: 'label' %>
<%= content_tag :div, revision.mime_type, class: 'value' %> <%= content_tag :div, revision.content_type, class: 'value' %>
</div> </div>
<% if revision.digest.present? %> <% if revision.checksum.present? %>
<div class="status attribute"> <div class="status attribute">
<%= content_tag :div, l(:field_digest), class: 'label' %> <%= content_tag :div, l(:field_digest), class: 'label' %>
<%= content_tag :div, "#{revision.digest_type}: #{revision.digest}", class: 'value wiki' %> <%= content_tag :div, revision.checksum, class: 'value wiki' %>
</div> </div>
<% end %> <% end %>
<%= render 'dmsf/custom_fields', object: revision %> <%= render 'dmsf/custom_fields', object: revision %>

View File

@ -20,7 +20,6 @@
<div class="box tabular dmfs-box-tabular"> <div class="box tabular dmfs-box-tabular">
<%= hidden_field_tag "committed_files[#{i}][disk_filename]", upload.disk_filename %> <%= hidden_field_tag "committed_files[#{i}][disk_filename]", upload.disk_filename %>
<%= hidden_field_tag "committed_files[#{i}][token]", upload.token %> <%= hidden_field_tag "committed_files[#{i}][token]", upload.token %>
<%= hidden_field_tag "committed_files[#{i}][digest]", upload.digest %>
<%= hidden_field_tag "committed_files[#{i}][size]", upload.size %> <%= hidden_field_tag "committed_files[#{i}][size]", upload.size %>
<div class="splitcontent"> <div class="splitcontent">
<div class="splitcontentleft"> <div class="splitcontentleft">

View File

@ -31,9 +31,8 @@
locals: { multiple: true, container: nil, description: true, awf: false } %> locals: { multiple: true, container: nil, description: true, awf: false } %>
</span> </span>
<div class="form-actions"> <div class="form-actions">
<%= submit_tag l(:label_upload), data: { cy: 'button__submit--dmsf-upload--project' }, class: 'button-positive', <%= submit_tag l(:label_upload), class: 'button-positive',
id: "dmsf-upload-button" %> id: "dmsf-upload-button" %>
<%= submit_tag l(:label_dmsf_upload_commit), data: { cy: 'button__submit--dmsf-upload-commit--project' }, <%= submit_tag l(:label_dmsf_upload_commit), class: 'button-positive' %>
class: 'button-positive' %>
</div> </div>
<% end %> <% end %>

View File

@ -22,6 +22,5 @@
"content_type": "<%= @tempfile.content_type.gsub('"', '').html_safe %>", "content_type": "<%= @tempfile.content_type.gsub('"', '').html_safe %>",
"disk_filename": "<%= @disk_filename.html_safe %>", "disk_filename": "<%= @disk_filename.html_safe %>",
"tempfile_path": "<%= @tempfile_path.html_safe %>", "tempfile_path": "<%= @tempfile_path.html_safe %>",
"digest": "<%= @digest.html_safe %>",
"token": "<%= @token.html_safe %>" "token": "<%= @token.html_safe %>"
} }

View File

@ -41,7 +41,6 @@
<% end %> <% end %>
<div class="form-actions"> <div class="form-actions">
<%= submit_tag l(:label_dmsf_commit), <%= submit_tag l(:label_dmsf_commit),
data: { cy: 'button__submit__commit-file--project' },
class: 'button-positive', class: 'button-positive',
onclick: "$('#ajax-indicator').show();" %> onclick: "$('#ajax-indicator').show();" %>
</div> </div>

View File

@ -86,7 +86,7 @@
<td><%= link_to_user User.find_by(id: row['author_id'].present? ? row['author_id'] : row['user_id']) %></td> <td><%= link_to_user User.find_by(id: row['author_id'].present? ? row['author_id'] : row['user_id']) %></td>
<td><%= DmsfWorkflowStepAction.action_str(row['action']) %></td> <td><%= DmsfWorkflowStepAction.action_str(row['action']) %></td>
<td> <td>
<% if (row['step'].to_i == @dmsf_workflow.dmsf_workflow_steps.last.step) && (revision.workflow == DmsfWorkflow::STATE_APPROVED) && (row['action'] != DmsfWorkflowStepAction::ACTION_DELEGATE) %> <% if (row['step'].to_i == @dmsf_workflow.dmsf_workflow_steps.last&.step) && (revision.workflow == DmsfWorkflow::STATE_APPROVED) && (row['action'] != DmsfWorkflowStepAction::ACTION_DELEGATE) %>
<%= l(:title_approved) if row['created_at'].present? %> <%= l(:title_approved) if row['created_at'].present? %>
<% else %> <% else %>
<%= DmsfWorkflowStepAction.workflow_str(row['action']) %> <%= DmsfWorkflowStepAction.workflow_str(row['action']) %>

View File

@ -23,12 +23,12 @@
<% @path = settings_project_path(@project, tab: 'dmsf_workflow') %> <% @path = settings_project_path(@project, tab: 'dmsf_workflow') %>
<p> <p>
<%= link_to sprite_icon('add', l(:label_dmsf_workflow_new)), new_dmsf_workflow_path(project_id: @project&.id), <%= link_to sprite_icon('add', l(:label_dmsf_workflow_new)), new_dmsf_workflow_path(project_id: @project&.id),
class: 'icon icon-add', data: { cy: "button__new--dmsf-workflow" } %> class: 'icon icon-add' %>
</p> </p>
<% else %> <% else %>
<div class="contextual"> <div class="contextual">
<%= link_to sprite_icon('add', l(:label_dmsf_workflow_new)), new_dmsf_workflow_path(project_id: @project&.id), <%= link_to sprite_icon('add', l(:label_dmsf_workflow_new)), new_dmsf_workflow_path(project_id: @project&.id),
class: 'icon icon-add', data: { cy: "button__new--dmsf-workflow" } %> class: 'icon icon-add' %>
</div> </div>
<h2><%= l(:label_dmsf_workflow_plural) %></h2> <h2><%= l(:label_dmsf_workflow_plural) %></h2>
<% end %> <% end %>

View File

@ -19,7 +19,8 @@
<div class="contextual"> <div class="contextual">
<%= link_to "#{l(:button_download)} (#{number_to_human_size(@file.size)})", <%= link_to "#{l(:button_download)} (#{number_to_human_size(@file.size)})",
static_dmsf_file_path(@file, download: @file.last_revision, filename: @file.last_revision.disk_filename), static_dmsf_file_path(@file, download: @file.last_revision,
filename: @file.last_revision.file&.blob&.filename),
class: 'icon icon-download', disabled: false %> class: 'icon icon-download', disabled: false %>
</div> </div>

View File

@ -328,7 +328,7 @@
<%= l(:label_full_text) %> <%= l(:label_full_text) %>
</em> </em>
<% if RedmineDmsf::Plugin.lib_available?('xapian') %> <% if RedmineDmsf.xapian_available %>
<p> <p>
<%= content_tag :label, l(:label_index_database) %> <%= content_tag :label, l(:label_index_database) %>
<%= text_field_tag 'settings[dmsf_index_database]', RedmineDmsf.dmsf_index_database, size: 50 %> <%= text_field_tag 'settings[dmsf_index_database]', RedmineDmsf.dmsf_index_database, size: 50 %>
@ -373,6 +373,14 @@
<%= l(:label_default)%>: <%= l(:general_text_No) %> <%= l(:label_default)%>: <%= l(:general_text_No) %>
</em> </em>
</p> </p>
<p>
<%= content_tag :label, l(:label_maximum_xapian_filesize) %>
<%= text_field_tag 'settings[dmsf_max_xapian_filesize]', RedmineDmsf.dmsf_max_xapian_filesize, size: 10 %>
<em class="info">
<%= l(:note_maximum_xapian_filesize) %><br>
<%= l(:label_default) %>: 3
</em>
</p>
<% else %> <% else %>
<p class="warning"><%= l(:warning_xapian_not_available) %></p> <p class="warning"><%= l(:warning_xapian_not_available) %></p>
<% end %> <% end %>

View File

@ -70,7 +70,7 @@ cs:
link_modified: Změněno link_modified: Změněno
link_ver: Ver. link_ver: Ver.
link_author: Autor link_author: Autor
title_check_for_zip_download_or_email: Vybrat pro stažení jako zip nebo emailem title_check_for_zip_download_or_email: Vybrat pro stažení jako zip nebo e-mailem
title_check_for_restore_or_delete: Vybrat pro obnovení nebo smazání title_check_for_restore_or_delete: Vybrat pro obnovení nebo smazání
title_notifications_active_deactivate: "Notifikace aktivní: Deaktivovat" title_notifications_active_deactivate: "Notifikace aktivní: Deaktivovat"
@ -82,9 +82,9 @@ cs:
title_unlock_file: Odemknout a umožnit změny ostatním uživatelům title_unlock_file: Odemknout a umožnit změny ostatním uživatelům
title_lock_file: Zamknout a zabránit změnám ostatních uživatelů title_lock_file: Zamknout a zabránit změnám ostatních uživatelů
title_download_checked: Stáhnout vybrané jako Zip title_download_checked: Stáhnout vybrané jako Zip
title_send_checked_by_email: Zaslat vybrané emailem title_send_checked_by_email: Zaslat vybrané e-mailem
link_user_preferences: Vaše nastavení link_user_preferences: Vaše nastavení
heading_send_documents_by_email: Odeslat dokumenty emailem heading_send_documents_by_email: Odeslat dokumenty e-mailem
label_email_from: Od label_email_from: Od
label_email_to: Komu label_email_to: Komu
label_email_cc: Kopie label_email_cc: Kopie
@ -125,7 +125,7 @@ cs:
option_version_custom: Vlastní option_version_custom: Vlastní
label_new_content: Nový obsah label_new_content: Nový obsah
label_maximum_files_download: Maximální počet najednou stažených souborů label_maximum_files_download: Maximální počet najednou stažených souborů
note_maximum_number_of_files_downloaded: Maximální počet najednou stažených souborů jako Zip nebo odeslaných emailem. note_maximum_number_of_files_downloaded: Maximální počet najednou stažených souborů jako Zip nebo odeslaných e-mailem.
0 znamená bez omezení. 0 znamená bez omezení.
label_file_storage_directory: Složka pro uložení souborů label_file_storage_directory: Složka pro uložení souborů
label_index_database: Index databáze label_index_database: Index databáze
@ -144,8 +144,8 @@ cs:
label_default_notifications: Výchozí notifikace pro soubory label_default_notifications: Výchozí notifikace pro soubory
heading_uploaded_files: Nahrané soubory heading_uploaded_files: Nahrané soubory
link_documents: Dokumenty link_documents: Dokumenty
permission_view_dmsf_file_revision_accesses: View downloads in Activity stream permission_view_dmsf_file_revision_accesses: Zobrazit stažení v aktivitách
permission_view_dmsf_file_revisions: View revisions in Activity stream permission_view_dmsf_file_revisions: Zobrazit revize v aktivitách
permission_view_dmsf_folders: Procházet dokumenty permission_view_dmsf_folders: Procházet dokumenty
permission_user_preferences: Nastavení uživatele permission_user_preferences: Nastavení uživatele
permission_view_dmsf_files: Zobrazit dokumenty permission_view_dmsf_files: Zobrazit dokumenty
@ -155,8 +155,8 @@ cs:
permission_manage_workflows: Spravovat schvalovací procesy permission_manage_workflows: Spravovat schvalovací procesy
permission_file_delete: Mazat dokumenty permission_file_delete: Mazat dokumenty
permission_display_system_folders: Zobrazit systémové složky permission_display_system_folders: Zobrazit systémové složky
permission_file_approval: File approval permission_file_approval: Schvalovat dokumenty
permission_email_documents: Email documents permission_email_documents: Posílat dokumenty e-mailem
label_file: Soubor label_file: Soubor
field_folder: Složka field_folder: Složka
error_file_commit_require_uploaded_file: Potvrzení vyžaduje nahraný soubor error_file_commit_require_uploaded_file: Potvrzení vyžaduje nahraný soubor
@ -182,7 +182,7 @@ cs:
menu_dmsf: DMS # Project tab title menu_dmsf: DMS # Project tab title
label_physical_file_delete: Fyzické smazání souboru label_physical_file_delete: Fyzické smazání souboru
user_is_not_project_member: Nejste členem projektu user_is_not_project_member: Nejste členem projektu
heading_access_downloads_emails: Stažené/Emaily heading_access_downloads_emails: Stažené/E-maily
heading_access_first: První heading_access_first: První
heading_access_last: Poslední heading_access_last: Poslední
label_dmsf_updated: Změněno label_dmsf_updated: Změněno
@ -199,11 +199,11 @@ cs:
error_target_folder_same: Cílový složka a projekt jsou stejné jako aktuální error_target_folder_same: Cílový složka a projekt jsou stejné jako aktuální
title_copy: Kopírovat title_copy: Kopírovat
error_max_email_filesize_exceeded: "Přesáhli jste maximální velikost souboru, který lze poslat emailem. error_max_email_filesize_exceeded: "Přesáhli jste maximální velikost souboru, který lze poslat e-mailem.
(%{number} MB)" (%{number} MB)"
note_maximum_email_filesize: Omezí se maximální velikost souboru, který může být poslán emailem. 0 znamená neomezený. note_maximum_email_filesize: Omezí se maximální velikost souboru, který může být poslán e-mailem. 0 znamená neomezený.
Číslo je v MB. Číslo je v MB.
label_maximum_email_filesize: Maximální velikost souboru emailu label_maximum_email_filesize: Maximální velikost souboru e-mailu
header_minimum_filesize: Chyba souboru. header_minimum_filesize: Chyba souboru.
error_minimum_filesize: "Soubor %{file} má nulovou velikost a nebude přiložen." error_minimum_filesize: "Soubor %{file} má nulovou velikost a nebude přiložen."
parent_directory: Nadřazená složka parent_directory: Nadřazená složka
@ -317,9 +317,9 @@ cs:
label_links_only: pouze odkazy label_links_only: pouze odkazy
label_display_notified_recipients: Zobrazit příjemce notifikací label_display_notified_recipients: Zobrazit příjemce notifikací
note_display_notified_recipients: Uživatel bude informován o příjemcích právě odeslané emailové notifikace. note_display_notified_recipients: Uživatel bude informován o příjemcích právě odeslané e-mailové notifikace.
warning_email_notifications: "Notifikační email poslán na uživatele %{to}" warning_email_notifications: "Notifikační e-mail poslán na uživatele %{to}"
link_trash_bin: Koš link_trash_bin: Koš
title_restore: Obnovit title_restore: Obnovit
@ -384,8 +384,7 @@ cs:
label_enable_cjk_ngrams: Povolit generování n-gramů pro CJK texty label_enable_cjk_ngrams: Povolit generování n-gramů pro CJK texty
text_enable_cjk_ngrams: "Pokud je povoleno, sekvence čínských nebo japonských znaků jsou rozděleny do jednotlivých text_enable_cjk_ngrams: "Pokud je povoleno, sekvence čínských nebo japonských znaků jsou rozděleny do jednotlivých
znaků nebo skupin znaků. Znaky si nesou informaci o své pozici. Znaky psané latinkou jsou rozděleny normálně do znaků nebo skupin znaků. Znaky si nesou informaci o své pozici. Znaky psané latinkou jsou rozděleny normálně do
slov. Odpovídající proměná prostředí musí být použita při indexaci. slov."
např.: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Rychlé odkazy label_dmsf_fast_links: Rychlé odkazy
text_dmsf_fast_links_info: Při vytváření odkazů budete moci zadat přímo ID cílové složky za účelem zrychlení text_dmsf_fast_links_info: Při vytváření odkazů budete moci zadat přímo ID cílové složky za účelem zrychlení
@ -493,6 +492,10 @@ cs:
warning_folder_unlockable: Složku nelze odemknout warning_folder_unlockable: Složku nelze odemknout
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximální velikost souboru pro indexaci
note_maximum_xapian_filesize: Omezuje maximální velikost souboru pro indexaci. Větší soubory nebudou indexovány.
Velikost je v MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -378,9 +378,7 @@ de:
label_enable_cjk_ngrams: Aktiviere die Erstellung von n-grams aus Koreanischen Texten label_enable_cjk_ngrams: Aktiviere die Erstellung von n-grams aus Koreanischen Texten
text_enable_cjk_ngrams: "Mit dieser Aktivierung werden Koreanische Zeichenfolgen in Monograms and Bigrams zerlegt. text_enable_cjk_ngrams: "Mit dieser Aktivierung werden Koreanische Zeichenfolgen in Monograms and Bigrams zerlegt.
Monograms enthalten Informationen zur Position. Nicht-Koreanische Zeichenfolgen werden in Wörter zerlegt. Die entsprechende Monograms enthalten Informationen zur Position. Nicht-Koreanische Zeichenfolgen werden in Wörter zerlegt."
Option muss beim Indexieren verwendet werden,
z.B. XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Schnelle Verknüpfung label_dmsf_fast_links: Schnelle Verknüpfung
text_dmsf_fast_links_info: Ermöglicht durch Eingabe der Ordner-ID auf einfache Art und Weise eine Verknüpfung text_dmsf_fast_links_info: Ermöglicht durch Eingabe der Ordner-ID auf einfache Art und Weise eine Verknüpfung
@ -489,6 +487,10 @@ de:
warning_folder_unlockable: Der Ordner kann nicht entsperrt werden warning_folder_unlockable: Der Ordner kann nicht entsperrt werden
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximale Dateigröße für den Index
note_maximum_xapian_filesize: Begrenzt die maximale Dateigröße für die Indizierung. Größere Dateien werden nicht
indiziert. Angabe in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ en:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ en:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ es:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ es:
label_dmsf_commit: Commit label_dmsf_commit: Commit
label_dmsf_upload_commit: Upload and commit label_dmsf_upload_commit: Upload and commit
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -471,6 +471,9 @@ fa:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ fr:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal"
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ fr:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -381,9 +381,7 @@ hu:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -491,6 +489,9 @@ hu:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ it: # Italian strings thx 2 Matteo Arceci!
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ it: # Italian strings thx 2 Matteo Arceci!
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ ja:
label_enable_cjk_ngrams: CJKテキストから n-grams の生成を有効にする label_enable_cjk_ngrams: CJKテキストから n-grams の生成を有効にする
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: 高速リンク label_dmsf_fast_links: 高速リンク
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -493,6 +491,9 @@ ja:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -381,10 +381,8 @@ ko:
label_email_reply_to: 회신 label_email_reply_to: 회신
label_enable_cjk_ngrams: CJK 텍스트로부터 n-gram 생성을 활성화 label_enable_cjk_ngrams: CJK 텍스트로부터 n-gram 생성을 활성화
text_enable_cjk_ngrams: "이 기능을 사용하면 CJK 문자 범위가 유니그램과 바이그램으로 분할되고, 유니그램은 위치 정보를 전달합니다. CJK 범위 외의 문자는 일반적인 단어로 분할됩니다. 해당 옵션은 색인 시점에 사용됩니다. 예: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv" text_enable_cjk_ngrams: "이 기능을 사용하면 CJK 문자 범위가 유니그램과 바이그램으로 분할되고, 유니그램은 위치 정보를 전달합니다.
CJK 범위 외의 문자는 일반적인 단어로 분할됩니다."
label_dmsf_fast_links: 빠른 링크 label_dmsf_fast_links: 빠른 링크
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ ko:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -492,6 +492,9 @@ nl:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ pl:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ pl:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ pt-BR:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ pt-BR:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ sl:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ sl:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -383,10 +383,7 @@ uk:
label_enable_cjk_ngrams: Дозводити генерацію n-грам з тексту CJK label_enable_cjk_ngrams: Дозводити генерацію n-грам з тексту CJK
text_enable_cjk_ngrams: "Якщо ввімкнено цю функцію, діапазони символів CJK розбиваються на уніграми і біграми, при text_enable_cjk_ngrams: "Якщо ввімкнено цю функцію, діапазони символів CJK розбиваються на уніграми і біграми, при
цьому уніграми містять позиційну інформацію. Символи, відмінні від CJK, розбиваються на слова як зазвичай. цьому уніграми містять позиційну інформацію. Символи, відмінні від CJK, розбиваються на слова як зазвичай."
Відповідна опція повинна бути використана під час індексування
Приклад: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Швидкі посилання label_dmsf_fast_links: Швидкі посилання
text_dmsf_fast_links_info: Ви зможете вручну ввести ідентифікатор кінцевої папки під час створення посилань або text_dmsf_fast_links_info: Ви зможете вручну ввести ідентифікатор кінцевої папки під час створення посилань або
переміщення файлів чи папок, щоб пришвидшити процес створення посилань. переміщення файлів чи папок, щоб пришвидшити процес створення посилань.
@ -494,6 +491,9 @@ uk:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -381,9 +381,8 @@ zh-TW:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -491,6 +490,9 @@ zh-TW:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -382,9 +382,7 @@ zh:
label_enable_cjk_ngrams: Enable generation of n-grams from CJK text label_enable_cjk_ngrams: Enable generation of n-grams from CJK text
text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the text_enable_cjk_ngrams: "With this enabled, spans of CJK characters are split into unigrams and bigrams, with the
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding unigrams carrying positional information. Non-CJK characters are split into words as normal."
option needs to have been used at index time.
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
label_dmsf_fast_links: Fast links label_dmsf_fast_links: Fast links
text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files text_dmsf_fast_links_info: You will be able to manually enter a target folder's ID when creating links or moving files
@ -492,6 +490,9 @@ zh:
warning_folder_unlockable: The folder can't be unlocked warning_folder_unlockable: The folder can't be unlocked
redmine_dmsf: Redmine DMSF redmine_dmsf: Redmine DMSF
label_maximum_xapian_filesize: Maximum Xapian file size
note_maximum_xapian_filesize: Limits maximum filesize for indexing. Larger files won't be indexed. Number is in MB.
activerecord: activerecord:
errors: errors:
messages: messages:

View File

@ -22,4 +22,8 @@ class AddDigestToRevision < ActiveRecord::Migration[4.2]
def up def up
add_column :dmsf_file_revisions, :digest, :string, limit: 40, default: '', null: false add_column :dmsf_file_revisions, :digest, :string, limit: 40, default: '', null: false
end end
def down
remove_column :dmsf_file_revisions, :digest
end
end end

View File

@ -25,7 +25,7 @@ class ChangeRevisionDigestLimitTo64 < ActiveRecord::Migration[4.2]
def down def down
# Mysql2::Error: Data too long for column 'digest' # Mysql2::Error: Data too long for column 'digest'
# Recalculation of checksums for all revisions is technically possible but costs are to high. # Recalculation of checksums for all revisions is technically possible but costs are too high.
# change_column :dmsf_file_revisions, :digest, :string, limit: 40 # change_column :dmsf_file_revisions, :digest, :string, limit: 40
end end
end end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Add index
class AddIndexOnSourceDmsfFileRevisionId < ActiveRecord::Migration[7.0]
def change
add_index :dmsf_file_revisions, :source_dmsf_file_revision_id
end
end

View File

@ -0,0 +1,158 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
# Migrate documents to/from Active Storage
class ActiveStorageMigration < ActiveRecord::Migration[7.0]
# File system -> Active Storage
def up
$stdout.puts 'It could be a very long process. Be patient...'
# We need to keep updated_at column unchanged and due to the asynchronous file analysis there is probably no better
# way how to achieve that.
add_column :dmsf_file_revisions, :temp_updated_at, :datetime,
default: nil, null: true, if_not_exists: true
DmsfFileRevision.update_all 'temp_updated_at = updated_at'
# Remove the Xapian database as it will be rebuilt from scratch during the migration
if xapian_database_removed?
$stdout.puts 'The Xapian database has been removed as it will be rebuilt from scratch during the migration'
end
Dir.glob(DmsfFile.storage_path.join('**/*')).each do |path|
# Print out the currently processed directory
unless File.file?(path)
$stdout.puts path
next
end
# Process a file
disk_filename = File.basename(path)
$stdout.print path
found = false
DmsfFileRevision.where(disk_filename: disk_filename)
.order(source_dmsf_file_revision_id: :asc)
.each
.with_index do |r, i|
found = true
if i.zero?
r.shared_file.attach io: File.open(path), filename: r.name
# Remove the original file
FileUtils.rm path
key = r.file.blob.key
$stdout.puts " => #{File.join(key[0..1], key[2..3], key)} (#{r.file.blob.filename})"
else
# The other revisions should have set the source revision
warn("r#{r.id}.source_dmsf_file_revision_id is null") unless r.source_dmsf_file_revision_id
end
end
found ? $stdout.print("\n") : $stdout.print(" revision not found, skipped\n")
end
# Remove columns duplicated in ActiveStorage
remove_column :dmsf_file_revisions, :digest
remove_column :dmsf_file_revisions, :mime_type
remove_column :dmsf_file_revisions, :disk_filename
remove_column :dmsf_files, :name
# We need to keep the size despite the fact that it's duplicated in active_storage_blobs to speed up the main
# document view
# Restore updated_at column
DmsfFileRevision.update_all 'updated_at = temp_updated_at'
remove_column :dmsf_file_revisions, :temp_updated_at
$stdout.puts 'Done'
end
# Active Storage -> File system
def down
$stdout.puts 'It could be a very long process. Be patient...'
# Restore removed columns
add_column :dmsf_file_revisions, :digest, :string, limit: 64, default: '', null: false
add_column :dmsf_file_revisions, :mime_type, :string
add_column :dmsf_file_revisions, :disk_filename, :string, default: '', null: false
add_column :dmsf_files, :name, :string, default: '', null: false
# Migrate attachments
ActiveStorage::Attachment.find_each do |a|
r = a.record
new_path = disk_file(r, a)
unless File.exist?(new_path)
a.blob.open do |f|
# Move the attachment
FileUtils.mv f.path, new_path
r.record_timestamps = false # Do not modify updated_at column
DmsfFileRevision.no_touching do
# Mime type
r.mime_type = a.blob.content_type
# Disk filename
r.disk_filename = File.basename(new_path)
# Digest
# We leave the digest calculation to dmsf_create_digests.rake task
r.save
end
r.dmsf_file.record_timestamps = false # Do not modify updated_at column
DmsfFile.no_touching do
# Filename
r.dmsf_file.name = r.dmsf_file.last_revision.name
r.dmsf_file.save
end
end
key = a.blob.key
$stdout.puts "#{File.join(key[0..1], key[2..3], key)} (#{a.blob.filename}) => #{new_path}"
end
# Remove the original file
r.record_timestamps = false # Do not modify updated_at column
DmsfFileRevision.no_touching do
a.purge
end
end
# Remove the Xapian database as it is useless now and has to be rebuilt with xapian_indexer.rb
if xapian_database_removed?
$stdout.puts 'Xapian database have been removed as it is useless now and has to be rebuilt with xapian_indexer.rb'
end
$stdout.puts 'Done'
end
private
# Delete Xapian database
def xapian_database_removed?
if RedmineDmsf.xapian_available
FileUtils.rm_rf File.join(RedmineDmsf.dmsf_index_database, RedmineDmsf.dmsf_stemming_lang)
true
else
false
end
end
def storage_base_path(rev)
time = rev.created_at || DateTime.current
DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m')
end
def new_storage_filename(rev, name)
filename = DmsfHelper.sanitize_filename(name)
timestamp = DateTime.current.strftime('%y%m%d%H%M%S')
timestamp.succ! while File.exist? storage_base_path(rev).join("#{timestamp}_#{rev.dmsf_file.id}_#{filename}")
"#{timestamp}_#{rev.dmsf_file.id}_#{filename}"
end
def disk_file(rev, attachment)
path = storage_base_path(rev)
begin
FileUtils.mkdir_p path
rescue StandardError => e
Rails.logger.error e.message
end
filename = new_storage_filename(rev, attachment.blob&.filename&.to_s)
path.join(filename).to_s
end
end

View File

@ -1,168 +0,0 @@
#!/usr/bin/ruby -W0
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Xabier Elkano, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
require 'optparse'
########################################################################################################################
# BEGIN Configuration parameters
# Configure the following parameters (most of them can be configured through the command line):
########################################################################################################################
# Redmine installation directory
REDMINE_ROOT = File.expand_path('../../../', __dir__)
# DMSF document location REDMINE_ROOT/FILES
FILES = 'dmsf'
# omindex binary path
# To index "non-text" files, use omindex filters
# e.g.: tesseract OCR engine as a filter for PNG files
OMINDEX = '/usr/bin/omindex'
# OMINDEX += " --filter=image/png:'tesseract -l chi_sim+chi_tra %f -'"
# OMINDEX += " --filter=image/jpeg:'tesseract -l chi_sim+chi_tra %f -'"
# Directory containing Xapian databases for omindex (Attachments indexing)
db_root_path = File.expand_path('dmsf_index', REDMINE_ROOT)
# Verbose output, false/true
verbose = false
# Define stemmed languages to index attachments Eg. [ 'english', 'italian', 'spanish' ]
# Available languages are danish, dutch, english, finnish, french, german, german2, hungarian, italian, kraaij_pohlmann,
# lovins, norwegian, porter, portuguese, romanian, russian, spanish, swedish and turkish.
stem_langs = ['english']
ENVIRONMENT = File.join(REDMINE_ROOT, 'config/environment.rb')
env = 'production'
########################################################################################################################
# END Configuration parameters
########################################################################################################################
retry_failed = false
no_delete = false
max_size = ''
overwrite = false
VERSION = '0.3'
optparse = OptionParser.new do |opts|
opts.banner = 'Usage: xapian_indexer.rb [OPTIONS...]'
opts.separator('')
opts.separator("Index Redmine's DMS documents")
opts.separator('')
opts.separator('')
opts.separator('Options:')
opts.on('-d', '--index_db DB_PATH', 'Absolute path to index database according plugin settings in UI') do |db|
db_root_path = db
end
opts.on('-s', '--stemming_lang a,b,c', Array, 'Comma separated list of stemming languages for indexing') do |s|
stem_langs = s
end
opts.on('-v', '--verbose', 'verbose') do
verbose = true
end
opts.on('-e', '--environment ENV', 'Rails ENVIRONMENT(development, testing or production), default production') do |e|
env = e
end
opts.on('-V', '--version', 'show version and exit') do
$stdout.puts VERSION
exit
end
opts.on('-h', '--help', 'show help and exit') do
$stdout.puts opts
exit
end
opts.on('-R', '--retry-failed', 'retry files which omindex failed to extract text') do
retry_failed = true
end
opts.on('-p', '--no-delete', 'skip the deletion of records corresponding to deleted files') do
no_delete = true
end
opts.on('-m', '--max-size SIZE', "maximum size of file to index(e.g.: '5M', '1G',...)") do |m|
max_size = m
end
opts.on('', '--overwrite', 'create the database anew instead of updating') do
overwrite = true
end
opts.separator('')
opts.separator('Examples:')
opts.separator(' xapian_indexer.rb -s english,italian -v')
opts.separator(' xapian_indexer.rb -d $HOME/index_db -s english,italian -v')
opts.separator('')
opts.summary_width = 25
end
optparse.parse!
ENV['RAILS_ENV'] = env
def log(text, verbose, error: false)
if error
warn text
elsif verbose
$stdout.puts text
end
end
def system_or_raise(command, verbose)
if verbose
system command, exception: true
else
system command, out: '/dev/null', exception: true
end
end
log "Trying to load Redmine environment <<#{ENVIRONMENT}>>...", verbose
begin
require ENVIRONMENT
log "Redmine environment [RAILS_ENV=#{env}] correctly loaded ...", verbose
# Indexing documents
stem_langs.each do |lang|
filespath = RedmineDmsf.dmsf_storage_directory
unless File.directory?(filespath)
warn "'#{filespath}' doesn't exist."
exit 1
end
databasepath = File.join(db_root_path, lang)
unless File.directory?(databasepath)
log "#{databasepath} does not exist, creating ...", verbose
FileUtils.mkdir_p databasepath
end
cmd = "#{OMINDEX} -s #{lang} --db #{databasepath} #{filespath} --url / --depth-limit=0"
cmd << ' -v' if verbose
cmd << ' --retry-failed' if retry_failed
cmd << ' -p' if no_delete
cmd << " -m #{max_size}" if max_size.present?
cmd << ' --overwrite' if overwrite
log cmd, verbose
system_or_raise cmd, verbose
end
log 'Redmine DMS documents indexed', verbose
rescue LoadError => e
warn e.message
exit 1
end
exit 0

View File

@ -27,7 +27,7 @@ Redmine::Plugin.register :redmine_dmsf do
author_url 'https://github.com/picman/redmine_dmsf/graphs/contributors' author_url 'https://github.com/picman/redmine_dmsf/graphs/contributors'
author 'Vít Jonáš / Daniel Munn / Karel Pičman' author 'Vít Jonáš / Daniel Munn / Karel Pičman'
description 'Document Management System Features' description 'Document Management System Features'
version '4.2.3' version '5.0.0 devel'
requires_redmine version_or_higher: '6.1.0' requires_redmine version_or_higher: '6.1.0'
@ -62,7 +62,8 @@ Redmine::Plugin.register :redmine_dmsf do
'empty_minor_version_by_default' => '0', 'empty_minor_version_by_default' => '0',
'remove_original_documents_module' => '0', 'remove_original_documents_module' => '0',
'dmsf_webdav_authentication' => 'Digest', 'dmsf_webdav_authentication' => 'Digest',
'dmsf_really_delete_files' => '0' 'dmsf_really_delete_files' => '0',
'dmsf_max_xapian_filesize' => 3
} }
end end

View File

@ -19,6 +19,18 @@
# Main module # Main module
module RedmineDmsf module RedmineDmsf
# Return true if the given gem is installed
def self.lib_available?(path)
require path
true
rescue LoadError => e
Rails.logger.debug e.message
false
end
mattr_accessor :xapian_available, instance_writer: false
@@xapian_available = RedmineDmsf.lib_available?('xapian')
# Settings # Settings
class << self class << self
def dmsf_max_file_download def dmsf_max_file_download
@ -38,11 +50,12 @@ module RedmineDmsf
end end
def dmsf_index_database def dmsf_index_database
if Setting.plugin_redmine_dmsf['dmsf_index_database'].present? dir = if Setting.plugin_redmine_dmsf['dmsf_index_database'].present?
Setting.plugin_redmine_dmsf['dmsf_index_database'].strip Setting.plugin_redmine_dmsf['dmsf_index_database'].strip
else else
File.expand_path('dmsf_index', Rails.root) File.expand_path('dmsf_index', Rails.root)
end end
FileUtils.mkdir_p dir
end end
def dmsf_stemming_lang def dmsf_stemming_lang
@ -211,6 +224,14 @@ module RedmineDmsf
value = Setting.plugin_redmine_dmsf['dmsf_default_notifications'] value = Setting.plugin_redmine_dmsf['dmsf_default_notifications']
value.to_i.positive? || value == 'true' value.to_i.positive? || value == 'true'
end end
def dmsf_max_xapian_filesize
if Setting.plugin_redmine_dmsf['dmsf_max_xapian_filesize'].present?
Setting.plugin_redmine_dmsf['dmsf_max_xapian_filesize'].to_i
else
3
end
end
end end
end end

View File

@ -47,7 +47,9 @@ module RedmineDmsf
end end
def add_dmsf_file(dmsf_file, member = nil, root_path = nil, path = nil) def add_dmsf_file(dmsf_file, member = nil, root_path = nil, path = nil)
raise DmsfFileNotFoundError unless dmsf_file&.last_revision && File.exist?(dmsf_file.last_revision.disk_file) raise DmsfFileNotFoundError unless dmsf_file&.last_revision
raise DmsfFileNotFoundError unless dmsf_file.last_revision.file.attached?
if path if path
string_path = path string_path = path
@ -62,7 +64,7 @@ module RedmineDmsf
zip_entry = ::Zip::Entry.new(@zip_file, string_path, nil, nil, nil, nil, nil, nil, zip_entry = ::Zip::Entry.new(@zip_file, string_path, nil, nil, nil, nil, nil, nil,
::Zip::DOSTime.at(dmsf_file.last_revision.updated_at)) ::Zip::DOSTime.at(dmsf_file.last_revision.updated_at))
@zip_file.put_next_entry zip_entry @zip_file.put_next_entry zip_entry
File.open(dmsf_file.last_revision.disk_file, 'rb') do |f| dmsf_file.last_revision.file.open do |f|
while (buffer = f.read(8192)) while (buffer = f.read(8192))
@zip_file.write buffer @zip_file.write buffer
end end
@ -71,31 +73,6 @@ module RedmineDmsf
@dmsf_files << dmsf_file @dmsf_files << dmsf_file
end end
def add_attachment(attachment, path)
return if @files.include?(path)
raise DmsfFileNotFoundError unless File.exist?(attachment.diskfile)
zip_entry = ::Zip::Entry.new(@zip_file, path, nil, nil, nil, nil, nil, nil,
::Zip::DOSTime.at(attachment.created_on))
@zip_file.put_next_entry zip_entry
File.open(attachment.diskfile, 'rb') do |f|
while (buffer = f.read(8192))
@zip_file.write buffer
end
end
@files << path
end
def add_raw_file(filename, data)
return if @files.include?(filename)
zip_entry = ::Zip::Entry.new(@zip_file, filename, nil, nil, nil, nil, nil, nil, ::Zip::DOSTime.now)
@zip_file.put_next_entry zip_entry
@zip_file.write data
@files << filename
end
def add_dmsf_folder(dmsf_folder, member, root_path = nil) def add_dmsf_folder(dmsf_folder, member, root_path = nil)
string_path = dmsf_folder.dmsf_path_str + File::SEPARATOR string_path = dmsf_folder.dmsf_path_str + File::SEPARATOR
string_path = string_path[(root_path.length + 1)..string_path.length] if root_path string_path = string_path[(root_path.length + 1)..string_path.length] if root_path

View File

@ -91,7 +91,7 @@ module RedmineDmsf
rel: 'noopener', rel: 'noopener',
class: 'icon icon-file', class: 'icon icon-file',
title: h(revision.try(:tooltip)), title: h(revision.try(:tooltip)),
'data-downloadurl' => "#{revision.detect_content_type}:#{h(revision.dmsf_file.name)}:#{file_view_url}" 'data-downloadurl' => "#{revision.content_type}:#{h(revision.dmsf_file.name)}:#{file_view_url}"
) )
end end
end end

View File

@ -174,7 +174,6 @@ module RedmineDmsf
uploaded_file[:size] = upload.size uploaded_file[:size] = upload.size
uploaded_file[:mime_type] = upload.mime_type uploaded_file[:mime_type] = upload.mime_type
uploaded_file[:tempfile_path] = upload.tempfile_path uploaded_file[:tempfile_path] = upload.tempfile_path
uploaded_file[:digest] = upload.digest
if params[:dmsf_attachments_wfs].present? && params[:dmsf_attachments_wfs][key].present? if params[:dmsf_attachments_wfs].present? && params[:dmsf_attachments_wfs][key].present?
uploaded_file[:workflow_id] = params[:dmsf_attachments_wfs][key].to_i uploaded_file[:workflow_id] = params[:dmsf_attachments_wfs][key].to_i
end end

View File

@ -49,7 +49,7 @@ module RedmineDmsf
target: '_blank', target: '_blank',
rel: 'noopener', rel: 'noopener',
title: h(revision.tooltip), title: h(revision.tooltip),
'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}" 'data-downloadurl' => "#{file.last_revision.content_type}:#{h(file.name)}:#{url}"
end end
# dmsff - link to a folder # dmsff - link to a folder
@ -241,16 +241,22 @@ module RedmineDmsf
{{dmsftn(file_id)}} -- with default height 200 (auto width) {{dmsftn(file_id)}} -- with default height 200 (auto width)
{{dmsftn(file_id1 file_id2 file_id3)}} -- multiple thumbnails {{dmsftn(file_id1 file_id2 file_id3)}} -- multiple thumbnails
{{dmsftn(file_id, size=300)}} -- with size 300x300 {{dmsftn(file_id, size=300)}} -- with size 300x300
{{dmsftn(file_id, height=300)}} -- with height (auto width) {{dmsftn(file_id, height=300)}} -- with height (default width)
{{dmsftn(file_id, width=300)}} -- with width (auto height) {{dmsftn(file_id, width=300)}} -- with width (default height)
{{dmsftn(file_id, size=640x480)}} -- with size 640x480} {{dmsftn(file_id, size=640x480)}} -- with size 640x480}
macro :dmsftn do |_obj, args| macro :dmsftn do |_obj, args|
raise ArgumentError if args.empty? # Requires file id raise ArgumentError if args.empty? # Requires file id
args, options = extract_macro_options(args, :size, :width, :height, :title) args, options = extract_macro_options(args, :size, :width, :height, :title)
size = options[:size]
width = options[:width] if options[:size].present?
height = options[:height] width, height = options[:size].split('x')
height = width if height.blank?
else
width = options[:width].presence || Setting.thumbnails_size.to_i
height = options[:height].presence || Setting.thumbnails_size.to_i
end
ids = args[0].split ids = args[0].split
html = [] html = []
ids.each do |id| ids.each do |id|
@ -260,25 +266,21 @@ module RedmineDmsf
next next
end end
raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project) raise ::I18n.t(:notice_not_authorized) unless User.current&.allowed_to?(:view_dmsf_files, file.project)
raise ::I18n.t(:error_not_supported_image_format) unless file.image? raise ::I18n.t(:error_not_supported_image_format) unless file&.thumbnailable?
member = Member.find_by(user_id: User.current.id, project_id: file.project.id) member = Member.find_by(user_id: User.current.id, project_id: file.project.id)
filename = file.last_revision.formatted_name(member) filename = file.last_revision.formatted_name(member)
url = static_dmsf_file_url(file, filename: filename) url = static_dmsf_file_url(file, filename: filename)
img = if size img = image_tag(file.last_revision&.file&.variant(resize_to_limit: [width, height]),
image_tag(url, alt: filename, title: file.title, size: size) alt: filename,
elsif height style: "max-width: #{width}px; max-height: #{height}px;",
image_tag(url, alt: filename, title: file.title, width: 'auto', height: height) loading: 'lazy')
elsif width html << link_to(img,
image_tag(url, alt: filename, title: file.title, width: width, height: 'auto') url,
else
image_tag(url, alt: filename, title: file.title, width: 'auto', height: 200)
end
html << link_to(img, url,
target: '_blank', target: '_blank',
rel: 'noopener', rel: 'noopener',
title: h(file.last_revision.try(:tooltip)), title: h(file.last_revision.try(:tooltip)),
'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}") 'data-downloadurl' => "#{file.last_revision.content_type}:#{h(file.name)}:#{url}")
end end
safe_join html safe_join html
end end

View File

@ -31,7 +31,7 @@ module RedmineDmsf
def get_image_filename(attrname) def get_image_filename(attrname)
if attrname =~ %r{/dmsf/files/(\d+)/} if attrname =~ %r{/dmsf/files/(\d+)/}
file = DmsfFile.find_by(id: Regexp.last_match(1)) file = DmsfFile.find_by(id: Regexp.last_match(1))
file&.last_revision&.disk_file file.last_revision.file&.blob&.filename if file&.last_revision
else else
super super
end end

View File

@ -49,9 +49,9 @@ module RedmineDmsf
# New methods # New methods
def self.prepended(base) def self.prepended(base)
base.class_eval do base.class_eval do
has_many :dmsf_files, -> { where(dmsf_folder_id: nil).order(:name) }, has_many :dmsf_files, -> { where(dmsf_folder_id: nil) },
class_name: 'DmsfFile', foreign_key: 'project_id', dependent: :destroy class_name: 'DmsfFile', foreign_key: 'project_id', dependent: :destroy
has_many :dmsf_folders, -> { where(dmsf_folder_id: nil).order(:title) }, has_many :dmsf_folders, -> { where(dmsf_folder_id: nil) },
class_name: 'DmsfFolder', foreign_key: 'project_id', dependent: :destroy class_name: 'DmsfFolder', foreign_key: 'project_id', dependent: :destroy
has_many :dmsf_workflows, dependent: :destroy has_many :dmsf_workflows, dependent: :destroy
has_many :folder_links, -> { where dmsf_folder_id: nil, target_type: 'DmsfFolder' }, has_many :folder_links, -> { where dmsf_folder_id: nil, target_type: 'DmsfFolder' },

View File

@ -39,4 +39,4 @@ module RedmineDmsf
end end
# Apply the patch # Apply the patch
Puma::Const.include RedmineDmsf::Patches::PumaPatch if RedmineDmsf::Plugin.lib_available?('puma/const') Puma::Const.include RedmineDmsf::Patches::PumaPatch if RedmineDmsf.lib_available?('puma/const')

View File

@ -36,14 +36,5 @@ module RedmineDmsf
end end
false false
end end
# Return true if the given gem is installed
def self.lib_available?(path)
require path
true
rescue LoadError => e
Rails.logger.debug e.message
false
end
end end
end end

View File

@ -34,8 +34,8 @@ module RedmineDmsf
@office_available = $CHILD_STATUS.success? @office_available = $CHILD_STATUS.success?
rescue StandardError rescue StandardError
@office_available = false @office_available = false
Rails.logger.warn l(:note_dmsf_office_bin_not_available, value: office_bin, locale: :en)
end end
Rails.logger.warn l(:note_dmsf_office_bin_not_available, value: office_bin, locale: :en) unless @office_available
@office_available @office_available
end end
@ -46,6 +46,8 @@ module RedmineDmsf
office_bin = RedmineDmsf.office_bin.presence || 'libreoffice' office_bin = RedmineDmsf.office_bin.presence || 'libreoffice'
cmd = "#{shell_quote(office_bin)} --convert-to pdf --headless --outdir #{shell_quote(dir)} #{shell_quote(source)}" cmd = "#{shell_quote(office_bin)} --convert-to pdf --headless --outdir #{shell_quote(dir)} #{shell_quote(source)}"
if system(cmd) if system(cmd)
filename = "#{File.basename(source, '.*')}.pdf"
FileUtils.mv File.join(dir, filename), target
target target
else else
Rails.logger.error "Creating preview failed (#{$CHILD_STATUS}):\nCommand: #{cmd}" Rails.logger.error "Creating preview failed (#{$CHILD_STATUS}):\nCommand: #{cmd}"

View File

@ -55,7 +55,7 @@ module RedmineDmsf
end end
# Gather collection of objects that denote current entities child entities # Gather collection of objects that denote current entities child entities
# Used for listing directories etc, implemented basic caching because otherwise # Used for listing directories etc., implemented basic caching because otherwise
# Our already quite heavy usage of DB would just get silly every time we called # Our already quite heavy usage of DB would just get silly every time we called
# this method. # this method.
def children def children
@ -63,12 +63,12 @@ module RedmineDmsf
@children = [] @children = []
if folder if folder
# Folders # Folders
folder.dmsf_folders.visible.each do |f| folder.dmsf_folders.visible.each do |folder|
@children.push child(f.title) if DmsfFolder.permissions?(f, allow_system: false) @children.push child(folder.title) if DmsfFolder.permissions?(folder, allow_system: false)
end end
# Files # Files
folder.dmsf_files.visible.pluck(:name).each do |name| folder.dmsf_files.visible.each do |file|
@children.push child(name) @children.push child(file.name)
end end
end end
end end
@ -92,7 +92,7 @@ module RedmineDmsf
def content_type def content_type
if file if file
if file.last_revision if file.last_revision
file.last_revision.detect_content_type file.last_revision.content_type
else else
'application/octet-stream' 'application/octet-stream'
end end
@ -126,13 +126,7 @@ module RedmineDmsf
end end
def etag def etag
ino = if file&.last_revision && File.exist?(file.last_revision.disk_file) format '%<node>x-%<size>x-%<modified>x', node: 0, size: content_length, modified: last_modified.to_i
File.stat(file.last_revision.disk_file).ino
else
2
end
format '%<node>x-%<size>x-%<modified>x',
node: ino, size: content_length, modified: (last_modified ? last_modified.to_i : 0)
end end
def content_length def content_length
@ -230,8 +224,9 @@ module RedmineDmsf
dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user)) dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user))
return PreconditionFailed if !dest.resource.is_a?(DmsfResource) || dest.resource.project.nil? return PreconditionFailed if !dest.resource.is_a?(DmsfResource) || dest.resource.project.nil?
parent = dest.resource.parent parent = dest_path.end_with?('/') && !collection? ? dest.resource : dest.resource.parent
raise Forbidden unless dest.resource.project.module_enabled?(:dmsf) raise Forbidden unless dest.resource.project.module_enabled?(:dmsf)
if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, allow_system: false) || if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, allow_system: false) ||
!DmsfFolder.permissions?(parent.folder, allow_system: false))) !DmsfFolder.permissions?(parent.folder, allow_system: false)))
raise Forbidden raise Forbidden
@ -247,13 +242,11 @@ module RedmineDmsf
!User.current.allowed_to?(:folder_manipulation, dest.resource.project)) !User.current.allowed_to?(:folder_manipulation, dest.resource.project))
raise Forbidden raise Forbidden
end end
return MethodNotAllowed unless folder # Moving sub-project not enabled return MethodNotAllowed unless folder # Moving subprojects is not enabled
raise Locked if folder.locked_for_user? raise Locked if folder.locked_for_user?
# Change the title # Change the title
folder.title = dest.resource.basename folder.title = dest.resource.basename
return PreconditionFailed unless folder.save
# Move to a new destination # Move to a new destination
folder.move_to(dest.resource.project, parent.folder) ? Created : PreconditionFailed folder.move_to(dest.resource.project, parent.folder) ? Created : PreconditionFailed
else else
@ -272,13 +265,9 @@ module RedmineDmsf
new_revision = dest.resource.file.last_revision.clone new_revision = dest.resource.file.last_revision.clone
new_revision.increase_version DmsfFileRevision::PATCH_VERSION new_revision.increase_version DmsfFileRevision::PATCH_VERSION
end end
# The file on disk must be renamed from .tmp to the correct filetype or else Xapian won't know how to index. # Copy the file
# Copy file.last_revision.disk_file to new_revision.disk_file
new_revision.size = file.last_revision.size new_revision.size = file.last_revision.size
new_revision.disk_filename = new_revision.new_storage_filename new_revision.copy_file_content StringIO.new(file.last_revision.file.download)
File.open(file.last_revision.disk_file, 'rb') do |f|
new_revision.copy_file_content f
end
# Save # Save
new_revision.save && dest.resource.file.save new_revision.save && dest.resource.file.save
# Delete (and destroy) the file that should have been renamed and return what should have been returned # Delete (and destroy) the file that should have been renamed and return what should have been returned
@ -304,9 +293,8 @@ module RedmineDmsf
# Update Revision and names of file [We can link to old physical resource, as it's not changed] # Update Revision and names of file [We can link to old physical resource, as it's not changed]
if file.last_revision if file.last_revision
file.last_revision.name = dest.resource.basename file.last_revision.name = dest.resource.basename
file.last_revision.title = DmsfFileRevision.filename_to_title(dest.resource.basename) file.last_revision.title = File.basename(dest.resource.basename, '.*')
end end
file.name = dest.resource.basename
# Save Changes # Save Changes
if file.last_revision.save && file.save if file.last_revision.save && file.save
dest.exist? ? NoContent : Created dest.exist? ? NoContent : Created
@ -340,7 +328,7 @@ module RedmineDmsf
res = NoContent res = NoContent
end end
return PreconditionFailed unless parent.exist? && parent.folder return PreconditionFailed unless parent.exist? && (parent.folder || parent.project)
if collection? if collection?
# Permission check if they can manipulate folders and view folders # Permission check if they can manipulate folders and view folders
@ -348,7 +336,7 @@ module RedmineDmsf
# Manipulate folders on destination project :folder_manipulation # Manipulate folders on destination project :folder_manipulation
# View folders on destination project :view_dmsf_folders # View folders on destination project :view_dmsf_folders
# View files on the source project :view_dmsf_files # View files on the source project :view_dmsf_files
# View fodlers on the source project :view_dmsf_folders # View folders on the source project :view_dmsf_folders
raise Forbidden unless User.current.admin? || raise Forbidden unless User.current.admin? ||
(User.current.allowed_to?(:folder_manipulation, dest.resource.project) && (User.current.allowed_to?(:folder_manipulation, dest.resource.project) &&
User.current.allowed_to?(:view_dmsf_folders, dest.resource.project) && User.current.allowed_to?(:view_dmsf_folders, dest.resource.project) &&
@ -378,13 +366,18 @@ module RedmineDmsf
# Update Revision and names of file (We can link to old physical resource, as it's not changed) # Update Revision and names of file (We can link to old physical resource, as it's not changed)
new_file.last_revision.name = dest.resource.basename new_file.last_revision.name = dest.resource.basename
new_file.name = dest.resource.basename new_file.last_revision.title = File.basename(dest.resource.basename, '.*')
# Save Changes # Save Changes
new_file.last_revision.save && new_file.save ? res : PreconditionFailed unless new_file.last_revision.save && new_file.save
new_file.delete commit: true
return PreconditionFailed
end
res
end end
end end
# Lock Check # Lock check
# Check for the existence of locks # Check for the existence of locks
def lock_check(args = {}) def lock_check(args = {})
entity = file || folder entity = file || folder
@ -584,19 +577,17 @@ module RedmineDmsf
else else
f = DmsfFile.new f = DmsfFile.new
f.project_id = project.id f.project_id = project.id
f.name = basename
f.dmsf_folder = parent.folder f.dmsf_folder = parent.folder
f.notification = RedmineDmsf.dmsf_default_notifications? f.notification = RedmineDmsf.dmsf_default_notifications?
new_revision = DmsfFileRevision.new new_revision = DmsfFileRevision.new
new_revision.minor_version = 1 new_revision.minor_version = 1
new_revision.major_version = 0 new_revision.major_version = 0
new_revision.title = DmsfFileRevision.filename_to_title(basename) new_revision.title = File.basename(basename, '.*')
end end
new_revision.dmsf_file = f new_revision.dmsf_file = f
new_revision.user = User.current new_revision.user = User.current
new_revision.name = basename new_revision.name = basename
new_revision.mime_type = Redmine::MimeType.of(new_revision.name)
# Phusion passenger does not have a method "length" in its model # Phusion passenger does not have a method "length" in its model
# however, includes a size method - so we instead use reflection # however, includes a size method - so we instead use reflection
@ -625,10 +616,19 @@ module RedmineDmsf
raise UnprocessableEntity raise UnprocessableEntity
end end
new_revision.disk_filename = new_revision.new_storage_filename unless reuse_revision
if new_revision.save if new_revision.save
new_revision.copy_file_content request.body if request.body.respond_to?(:rewind)
request.body.rewind
new_revision.copy_file_content request.body
else # A workaround for Webrick that doesn't support rewind
stream = StringIO.new
while (buffer = request.body.read(8_192))
stream.write buffer
end
stream.rewind
new_revision.copy_file_content stream
stream.close
end
new_revision.save new_revision.save
# Notifications # Notifications
DmsfMailer.deliver_files_updated project, [f] DmsfMailer.deliver_files_updated project, [f]
@ -636,7 +636,6 @@ module RedmineDmsf
Rails.logger.error new_revision.errors.full_messages.to_sentence Rails.logger.error new_revision.errors.full_messages.to_sentence
raise InternalServerError raise InternalServerError
end end
Created Created
end end
@ -697,10 +696,7 @@ module RedmineDmsf
# implementation of service for request, which allows for us to pipe a single file through # implementation of service for request, which allows for us to pipe a single file through
# also best-utilising Dav4rack's implementation. # also best-utilising Dav4rack's implementation.
def download def download
raise NotFound unless file&.last_revision raise NotFound unless file.last_revision&.file&.attached?
disk_file = file.last_revision.disk_file
raise NotFound unless disk_file && File.exist?(disk_file)
raise Forbidden unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder) raise Forbidden unless !parent.exist? || !parent.folder || DmsfFolder.permissions?(parent.folder)
# If there is no range (start of ranged download, or direct download) then we log the # If there is no range (start of ranged download, or direct download) then we log the
@ -719,7 +715,9 @@ module RedmineDmsf
Rails.logger.error "Could not send email notifications: #{e.message}" Rails.logger.error "Could not send email notifications: #{e.message}"
end end
end end
File.new disk_file file.last_revision.file.open do |f|
File.new f.path
end
end end
def reuse_version_for_locked_file?(file) def reuse_version_for_locked_file?(file)
@ -766,27 +764,28 @@ module RedmineDmsf
def create_empty_file def create_empty_file
f = DmsfFile.new f = DmsfFile.new
f.project_id = project.id f.project_id = project.id
f.name = basename
f.dmsf_folder = parent.folder f.dmsf_folder = parent.folder
if f.save(validate: false) # Skip validation due to invalid characters in the filename if f.save
r = DmsfFileRevision.new r = DmsfFileRevision.new
r.minor_version = 1 r.minor_version = 1
r.major_version = 0 r.major_version = 0
r.title = DmsfFileRevision.filename_to_title(basename) r.title = File.basename(basename, '.*')
r.dmsf_file = f r.dmsf_file = f
r.user = User.current r.user = User.current
r.name = basename r.name = basename
r.mime_type = Redmine::MimeType.of(r.name)
r.size = 0 r.size = 0
r.digest = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
r.disk_filename = r.new_storage_filename
r.available_custom_fields.each do |cf| # Add default value for CFs not existing r.available_custom_fields.each do |cf| # Add default value for CFs not existing
next unless cf.default_value next unless cf.default_value
r.custom_field_values << CustomValue.new({ custom_field: cf, value: cf.default_value }) r.custom_field_values << CustomValue.new({ custom_field: cf, value: cf.default_value })
end end
if r.save(validate: false) # Skip validation due to invalid characters in the filename if r.save(validate: false) # Skip validation due to invalid characters in the filename
FileUtils.touch r.disk_file(search_if_not_exists: false) r.shared_file.attach(
io: File.new(DmsfHelper.temp_filename(basename), File::CREAT),
filename: basename,
content_type: 'application/octet-stream',
identify: false
)
return f return f
end end
end end

View File

@ -41,8 +41,8 @@ module RedmineDmsf
end end
# Files # Files
if User.current.allowed_to?(:view_dmsf_files, project) if User.current.allowed_to?(:view_dmsf_files, project)
project.dmsf_files.visible.pluck(:name).each do |name| project.dmsf_files.visible.each do |file|
@children.push child(name) @children.push child(file.name)
end end
end end
@children @children

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# This file is part of Redmine DMSF plugin.
#
# Redmine DMSF plugin 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 3 of the License, or (at your option) any
# later version.
#
# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see
# <https://www.gnu.org/licenses/>.
module RedmineDmsf
# ActiveRecord Analyzer for Xapian
class XapianAnalyzer < ActiveStorage::Analyzer
def self.accept?(blob)
return false unless RedmineDmsf.xapian_available &&
blob.byte_size < 1_024 * 1_024 * RedmineDmsf.dmsf_max_xapian_filesize # MB
@blob = blob
true
end
def metadata
{ xapian: indexed? }
end
private
def indexed?
stem_lang = RedmineDmsf.dmsf_stemming_lang
db_path = File.join RedmineDmsf.dmsf_index_database, stem_lang
url = File.join(@blob.key[0..1], @blob.key[2..3])
dir = File.join(Dir.tmpdir, @blob.key)
env = 'XAPIAN_CJK_NGRAM=true ' if RedmineDmsf.dmsf_enable_cjk_ngrams?
FileUtils.mkdir dir
@blob.open do |file|
FileUtils.mv file.path, File.join(dir, @blob.key)
system "#{env}omindex -s \"#{stem_lang}\" -D \"#{db_path}\" --url=/#{url} \"#{dir}\" -p", exception: true
end
true
rescue StandardError => e
Rails.logger.error e.message
false
ensure
FileUtils.rm_f dir
end
end
end

View File

@ -0,0 +1,104 @@
---
active_storage_attachment_1:
id: 1
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 1
blob_id: 1
created_at: <%= Time.now %>
active_storage_attachment_2:
id: 2
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 2
blob_id: 2
created_at: <%= Time.now %>
active_storage_attachment_3:
id: 3
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 3
blob_id: 3
created_at: <%= Time.now %>
active_storage_attachment_4:
id: 4
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 4
blob_id: 4
created_at: <%= Time.now %>
active_storage_attachment_6:
id: 6
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 6
blob_id: 6
created_at: <%= Time.now %>
active_storage_attachment_7:
id: 7
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 7
blob_id: 7
created_at: <%= Time.now %>
active_storage_attachment_8:
id: 8
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 8
blob_id: 8
created_at: <%= Time.now %>
active_storage_attachment_9:
id: 9
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 9
blob_id: 9
created_at: <%= Time.now %>
active_storage_attachment_10:
id: 10
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 10
blob_id: 10
created_at: <%= Time.now %>
active_storage_attachment_11:
id: 11
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 11
blob_id: 11
created_at: <%= Time.now %>
active_storage_attachment_12:
id: 12
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 12
blob_id: 12
created_at: <%= Time.now %>
active_storage_attachment_13:
id: 13
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 13
blob_id: 13
created_at: <%= Time.now %>
active_storage_attachment_14:
id: 14
name: 'shared_file'
record_type: 'DmsfFileRevision'
record_id: 14
blob_id: 14
created_at: <%= Time.now %>

143
test/fixtures/active_storage_blobs.yml vendored Normal file
View File

@ -0,0 +1,143 @@
---
active_storage_blob_1:
id: 1
key: '5lge4yv88jwzt7xl76vri2be1v01'
filename: 'test.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_2:
id: 2
key: '5lge4yv88jwzt7xl76vri2be1v02'
filename: 'test2.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_3:
id: 3
key: '5lge4yv88jwzt7xl76vri2be1v03'
filename: 'deleted.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_4:
id: 4
key: '5lge4yv88jwzt7xl76vri2be1v04'
filename: 'test4.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_6:
id: 6
key: '5lge4yv88jwzt7xl76vri2be1v06'
filename: 'test.gif'
content_type: 'image/gif'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 310
checksum : 'iuUAMbuGLEpp8rq1zR8gUQ=='
created_at: <%= Time.now %>
active_storage_blob_7:
id: 7
key: '5lge4yv88jwzt7xl76vri2be1v07'
filename: 'test.pdf'
content_type: 'application/pdf'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 6942
checksum : 'U3aozufhXIqhAj0n8yCXIA=='
created_at: <%= Time.now %>
active_storage_blob_8: # File is not physically present
id: 8
key: '5lge4yv88jwzt7xl76vri2be1v08'
filename: 'myfile.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_9:
id: 9
key: '5lge4yv88jwzt7xl76vri2be1v09'
filename: 'zero.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 0
checksum : '1B2M2Y8AsgTpgAmY7PhCfg=='
created_at: <%= Time.now %>
active_storage_blob_10:
id: 10
key: '5lge4yv88jwzt7xl76vri2be1v10'
filename: 'test.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_11:
id: 11
key: '5lge4yv88jwzt7xl76vri2be1v11'
filename: 'test.txt'
content_type: 'text/plain'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 3
checksum : 'ICy5YqxZB1uWSwcVLSNLcA=='
created_at: <%= Time.now %>
active_storage_blob_12:
id: 12
key: '5lge4yv88jwzt7xl76vri2be1v12'
filename: 'test.mp4'
content_type: 'video/mp4'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 2037627
checksum : 'rqqmDUGlB3dKOB6bUVPV+g=='
created_at: <%= Time.now %>
active_storage_blob_13:
id: 13
key: '5lge4yv88jwzt7xl76vri2be1v13'
filename: 'test.odt'
content_type: 'application/vnd.oasis.opendocument.text'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 10179
checksum : 'k08HeKksIVI7PXr1aEVbjg=='
created_at: <%= Time.now %>
active_storage_blob_14:
id: 14
key: '5lge4yv88jwzt7xl76vri2be1v14'
filename: 'test.html'
content_type: 'text/html'
metadata: '{"identified":true,"analyzed":true}'
service_name: 'test'
byte_size: 10179
checksum : 'RV3RPuaIjvHzOXpvTLhI3w=='
created_at: <%= Time.now %>

View File

@ -4,9 +4,7 @@ dmsf_file_revisions_001:
dmsf_file_id: 1 dmsf_file_id: 1
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: "test.txt" name: "test.txt"
disk_filename: "test.txt" size: 3
size: 4
mime_type: text/plain
title: "Test File" title: "Test File"
description: 'Some file :-)' description: 'Some file :-)'
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
@ -18,7 +16,6 @@ dmsf_file_revisions_001:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: 1 dmsf_workflow_assigned_by_user_id: 1
dmsf_workflow_started_by_user_id: 1 dmsf_workflow_started_by_user_id: 1
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
#revision for file on non-enabled project #revision for file on non-enabled project
@ -27,9 +24,7 @@ dmsf_file_revisions_002:
dmsf_file_id: 2 dmsf_file_id: 2
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: "test2.txt" name: "test2.txt"
disk_filename: "test2.txt" size: 3
size: 4
mime_type: text/plain
title: "Test File" title: "Test File"
description: NULL description: NULL
workflow: NULL workflow: NULL
@ -41,7 +36,6 @@ dmsf_file_revisions_002:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
#revision for deleted file on dmsf-enabled project #revision for deleted file on dmsf-enabled project
@ -50,9 +44,7 @@ dmsf_file_revisions_003:
dmsf_file_id: 3 dmsf_file_id: 3
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'deleted.txt' name: 'deleted.txt'
disk_filename: 'deleted.txt' size: 3
size: 4
mime_type: 'text/plain'
title: 'Test File' title: 'Test File'
description: NULL description: NULL
workflow: NULL workflow: NULL
@ -64,7 +56,6 @@ dmsf_file_revisions_003:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_004: dmsf_file_revisions_004:
@ -72,9 +63,7 @@ dmsf_file_revisions_004:
dmsf_file_id: 4 dmsf_file_id: 4
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'test4.txt' name: 'test4.txt'
disk_filename: 'test4.txt' size: 3
size: 4
mime_type: 'text/plain'
title: 'Test File' title: 'Test File'
description: NULL description: NULL
workflow: NULL workflow: NULL
@ -86,39 +75,14 @@ dmsf_file_revisions_004:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_005:
id: 5
dmsf_file_id: 1
source_dmsf_file_revision_id: NULL
name: 'test5.txt'
disk_filename: 'test5.txt'
size: 4
mime_type: 'application/vnd.oasis.opendocument.text'
title: 'Test file'
description: NULL
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
minor_version: 1
major_version: 1
comment: 'Wrong mime type in order to have Edit content menu item'
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: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:28 +02:00
dmsf_file_revisions_006: dmsf_file_revisions_006:
id: 6 id: 6
dmsf_file_id: 7 dmsf_file_id: 7
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'test.gif' name: 'test.gif'
disk_filename: 'test.gif' size: 310
size: 4
mime_type: 'image/gif'
title: 'Test image' title: 'Test image'
description: NULL description: NULL
workflow: NULL workflow: NULL
@ -130,7 +94,6 @@ dmsf_file_revisions_006:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_007: dmsf_file_revisions_007:
@ -138,9 +101,7 @@ dmsf_file_revisions_007:
dmsf_file_id: 8 dmsf_file_id: 8
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'test.pdf' name: 'test.pdf'
disk_filename: 'test.pdf' size: 6942
size: 4
mime_type: 'application/pdf'
title: 'Test PDF' title: 'Test PDF'
description: NULL description: NULL
workflow: NULL workflow: NULL
@ -152,17 +113,14 @@ dmsf_file_revisions_007:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_008: dmsf_file_revisions_008:
id: 8 id: 8
dmsf_file_id: 9 dmsf_file_id: 9
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'myfile.txt' name: 'myfile.txt' # The file is not physically present
disk_filename: 'myfile.txt' # The file is not physically present
size: 0 size: 0
mime_type: 'text/plain'
title: 'My File' title: 'My File'
description: NULL description: NULL
workflow: NULL workflow: NULL
@ -181,9 +139,7 @@ dmsf_file_revisions_009:
dmsf_file_id: 10 dmsf_file_id: 10
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'zero.txt' name: 'zero.txt'
disk_filename: 'zero.txt'
size: 0 size: 0
mime_type: 'text/plain'
title: 'Zero Size File' title: 'Zero Size File'
description: NULL description: NULL
workflow: NULL workflow: NULL
@ -195,7 +151,6 @@ dmsf_file_revisions_009:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_010: dmsf_file_revisions_010:
@ -203,9 +158,7 @@ dmsf_file_revisions_010:
dmsf_file_id: 5 dmsf_file_id: 5
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'test.txt' name: 'test.txt'
disk_filename: 'test.txt'
size: 4 size: 4
mime_type: 'text/plain'
title: 'Test File' title: 'Test File'
description: 'Some file :-)' description: 'Some file :-)'
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
@ -217,7 +170,6 @@ dmsf_file_revisions_010:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: 1 dmsf_workflow_assigned_by_user_id: 1
dmsf_workflow_started_by_user_id: 1 dmsf_workflow_started_by_user_id: 1
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_011: dmsf_file_revisions_011:
@ -225,9 +177,7 @@ dmsf_file_revisions_011:
dmsf_file_id: 12 dmsf_file_id: 12
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'test.txt' name: 'test.txt'
disk_filename: 'test.txt'
size: 4 size: 4
mime_type: 'text/plain'
title: 'Test File' title: 'Test File'
description: 'Some file :-)' description: 'Some file :-)'
workflow: 0 workflow: 0
@ -239,7 +189,6 @@ dmsf_file_revisions_011:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: 1 dmsf_workflow_assigned_by_user_id: 1
dmsf_workflow_started_by_user_id: 1 dmsf_workflow_started_by_user_id: 1
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2017-04-18 14:52:27 +02:00 created_at: 2017-04-18 14:52:27 +02:00
dmsf_file_revisions_012: dmsf_file_revisions_012:
@ -247,9 +196,7 @@ dmsf_file_revisions_012:
dmsf_file_id: 6 dmsf_file_id: 6
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'test.mp4' name: 'test.mp4'
disk_filename: 'test.mp4'
size: 4 size: 4
mime_type: 'video/mp4'
title: 'test video' title: 'test video'
description: 'A video :-)' description: 'A video :-)'
workflow: 0 workflow: 0
@ -261,7 +208,6 @@ dmsf_file_revisions_012:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: '81dc9bdb52d04dc20036dbd8313ed055'
created_at: 2022-02-03 13:39:27 +02:00 created_at: 2022-02-03 13:39:27 +02:00
dmsf_file_revisions_013: dmsf_file_revisions_013:
@ -269,9 +215,7 @@ dmsf_file_revisions_013:
dmsf_file_id: 13 dmsf_file_id: 13
source_dmsf_file_revision_id: NULL source_dmsf_file_revision_id: NULL
name: 'test.odt' name: 'test.odt'
disk_filename: 'test.odt' size: 10445
size: 4
mime_type: 'application/vnd.oasis.opendocument.text'
title: 'Test office document' title: 'Test office document'
description: 'LibreOffice text' description: 'LibreOffice text'
workflow: 0 workflow: 0
@ -283,5 +227,23 @@ dmsf_file_revisions_013:
user_id: 1 user_id: 1
dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_assigned_by_user_id: NULL
dmsf_workflow_started_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL
digest: '89a6d0ea9aafc21a978152f3e4977812d5d7d623505749471f256a90fc7c5f72' created_at: 2017-04-01 08:54:00 +02:00
dmsf_file_revisions_014:
id: 14
dmsf_file_id: 14
source_dmsf_file_revision_id: NULL
name: 'test.html'
size: 10445
title: 'Webpage'
description: 'HTML document'
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
created_at: 2017-04-01 08:54:00 +02:00 created_at: 2017-04-01 08:54:00 +02:00

View File

@ -3,7 +3,6 @@ dmsf_files_001:
id: 1 id: 1
project_id: 1 project_id: 1
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'test.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -13,7 +12,6 @@ dmsf_files_002:
id: 2 id: 2
project_id: 2 project_id: 2
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'test.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -23,7 +21,6 @@ dmsf_files_003:
id: 3 id: 3
project_id: 1 project_id: 1
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'deleted.txt'
notification: false notification: false
deleted: 1 deleted: 1
deleted_by_user_id: 1 deleted_by_user_id: 1
@ -32,7 +29,6 @@ dmsf_files_004:
id: 4 id: 4
project_id: 1 project_id: 1
dmsf_folder_id: 2 dmsf_folder_id: 2
name: 'test.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -41,7 +37,6 @@ dmsf_files_005:
id: 5 id: 5
project_id: 1 project_id: 1
dmsf_folder_id: 5 dmsf_folder_id: 5
name: 'test.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -50,7 +45,6 @@ dmsf_files_006:
id: 6 id: 6
project_id: 2 project_id: 2
dmsf_folder_id: 3 dmsf_folder_id: 3
name: 'test.mp4'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -59,7 +53,6 @@ dmsf_files_007:
id: 7 id: 7
project_id: 1 project_id: 1
dmsf_folder_id: 8 dmsf_folder_id: 8
name: 'test.gif'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -68,7 +61,6 @@ dmsf_files_008:
id: 8 id: 8
project_id: 1 project_id: 1
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'test.pdf'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -77,7 +69,6 @@ dmsf_files_009:
id: 9 id: 9
project_id: 1 project_id: 1
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'myfile.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -86,7 +77,6 @@ dmsf_files_010:
id: 10 id: 10
project_id: 1 project_id: 1
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'zero.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -95,7 +85,6 @@ dmsf_files_011:
id: 11 id: 11
project_id: 1 project_id: 1
dmsf_folder_id: 9 dmsf_folder_id: 9
name: 'zero.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -104,7 +93,6 @@ dmsf_files_012:
id: 12 id: 12
project_id: 5 project_id: 5
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'test.txt'
notification: false notification: false
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL
@ -113,6 +101,12 @@ dmsf_files_013:
id: 13 id: 13
project_id: 1 project_id: 1
dmsf_folder_id: NULL dmsf_folder_id: NULL
name: 'test.odt' deleted: 0
deleted_by_user_id: NULL
dmsf_files_014:
id: 14
project_id: 1
dmsf_folder_id: NULL
deleted: 0 deleted: 0
deleted_by_user_id: NULL deleted_by_user_id: NULL

View File

@ -52,3 +52,9 @@ wfsa9:
dmsf_workflow_step_id: 5 dmsf_workflow_step_id: 5
user_id: 2 user_id: 2
dmsf_file_revision_id: 1 dmsf_file_revision_id: 1
wfsa10:
id: 10
dmsf_workflow_step_id: 6
user_id: 2
dmsf_file_revision_id: 2

View File

@ -37,3 +37,11 @@ wfs5:
name: '3rd step' name: '3rd step'
user_id: 2 user_id: 2
operator: 1 operator: 1
wfs6:
id: 6
dmsf_workflow_id: 2
step: 1
name: '1st step'
user_id: 2
operator: 1

View File

@ -1 +0,0 @@
123

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<body>
Hello world!
</body>
</html>

View File

@ -73,7 +73,7 @@ class DmsfContextMenusControllerTest < RedmineDmsf::Test::TestCase
assert_select 'a.icon-unlock', text: l(:button_unlock) assert_select 'a.icon-unlock', text: l(:button_unlock)
assert_select 'a.icon-unlock.disabled', text: l(:button_edit_content), count: 0 assert_select 'a.icon-unlock.disabled', text: l(:button_edit_content), count: 0
assert_select 'a.icon-file', text: l(:button_edit_content) assert_select 'a.icon-file', text: l(:button_edit_content)
assert_select 'a.icon-file.disabled', text: l(:button_edit_content), count: 0 assert_select 'a.icon-file.disabled', text: l(:button_edit_content), count: 1
end end
end end

View File

@ -142,14 +142,14 @@ class DmsfFilesControllerTest < RedmineDmsf::Test::TestCase
version_major: @file1.last_revision.major_version, version_major: @file1.last_revision.major_version,
version_minor: @file1.last_revision.minor_version + 1, version_minor: @file1.last_revision.minor_version + 1,
dmsf_file_revision: { dmsf_file_revision: {
title: @file1.last_revision.title, title: @file1.title,
name: @file1.last_revision.name, name: @file1.name,
description: @file1.last_revision.description, description: @file1.description,
comment: 'New revision' comment: 'New revision'
} }
} }
end end
assert_redirected_to dmsf_folder_path(id: @file1.project) assert_redirected_to dmsf_folder_path(id: @file1.project)
assert_not_nil @file1.last_revision.digest assert_not_nil @file1.last_revision.checksum
end end
end end

View File

@ -31,6 +31,7 @@ class DmsfWorkflowsControllerTest < RedmineDmsf::Test::TestCase
@wfs4 = DmsfWorkflowStep.find 4 # step 2 @wfs4 = DmsfWorkflowStep.find 4 # step 2
@wfs5 = DmsfWorkflowStep.find 5 # step 3 @wfs5 = DmsfWorkflowStep.find 5 # step 3
@wf1 = DmsfWorkflow.find 1 @wf1 = DmsfWorkflow.find 1
@wf2 = DmsfWorkflow.find 2
@wf3 = DmsfWorkflow.find 3 @wf3 = DmsfWorkflow.find 3
@wfsa2 = DmsfWorkflowStepAssignment.find 2 @wfsa2 = DmsfWorkflowStepAssignment.find 2
@revision1 = DmsfFileRevision.find 1 @revision1 = DmsfFileRevision.find 1
@ -429,8 +430,8 @@ class DmsfWorkflowsControllerTest < RedmineDmsf::Test::TestCase
def test_log_member_global_wf def test_log_member_global_wf
post '/login', params: { username: 'jsmith', password: 'jsmith' } post '/login', params: { username: 'jsmith', password: 'jsmith' }
get "/dmsf_workflows/#{@wf3.id}/log", get "/dmsf_workflows/#{@wf2.id}/log",
params: { project_id: @project1.id, dmsf_file_id: @file1.id, format: 'js' }, params: { project_id: @project1.id, dmsf_file_id: @file2.id, format: 'js' },
xhr: true xhr: true
assert_response :success assert_response :success
assert_template :log assert_template :log

View File

@ -36,9 +36,9 @@ class MyControllerTest < RedmineDmsf::Test::TestCase
end end
def test_page_with_open_approvals_no_approval def test_page_with_open_approvals_no_approval
post '/login', params: { username: 'jsmith', password: 'jsmith' } post '/login', params: { username: 'admin', password: 'admin' }
@jsmith.pref[:my_page_layout] = { 'top' => ['open_approvals'] } @admin.pref[:my_page_layout] = { 'top' => ['open_approvals'] }
@jsmith.pref.save! @admin.pref.save!
get '/my/page' get '/my/page'
assert_response :success assert_response :success
assert_select 'div#list-top' do assert_select 'div#list-top' do

Some files were not shown because too many files have changed in this diff Show More