Compare commits
No commits in common. "main" and "active_storage" have entirely different histories.
main
...
active_sto
195
.github/workflows/rubyonrails.yml
vendored
Normal file
195
.github/workflows/rubyonrails.yml
vendored
Normal 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
|
||||
@ -118,6 +118,8 @@ Rails/SkipsModelValidations:
|
||||
- db/migrate/20140519133201_trash_bin.rb
|
||||
- db/migrate/07_dmsf_1_4_4.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:
|
||||
Exclude:
|
||||
@ -130,6 +132,10 @@ Rails/UniqueValidationWithoutIndex:
|
||||
- app/models/dmsf_file.rb
|
||||
- app/models/dmsf_workflow_step.rb # Impossible due to steps sorting
|
||||
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- lib/redmine_dmsf.rb # @@xapian_available
|
||||
|
||||
Style/ExpandPathArguments:
|
||||
Enabled: false
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
Changelog for Redmine DMSF
|
||||
==========================
|
||||
|
||||
4.2.4 *????-??-??*
|
||||
------------------
|
||||
|
||||
4.2.3 *2025-10-06*
|
||||
------------------
|
||||
|
||||
|
||||
8
Gemfile
8
Gemfile
@ -18,14 +18,14 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
|
||||
source 'https://rubygems.org' do
|
||||
gem 'active_record_union'
|
||||
gem 'activestorage'
|
||||
gem 'image_processing', '~> 1.2'
|
||||
gem 'ox' # Dav4Rack
|
||||
gem 'rake' unless Dir.exist?(File.expand_path('../../redmine_dashboard', __FILE__))
|
||||
gem 'simple_enum'
|
||||
gem 'uuidtools'
|
||||
gem 'zip-zip' unless Dir.exist?(File.expand_path('../../vault', __FILE__))
|
||||
|
||||
# Redmine extensions
|
||||
gem 'active_record_union'
|
||||
gem 'simple_enum'
|
||||
group :xapian do
|
||||
gem 'xapian-ruby'
|
||||
end
|
||||
|
||||
377
README.md
Normal file
377
README.md
Normal file
@ -0,0 +1,377 @@
|
||||
# Redmine DMSF Plugin 5.0.0 devel
|
||||
|
||||
[](https://github.com/picman/redmine_dmsf/actions/workflows/rubyonrails.yml)
|
||||
[](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.
|
||||
@ -510,7 +510,7 @@ class DmsfController < ApplicationController
|
||||
raise DmsfAccessError unless User.current.allowed_to?(:email_documents, @project)
|
||||
|
||||
zip = Zip.new
|
||||
zip_entries(zip, selected_folders, selected_files)
|
||||
zip_entries zip, selected_folders, selected_files
|
||||
zipped_content = zip.finish
|
||||
|
||||
max_filesize = RedmineDmsf.dmsf_max_email_filesize
|
||||
@ -548,7 +548,7 @@ class DmsfController < ApplicationController
|
||||
|
||||
def download_entries(selected_folders, selected_files)
|
||||
zip = Zip.new
|
||||
zip_entries(zip, selected_folders, selected_files)
|
||||
zip_entries zip, selected_folders, selected_files
|
||||
zip.dmsf_files.each do |f|
|
||||
# Action
|
||||
audit = DmsfFileRevisionAccess.new
|
||||
@ -583,7 +583,7 @@ class DmsfController < ApplicationController
|
||||
end
|
||||
selected_files.each do |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)
|
||||
raise DmsfAccessError
|
||||
|
||||
@ -52,7 +52,7 @@ class DmsfFilesController < ApplicationController
|
||||
end
|
||||
|
||||
check_project @revision.dmsf_file
|
||||
raise ActionController::MissingFile if @file.deleted?
|
||||
raise ActionController::MissingFile if @file.deleted? || !@revision.file.attached?
|
||||
|
||||
# Action
|
||||
access = DmsfFileRevisionAccess.new
|
||||
@ -67,7 +67,7 @@ class DmsfFilesController < ApplicationController
|
||||
Rails.logger.error "Could not send email notifications: #{e.message}"
|
||||
end
|
||||
# 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
|
||||
|
||||
member = Member.find_by(user_id: User.current.id, project_id: @file.project.id)
|
||||
@ -82,14 +82,14 @@ class DmsfFilesController < ApplicationController
|
||||
# Text preview
|
||||
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)
|
||||
@content = File.read(@revision.disk_file, mode: 'rb')
|
||||
@content = @revision.file.download
|
||||
render action: 'document'
|
||||
# Offer the file for download
|
||||
else
|
||||
params[:disposition] = 'attachment' if params[:filename].present?
|
||||
send_file @revision.disk_file,
|
||||
send_data @revision.file.download,
|
||||
filename: filename,
|
||||
type: @revision.detect_content_type,
|
||||
type: @revision.content_type,
|
||||
disposition: params[:disposition].presence || @revision.dmsf_file.disposition
|
||||
end
|
||||
rescue DmsfAccessError => e
|
||||
@ -131,41 +131,30 @@ class DmsfFilesController < ApplicationController
|
||||
revision.minor_version = DmsfUploadHelper.db_version(params[:version_minor])
|
||||
revision.patch_version = DmsfUploadHelper.db_version(params[:version_patch])
|
||||
|
||||
# New content
|
||||
if params[:dmsf_attachments].present?
|
||||
keys = params[:dmsf_attachments].keys
|
||||
file_upload = params[:dmsf_attachments][keys.first] if keys&.first
|
||||
end
|
||||
if file_upload
|
||||
upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, file_upload)
|
||||
if upload
|
||||
revision.size = upload.size
|
||||
revision.disk_filename = revision.new_storage_filename
|
||||
revision.mime_type = upload.mime_type
|
||||
revision.digest = upload.digest
|
||||
a = Attachment.find_by_token(file_upload[:token]) if file_upload
|
||||
if a
|
||||
revision.size = a.filesize
|
||||
revision.shared_file.attach(
|
||||
io: File.open(a.diskfile),
|
||||
filename: a.filename,
|
||||
content_type: a.content_type.presence || 'application/octet-stream',
|
||||
identify: false
|
||||
)
|
||||
end
|
||||
else
|
||||
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
|
||||
|
||||
# Custom fields
|
||||
revision.copy_custom_field_values(params[:dmsf_file_revision][:custom_field_values], last_revision)
|
||||
@file.name = revision.name
|
||||
ok = true
|
||||
if revision.save
|
||||
revision.assign_workflow params[:dmsf_workflow_id]
|
||||
if upload
|
||||
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?
|
||||
if @file.locked? && !@file.locks.empty?
|
||||
begin
|
||||
@file.unlock!
|
||||
flash[:notice] = "#{l(:notice_file_unlocked)}, "
|
||||
@ -333,20 +322,6 @@ class DmsfFilesController < ApplicationController
|
||||
redirect_to trash_dmsf_path(@project)
|
||||
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
|
||||
|
||||
def find_file
|
||||
|
||||
@ -29,10 +29,10 @@ class DmsfPublicUrlsController < ApplicationController
|
||||
revision = dmsf_public_url.dmsf_file.last_revision
|
||||
begin
|
||||
# IE has got a tendency to cache files
|
||||
expires_in(0.years, 'must-revalidate' => true)
|
||||
send_file(revision.disk_file,
|
||||
expires_in 0.years, 'must-revalidate' => true
|
||||
send_data(revision.file.download,
|
||||
filename: filename_for_content_disposition(revision.name),
|
||||
type: revision.detect_content_type,
|
||||
type: revision.content_type,
|
||||
disposition: dmsf_public_url.dmsf_file.disposition)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e.message
|
||||
|
||||
@ -49,7 +49,6 @@ class DmsfUploadController < ApplicationController
|
||||
|
||||
@uploads.push upload
|
||||
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
|
||||
end
|
||||
commit_files if params[:committed_files].present?
|
||||
@ -66,11 +65,6 @@ class DmsfUploadController < ApplicationController
|
||||
|
||||
# REST API and Redmine attachment form
|
||||
def upload
|
||||
unless request.media_type == 'application/octet-stream'
|
||||
head :not_acceptable
|
||||
return
|
||||
end
|
||||
|
||||
@attachment = Attachment.new(file: request.body)
|
||||
@attachment.author = User.current
|
||||
@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[:tempfile_path] = upload.tempfile_path
|
||||
uploaded_file[:size] = upload.size
|
||||
uploaded_file[:digest] = upload.digest
|
||||
uploaded_file[:mime_type] = upload.mime_type
|
||||
end
|
||||
commit_files_internal uploaded_files
|
||||
|
||||
@ -34,7 +34,7 @@ module DmsfHelper
|
||||
def self.sanitize_filename(filename)
|
||||
# Get only the filename, not the whole path
|
||||
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.\-]/, '_')
|
||||
# Keep the extension if any
|
||||
if !/^[a-zA-Z0-9_.\-]*$/.match?(just_filename) && just_filename =~ /(.[a-zA-Z0-9]+)$/
|
||||
|
||||
@ -42,7 +42,6 @@ module DmsfUploadHelper
|
||||
else
|
||||
file = DmsfFile.new
|
||||
file.project_id = project.id
|
||||
file.name = name
|
||||
file.dmsf_folder = folder
|
||||
file.notification = RedmineDmsf.dmsf_default_notifications?
|
||||
end
|
||||
@ -68,9 +67,7 @@ module DmsfUploadHelper
|
||||
new_revision.patch_version = if committed_file[:version_patch].present?
|
||||
DmsfUploadHelper.db_version committed_file[:version_patch]
|
||||
end
|
||||
new_revision.mime_type = committed_file[:mime_type]
|
||||
new_revision.size = committed_file[:size]
|
||||
new_revision.digest = committed_file[:digest]
|
||||
# Custom fields
|
||||
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.
|
||||
@ -86,8 +83,6 @@ module DmsfUploadHelper
|
||||
next
|
||||
end
|
||||
|
||||
new_revision.disk_filename = new_revision.new_storage_filename
|
||||
|
||||
if new_revision.save
|
||||
new_revision.assign_workflow committed_file[:dmsf_workflow_id]
|
||||
begin
|
||||
@ -96,8 +91,12 @@ module DmsfUploadHelper
|
||||
a = Attachment.find_by_token(committed_file[:token])
|
||||
committed_file[:tempfile_path] = a.diskfile if a
|
||||
end
|
||||
FileUtils.mv committed_file[:tempfile_path], new_revision.disk_file(search_if_not_exists: false)
|
||||
FileUtils.chmod 'u=wr,g=r', new_revision.disk_file(search_if_not_exists: false)
|
||||
new_revision.shared_file.attach(
|
||||
io: File.open(committed_file[:tempfile_path]),
|
||||
filename: new_revision.name,
|
||||
content_type: committed_file[:mime_type],
|
||||
identify: false
|
||||
)
|
||||
file.last_revision = new_revision
|
||||
files.push file
|
||||
container.dmsf_file_added file if container && !new_object
|
||||
|
||||
47
app/jobs/remove_from_index_job.rb
Normal file
47
app/jobs/remove_from_index_job.rb
Normal 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
|
||||
@ -42,15 +42,6 @@ class DmsfFile < ApplicationRecord
|
||||
scope :visible, -> { where(deleted: STATUS_ACTIVE) }
|
||||
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(
|
||||
title: proc { |o|
|
||||
@searched_revision = nil
|
||||
@ -81,7 +72,7 @@ class DmsfFile < ApplicationRecord
|
||||
url: proc { |o|
|
||||
if @searched_revision
|
||||
{ controller: 'dmsf_files', action: 'view', id: o.id, download: @searched_revision.id,
|
||||
filename: o.name }
|
||||
filename: @searched_revision.name }
|
||||
else
|
||||
{ controller: 'dmsf_files', action: 'view', id: o.id, filename: o.name }
|
||||
end
|
||||
@ -104,7 +95,7 @@ class DmsfFile < ApplicationRecord
|
||||
acts_as_watchable
|
||||
acts_as_searchable(
|
||||
columns: [
|
||||
"#{table_name}.name",
|
||||
"#{DmsfFileRevision.table_name}.name",
|
||||
"#{DmsfFileRevision.table_name}.title",
|
||||
"#{DmsfFileRevision.table_name}.description",
|
||||
"#{DmsfFileRevision.table_name}.comment"
|
||||
@ -143,11 +134,19 @@ class DmsfFile < ApplicationRecord
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def self.findn_file_by_name(project_id, folder, name)
|
||||
visible.find_by project_id: project_id, dmsf_folder_id: folder&.id, name: name
|
||||
def self.find_file_by_title(project, 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.title == name
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def approval_allowed_zero_minor
|
||||
@ -155,10 +154,7 @@ class DmsfFile < ApplicationRecord
|
||||
end
|
||||
|
||||
def last_revision
|
||||
unless defined?(@last_revision)
|
||||
@last_revision = deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first
|
||||
end
|
||||
@last_revision
|
||||
@last_revision ||= deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first
|
||||
end
|
||||
|
||||
def deleted?
|
||||
@ -177,21 +173,21 @@ class DmsfFile < ApplicationRecord
|
||||
if locked_for_user? && !User.current.allowed_to?(:force_file_unlock, project)
|
||||
Rails.logger.info l(:error_file_is_locked)
|
||||
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
|
||||
errors.add(:base, l(:error_file_is_locked))
|
||||
errors.add :base, l(:error_file_is_locked)
|
||||
end
|
||||
return false
|
||||
end
|
||||
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
|
||||
destroy
|
||||
else
|
||||
self.deleted = STATUS_DELETED
|
||||
self.deleted_by_user = User.current
|
||||
save
|
||||
# Associated revisions should be marked as deleted too
|
||||
dmsf_file_revisions.each { |r| r.delete(commit: commit, force: true) }
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e.message
|
||||
@ -211,16 +207,20 @@ class DmsfFile < ApplicationRecord
|
||||
save
|
||||
end
|
||||
|
||||
def name
|
||||
last_revision&.name.to_s
|
||||
end
|
||||
|
||||
def title
|
||||
last_revision ? last_revision.title : name
|
||||
last_revision&.title.to_s
|
||||
end
|
||||
|
||||
def description
|
||||
last_revision ? last_revision.description : ''
|
||||
last_revision&.description.to_s
|
||||
end
|
||||
|
||||
def version
|
||||
last_revision ? last_revision.version : '0'
|
||||
last_revision&.version.to_s
|
||||
end
|
||||
|
||||
def workflow
|
||||
@ -228,7 +228,7 @@ class DmsfFile < ApplicationRecord
|
||||
end
|
||||
|
||||
def size
|
||||
last_revision ? last_revision.size : 0
|
||||
last_revision&.size.to_i
|
||||
end
|
||||
|
||||
def dmsf_path
|
||||
@ -313,22 +313,13 @@ class DmsfFile < ApplicationRecord
|
||||
file = DmsfFile.new
|
||||
file.dmsf_folder_id = folder.id if folder
|
||||
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?
|
||||
if file.save && last_revision
|
||||
new_revision = last_revision.clone
|
||||
new_revision.name = filename
|
||||
new_revision.title = File.basename(filename, '.*')
|
||||
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.dmsf_workflow_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.assign_workflow wf.id
|
||||
end
|
||||
if File.exist? last_revision.disk_file
|
||||
FileUtils.cp last_revision.disk_file, new_revision.disk_file(search_if_not_exists: false)
|
||||
if last_revision.file.attached?
|
||||
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
|
||||
new_revision.comment = l(:comment_copied_from, source: "#{self.project.identifier}:#{dmsf_path_str}")
|
||||
new_revision.custom_values = []
|
||||
@ -351,10 +351,18 @@ class DmsfFile < ApplicationRecord
|
||||
v.value = cv.value
|
||||
new_revision.custom_values << v
|
||||
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
|
||||
file.last_revision = new_revision
|
||||
else
|
||||
errors.add :base, new_revision.errors.full_messages.to_sentence
|
||||
Rails.logger.error new_revision.errors.full_messages.to_sentence
|
||||
file.delete commit: true
|
||||
file = nil
|
||||
@ -397,7 +405,7 @@ class DmsfFile < ApplicationRecord
|
||||
results = scope.where(find_options).uniq.to_a
|
||||
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
|
||||
begin
|
||||
lang = RedmineDmsf.dmsf_stemming_lang
|
||||
@ -444,25 +452,24 @@ class DmsfFile < ApplicationRecord
|
||||
matchset = enquire.mset(0, 1000)
|
||||
|
||||
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]
|
||||
filename = dochash['url']
|
||||
next unless filename
|
||||
next unless dochash['url'] =~ %r{^\w{2}/\w{2}/(\w+)$} # /76/df/76dfsp2ubbgq4yvq90zrfoyxt012
|
||||
|
||||
dmsf_attrs = filename.scan(%r{^\d{4}/\d{2}/(\d{12}_(\d+)_.*)$})
|
||||
id_attribute = 0
|
||||
id_attribute = dmsf_attrs[0][1] if dmsf_attrs.length.positive?
|
||||
next if dmsf_attrs.empty? || id_attribute.to_i.zero?
|
||||
key = Regexp.last_match(1)
|
||||
blob = ActiveStorage::Blob.find_by(key: key)
|
||||
attachment = blob&.attachments&.first
|
||||
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) &&
|
||||
user.allowed_to?(:view_dmsf_files, dmsf_file.project) &&
|
||||
(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']
|
||||
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'))
|
||||
end
|
||||
break if options[:limit].present? && results.count >= options[:limit]
|
||||
@ -489,29 +496,34 @@ class DmsfFile < ApplicationRecord
|
||||
end
|
||||
|
||||
def text?
|
||||
filename = last_revision&.disk_filename
|
||||
Redmine::MimeType.is_type?('text', filename) ||
|
||||
Redmine::SyntaxHighlighting.filename_supported?(filename)
|
||||
return false unless last_revision
|
||||
|
||||
filename = last_revision.file&.blob&.filename.to_s
|
||||
last_revision.file&.blob&.text? || Redmine::SyntaxHighlighting.filename_supported?(filename)
|
||||
end
|
||||
|
||||
def image?
|
||||
Redmine::MimeType.is_type?('image', last_revision&.disk_filename)
|
||||
last_revision && last_revision.file&.blob&.image?
|
||||
end
|
||||
|
||||
def pdf?
|
||||
Redmine::MimeType.of(last_revision&.disk_filename) == 'application/pdf'
|
||||
last_revision&.content_type == 'application/pdf'
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def html?
|
||||
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/html'
|
||||
last_revision&.content_type == 'text/html'
|
||||
end
|
||||
|
||||
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
|
||||
'.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office
|
||||
'.rtf' # Universal
|
||||
@ -522,11 +534,11 @@ class DmsfFile < ApplicationRecord
|
||||
end
|
||||
|
||||
def markdown?
|
||||
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/markdown'
|
||||
last_revision&.content_type == 'text/markdown'
|
||||
end
|
||||
|
||||
def textile?
|
||||
Redmine::MimeType.of(last_revision&.disk_filename) == 'text/x-textile'
|
||||
last_revision&.content_type == 'text/textile'
|
||||
end
|
||||
|
||||
def disposition
|
||||
@ -534,7 +546,7 @@ class DmsfFile < ApplicationRecord
|
||||
end
|
||||
|
||||
def thumbnailable?
|
||||
Redmine::Thumbnail.convert_available? && (image? || (pdf? && Redmine::Thumbnail.gs_available?))
|
||||
last_revision.file&.variable?
|
||||
end
|
||||
|
||||
def previewable?
|
||||
@ -551,13 +563,14 @@ class DmsfFile < ApplicationRecord
|
||||
def pdf_preview
|
||||
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
|
||||
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
|
||||
Rails.logger.error do
|
||||
%(An error occurred while generating preview for #{last_revision&.disk_file} to #{target}\n
|
||||
Exception was: #{e.message})
|
||||
%(An error occurred while generating preview for #{name} to #{target}\nException was: #{e.message})
|
||||
end
|
||||
''
|
||||
end
|
||||
@ -567,15 +580,16 @@ class DmsfFile < ApplicationRecord
|
||||
result = +'No preview available'
|
||||
if text?
|
||||
begin
|
||||
f = File.new(last_revision.disk_file)
|
||||
f.each_line do |line|
|
||||
case f.lineno
|
||||
when 1
|
||||
result = line
|
||||
when limit.to_i + 1
|
||||
break
|
||||
else
|
||||
result << line
|
||||
last_revision.file.open do |f|
|
||||
f.each_line do |line|
|
||||
case f.lineno
|
||||
when 1
|
||||
result = line
|
||||
when limit.to_i + 1
|
||||
break
|
||||
else
|
||||
result << line
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
@ -586,11 +600,7 @@ class DmsfFile < ApplicationRecord
|
||||
end
|
||||
|
||||
def formatted_name(member)
|
||||
if last_revision
|
||||
last_revision.formatted_name(member)
|
||||
else
|
||||
name
|
||||
end
|
||||
last_revision&.formatted_name(member)
|
||||
end
|
||||
|
||||
def owner?(user)
|
||||
@ -622,33 +632,6 @@ class DmsfFile < ApplicationRecord
|
||||
nil
|
||||
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
|
||||
if locked_for_user?
|
||||
return l(:title_locked_by_user, user: lock.reverse[0].user) if lock.reverse[0].user
|
||||
|
||||
@ -30,6 +30,8 @@ class DmsfFileRevision < ApplicationRecord
|
||||
belongs_to :dmsf_workflow_assigned_by_user, class_name: 'User'
|
||||
belongs_to :dmsf_workflow
|
||||
|
||||
has_one_attached :shared_file
|
||||
|
||||
has_many :dmsf_file_revision_access, dependent: :destroy
|
||||
has_many :dmsf_workflow_step_assignment, dependent: :destroy
|
||||
|
||||
@ -88,16 +90,42 @@ class DmsfFileRevision < ApplicationRecord
|
||||
}
|
||||
)
|
||||
|
||||
validates :title, presence: true
|
||||
validates :title, length: { maximum: 255 }
|
||||
validates :title, presence: true, length: { maximum: 255 }, dmsf_file_name: true
|
||||
validates :major_version, presence: true
|
||||
validates :name, dmsf_file_name: true
|
||||
validates :name, length: { maximum: 255 }
|
||||
validates :disk_filename, length: { maximum: 255 }
|
||||
validates :name, dmsf_file_extension: true
|
||||
validates :name,
|
||||
presence: true,
|
||||
dmsf_file_name: true,
|
||||
length: { maximum: 255 },
|
||||
dmsf_file_extension: true,
|
||||
dmsf_file_revision_name: true
|
||||
validates :description, length: { maximum: 1.kilobyte }
|
||||
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)
|
||||
deleted == STATUS_ACTIVE
|
||||
end
|
||||
@ -110,14 +138,6 @@ class DmsfFileRevision < ApplicationRecord
|
||||
dmsf_file&.dmsf_folder
|
||||
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)
|
||||
if dmsf_file.locked_for_user?
|
||||
errors.add :base, l(:error_file_is_locked)
|
||||
@ -167,51 +187,10 @@ class DmsfFileRevision < ApplicationRecord
|
||||
ver
|
||||
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
|
||||
new_revision = DmsfFileRevision.new
|
||||
new_revision.dmsf_file = dmsf_file
|
||||
new_revision.disk_filename = disk_filename
|
||||
new_revision.size = size
|
||||
new_revision.mime_type = mime_type
|
||||
new_revision.title = title
|
||||
new_revision.description = description
|
||||
new_revision.workflow = workflow
|
||||
@ -221,7 +200,6 @@ class DmsfFileRevision < ApplicationRecord
|
||||
new_revision.source_revision = self
|
||||
new_revision.user = User.current
|
||||
new_revision.name = name
|
||||
new_revision.digest = digest
|
||||
new_revision
|
||||
end
|
||||
|
||||
@ -300,19 +278,7 @@ class DmsfFileRevision < ApplicationRecord
|
||||
end
|
||||
|
||||
def copy_file_content(open_file)
|
||||
sha = Digest::SHA256.new
|
||||
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
|
||||
shared_file.attach io: open_file, filename: dmsf_file.name
|
||||
end
|
||||
|
||||
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
|
||||
@ -350,20 +316,6 @@ class DmsfFileRevision < ApplicationRecord
|
||||
format2
|
||||
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
|
||||
text = description.presence || +''
|
||||
if comment.present?
|
||||
@ -395,19 +347,32 @@ class DmsfFileRevision < ApplicationRecord
|
||||
end
|
||||
|
||||
def protocol
|
||||
@protocol ||= PROTOCOLS[mime_type.downcase] if mime_type
|
||||
@protocol ||= PROTOCOLS[content_type.downcase] if content_type.present?
|
||||
@protocol
|
||||
end
|
||||
|
||||
def delete_source_revision
|
||||
derived_revisions = []
|
||||
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.save!
|
||||
end
|
||||
return unless RedmineDmsf.physical_file_delete?
|
||||
return unless shared_file.attached?
|
||||
|
||||
dependencies = DmsfFileRevision.where(disk_filename: disk_filename).all.size
|
||||
FileUtils.rm_f(disk_file) if dependencies <= 1
|
||||
if derived_revisions.empty?
|
||||
# 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
|
||||
|
||||
def copy_custom_field_values(values, source_revision = nil)
|
||||
|
||||
@ -28,9 +28,9 @@ class DmsfFolder < ApplicationRecord
|
||||
belongs_to :deleted_by_user, class_name: '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 :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
|
||||
has_many :file_links, -> { where(target_type: 'DmsfFile') },
|
||||
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 },
|
||||
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],
|
||||
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true }
|
||||
validates :description, length: { maximum: 65_535 }
|
||||
|
||||
@ -26,7 +26,9 @@ class DmsfLink < ApplicationRecord
|
||||
belongs_to :deleted_by_user, class_name: '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
|
||||
# issue.
|
||||
validates :external_url, length: { maximum: 255 }
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
# Upload
|
||||
class DmsfUpload
|
||||
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
|
||||
|
||||
def disk_file
|
||||
@ -32,12 +32,11 @@ class DmsfUpload
|
||||
if a
|
||||
uploaded = {
|
||||
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,
|
||||
comment: uploaded_file[:description],
|
||||
tempfile_path: a.diskfile,
|
||||
token: uploaded_file[:token],
|
||||
digest: a.digest
|
||||
token: uploaded_file[:token]
|
||||
}
|
||||
DmsfUpload.new project, folder, uploaded
|
||||
else
|
||||
@ -54,7 +53,6 @@ class DmsfUpload
|
||||
@size = 0
|
||||
@tempfile_path = ''
|
||||
@token = ''
|
||||
@digest = ''
|
||||
if RedmineDmsf.empty_minor_version_by_default?
|
||||
@major_version = 1
|
||||
@minor_version = nil
|
||||
@ -86,10 +84,9 @@ class DmsfUpload
|
||||
end
|
||||
@tempfile_path = uploaded[:tempfile_path]
|
||||
@token = uploaded[:token]
|
||||
@digest = uploaded[:digest]
|
||||
|
||||
if file.nil? || file.last_revision.nil?
|
||||
@title = DmsfFileRevision.filename_to_title(@name)
|
||||
@title = File.basename(@name, '.*')
|
||||
@description = uploaded[:comment]
|
||||
if RedmineDmsf.empty_minor_version_by_default?
|
||||
@major_version = 1
|
||||
|
||||
@ -22,11 +22,10 @@ class DmsfFileExtensionValidator < ActiveModel::EachValidator
|
||||
include Redmine::I18n
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
return unless attribute.to_s == 'name'
|
||||
|
||||
extension = File.extname(value)
|
||||
|
||||
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
|
||||
|
||||
@ -22,6 +22,7 @@ class DmsfFileNameValidator < ActiveModel::EachValidator
|
||||
ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
# Check invalid characters
|
||||
record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value)
|
||||
end
|
||||
end
|
||||
|
||||
35
app/validators/dmsf_file_revision_name_validator.rb
Normal file
35
app/validators/dmsf_file_revision_name_validator.rb
Normal 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
|
||||
@ -23,12 +23,11 @@
|
||||
<% unless @locked || @system_folder %>
|
||||
<% if @file_manipulation_allowed %>
|
||||
<%= link_to sprite_icon('add', l(:label_document_new)),
|
||||
multi_dmsf_upload_path(id: @project, folder_id: @folder), class: 'icon icon-add',
|
||||
data: { cy: 'button__new-file--dmsf' } %>
|
||||
multi_dmsf_upload_path(id: @project, folder_id: @folder), class: 'icon icon-add' %>
|
||||
<% end %>
|
||||
<% if @folder_manipulation_allowed %>
|
||||
<%= 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 %>
|
||||
<%= actions_dropdown do %>
|
||||
|
||||
@ -46,11 +46,11 @@
|
||||
</p>
|
||||
</div>
|
||||
<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 %>
|
||||
<% if User.current.allowed_to?(:folder_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 %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@ -120,8 +120,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive',
|
||||
data: { cy: "button__submit--dmsf_folder" } %>
|
||||
<%= submit_tag create ? l(:button_create) : l(:submit_save), class: 'button-positive' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@ -19,32 +19,28 @@
|
||||
|
||||
<li>
|
||||
<%= 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}" },
|
||||
disabled: !allowed || (locked && !unlockable) %>
|
||||
class: 'icon icon-edit', disabled: !allowed || (locked && !unlockable) %>
|
||||
</li>
|
||||
<% unless dmsf_link %>
|
||||
<li>
|
||||
<%= 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}"],
|
||||
back_url: back_url), title: l(:title_copy), class: 'icon icon-copy',
|
||||
data: { cy: "icon__copy--dmsf_file_#{dmsf_file.id}" } %>
|
||||
back_url: back_url), title: l(:title_copy), class: 'icon icon-copy' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= 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,
|
||||
dmsf_file_id: dmsf_file.id, type: 'link_to', back_url: back_url),
|
||||
title: l(:title_create_link), class: 'icon dmsf-icon-link',
|
||||
data: { cy: "icon__link_to--dmsf_file_#{dmsf_file.id}" } %>
|
||||
title: l(:title_create_link), class: 'icon dmsf-icon-link' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<% if locked %>
|
||||
<%= 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}" },
|
||||
title: l(:title_locked_by_user, user: dmsf_file.locked_by), disabled: !unlockable %>
|
||||
class: 'icon icon-unlock', title: l(:title_locked_by_user, user: dmsf_file.locked_by), disabled: !unlockable %>
|
||||
<% else %>
|
||||
<%= 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 %>
|
||||
</li>
|
||||
<% if notifications %>
|
||||
@ -52,14 +48,11 @@
|
||||
<% if dmsf_file.notification %>
|
||||
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
|
||||
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}" },
|
||||
disabled: !allowed || locked %>
|
||||
class: 'icon icon-email', disabled: !allowed || locked %>
|
||||
<% else %>
|
||||
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
|
||||
notify_activate_dmsf_files_path(id: dmsf_file, back_url: back_url),
|
||||
class: 'icon icon-email-add',
|
||||
data: { cy: "icon__email_add--dmsf_file_#{dmsf_file.id}" },
|
||||
disabled: !allowed || locked %>
|
||||
class: 'icon icon-email-add', disabled: !allowed || locked %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
@ -71,16 +64,14 @@
|
||||
<% member = Member.find_by(user_id: User.current.id, project_id: dmsf_file.project.id) %>
|
||||
<% filename = dmsf_file.last_revision&.formatted_name(member) %>
|
||||
<%= context_menu_link sprite_icon('download', l(:button_download)),
|
||||
static_dmsf_file_path(dmsf_file, filename: filename),
|
||||
class: 'icon icon-download', data: { cy: "icon__download--dmsf_file_#{dmsf_file.id}" },
|
||||
disabled: false %>
|
||||
static_dmsf_file_path(dmsf_file, filename: filename, download: dmsf_file.last_revision&.id),
|
||||
class: 'icon icon-download', disabled: false %>
|
||||
</li>
|
||||
<li>
|
||||
<%= context_menu_link sprite_icon('email', l(:field_mail)),
|
||||
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
|
||||
email_entries: true, back_url: back_url),
|
||||
method: :post, class: 'icon icon-email',
|
||||
data: { cy: "icon__email--dmsf_file_#{dmsf_file.id}" }, disabled: !email_allowed %>
|
||||
method: :post, class: 'icon icon-email', disabled: !email_allowed %>
|
||||
</li>
|
||||
<% if RedmineDmsf.dmsf_webdav? %>
|
||||
<li>
|
||||
|
||||
@ -21,23 +21,19 @@
|
||||
<li>
|
||||
<%= context_menu_link sprite_icon('edit', l(:button_edit)),
|
||||
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}" },
|
||||
disabled: !allowed || locked %>
|
||||
class: 'icon icon-edit', disabled: !allowed || locked %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% unless dmsf_link %>
|
||||
<li>
|
||||
<%= 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}"],
|
||||
back_url: back_url), class: 'icon icon-copy',
|
||||
data: { cy: "icon__copy--dmsf_folder_#{dmsf_folder.id}" },
|
||||
disabled: !allowed || locked %>
|
||||
back_url: back_url), class: 'icon icon-copy', disabled: !allowed || locked %>
|
||||
</li>
|
||||
<li>
|
||||
<%= 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',
|
||||
back_url: back_url), class: 'icon dmsf-icon-link',
|
||||
data: { cy: "icon__link_to--dmsf_folder_#{dmsf_folder.id}" } %>
|
||||
back_url: back_url), class: 'icon dmsf-icon-link' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% unless edit %>
|
||||
@ -46,13 +42,11 @@
|
||||
<%= context_menu_link sprite_icon('unlock', l(:button_unlock)),
|
||||
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',
|
||||
data: { cy: "icon__unlock--dmsf_folder_#{dmsf_folder.id}" },
|
||||
disabled: !allowed || !unlockable %>
|
||||
<% else %>
|
||||
<%= context_menu_link sprite_icon('lock', l(:button_lock)),
|
||||
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}" },
|
||||
disabled: !allowed %>
|
||||
class: 'icon icon-lock', disabled: !allowed %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
@ -62,13 +56,11 @@
|
||||
<%= context_menu_link sprite_icon('email', l(:label_notifications_off)),
|
||||
notify_deactivate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
|
||||
back_url: back_url), class: 'icon icon-email',
|
||||
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
|
||||
disabled: !allowed || locked || !dmsf_folder.notification? %>
|
||||
<% else %>
|
||||
<%= context_menu_link sprite_icon('email-disabled', l(:label_notifications_on)),
|
||||
notify_activate_dmsf_path(id: dmsf_folder.project, folder_id: dmsf_folder,
|
||||
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? %>
|
||||
<% end %>
|
||||
</li>
|
||||
@ -79,7 +71,6 @@
|
||||
entries_operations_dmsf_path(id: project, folder_id: folder, ids: params[:ids],
|
||||
download_entries: true, back_url: back_url),
|
||||
method: :post, class: 'icon icon-download',
|
||||
data: { cy: "icon__download--dmsf_folder_#{dmsf_folder.id}" },
|
||||
id: 'dmsf-cm-download', disabled: false %>
|
||||
</li>
|
||||
<li>
|
||||
@ -87,7 +78,6 @@
|
||||
entries_operations_dmsf_path(id: dmsf_folder.project, folder_id: folder, ids: params[:ids],
|
||||
email_entries: true, back_url: back_url),
|
||||
method: :post, class: 'icon icon-email',
|
||||
data: { cy: "icon__email--dmsf_folder_#{dmsf_folder.id}" },
|
||||
disabled: !email_allowed %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
@ -22,14 +22,12 @@
|
||||
<%= link_to sprite_icon('edit', l(:button_edit)),
|
||||
edit_root_dmsf_path(id: project),
|
||||
title: l(:link_edit, title: l(:link_documents)),
|
||||
class: 'icon icon-edit',
|
||||
data: { cy: 'button__edit--dmsf' } %>
|
||||
class: 'icon icon-edit' %>
|
||||
<% elsif !locked %>
|
||||
<%= link_to sprite_icon('edit', l(:button_edit)),
|
||||
edit_dmsf_path(id: project, folder_id: folder, redirect_to_folder_id: folder.id),
|
||||
title: l(:link_edit, title: h(folder.title)),
|
||||
class: 'icon icon-edit',
|
||||
data: { cy: 'button__edit--dmsf' } %>
|
||||
class: 'icon icon-edit' %>
|
||||
<% end %>
|
||||
<% if folder && (!locked || User.current.allowed_to?(:force_file_unlock, project)) %>
|
||||
<% if folder.locked? %>
|
||||
@ -37,13 +35,11 @@
|
||||
sprite_icon('unlock', l(:button_unlock)),
|
||||
unlock_dmsf_path(id: project, folder_id: folder, current: request.url),
|
||||
title: l(:title_unlock_folder),
|
||||
class: 'icon icon-unlock',
|
||||
data: { cy: 'button__unlock--dmsf' } %>
|
||||
class: 'icon icon-unlock' %>
|
||||
<% else %>
|
||||
<%= link_to sprite_icon('lock', l(:button_lock)),
|
||||
lock_dmsf_path(id: project, folder_id: folder, current: request.url),
|
||||
title: l(:title_lock_folder), class: 'icon icon-lock',
|
||||
data: { cy: 'button__lock--dmsf' } %>
|
||||
title: l(:title_lock_folder), class: 'icon icon-lock' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if notifications && !locked %>
|
||||
@ -51,14 +47,12 @@
|
||||
<%= link_to sprite_icon('email', l(:label_notifications_off)),
|
||||
notify_deactivate_dmsf_path(id: project, folder_id: folder),
|
||||
title: l(:title_notifications_active_deactivate),
|
||||
class: 'icon icon-email',
|
||||
data: { cy: 'button__notifications-off--dmsf' } %>
|
||||
class: 'icon icon-email' %>
|
||||
<% else %>
|
||||
<%= link_to sprite_icon('email-disabled', l(:label_notifications_on)),
|
||||
notify_activate_dmsf_path(id: project, folder_id: folder),
|
||||
title: l(:title_notifications_not_active_activate),
|
||||
class: 'icon icon-email-add',
|
||||
data: { cy: 'button__notifications-on--dmsf' } %>
|
||||
class: 'icon icon-email-add' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if file_manipulation_allowed && !locked %>
|
||||
@ -66,8 +60,7 @@
|
||||
new_dmsf_link_path(project_id: project.id, dmsf_folder_id: folder ? folder.id : folder,
|
||||
type: 'link_from'),
|
||||
title: l(:title_create_link),
|
||||
class: 'icon dmsf-icon-link',
|
||||
data: { cy: 'button__create-link--dmsf' } %>
|
||||
class: 'icon dmsf-icon-link' %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= render partial: 'dmsf_context_menus/watch', locals: { object: folder ? folder : project } %>
|
||||
@ -75,8 +68,7 @@
|
||||
<%= link_to sprite_icon('del', l(:link_trash_bin)),
|
||||
trash_dmsf_path(project),
|
||||
title: l(:link_trash_bin),
|
||||
class: 'icon icon-del',
|
||||
data: { cy: 'button__trash--dmsf' } %>
|
||||
class: 'icon icon-del' %>
|
||||
<% else %>
|
||||
<span class="icon icon-del">
|
||||
<%= sprite_icon('del', l(:link_trash_bin)) %>
|
||||
|
||||
@ -40,7 +40,8 @@
|
||||
<% member = Member.find_by(user_id: User.current.id, project_id: file.project.id) %>
|
||||
<% filename = file.last_revision&.formatted_name(member) %>
|
||||
<%= 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 } %>
|
||||
<%= 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 %>
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
<%= f.text_area :comment, rows: 2, label: l(:label_comment), class: 'wiki-edit dmsf-description' %>
|
||||
</p>
|
||||
<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>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
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 class="<%= cls %>">
|
||||
<span class="size">(<%= number_to_human_size dmsf_file.last_revision.size %>)</span>
|
||||
|
||||
@ -25,12 +25,20 @@
|
||||
<div class="thumbnails">
|
||||
<% end %>
|
||||
<% images.each do |file| %>
|
||||
<div>
|
||||
<div class="thumbnail" title="<%= file.name %>">
|
||||
<% 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 %>
|
||||
<%= image_tag(dmsf_thumbnail_path(file),
|
||||
{ :'data-fullsrc' => view_dmsf_file_url(file), alt: file.title }) %>
|
||||
<%= image_tag(file.last_revision&.file&.variant(resize_to_limit: [size, size]),
|
||||
:'data-fullsrc' => view_dmsf_file_url(file),
|
||||
alt: file.title,
|
||||
style: "max-width: #{size}px; max-height: #{size}px;",
|
||||
loading: 'lazy') %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@ -14,7 +14,7 @@ api.dmsf_file do
|
||||
api.dmsf_string "{{dmsf(#{@file.id},#{@file.name},#{r.id})}}"
|
||||
api.content_url view_dmsf_file_url(@file, download: r)
|
||||
api.size r.size
|
||||
api.mime_type r.mime_type
|
||||
api.mime_type r.content_type
|
||||
api.title r.title
|
||||
api.description r.description
|
||||
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_at r.dmsf_workflow_started_at
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
@ -133,12 +133,12 @@
|
||||
</div>
|
||||
<div class="status attribute">
|
||||
<%= 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>
|
||||
<% if revision.digest.present? %>
|
||||
<% if revision.checksum.present? %>
|
||||
<div class="status attribute">
|
||||
<%= 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>
|
||||
<% end %>
|
||||
<%= render 'dmsf/custom_fields', object: revision %>
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
<div class="box tabular dmfs-box-tabular">
|
||||
<%= 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}][digest]", upload.digest %>
|
||||
<%= hidden_field_tag "committed_files[#{i}][size]", upload.size %>
|
||||
<div class="splitcontent">
|
||||
<div class="splitcontentleft">
|
||||
|
||||
@ -31,9 +31,8 @@
|
||||
locals: { multiple: true, container: nil, description: true, awf: false } %>
|
||||
</span>
|
||||
<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" %>
|
||||
<%= submit_tag l(:label_dmsf_upload_commit), data: { cy: 'button__submit--dmsf-upload-commit--project' },
|
||||
class: 'button-positive' %>
|
||||
<%= submit_tag l(:label_dmsf_upload_commit), class: 'button-positive' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@ -22,6 +22,5 @@
|
||||
"content_type": "<%= @tempfile.content_type.gsub('"', '').html_safe %>",
|
||||
"disk_filename": "<%= @disk_filename.html_safe %>",
|
||||
"tempfile_path": "<%= @tempfile_path.html_safe %>",
|
||||
"digest": "<%= @digest.html_safe %>",
|
||||
"token": "<%= @token.html_safe %>"
|
||||
}
|
||||
|
||||
@ -41,7 +41,6 @@
|
||||
<% end %>
|
||||
<div class="form-actions">
|
||||
<%= submit_tag l(:label_dmsf_commit),
|
||||
data: { cy: 'button__submit__commit-file--project' },
|
||||
class: 'button-positive',
|
||||
onclick: "$('#ajax-indicator').show();" %>
|
||||
</div>
|
||||
|
||||
@ -86,7 +86,7 @@
|
||||
<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>
|
||||
<% 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? %>
|
||||
<% else %>
|
||||
<%= DmsfWorkflowStepAction.workflow_str(row['action']) %>
|
||||
|
||||
@ -23,12 +23,12 @@
|
||||
<% @path = settings_project_path(@project, tab: 'dmsf_workflow') %>
|
||||
<p>
|
||||
<%= 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>
|
||||
<% else %>
|
||||
<div class="contextual">
|
||||
<%= 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>
|
||||
<h2><%= l(:label_dmsf_workflow_plural) %></h2>
|
||||
<% end %>
|
||||
|
||||
@ -19,7 +19,8 @@
|
||||
|
||||
<div class="contextual">
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
|
||||
@ -328,7 +328,7 @@
|
||||
<%= l(:label_full_text) %>
|
||||
</em>
|
||||
|
||||
<% if RedmineDmsf::Plugin.lib_available?('xapian') %>
|
||||
<% if RedmineDmsf.xapian_available %>
|
||||
<p>
|
||||
<%= content_tag :label, l(:label_index_database) %>
|
||||
<%= text_field_tag 'settings[dmsf_index_database]', RedmineDmsf.dmsf_index_database, size: 50 %>
|
||||
@ -373,6 +373,14 @@
|
||||
<%= l(:label_default)%>: <%= l(:general_text_No) %>
|
||||
</em>
|
||||
</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 %>
|
||||
<p class="warning"><%= l(:warning_xapian_not_available) %></p>
|
||||
<% end %>
|
||||
|
||||
@ -70,7 +70,7 @@ cs:
|
||||
link_modified: Změněno
|
||||
link_ver: Ver.
|
||||
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_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_lock_file: Zamknout a zabránit změnám ostatních uživatelů
|
||||
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í
|
||||
heading_send_documents_by_email: Odeslat dokumenty emailem
|
||||
heading_send_documents_by_email: Odeslat dokumenty e-mailem
|
||||
label_email_from: Od
|
||||
label_email_to: Komu
|
||||
label_email_cc: Kopie
|
||||
@ -125,7 +125,7 @@ cs:
|
||||
option_version_custom: Vlastní
|
||||
label_new_content: Nový obsah
|
||||
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í.
|
||||
label_file_storage_directory: Složka pro uložení souborů
|
||||
label_index_database: Index databáze
|
||||
@ -144,8 +144,8 @@ cs:
|
||||
label_default_notifications: Výchozí notifikace pro soubory
|
||||
heading_uploaded_files: Nahrané soubory
|
||||
link_documents: Dokumenty
|
||||
permission_view_dmsf_file_revision_accesses: View downloads in Activity stream
|
||||
permission_view_dmsf_file_revisions: View revisions in Activity stream
|
||||
permission_view_dmsf_file_revision_accesses: Zobrazit stažení v aktivitách
|
||||
permission_view_dmsf_file_revisions: Zobrazit revize v aktivitách
|
||||
permission_view_dmsf_folders: Procházet dokumenty
|
||||
permission_user_preferences: Nastavení uživatele
|
||||
permission_view_dmsf_files: Zobrazit dokumenty
|
||||
@ -155,8 +155,8 @@ cs:
|
||||
permission_manage_workflows: Spravovat schvalovací procesy
|
||||
permission_file_delete: Mazat dokumenty
|
||||
permission_display_system_folders: Zobrazit systémové složky
|
||||
permission_file_approval: File approval
|
||||
permission_email_documents: Email documents
|
||||
permission_file_approval: Schvalovat dokumenty
|
||||
permission_email_documents: Posílat dokumenty e-mailem
|
||||
label_file: Soubor
|
||||
field_folder: Složka
|
||||
error_file_commit_require_uploaded_file: Potvrzení vyžaduje nahraný soubor
|
||||
@ -182,7 +182,7 @@ cs:
|
||||
menu_dmsf: DMS # Project tab title
|
||||
label_physical_file_delete: Fyzické smazání souboru
|
||||
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_last: Poslední
|
||||
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í
|
||||
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)"
|
||||
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.
|
||||
label_maximum_email_filesize: Maximální velikost souboru emailu
|
||||
label_maximum_email_filesize: Maximální velikost souboru e-mailu
|
||||
header_minimum_filesize: Chyba souboru.
|
||||
error_minimum_filesize: "Soubor %{file} má nulovou velikost a nebude přiložen."
|
||||
parent_directory: Nadřazená složka
|
||||
@ -317,9 +317,9 @@ cs:
|
||||
label_links_only: pouze odkazy
|
||||
|
||||
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š
|
||||
title_restore: Obnovit
|
||||
@ -384,8 +384,7 @@ cs:
|
||||
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
|
||||
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.
|
||||
např.: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
slov."
|
||||
|
||||
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í
|
||||
@ -493,6 +492,10 @@ cs:
|
||||
warning_folder_unlockable: Složku nelze odemknout
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -378,9 +378,7 @@ de:
|
||||
|
||||
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.
|
||||
Monograms enthalten Informationen zur Position. Nicht-Koreanische Zeichenfolgen werden in Wörter zerlegt. Die entsprechende
|
||||
Option muss beim Indexieren verwendet werden,
|
||||
z.B. XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
Monograms enthalten Informationen zur Position. Nicht-Koreanische Zeichenfolgen werden in Wörter zerlegt."
|
||||
|
||||
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
|
||||
@ -489,6 +487,10 @@ de:
|
||||
warning_folder_unlockable: Der Ordner kann nicht entsperrt werden
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ en:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ en:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ es:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ es:
|
||||
label_dmsf_commit: 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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -471,6 +471,9 @@ fa:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ fr:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal"
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ fr:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -381,9 +381,7 @@ hu:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -491,6 +489,9 @@ hu:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ it: # Italian strings thx 2 Matteo Arceci!
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ it: # Italian strings thx 2 Matteo Arceci!
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ ja:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -493,6 +491,9 @@ ja:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -381,10 +381,8 @@ ko:
|
||||
label_email_reply_to: 회신
|
||||
|
||||
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: 빠른 링크
|
||||
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
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -492,6 +492,9 @@ nl:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ pl:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ pl:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ pt-BR:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ pt-BR:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ sl:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ sl:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -383,10 +383,7 @@ uk:
|
||||
|
||||
label_enable_cjk_ngrams: Дозводити генерацію n-грам з тексту CJK
|
||||
text_enable_cjk_ngrams: "Якщо ввімкнено цю функцію, діапазони символів CJK розбиваються на уніграми і біграми, при
|
||||
цьому уніграми містять позиційну інформацію. Символи, відмінні від CJK, розбиваються на слова як зазвичай.
|
||||
Відповідна опція повинна бути використана під час індексування
|
||||
Приклад: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
|
||||
цьому уніграми містять позиційну інформацію. Символи, відмінні від CJK, розбиваються на слова як зазвичай."
|
||||
label_dmsf_fast_links: Швидкі посилання
|
||||
text_dmsf_fast_links_info: Ви зможете вручну ввести ідентифікатор кінцевої папки під час створення посилань або
|
||||
переміщення файлів чи папок, щоб пришвидшити процес створення посилань.
|
||||
@ -494,6 +491,9 @@ uk:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -381,9 +381,8 @@ zh-TW:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
|
||||
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
|
||||
@ -491,6 +490,9 @@ zh-TW:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -382,9 +382,7 @@ zh:
|
||||
|
||||
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
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal. The corresponding
|
||||
option needs to have been used at index time.
|
||||
e.g: XAPIAN_CJK_NGRAM=true ruby plugins/redmine_dmsf/extra/xapian_indexer.rb -fv"
|
||||
unigrams carrying positional information. Non-CJK characters are split into words as normal."
|
||||
|
||||
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
|
||||
@ -492,6 +490,9 @@ zh:
|
||||
warning_folder_unlockable: The folder can't be unlocked
|
||||
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:
|
||||
errors:
|
||||
messages:
|
||||
|
||||
@ -22,4 +22,8 @@ class AddDigestToRevision < ActiveRecord::Migration[4.2]
|
||||
def up
|
||||
add_column :dmsf_file_revisions, :digest, :string, limit: 40, default: '', null: false
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :dmsf_file_revisions, :digest
|
||||
end
|
||||
end
|
||||
|
||||
@ -25,7 +25,7 @@ class ChangeRevisionDigestLimitTo64 < ActiveRecord::Migration[4.2]
|
||||
|
||||
def down
|
||||
# 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
|
||||
end
|
||||
end
|
||||
|
||||
@ -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
|
||||
158
db/migrate/20251015130601_active_storage_migration.rb
Normal file
158
db/migrate/20251015130601_active_storage_migration.rb
Normal 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
|
||||
@ -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
|
||||
5
init.rb
5
init.rb
@ -27,7 +27,7 @@ Redmine::Plugin.register :redmine_dmsf do
|
||||
author_url 'https://github.com/picman/redmine_dmsf/graphs/contributors'
|
||||
author 'Vít Jonáš / Daniel Munn / Karel Pičman'
|
||||
description 'Document Management System Features'
|
||||
version '4.2.3'
|
||||
version '5.0.0 devel'
|
||||
|
||||
requires_redmine version_or_higher: '6.1.0'
|
||||
|
||||
@ -62,7 +62,8 @@ Redmine::Plugin.register :redmine_dmsf do
|
||||
'empty_minor_version_by_default' => '0',
|
||||
'remove_original_documents_module' => '0',
|
||||
'dmsf_webdav_authentication' => 'Digest',
|
||||
'dmsf_really_delete_files' => '0'
|
||||
'dmsf_really_delete_files' => '0',
|
||||
'dmsf_max_xapian_filesize' => 3
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@ -19,6 +19,18 @@
|
||||
|
||||
# Main module
|
||||
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
|
||||
class << self
|
||||
def dmsf_max_file_download
|
||||
@ -38,11 +50,12 @@ module RedmineDmsf
|
||||
end
|
||||
|
||||
def dmsf_index_database
|
||||
if Setting.plugin_redmine_dmsf['dmsf_index_database'].present?
|
||||
Setting.plugin_redmine_dmsf['dmsf_index_database'].strip
|
||||
else
|
||||
File.expand_path('dmsf_index', Rails.root)
|
||||
end
|
||||
dir = if Setting.plugin_redmine_dmsf['dmsf_index_database'].present?
|
||||
Setting.plugin_redmine_dmsf['dmsf_index_database'].strip
|
||||
else
|
||||
File.expand_path('dmsf_index', Rails.root)
|
||||
end
|
||||
FileUtils.mkdir_p dir
|
||||
end
|
||||
|
||||
def dmsf_stemming_lang
|
||||
@ -211,6 +224,14 @@ module RedmineDmsf
|
||||
value = Setting.plugin_redmine_dmsf['dmsf_default_notifications']
|
||||
value.to_i.positive? || value == 'true'
|
||||
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
|
||||
|
||||
|
||||
@ -47,7 +47,9 @@ module RedmineDmsf
|
||||
end
|
||||
|
||||
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
|
||||
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::DOSTime.at(dmsf_file.last_revision.updated_at))
|
||||
@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))
|
||||
@zip_file.write buffer
|
||||
end
|
||||
@ -71,31 +73,6 @@ module RedmineDmsf
|
||||
@dmsf_files << dmsf_file
|
||||
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)
|
||||
string_path = dmsf_folder.dmsf_path_str + File::SEPARATOR
|
||||
string_path = string_path[(root_path.length + 1)..string_path.length] if root_path
|
||||
|
||||
@ -91,7 +91,7 @@ module RedmineDmsf
|
||||
rel: 'noopener',
|
||||
class: 'icon icon-file',
|
||||
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
|
||||
|
||||
@ -174,7 +174,6 @@ module RedmineDmsf
|
||||
uploaded_file[:size] = upload.size
|
||||
uploaded_file[:mime_type] = upload.mime_type
|
||||
uploaded_file[:tempfile_path] = upload.tempfile_path
|
||||
uploaded_file[:digest] = upload.digest
|
||||
if params[:dmsf_attachments_wfs].present? && params[:dmsf_attachments_wfs][key].present?
|
||||
uploaded_file[:workflow_id] = params[:dmsf_attachments_wfs][key].to_i
|
||||
end
|
||||
|
||||
@ -49,7 +49,7 @@ module RedmineDmsf
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
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
|
||||
|
||||
# dmsff - link to a folder
|
||||
@ -241,16 +241,22 @@ module RedmineDmsf
|
||||
{{dmsftn(file_id)}} -- with default height 200 (auto width)
|
||||
{{dmsftn(file_id1 file_id2 file_id3)}} -- multiple thumbnails
|
||||
{{dmsftn(file_id, size=300)}} -- with size 300x300
|
||||
{{dmsftn(file_id, height=300)}} -- with height (auto width)
|
||||
{{dmsftn(file_id, width=300)}} -- with width (auto height)
|
||||
{{dmsftn(file_id, height=300)}} -- with height (default width)
|
||||
{{dmsftn(file_id, width=300)}} -- with width (default height)
|
||||
{{dmsftn(file_id, size=640x480)}} -- with size 640x480}
|
||||
macro :dmsftn do |_obj, args|
|
||||
raise ArgumentError if args.empty? # Requires file id
|
||||
|
||||
args, options = extract_macro_options(args, :size, :width, :height, :title)
|
||||
size = options[:size]
|
||||
width = options[:width]
|
||||
height = options[:height]
|
||||
|
||||
if options[:size].present?
|
||||
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
|
||||
html = []
|
||||
ids.each do |id|
|
||||
@ -260,25 +266,21 @@ module RedmineDmsf
|
||||
next
|
||||
end
|
||||
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)
|
||||
filename = file.last_revision.formatted_name(member)
|
||||
url = static_dmsf_file_url(file, filename: filename)
|
||||
img = if size
|
||||
image_tag(url, alt: filename, title: file.title, size: size)
|
||||
elsif height
|
||||
image_tag(url, alt: filename, title: file.title, width: 'auto', height: height)
|
||||
elsif width
|
||||
image_tag(url, alt: filename, title: file.title, width: width, height: 'auto')
|
||||
else
|
||||
image_tag(url, alt: filename, title: file.title, width: 'auto', height: 200)
|
||||
end
|
||||
html << link_to(img, url,
|
||||
img = image_tag(file.last_revision&.file&.variant(resize_to_limit: [width, height]),
|
||||
alt: filename,
|
||||
style: "max-width: #{width}px; max-height: #{height}px;",
|
||||
loading: 'lazy')
|
||||
html << link_to(img,
|
||||
url,
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
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
|
||||
safe_join html
|
||||
end
|
||||
|
||||
@ -31,7 +31,7 @@ module RedmineDmsf
|
||||
def get_image_filename(attrname)
|
||||
if attrname =~ %r{/dmsf/files/(\d+)/}
|
||||
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
|
||||
super
|
||||
end
|
||||
|
||||
@ -49,9 +49,9 @@ module RedmineDmsf
|
||||
# New methods
|
||||
def self.prepended(base)
|
||||
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
|
||||
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
|
||||
has_many :dmsf_workflows, dependent: :destroy
|
||||
has_many :folder_links, -> { where dmsf_folder_id: nil, target_type: 'DmsfFolder' },
|
||||
|
||||
@ -39,4 +39,4 @@ module RedmineDmsf
|
||||
end
|
||||
|
||||
# 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')
|
||||
|
||||
@ -36,14 +36,5 @@ module RedmineDmsf
|
||||
end
|
||||
false
|
||||
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
|
||||
|
||||
@ -34,8 +34,8 @@ module RedmineDmsf
|
||||
@office_available = $CHILD_STATUS.success?
|
||||
rescue StandardError
|
||||
@office_available = false
|
||||
Rails.logger.warn l(:note_dmsf_office_bin_not_available, value: office_bin, locale: :en)
|
||||
end
|
||||
Rails.logger.warn l(:note_dmsf_office_bin_not_available, value: office_bin, locale: :en) unless @office_available
|
||||
@office_available
|
||||
end
|
||||
|
||||
@ -46,6 +46,8 @@ module RedmineDmsf
|
||||
office_bin = RedmineDmsf.office_bin.presence || 'libreoffice'
|
||||
cmd = "#{shell_quote(office_bin)} --convert-to pdf --headless --outdir #{shell_quote(dir)} #{shell_quote(source)}"
|
||||
if system(cmd)
|
||||
filename = "#{File.basename(source, '.*')}.pdf"
|
||||
FileUtils.mv File.join(dir, filename), target
|
||||
target
|
||||
else
|
||||
Rails.logger.error "Creating preview failed (#{$CHILD_STATUS}):\nCommand: #{cmd}"
|
||||
|
||||
@ -55,7 +55,7 @@ module RedmineDmsf
|
||||
end
|
||||
|
||||
# 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
|
||||
# this method.
|
||||
def children
|
||||
@ -63,12 +63,12 @@ module RedmineDmsf
|
||||
@children = []
|
||||
if folder
|
||||
# Folders
|
||||
folder.dmsf_folders.visible.each do |f|
|
||||
@children.push child(f.title) if DmsfFolder.permissions?(f, allow_system: false)
|
||||
folder.dmsf_folders.visible.each do |folder|
|
||||
@children.push child(folder.title) if DmsfFolder.permissions?(folder, allow_system: false)
|
||||
end
|
||||
# Files
|
||||
folder.dmsf_files.visible.pluck(:name).each do |name|
|
||||
@children.push child(name)
|
||||
folder.dmsf_files.visible.each do |file|
|
||||
@children.push child(file.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -92,7 +92,7 @@ module RedmineDmsf
|
||||
def content_type
|
||||
if file
|
||||
if file.last_revision
|
||||
file.last_revision.detect_content_type
|
||||
file.last_revision.content_type
|
||||
else
|
||||
'application/octet-stream'
|
||||
end
|
||||
@ -126,13 +126,7 @@ module RedmineDmsf
|
||||
end
|
||||
|
||||
def etag
|
||||
ino = if file&.last_revision && File.exist?(file.last_revision.disk_file)
|
||||
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)
|
||||
format '%<node>x-%<size>x-%<modified>x', node: 0, size: content_length, modified: last_modified.to_i
|
||||
end
|
||||
|
||||
def content_length
|
||||
@ -230,8 +224,9 @@ module RedmineDmsf
|
||||
dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user))
|
||||
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)
|
||||
|
||||
if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, allow_system: false) ||
|
||||
!DmsfFolder.permissions?(parent.folder, allow_system: false)))
|
||||
raise Forbidden
|
||||
@ -247,13 +242,11 @@ module RedmineDmsf
|
||||
!User.current.allowed_to?(:folder_manipulation, dest.resource.project))
|
||||
raise Forbidden
|
||||
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?
|
||||
|
||||
# Change the title
|
||||
folder.title = dest.resource.basename
|
||||
return PreconditionFailed unless folder.save
|
||||
|
||||
# Move to a new destination
|
||||
folder.move_to(dest.resource.project, parent.folder) ? Created : PreconditionFailed
|
||||
else
|
||||
@ -272,13 +265,9 @@ module RedmineDmsf
|
||||
new_revision = dest.resource.file.last_revision.clone
|
||||
new_revision.increase_version DmsfFileRevision::PATCH_VERSION
|
||||
end
|
||||
# The file on disk must be renamed from .tmp to the correct filetype or else Xapian won't know how to index.
|
||||
# Copy file.last_revision.disk_file to new_revision.disk_file
|
||||
# Copy the file
|
||||
new_revision.size = file.last_revision.size
|
||||
new_revision.disk_filename = new_revision.new_storage_filename
|
||||
File.open(file.last_revision.disk_file, 'rb') do |f|
|
||||
new_revision.copy_file_content f
|
||||
end
|
||||
new_revision.copy_file_content StringIO.new(file.last_revision.file.download)
|
||||
# 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
|
||||
@ -304,9 +293,8 @@ module RedmineDmsf
|
||||
# Update Revision and names of file [We can link to old physical resource, as it's not changed]
|
||||
if file.last_revision
|
||||
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
|
||||
file.name = dest.resource.basename
|
||||
# Save Changes
|
||||
if file.last_revision.save && file.save
|
||||
dest.exist? ? NoContent : Created
|
||||
@ -340,7 +328,7 @@ module RedmineDmsf
|
||||
|
||||
res = NoContent
|
||||
end
|
||||
return PreconditionFailed unless parent.exist? && parent.folder
|
||||
return PreconditionFailed unless parent.exist? && (parent.folder || parent.project)
|
||||
|
||||
if collection?
|
||||
# Permission check if they can manipulate folders and view folders
|
||||
@ -348,7 +336,7 @@ module RedmineDmsf
|
||||
# Manipulate folders on destination project :folder_manipulation
|
||||
# View folders on destination project :view_dmsf_folders
|
||||
# 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? ||
|
||||
(User.current.allowed_to?(:folder_manipulation, 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)
|
||||
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
|
||||
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
|
||||
|
||||
# Lock Check
|
||||
# Lock check
|
||||
# Check for the existence of locks
|
||||
def lock_check(args = {})
|
||||
entity = file || folder
|
||||
@ -584,19 +577,17 @@ module RedmineDmsf
|
||||
else
|
||||
f = DmsfFile.new
|
||||
f.project_id = project.id
|
||||
f.name = basename
|
||||
f.dmsf_folder = parent.folder
|
||||
f.notification = RedmineDmsf.dmsf_default_notifications?
|
||||
new_revision = DmsfFileRevision.new
|
||||
new_revision.minor_version = 1
|
||||
new_revision.major_version = 0
|
||||
new_revision.title = DmsfFileRevision.filename_to_title(basename)
|
||||
new_revision.title = File.basename(basename, '.*')
|
||||
end
|
||||
|
||||
new_revision.dmsf_file = f
|
||||
new_revision.user = User.current
|
||||
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
|
||||
# however, includes a size method - so we instead use reflection
|
||||
@ -625,10 +616,19 @@ module RedmineDmsf
|
||||
raise UnprocessableEntity
|
||||
end
|
||||
|
||||
new_revision.disk_filename = new_revision.new_storage_filename unless reuse_revision
|
||||
|
||||
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
|
||||
# Notifications
|
||||
DmsfMailer.deliver_files_updated project, [f]
|
||||
@ -636,7 +636,6 @@ module RedmineDmsf
|
||||
Rails.logger.error new_revision.errors.full_messages.to_sentence
|
||||
raise InternalServerError
|
||||
end
|
||||
|
||||
Created
|
||||
end
|
||||
|
||||
@ -697,10 +696,7 @@ module RedmineDmsf
|
||||
# implementation of service for request, which allows for us to pipe a single file through
|
||||
# also best-utilising Dav4rack's implementation.
|
||||
def download
|
||||
raise NotFound unless file&.last_revision
|
||||
|
||||
disk_file = file.last_revision.disk_file
|
||||
raise NotFound unless disk_file && File.exist?(disk_file)
|
||||
raise NotFound unless file.last_revision&.file&.attached?
|
||||
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
|
||||
@ -719,7 +715,9 @@ module RedmineDmsf
|
||||
Rails.logger.error "Could not send email notifications: #{e.message}"
|
||||
end
|
||||
end
|
||||
File.new disk_file
|
||||
file.last_revision.file.open do |f|
|
||||
File.new f.path
|
||||
end
|
||||
end
|
||||
|
||||
def reuse_version_for_locked_file?(file)
|
||||
@ -766,27 +764,28 @@ module RedmineDmsf
|
||||
def create_empty_file
|
||||
f = DmsfFile.new
|
||||
f.project_id = project.id
|
||||
f.name = basename
|
||||
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.minor_version = 1
|
||||
r.major_version = 0
|
||||
r.title = DmsfFileRevision.filename_to_title(basename)
|
||||
r.title = File.basename(basename, '.*')
|
||||
r.dmsf_file = f
|
||||
r.user = User.current
|
||||
r.name = basename
|
||||
r.mime_type = Redmine::MimeType.of(r.name)
|
||||
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
|
||||
next unless cf.default_value
|
||||
|
||||
r.custom_field_values << CustomValue.new({ custom_field: cf, value: cf.default_value })
|
||||
end
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
@ -41,8 +41,8 @@ module RedmineDmsf
|
||||
end
|
||||
# Files
|
||||
if User.current.allowed_to?(:view_dmsf_files, project)
|
||||
project.dmsf_files.visible.pluck(:name).each do |name|
|
||||
@children.push child(name)
|
||||
project.dmsf_files.visible.each do |file|
|
||||
@children.push child(file.name)
|
||||
end
|
||||
end
|
||||
@children
|
||||
|
||||
56
lib/redmine_dmsf/xapian_analyzer.rb
Normal file
56
lib/redmine_dmsf/xapian_analyzer.rb
Normal 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
|
||||
104
test/fixtures/active_storage_attachments.yml
vendored
Normal file
104
test/fixtures/active_storage_attachments.yml
vendored
Normal 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
143
test/fixtures/active_storage_blobs.yml
vendored
Normal 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 %>
|
||||
94
test/fixtures/dmsf_file_revisions.yml
vendored
94
test/fixtures/dmsf_file_revisions.yml
vendored
@ -3,10 +3,8 @@ dmsf_file_revisions_001:
|
||||
id: 1
|
||||
dmsf_file_id: 1
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: "test.txt"
|
||||
disk_filename: "test.txt"
|
||||
size: 4
|
||||
mime_type: text/plain
|
||||
name: "test.txt"
|
||||
size: 3
|
||||
title: "Test File"
|
||||
description: 'Some file :-)'
|
||||
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
|
||||
@ -18,7 +16,6 @@ dmsf_file_revisions_001:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: 1
|
||||
dmsf_workflow_started_by_user_id: 1
|
||||
digest: '81dc9bdb52d04dc20036dbd8313ed055'
|
||||
created_at: 2017-04-18 14:52:27 +02:00
|
||||
|
||||
#revision for file on non-enabled project
|
||||
@ -27,9 +24,7 @@ dmsf_file_revisions_002:
|
||||
dmsf_file_id: 2
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: "test2.txt"
|
||||
disk_filename: "test2.txt"
|
||||
size: 4
|
||||
mime_type: text/plain
|
||||
size: 3
|
||||
title: "Test File"
|
||||
description: NULL
|
||||
workflow: NULL
|
||||
@ -41,7 +36,6 @@ dmsf_file_revisions_002:
|
||||
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:27 +02:00
|
||||
|
||||
#revision for deleted file on dmsf-enabled project
|
||||
@ -50,9 +44,7 @@ dmsf_file_revisions_003:
|
||||
dmsf_file_id: 3
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'deleted.txt'
|
||||
disk_filename: 'deleted.txt'
|
||||
size: 4
|
||||
mime_type: 'text/plain'
|
||||
size: 3
|
||||
title: 'Test File'
|
||||
description: NULL
|
||||
workflow: NULL
|
||||
@ -64,7 +56,6 @@ dmsf_file_revisions_003:
|
||||
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:27 +02:00
|
||||
|
||||
dmsf_file_revisions_004:
|
||||
@ -72,9 +63,7 @@ dmsf_file_revisions_004:
|
||||
dmsf_file_id: 4
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'test4.txt'
|
||||
disk_filename: 'test4.txt'
|
||||
size: 4
|
||||
mime_type: 'text/plain'
|
||||
size: 3
|
||||
title: 'Test File'
|
||||
description: NULL
|
||||
workflow: NULL
|
||||
@ -86,39 +75,14 @@ dmsf_file_revisions_004:
|
||||
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: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:
|
||||
id: 6
|
||||
dmsf_file_id: 7
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'test.gif'
|
||||
disk_filename: 'test.gif'
|
||||
size: 4
|
||||
mime_type: 'image/gif'
|
||||
size: 310
|
||||
title: 'Test image'
|
||||
description: NULL
|
||||
workflow: NULL
|
||||
@ -130,7 +94,6 @@ dmsf_file_revisions_006:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: NULL
|
||||
dmsf_workflow_started_by_user_id: NULL
|
||||
digest: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
|
||||
created_at: 2017-04-18 14:52:27 +02:00
|
||||
|
||||
dmsf_file_revisions_007:
|
||||
@ -138,9 +101,7 @@ dmsf_file_revisions_007:
|
||||
dmsf_file_id: 8
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'test.pdf'
|
||||
disk_filename: 'test.pdf'
|
||||
size: 4
|
||||
mime_type: 'application/pdf'
|
||||
size: 6942
|
||||
title: 'Test PDF'
|
||||
description: NULL
|
||||
workflow: NULL
|
||||
@ -152,17 +113,14 @@ dmsf_file_revisions_007:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: NULL
|
||||
dmsf_workflow_started_by_user_id: NULL
|
||||
digest: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
|
||||
created_at: 2017-04-18 14:52:27 +02:00
|
||||
|
||||
dmsf_file_revisions_008:
|
||||
id: 8
|
||||
dmsf_file_id: 9
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'myfile.txt'
|
||||
disk_filename: 'myfile.txt' # The file is not physically present
|
||||
name: 'myfile.txt' # The file is not physically present
|
||||
size: 0
|
||||
mime_type: 'text/plain'
|
||||
title: 'My File'
|
||||
description: NULL
|
||||
workflow: NULL
|
||||
@ -181,9 +139,7 @@ dmsf_file_revisions_009:
|
||||
dmsf_file_id: 10
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'zero.txt'
|
||||
disk_filename: 'zero.txt'
|
||||
size: 0
|
||||
mime_type: 'text/plain'
|
||||
title: 'Zero Size File'
|
||||
description: NULL
|
||||
workflow: NULL
|
||||
@ -195,7 +151,6 @@ dmsf_file_revisions_009:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: NULL
|
||||
dmsf_workflow_started_by_user_id: NULL
|
||||
digest: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
||||
created_at: 2017-04-18 14:52:27 +02:00
|
||||
|
||||
dmsf_file_revisions_010:
|
||||
@ -203,9 +158,7 @@ dmsf_file_revisions_010:
|
||||
dmsf_file_id: 5
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'test.txt'
|
||||
disk_filename: 'test.txt'
|
||||
size: 4
|
||||
mime_type: 'text/plain'
|
||||
title: 'Test File'
|
||||
description: 'Some file :-)'
|
||||
workflow: 1 # DmsfWorkflow::STATE_WAITING_FOR_APPROVAL
|
||||
@ -217,7 +170,6 @@ dmsf_file_revisions_010:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: 1
|
||||
dmsf_workflow_started_by_user_id: 1
|
||||
digest: '81dc9bdb52d04dc20036dbd8313ed055'
|
||||
created_at: 2017-04-18 14:52:27 +02:00
|
||||
|
||||
dmsf_file_revisions_011:
|
||||
@ -225,9 +177,7 @@ dmsf_file_revisions_011:
|
||||
dmsf_file_id: 12
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'test.txt'
|
||||
disk_filename: 'test.txt'
|
||||
size: 4
|
||||
mime_type: 'text/plain'
|
||||
title: 'Test File'
|
||||
description: 'Some file :-)'
|
||||
workflow: 0
|
||||
@ -239,7 +189,6 @@ dmsf_file_revisions_011:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: 1
|
||||
dmsf_workflow_started_by_user_id: 1
|
||||
digest: '81dc9bdb52d04dc20036dbd8313ed055'
|
||||
created_at: 2017-04-18 14:52:27 +02:00
|
||||
|
||||
dmsf_file_revisions_012:
|
||||
@ -247,9 +196,7 @@ dmsf_file_revisions_012:
|
||||
dmsf_file_id: 6
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'test.mp4'
|
||||
disk_filename: 'test.mp4'
|
||||
size: 4
|
||||
mime_type: 'video/mp4'
|
||||
title: 'test video'
|
||||
description: 'A video :-)'
|
||||
workflow: 0
|
||||
@ -261,7 +208,6 @@ dmsf_file_revisions_012:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: NULL
|
||||
dmsf_workflow_started_by_user_id: NULL
|
||||
digest: '81dc9bdb52d04dc20036dbd8313ed055'
|
||||
created_at: 2022-02-03 13:39:27 +02:00
|
||||
|
||||
dmsf_file_revisions_013:
|
||||
@ -269,9 +215,7 @@ dmsf_file_revisions_013:
|
||||
dmsf_file_id: 13
|
||||
source_dmsf_file_revision_id: NULL
|
||||
name: 'test.odt'
|
||||
disk_filename: 'test.odt'
|
||||
size: 4
|
||||
mime_type: 'application/vnd.oasis.opendocument.text'
|
||||
size: 10445
|
||||
title: 'Test office document'
|
||||
description: 'LibreOffice text'
|
||||
workflow: 0
|
||||
@ -283,5 +227,23 @@ dmsf_file_revisions_013:
|
||||
user_id: 1
|
||||
dmsf_workflow_assigned_by_user_id: NULL
|
||||
dmsf_workflow_started_by_user_id: NULL
|
||||
digest: '89a6d0ea9aafc21a978152f3e4977812d5d7d623505749471f256a90fc7c5f72'
|
||||
created_at: 2017-04-01 08:54:00 +02:00
|
||||
|
||||
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
|
||||
20
test/fixtures/dmsf_files.yml
vendored
20
test/fixtures/dmsf_files.yml
vendored
@ -3,7 +3,6 @@ dmsf_files_001:
|
||||
id: 1
|
||||
project_id: 1
|
||||
dmsf_folder_id: NULL
|
||||
name: 'test.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -13,7 +12,6 @@ dmsf_files_002:
|
||||
id: 2
|
||||
project_id: 2
|
||||
dmsf_folder_id: NULL
|
||||
name: 'test.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -23,7 +21,6 @@ dmsf_files_003:
|
||||
id: 3
|
||||
project_id: 1
|
||||
dmsf_folder_id: NULL
|
||||
name: 'deleted.txt'
|
||||
notification: false
|
||||
deleted: 1
|
||||
deleted_by_user_id: 1
|
||||
@ -32,7 +29,6 @@ dmsf_files_004:
|
||||
id: 4
|
||||
project_id: 1
|
||||
dmsf_folder_id: 2
|
||||
name: 'test.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -41,7 +37,6 @@ dmsf_files_005:
|
||||
id: 5
|
||||
project_id: 1
|
||||
dmsf_folder_id: 5
|
||||
name: 'test.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -50,7 +45,6 @@ dmsf_files_006:
|
||||
id: 6
|
||||
project_id: 2
|
||||
dmsf_folder_id: 3
|
||||
name: 'test.mp4'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -59,7 +53,6 @@ dmsf_files_007:
|
||||
id: 7
|
||||
project_id: 1
|
||||
dmsf_folder_id: 8
|
||||
name: 'test.gif'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -68,7 +61,6 @@ dmsf_files_008:
|
||||
id: 8
|
||||
project_id: 1
|
||||
dmsf_folder_id: NULL
|
||||
name: 'test.pdf'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -77,7 +69,6 @@ dmsf_files_009:
|
||||
id: 9
|
||||
project_id: 1
|
||||
dmsf_folder_id: NULL
|
||||
name: 'myfile.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -86,7 +77,6 @@ dmsf_files_010:
|
||||
id: 10
|
||||
project_id: 1
|
||||
dmsf_folder_id: NULL
|
||||
name: 'zero.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -95,7 +85,6 @@ dmsf_files_011:
|
||||
id: 11
|
||||
project_id: 1
|
||||
dmsf_folder_id: 9
|
||||
name: 'zero.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -104,7 +93,6 @@ dmsf_files_012:
|
||||
id: 12
|
||||
project_id: 5
|
||||
dmsf_folder_id: NULL
|
||||
name: 'test.txt'
|
||||
notification: false
|
||||
deleted: 0
|
||||
deleted_by_user_id: NULL
|
||||
@ -113,6 +101,12 @@ dmsf_files_013:
|
||||
id: 13
|
||||
project_id: 1
|
||||
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_by_user_id: NULL
|
||||
@ -51,4 +51,10 @@ wfsa9:
|
||||
id: 9
|
||||
dmsf_workflow_step_id: 5
|
||||
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
|
||||
10
test/fixtures/dmsf_workflow_steps.yml
vendored
10
test/fixtures/dmsf_workflow_steps.yml
vendored
@ -36,4 +36,12 @@ wfs5:
|
||||
step: 3
|
||||
name: '3rd step'
|
||||
user_id: 2
|
||||
operator: 1
|
||||
operator: 1
|
||||
|
||||
wfs6:
|
||||
id: 6
|
||||
dmsf_workflow_id: 2
|
||||
step: 1
|
||||
name: '1st step'
|
||||
user_id: 2
|
||||
operator: 1
|
||||
|
||||
1
test/fixtures/files/2017/04/test5.txt
vendored
1
test/fixtures/files/2017/04/test5.txt
vendored
@ -1 +0,0 @@
|
||||
123
|
||||
BIN
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v06
vendored
Normal file
BIN
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v06
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 B |
BIN
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v07
vendored
Normal file
BIN
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v07
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v12
vendored
Normal file
BIN
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v12
vendored
Normal file
Binary file not shown.
6
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v14
vendored
Normal file
6
test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v14
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
Hello world!
|
||||
</body>
|
||||
</html>
|
||||
@ -73,7 +73,7 @@ class DmsfContextMenusControllerTest < RedmineDmsf::Test::TestCase
|
||||
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-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
|
||||
|
||||
|
||||
@ -142,14 +142,14 @@ class DmsfFilesControllerTest < RedmineDmsf::Test::TestCase
|
||||
version_major: @file1.last_revision.major_version,
|
||||
version_minor: @file1.last_revision.minor_version + 1,
|
||||
dmsf_file_revision: {
|
||||
title: @file1.last_revision.title,
|
||||
name: @file1.last_revision.name,
|
||||
description: @file1.last_revision.description,
|
||||
title: @file1.title,
|
||||
name: @file1.name,
|
||||
description: @file1.description,
|
||||
comment: 'New revision'
|
||||
}
|
||||
}
|
||||
end
|
||||
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
|
||||
|
||||
@ -31,6 +31,7 @@ class DmsfWorkflowsControllerTest < RedmineDmsf::Test::TestCase
|
||||
@wfs4 = DmsfWorkflowStep.find 4 # step 2
|
||||
@wfs5 = DmsfWorkflowStep.find 5 # step 3
|
||||
@wf1 = DmsfWorkflow.find 1
|
||||
@wf2 = DmsfWorkflow.find 2
|
||||
@wf3 = DmsfWorkflow.find 3
|
||||
@wfsa2 = DmsfWorkflowStepAssignment.find 2
|
||||
@revision1 = DmsfFileRevision.find 1
|
||||
@ -429,8 +430,8 @@ class DmsfWorkflowsControllerTest < RedmineDmsf::Test::TestCase
|
||||
|
||||
def test_log_member_global_wf
|
||||
post '/login', params: { username: 'jsmith', password: 'jsmith' }
|
||||
get "/dmsf_workflows/#{@wf3.id}/log",
|
||||
params: { project_id: @project1.id, dmsf_file_id: @file1.id, format: 'js' },
|
||||
get "/dmsf_workflows/#{@wf2.id}/log",
|
||||
params: { project_id: @project1.id, dmsf_file_id: @file2.id, format: 'js' },
|
||||
xhr: true
|
||||
assert_response :success
|
||||
assert_template :log
|
||||
|
||||
@ -36,9 +36,9 @@ class MyControllerTest < RedmineDmsf::Test::TestCase
|
||||
end
|
||||
|
||||
def test_page_with_open_approvals_no_approval
|
||||
post '/login', params: { username: 'jsmith', password: 'jsmith' }
|
||||
@jsmith.pref[:my_page_layout] = { 'top' => ['open_approvals'] }
|
||||
@jsmith.pref.save!
|
||||
post '/login', params: { username: 'admin', password: 'admin' }
|
||||
@admin.pref[:my_page_layout] = { 'top' => ['open_approvals'] }
|
||||
@admin.pref.save!
|
||||
get '/my/page'
|
||||
assert_response :success
|
||||
assert_select 'div#list-top' do
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user